Skip to content

Commit 1c84546

Browse files
committed
feat: add value getter to the store selctor for easy access to the selected slice of the state
1 parent 13cc421 commit 1c84546

4 files changed

Lines changed: 111 additions & 8 deletions

File tree

.changeset/light-cobras-live.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/lit-store': minor
3+
---
4+
5+
Add a value getter for the TanstackStoreSelector to allow for easy access to fine-grained reactivity via the controller

docs/framework/lit/quick-start.md

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ id: quick-start
55

66
The basic Lit app example to get started with TanStack `lit-store`.
77

8+
You can use `TanStackStoreSelector` in two ways:
9+
10+
- Trigger rerenders when a selected slice changes
11+
- Access the selected value directly through `.value`
12+
813
```ts
914
import { LitElement, html } from 'lit'
1015
import { customElement, property } from 'lit/decorators.js'
@@ -25,21 +30,31 @@ const updateState = (animal: Animal) => {
2530
}))
2631
}
2732

28-
// This will only re-render when `state[animal]` changes. If an unrelated
29-
// store property changes, it won't re-render.
3033
@customElement('animal-display')
3134
export class AnimalDisplay extends LitElement {
3235
@property({ type: String }) animal: Animal = 'dogs'
3336

34-
// Subscribes the host to changes in `state[animal]` only.
35-
_ = new TanStackStoreSelector(
37+
// Subscribes only to `state[animal]`
38+
counter = new TanStackStoreSelector(
3639
this,
3740
() => store,
3841
(state) => state[this.animal],
3942
)
4043

4144
render() {
42-
return html`<div>${this.animal}: ${store.state[this.animal]}</div>`
45+
return html`
46+
<div>
47+
<p>
48+
Using selector.value:
49+
${this.counter.value}
50+
</p>
51+
52+
<p>
53+
Reading directly from store.state:
54+
${store.state[this.animal]}
55+
</p>
56+
</div>
57+
`
4358
}
4459
}
4560

@@ -62,12 +77,15 @@ export class TanStackStoreDemo extends LitElement {
6277
return html`
6378
<div>
6479
<h1>How many of your friends like cats or dogs?</h1>
80+
6581
<p>
66-
Press one of the buttons to add a counter of how many of your
67-
friends like cats or dogs
82+
Press one of the buttons to increment how many of your friends
83+
like cats or dogs.
6884
</p>
85+
6986
<animal-increment animal="dogs"></animal-increment>
7087
<animal-display animal="dogs"></animal-display>
88+
7189
<animal-increment animal="cats"></animal-increment>
7290
<animal-display animal="cats"></animal-display>
7391
</div>
@@ -76,6 +94,13 @@ export class TanStackStoreDemo extends LitElement {
7694
}
7795
```
7896

97+
`selector.value` returns the latest selected value and only updates when
98+
that specific selection changes.
99+
100+
Reading from `store.state` accesses the full store state directly. The
101+
component still rerenders because the selector subscription is active,
102+
but the rendered value itself comes from the store.
103+
79104
Then mount the root element in your HTML:
80105

81106
```html

packages/lit-store/src/tan-stack-store-selector.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,43 @@ type SelectionSource<T> = {
1919
}
2020
}
2121

22+
/**
23+
* Subscribes a Lit host to a TanStack Store and exposes a selected slice of its state.
24+
*
25+
* The host will only re-render when the selected value actually changes
26+
* (according to the configured `compare` function).
27+
*
28+
* @example
29+
* ```ts
30+
* class UserNameEl extends LitElement {
31+
* #name = new TanStackStoreSelector(
32+
* this,
33+
* () => userStore,
34+
* (snapshot) => snapshot.name,
35+
* )
36+
*
37+
* render() {
38+
* return html`<p>${this.#name.value}</p>`
39+
* }
40+
* }
41+
* ```
42+
*
43+
* @example
44+
* ```ts
45+
* class UserNameEl extends LitElement {
46+
* _ = new TanStackStoreAtom(
47+
* this,
48+
* () => userStore,
49+
* (snapshot) => snapshot.name,
50+
* )
51+
*
52+
* render() {
53+
* return html`<p>${userStore.state.name}</p>`
54+
* }
55+
* }
56+
* ```
57+
*
58+
*/
2259
export class TanStackStoreSelector<
2360
TSource,
2461
TSelected = NoInfer<TSource>,
@@ -45,6 +82,10 @@ export class TanStackStoreSelector<
4582
host.addController(this)
4683
}
4784

85+
get value(): TSelected | undefined {
86+
return this.#lastSelected
87+
}
88+
4889
hostUpdate() {
4990
const store = this.#getStore()
5091
if (store === this.#subscribedStore) return

packages/lit-store/tests/selector.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { defineOnce, mount } from './utils'
99

1010
const user = userEvent.setup()
1111

12-
describe('Lit Store Tests', async () => {
12+
describe('Lit Store Tests', () => {
1313
it('should update when a store is selected with no selector', async () => {
1414
const counter = createStore(0)
1515

@@ -41,6 +41,38 @@ describe('Lit Store Tests', async () => {
4141
expect(getBtn()).toHaveTextContent('1')
4242
})
4343

44+
it('should update value setter is used', async () => {
45+
const counter = createStore(0)
46+
47+
function add() {
48+
counter.setState((prev) => prev + 1)
49+
}
50+
51+
class TestForm extends LitElement {
52+
#selector = new TanStackStoreSelector(this, () => counter)
53+
54+
render() {
55+
return html`<button id="btn" @click=${add}>${this.#selector.value}</button>`
56+
}
57+
}
58+
59+
const tag = defineOnce('test-form', TestForm)
60+
61+
const element = await mount<TestForm>(tag)
62+
63+
const getBtn = () =>
64+
element.shadowRoot!.querySelector<HTMLButtonElement>('#btn')
65+
66+
expect(getBtn()).toHaveTextContent('0')
67+
68+
expect(counter.state).toBe(0)
69+
70+
await user.click(getBtn()!)
71+
expect(counter.state).toBe(1)
72+
expect(getBtn()).toHaveTextContent('1')
73+
})
74+
75+
4476
it('should ignore updates when a store is selected with a selector', async () => {
4577
const counter = createStore({ count: 0, ignore: 1 })
4678

0 commit comments

Comments
 (0)