Skip to content
Merged
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ export const Header = () => {
}
```

Use the `keys` option to re-render only on specific key changes:

```tsx
export const Header = () => {
const profile = useStore($profile, { keys: 'name' })
return <header>{profile.name}</header>
}
```

[Nano Stores]: https://github.com/nanostores/nanostores/

---
Expand Down
10 changes: 6 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { listenKeys } from 'nanostores'
import { useEffect, useState } from 'preact/hooks'

export function useStore(store, opts = {}) {
let [hydrated, setHydrated] = useState(false)
let [, forceRender] = useState({})
let [valueBeforeEffect] = useState(store.get())

// A re-render is always forced on mount and hydrate
useEffect(() => {
valueBeforeEffect !== store.get() && forceRender({})
setHydrated(true)
}, [])

useEffect(() => {
let batching, timer, unlisten
let rerender = () => {
Expand All @@ -31,5 +32,6 @@ export function useStore(store, opts = {}) {
}
}, [store, '' + opts.keys])

return store.get()
// `'init' in store` check for compatibility with nanostores <= 1.1.1
return !hydrated && 'init' in store ? store.init : store.get()
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"nanodelay": "^2.0.2",
"nanostores": "^1.0.1",
"preact": "^10.26.5",
"preact-render-to-string": "^6.6.6",
"size-limit": "^11.2.0",
"tsx": "^4.19.3",
"typescript": "^5.8.3"
Expand Down Expand Up @@ -88,7 +89,7 @@
"index.js": "{ useStore }",
"nanostores": "{ map, computed }"
},
"limit": "922 B"
"limit": "943 B"
}
]
}
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

76 changes: 67 additions & 9 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { atom, map, onMount, STORE_UNMOUNT_DELAY } from 'nanostores'
import { deepStrictEqual, equal } from 'node:assert'
import { afterEach, test } from 'node:test'
import type { FunctionalComponent as FC } from 'preact'
import { h } from 'preact'
import { h, hydrate } from 'preact'
import { renderToString } from 'preact-render-to-string'
import { useState } from 'preact/hooks'

import { useStore } from '../index.js'
Expand Down Expand Up @@ -60,7 +61,7 @@ test('renders simple store', async () => {
deepStrictEqual(events, ['constructor'])
equal(screen.getByTestId('test1').textContent, 'a')
equal(screen.getByTestId('test2').textContent, 'a')
equal(renders, 1)
equal(renders, 2)

await act(async () => {
letter.set('b')
Expand All @@ -70,13 +71,13 @@ test('renders simple store', async () => {

equal(screen.getByTestId('test1').textContent, 'c')
equal(screen.getByTestId('test2').textContent, 'c')
equal(renders, 2)
equal(renders, 3)

act(() => {
screen.getByRole('button').click()
})
equal(screen.queryByTestId('test'), null)
equal(renders, 2)
equal(renders, 3)
await delay(STORE_UNMOUNT_DELAY)

deepStrictEqual(events, ['constructor', 'destroy'])
Expand Down Expand Up @@ -178,7 +179,7 @@ test('has keys option', async () => {
render(h(Wrapper, {}, h(MapTest, {})))

equal(screen.getByTestId('map-test').textContent, 'map:undefined-undefined')
equal(renderCount, 1)
equal(renderCount, 2)

// updates on init
await act(async () => {
Expand All @@ -187,7 +188,7 @@ test('has keys option', async () => {
})

equal(screen.getByTestId('map-test').textContent, 'map:undefined-undefined')
equal(renderCount, 2)
equal(renderCount, 3)

// updates when has key
await act(async () => {
Expand All @@ -196,7 +197,7 @@ test('has keys option', async () => {
})

equal(screen.getByTestId('map-test').textContent, 'map:a-undefined')
equal(renderCount, 3)
equal(renderCount, 4)

// does not update when has no key
await act(async () => {
Expand All @@ -205,7 +206,7 @@ test('has keys option', async () => {
})

equal(screen.getByTestId('map-test').textContent, 'map:a-undefined')
equal(renderCount, 3)
equal(renderCount, 4)

// reacts on parameter changes
await act(async () => {
Expand All @@ -214,7 +215,7 @@ test('has keys option', async () => {
})

equal(screen.getByTestId('map-test').textContent, 'map:a-b')
equal(renderCount, 4)
equal(renderCount, 5)
})

test('supports atom changes between rendering and useEffect', () => {
Expand Down Expand Up @@ -254,3 +255,60 @@ test('supports map changes between rendering and useEffect', () => {
let result = screen.getByText('new').textContent
equal(result, 'new')
})

test('returns initial value until hydrated', () => {
type Value = 'new' | 'old'
let atomStore = atom<Value>('old')
let mapStore = map<{ value: Value }>({ value: 'old' })

let atomValues: Value[] = [] // Track values used across renders

let AtomTest: FC = () => {
let value = useStore(atomStore)
atomValues.push(value)
return h('div', { 'data-testid': 'atom-test' }, value)
}

let mapValues: Value[] = [] // Track values used across renders

let MapTest: FC = () => {
let value = useStore(mapStore).value
mapValues.push(value)
return h('div', { 'data-testid': 'map-test' }, value)
}

let Wrapper: FC = () => {
return h(
'div',
{ 'data-testid': 'test' },
h(AtomTest, null),
h(MapTest, null)
)
}

// Create a "server" rendered element to re-hydrate
let ssrElement = document.createElement('div')
document.body.appendChild(ssrElement)
let html = renderToString(h(Wrapper, null))
ssrElement.innerHTML = html

equal(screen.getByTestId('atom-test').textContent, 'old')
equal(screen.getByTestId('map-test').textContent, 'old')

// Simulate store state change on client-side, after "server" render
atomStore.set('new')
mapStore.set({ value: 'new' })

// Hydrate into SSR element
act(() => {
hydrate(h(Wrapper, null), ssrElement)
})

// Confirm "server" render got old values, initial client render got old
// values at hydration, then post-hydration render got new values
deepStrictEqual(atomValues, ['old', 'old', 'new'])
deepStrictEqual(mapValues, ['old', 'old', 'new'])

equal(screen.getByTestId('atom-test').textContent, 'new')
equal(screen.getByTestId('map-test').textContent, 'new')
})