Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/light-cobras-live.md
Original file line number Diff line number Diff line change
@@ -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
39 changes: 32 additions & 7 deletions docs/framework/lit/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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`<div>${this.animal}: ${store.state[this.animal]}</div>`
return html`
<div>
<p>
Using selector.value:
${this.counter.value}
</p>

<p>
Reading directly from store.state:
${store.state[this.animal]}
</p>
</div>
`
}
}

Expand All @@ -62,12 +77,15 @@ export class TanStackStoreDemo extends LitElement {
return html`
<div>
<h1>How many of your friends like cats or dogs?</h1>

<p>
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.
</p>

<animal-increment animal="dogs"></animal-increment>
<animal-display animal="dogs"></animal-display>

<animal-increment animal="cats"></animal-increment>
<animal-display animal="cats"></animal-display>
</div>
Expand All @@ -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
Expand Down
41 changes: 41 additions & 0 deletions packages/lit-store/src/tan-stack-store-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,43 @@ type SelectionSource<T> = {
}
}

/**
* 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`<p>${this.#name.value}</p>`
* }
* }
* ```
*
* @example
* ```ts
* class UserNameEl extends LitElement {
* _ = new TanStackStoreAtom(
* this,
* () => userStore,
* (snapshot) => snapshot.name,
* )
*
* render() {
* return html`<p>${userStore.state.name}</p>`
* }
* }
* ```
*
*/
export class TanStackStoreSelector<
TSource,
TSelected = NoInfer<TSource>,
Expand All @@ -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
Expand Down
34 changes: 33 additions & 1 deletion packages/lit-store/tests/selector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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`<button id="btn" @click=${add}>${this.#selector.value}</button>`
}
}

const tag = defineOnce('test-form', TestForm)

const element = await mount<TestForm>(tag)

const getBtn = () =>
element.shadowRoot!.querySelector<HTMLButtonElement>('#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 })

Expand Down