diff --git a/.changeset/light-cobras-live.md b/.changeset/light-cobras-live.md new file mode 100644 index 00000000..8793ce0f --- /dev/null +++ b/.changeset/light-cobras-live.md @@ -0,0 +1,5 @@ +--- +'@tanstack/lit-store': minor +--- + +Add a value getter for the TanstackStoreSelector to allow for easy access to fine-grained reactivity via the controller diff --git a/docs/framework/lit/quick-start.md b/docs/framework/lit/quick-start.md index c8b068ce..c2f6167f 100644 --- a/docs/framework/lit/quick-start.md +++ b/docs/framework/lit/quick-start.md @@ -5,6 +5,11 @@ id: quick-start The basic Lit app example to get started with TanStack `lit-store`. +You can use `TanStackStoreSelector` in two ways: + +- Trigger rerenders when a selected slice changes +- Access the selected value directly through `.value` + ```ts import { LitElement, html } from 'lit' import { customElement, property } from 'lit/decorators.js' @@ -25,21 +30,31 @@ const updateState = (animal: Animal) => { })) } -// This will only re-render when `state[animal]` changes. If an unrelated -// store property changes, it won't re-render. @customElement('animal-display') export class AnimalDisplay extends LitElement { @property({ type: String }) animal: Animal = 'dogs' - // Subscribes the host to changes in `state[animal]` only. - _ = new TanStackStoreSelector( + // Subscribes only to `state[animal]` + counter = new TanStackStoreSelector( this, () => store, (state) => state[this.animal], ) render() { - return html`
${this.animal}: ${store.state[this.animal]}
` + return html` +
+

+ Using selector.value: + ${this.counter.value} +

+ +

+ Reading directly from store.state: + ${store.state[this.animal]} +

+
+ ` } } @@ -62,12 +77,15 @@ export class TanStackStoreDemo extends LitElement { return html`

How many of your friends like cats or dogs?

+

- Press one of the buttons to add a counter of how many of your - friends like cats or dogs + Press one of the buttons to increment how many of your friends + like cats or dogs.

+ +
@@ -76,6 +94,13 @@ export class TanStackStoreDemo extends LitElement { } ``` +`selector.value` returns the latest selected value and only updates when +that specific selection changes. + +Reading from `store.state` accesses the full store state directly. The +component still rerenders because the selector subscription is active, +but the rendered value itself comes from the store. + Then mount the root element in your HTML: ```html diff --git a/packages/lit-store/src/tan-stack-store-selector.ts b/packages/lit-store/src/tan-stack-store-selector.ts index 912e0a3e..f9b62901 100644 --- a/packages/lit-store/src/tan-stack-store-selector.ts +++ b/packages/lit-store/src/tan-stack-store-selector.ts @@ -19,6 +19,43 @@ type SelectionSource = { } } +/** + * Subscribes a Lit host to a TanStack Store and exposes a selected slice of its state. + * + * The host will only re-render when the selected value actually changes + * (according to the configured `compare` function). + * + * @example + * ```ts + * class UserNameEl extends LitElement { + * #name = new TanStackStoreSelector( + * this, + * () => userStore, + * (snapshot) => snapshot.name, + * ) + * + * render() { + * return html`

${this.#name.value}

` + * } + * } + * ``` + * + * @example + * ```ts + * class UserNameEl extends LitElement { + * _ = new TanStackStoreAtom( + * this, + * () => userStore, + * (snapshot) => snapshot.name, + * ) + * + * render() { + * return html`

${userStore.state.name}

` + * } + * } + * ``` + * + */ export class TanStackStoreSelector< TSource, TSelected = NoInfer, @@ -45,6 +82,10 @@ export class TanStackStoreSelector< host.addController(this) } + get value(): TSelected | undefined { + return this.#lastSelected + } + hostUpdate() { const store = this.#getStore() if (store === this.#subscribedStore) return diff --git a/packages/lit-store/tests/selector.test.ts b/packages/lit-store/tests/selector.test.ts index 88866f34..24c07d39 100644 --- a/packages/lit-store/tests/selector.test.ts +++ b/packages/lit-store/tests/selector.test.ts @@ -9,7 +9,7 @@ import { defineOnce, mount } from './utils' const user = userEvent.setup() -describe('Lit Store Tests', async () => { +describe('Lit Store Tests', () => { it('should update when a store is selected with no selector', async () => { const counter = createStore(0) @@ -41,6 +41,38 @@ describe('Lit Store Tests', async () => { expect(getBtn()).toHaveTextContent('1') }) + it('should update value setter is used', async () => { + const counter = createStore(0) + + function add() { + counter.setState((prev) => prev + 1) + } + + class TestForm extends LitElement { + #selector = new TanStackStoreSelector(this, () => counter) + + render() { + return html`` + } + } + + const tag = defineOnce('test-form', TestForm) + + const element = await mount(tag) + + const getBtn = () => + element.shadowRoot!.querySelector('#btn') + + expect(getBtn()).toHaveTextContent('0') + + expect(counter.state).toBe(0) + + await user.click(getBtn()!) + expect(counter.state).toBe(1) + expect(getBtn()).toHaveTextContent('1') + }) + + it('should ignore updates when a store is selected with a selector', async () => { const counter = createStore({ count: 0, ignore: 1 })