diff --git a/README.md b/README.md
index 345baf5..8ee69c3 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,6 @@ export const Header = ({ postId }) => {
---
-
## Options
### Keys
@@ -85,3 +84,69 @@ export const Header = () => {
A function set on `ssr` is provided to React's [`useSyncExternalStore`](https://react.dev/reference/react/useSyncExternalStore)
as the `getServerSnapshot` option.
+
+### Selector
+
+In some cases using selector store might be the preferred way of subscribing to store changes. In that case you can use `useSelector` hook.
+
+```tsx
+import { useStore, useSelector } from '@nanostores/react'
+import { $profiles } from '../stores/profiles.js'
+
+export const HeaderWithKeys = ({ userId }) => {
+ const profiles = useStore($profiles, { keys: [userId, 'name'] })
+ return {profiles[userId].name.toUpperCase()}
+}
+
+export const HeaderWithSelector = ({ userId }) => {
+ // selector function automatically handles keys changes
+ const name = useSelector($profiles, profiles =>
+ profiles[userId].name.toUppercase()
+ )
+ return
+}
+```
+
+### Compare
+
+You can pass a different comparison function to `useSelector`, to determine when the data should trigger a re-render.
+
+```tsx
+import { useSelector } from '@nanostores/react'
+import { $user } from '../stores/user.js'
+import { shallowCompare } from '../utils/shallow.js'
+
+export const Posts = () => {
+ const ids = useSelector(
+ $user,
+ user => user.postIds, // [1, 2, 3]
+ { compare: shallowCompare }
+ )
+
+ return (
+
+ Post ids: {ids.join(', ')}
+ {/* does not trigger the re-render because ids are shallowly equal */}
+
+
+ )
+}
+```
+
+### SSR with useSelector
+
+SSR in `useSelector` works like in `useStore`. If you are passing a function to `ssr` make sure to return snapshot of the store you're passing into useSelector, not selected value.
+
+```tsx
+const profileFromServer = { name: 'A User' }
+
+export const Header = () => {
+ const name = useSelector($profile, profile => profile.name, {
+ ssr: typeof window === 'undefined' ? false : () => profileFromServer
+ })
+
+ return
+}
+```
diff --git a/index.d.ts b/index.d.ts
index da56581..1300c9d 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,56 +1,2 @@
-import type { Store, StoreValue } from 'nanostores'
-import type { DependencyList } from 'react'
-
-type StoreKeys = T extends { setKey: (k: infer K, v: any) => unknown }
- ? K
- : never
-
-export interface UseStoreOptions {
- /**
- * @default
- * ```ts
- * [store, options.keys]
- * ```
- */
- deps?: DependencyList
-
- /**
- * Will re-render components only on specific key changes.
- */
- keys?: StoreKeys[]
-
- /**
- * Enable SSR support. Set `initial` when store's initial value is the same on
- * server and client, or provide a function to return the server store state
- * for advanced cases (per useSyncExternalStore's getServerSnapshot).
- */
- ssr?: (() => StoreValue) | 'initial' | false
-}
-
-/**
- * Subscribe to store changes and get store’s value.
- *
- * Can be user with store builder too.
- *
- * ```js
- * import { useStore } from 'nanostores/react'
- *
- * import { router } from '../store/router'
- *
- * export const Layout = () => {
- * let page = useStore(router)
- * if (page.route === 'home') {
- * return
- * } else {
- * return
- * }
- * }
- * ```
- *
- * @param store Store instance.
- * @returns Store value.
- */
-export function useStore(
- store: SomeStore,
- options?: UseStoreOptions
-): StoreValue
+export { UseStoreOptions, useStore } from './use-store/index.d.ts'
+export { UseSelectorOptions, useSelector } from './use-selector/index.d.ts'
diff --git a/index.js b/index.js
index 652a73a..e2c557f 100644
--- a/index.js
+++ b/index.js
@@ -1,34 +1,2 @@
-import { listenKeys } from 'nanostores'
-import { useCallback, useRef, useSyncExternalStore } from 'react'
-
-let emit = (snapshotRef, onChange) => value => {
- if (snapshotRef.current === value) return
- snapshotRef.current = value
- onChange()
-}
-
-export function test() {
- return 1
-}
-
-export function useStore(store, { keys, deps = [store, keys], ssr } = {}) {
- let snapshotRef = useRef()
- snapshotRef.current = store.get()
-
- let subscribe = useCallback(onChange => {
- emit(snapshotRef, onChange)(store.value)
-
- return keys?.length > 0
- ? listenKeys(store, keys, emit(snapshotRef, onChange))
- : store.listen(emit(snapshotRef, onChange))
- }, deps)
-
- let get = () => snapshotRef.current
-
- let server = get
- if (ssr && 'init' in store) {
- server = ssr === 'initial' ? () => store.init : ssr
- }
-
- return useSyncExternalStore(subscribe, get, server)
-}
+export { useStore } from './use-store/index.js'
+export { useSelector } from './use-selector/index.js'
diff --git a/oxfmt.config.ts b/oxfmt.config.ts
index 3258050..255d864 100644
--- a/oxfmt.config.ts
+++ b/oxfmt.config.ts
@@ -1,3 +1,3 @@
-import loguxOxfmtConfig from "@logux/oxc-configs/fmt";
+import loguxOxfmtConfig from '@logux/oxc-configs/fmt'
-export default loguxOxfmtConfig;
+export default loguxOxfmtConfig
diff --git a/package.json b/package.json
index ae46c16..55fddd0 100644
--- a/package.json
+++ b/package.json
@@ -61,11 +61,20 @@
},
"size-limit": [
{
+ "name": "min",
"import": {
"index.js": "{ useStore }",
"nanostores": "{ map, computed }"
},
- "limit": "958 B"
+ "limit": "954 B"
+ },
+ {
+ "name": "max",
+ "import": {
+ "index.js": "{ useStore, useSelector }",
+ "nanostores": "{ map, computed }"
+ },
+ "limit": "1084 B"
}
],
"engines": {
diff --git a/test/errors.ts b/test/errors.ts
index 7a07746..8b803cd 100644
--- a/test/errors.ts
+++ b/test/errors.ts
@@ -1,4 +1,5 @@
import { map, WritableAtom } from 'nanostores'
+
import { useStore } from '../index.js'
type TestType =
diff --git a/test/use-selector.test.ts b/test/use-selector.test.ts
new file mode 100644
index 0000000..490fb5b
--- /dev/null
+++ b/test/use-selector.test.ts
@@ -0,0 +1,422 @@
+import './setup.js'
+
+import { act, render, screen } from '@testing-library/react'
+import { delay } from 'nanodelay'
+import { atom, computed, map } from 'nanostores'
+import { equal, deepStrictEqual } from 'node:assert'
+import { afterEach, test } from 'node:test'
+import type { FC } from 'react'
+import React from 'react'
+import { hydrateRoot } from 'react-dom/client'
+import { renderToString } from 'react-dom/server'
+
+import { useSelector } from '../index.js'
+
+let { createElement: h, useState } = React
+
+afterEach(() => {
+ window.document.head.innerHTML = ''
+ window.document.body.innerHTML = ''
+})
+
+test('renders a simple store', () => {
+ let $store = map({ select: 0, ignored: 1 })
+
+ let Test: FC = () => {
+ let select = useSelector($store, v => v.select)
+ return h('p', { 'data-testid': 'text' }, `Select: ${select}`)
+ }
+
+ render(h(Test))
+ equal(screen.getByTestId('text').textContent, 'Select: 0')
+})
+
+test('only triggers a re-render when selector state is updated', async () => {
+ let renders = 0
+ let $store = map({ select: 0, ignored: 1 })
+
+ let Test: FC = () => {
+ renders += 1
+ let select = useSelector($store, v => v.select)
+
+ return h(
+ 'div',
+ {},
+ h('p', { 'data-testid': 'select' }, `Select: ${select}`),
+ h('button', {
+ 'data-testid': 'btn-select',
+ 'onClick': () => {
+ $store.set({ ...$store.get(), select: 10 })
+ }
+ }),
+ h('button', {
+ 'data-testid': 'btn-ignored',
+ 'onClick': () => {
+ $store.set({ ...$store.get(), ignored: 10 })
+ }
+ })
+ )
+ }
+
+ render(h(Test))
+ equal(screen.getByTestId('select').textContent, 'Select: 0')
+ equal(renders, 1)
+
+ await act(async () => {
+ screen.getByTestId('btn-select').click()
+ await delay(1)
+ })
+
+ equal(screen.getByTestId('select').textContent, 'Select: 10')
+ equal(renders, 2)
+
+ await act(async () => {
+ screen.getByTestId('btn-ignored').click()
+ await delay(1)
+ })
+
+ equal(renders, 2)
+})
+
+test('allows specifying custom equality function', async () => {
+ let renders = 0
+ let $store = atom({
+ array: [
+ { select: 0, ignore: 1 },
+ { select: 0, ignore: 1 }
+ ]
+ })
+
+ function deepEqual(objA: unknown, objB: unknown): boolean {
+ return JSON.stringify(objA) === JSON.stringify(objB)
+ }
+
+ let Test: FC = () => {
+ renders += 1
+ let store = useSelector(
+ $store,
+ state => state.array.map(v => ({ select: v.select })),
+ { compare: deepEqual }
+ )
+
+ let value = store
+ .map(item => item.select)
+ .reduce((total, num) => total + num, 0)
+
+ return h(
+ 'div',
+ {},
+ h('p', { 'data-testid': 'select' }, `Select: ${value}`),
+ h('button', {
+ 'data-testid': 'btn-select',
+ 'onClick': () => {
+ $store.set({
+ array: $store.get().array.map(item => ({
+ ...item,
+ select: item.select + 5
+ }))
+ })
+ }
+ }),
+ h('button', {
+ 'data-testid': 'btn-ignored',
+ 'onClick': () => {
+ $store.set({
+ array: $store.get().array.map(item => ({
+ ...item,
+ ignore: item.ignore + 2
+ }))
+ })
+ }
+ })
+ )
+ }
+
+ render(h(Test))
+ equal(screen.getByTestId('select').textContent, 'Select: 0')
+ equal(renders, 1)
+
+ await act(async () => {
+ screen.getByTestId('btn-select').click()
+ await delay(1)
+ })
+
+ equal(screen.getByTestId('select').textContent, 'Select: 10')
+ equal(renders, 2)
+
+ await act(async () => {
+ screen.getByTestId('btn-ignored').click()
+ await delay(1)
+ })
+
+ equal(renders, 2)
+})
+
+test('works with computed stores', async () => {
+ let $store = atom(0)
+ let $computed = computed($store, store => ({ val: store * 2 }))
+
+ let Test: FC = () => {
+ let val = useSelector($computed, state => state.val)
+
+ return h(
+ 'div',
+ {},
+ h('p', { 'data-testid': 'computed' }, `Computed: ${val}`),
+ h('button', {
+ 'data-testid': 'btn-change',
+ 'onClick': () => {
+ $store.set($store.get() + 1)
+ }
+ })
+ )
+ }
+
+ render(h(Test))
+ equal(screen.getByTestId('computed').textContent, 'Computed: 0')
+
+ await act(async () => {
+ screen.getByTestId('btn-change').click()
+ await delay(1)
+ })
+
+ equal(screen.getByTestId('computed').textContent, 'Computed: 2')
+})
+
+test('reacts to selector function changes and updates if value changes', async () => {
+ type Value = {
+ a: string
+ b: string
+ }
+
+ let renders = 0
+ let $store = map({ a: 'valueA', b: 'valueB' })
+
+ let Test: FC = () => {
+ renders += 1
+ let [useKeyB, setUseKeyB] = useState(false)
+
+ let selector = useKeyB ? (v: Value) => v.b : (v: Value) => v.a
+ let value = useSelector($store, selector)
+
+ return h(
+ 'div',
+ {},
+ h('p', { 'data-testid': 'value' }, value),
+ h('button', {
+ onClick: () => {
+ setUseKeyB(true)
+ }
+ })
+ )
+ }
+
+ render(h(Test))
+ equal(screen.getByTestId('value').textContent, 'valueA')
+ equal(renders, 1)
+
+ await act(async () => {
+ screen.getByRole('button').click()
+ await delay(1)
+ })
+
+ equal(screen.getByTestId('value').textContent, 'valueB')
+ equal(renders, 2)
+})
+
+test('support for SSR does not break server behaviour in non-SSR projects', () => {
+ type Value = 'new' | 'old'
+ let atomStore = atom('old')
+ let mapStore = map<{ value: Value }>({ value: 'old' })
+
+ let atomValues: Uppercase[] = [] // Track values used across renders
+
+ let AtomTest: FC = () => {
+ let value = useSelector(atomStore, v => v.toUpperCase() as Uppercase)
+ atomValues.push(value)
+ return h('div', { 'data-testid': 'atom-test' }, value)
+ }
+
+ let mapValues: Uppercase[] = [] // Track values used across renders
+
+ let MapTest: FC = () => {
+ let value = useSelector(
+ mapStore,
+ v => v.value.toUpperCase(),
+ // Setting `ssr:false` should be equivalent to not setting `ssr` at all
+ { ssr: false }
+ )
+ mapValues.push(value as Uppercase)
+ return h('div', { 'data-testid': 'map-test' }, value)
+ }
+
+ let Wrapper: FC = () => {
+ return h('div', { 'data-testid': 'test' }, h(AtomTest), h(MapTest))
+ }
+
+ // Simulate store state change on server side
+ atomStore.set('new')
+ mapStore.set({ value: 'new' })
+
+ let ssrElement = document.createElement('div')
+ document.body.appendChild(ssrElement)
+ let html = renderToString(h(Wrapper))
+ ssrElement.innerHTML = html
+
+ // Confirm server rendered HTML includes the latest store data
+ equal(screen.getByTestId('atom-test').textContent, 'NEW')
+ equal(screen.getByTestId('map-test').textContent, 'NEW')
+})
+
+test('support SSR to fix client hydration errors, use initial data', t => {
+ type Value = 'new' | 'old'
+ let atomStore = atom('old')
+ let mapStore = map<{ value: Value }>({ value: 'old' })
+
+ let atomValues: Uppercase[] = []
+
+ let AtomTest: FC = () => {
+ let value = useSelector(atomStore, v => v.toUpperCase(), { ssr: 'initial' })
+ atomValues.push(value as Uppercase)
+ return h('div', { 'data-testid': 'atom-test' }, value)
+ }
+
+ let mapValues: Uppercase[] = []
+
+ let MapTest: FC = () => {
+ let value = useSelector(mapStore, v => v.value.toUpperCase(), {
+ ssr: 'initial'
+ })
+ mapValues.push(value as Uppercase)
+ return h('div', { 'data-testid': 'map-test' }, value)
+ }
+
+ let Wrapper: FC = () => {
+ return h('div', { 'data-testid': 'test' }, h(AtomTest), h(MapTest))
+ }
+
+ let ssrElement = document.createElement('div')
+ document.body.appendChild(ssrElement)
+ let html = renderToString(h(Wrapper))
+ ssrElement.innerHTML = html
+
+ equal(screen.getByTestId('atom-test').textContent, 'OLD')
+ equal(screen.getByTestId('map-test').textContent, 'OLD')
+
+ // Simulate store change on client, now different from value at "server" SSR
+ atomStore.set('new')
+ mapStore.set({ value: 'new' })
+
+ // Hydrate into SSR element. Logs errors to console on hydration failure
+ let consoleErrorMock = t.mock.method(console, 'error', () => {})
+ act(() => {
+ hydrateRoot(ssrElement, h(Wrapper))
+ })
+
+ // Check nothing was logged to `console.error()`
+ let consoleErrorCall = consoleErrorMock.mock.calls[0] as
+ | { arguments: any }
+ | undefined
+ let consoleErrorMessage = String(consoleErrorCall?.arguments?.[0] ?? '')
+ equal(consoleErrorMessage, '')
+
+ // Confirm "server" render (renderToString) 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')
+})
+
+test('support SSR to fix client hydration errors, server passes data to client', t => {
+ type Value = 'initial' | 'update on client' | 'update on server'
+ let atomStore = atom('initial')
+ let mapStore = map<{ value: Value }>({ value: 'initial' })
+
+ let ssrDataFnForAtom: typeof atomStore.get | undefined
+ let ssrDataFnForMap: typeof mapStore.get | undefined
+
+ let atomValues: Uppercase[] = [] // Track values used across renders
+
+ let AtomTest: FC = () => {
+ let value = useSelector(atomStore, v => v.toUpperCase(), {
+ ssr: ssrDataFnForAtom
+ })
+ atomValues.push(value as Uppercase)
+ return h('div', { 'data-testid': 'atom-test' }, value)
+ }
+
+ let mapValues: Uppercase[] = [] // Track values used across renders
+
+ let MapTest: FC = () => {
+ let value = useSelector(mapStore, v => v.value.toUpperCase(), {
+ ssr: ssrDataFnForMap
+ })
+ mapValues.push(value as Uppercase)
+ return h('div', { 'data-testid': 'map-test' }, value)
+ }
+
+ let Wrapper: FC = () => {
+ return h(
+ 'div',
+ { 'data-testid': 'test' },
+ h(AtomTest, null),
+ h(MapTest, null)
+ )
+ }
+
+ // Simulate store state change on server side
+ atomStore.set('update on server')
+ mapStore.set({ value: 'update on server' })
+
+ let ssrElement = document.createElement('div')
+ document.body.appendChild(ssrElement)
+ let html = renderToString(h(Wrapper))
+ ssrElement.innerHTML = html
+
+ // Confirm server render includes latest updates to server store
+ equal(screen.getByTestId('atom-test').textContent, 'UPDATE ON SERVER')
+ equal(screen.getByTestId('map-test').textContent, 'UPDATE ON SERVER')
+
+ // Simulate store change on client, now different from value at "server" SSR
+ atomStore.set('update on client')
+ mapStore.set({ value: 'update on client' })
+
+ // Simulate passing of store state data from server to client, provided to
+ // hook via `ssr` option
+ ssrDataFnForAtom = (): Value => 'update on server'
+ let serverDataForMap = { value: 'update on server' as Value }
+ ssrDataFnForMap = (): { value: Value } => serverDataForMap
+
+ // Hydrate into SSR element. Logs errors to console on hydration failure
+ let consoleErrorMock = t.mock.method(console, 'error', () => {})
+ act(() => {
+ hydrateRoot(ssrElement, h(Wrapper))
+ })
+
+ // Check nothing was logged to `console.error()`
+ let consoleErrorCall = consoleErrorMock.mock.calls[0] as
+ | { arguments: any }
+ | undefined
+ let consoleErrorMessage = String(consoleErrorCall?.arguments?.[0] ?? '')
+ equal(consoleErrorMessage, '')
+
+ // Confirm "server" render (renderToString) got latest update on server,
+ // initial client render got latest server update at hydration, then
+ // post-hydration render got latest client update
+ deepStrictEqual(atomValues, [
+ 'UPDATE ON SERVER',
+ 'UPDATE ON SERVER',
+ 'UPDATE ON CLIENT'
+ ])
+ deepStrictEqual(mapValues, [
+ 'UPDATE ON SERVER',
+ 'UPDATE ON SERVER',
+ 'UPDATE ON CLIENT'
+ ])
+
+ // Confirm final rendered version has latest updates to client store
+ equal(screen.getByTestId('atom-test').textContent, 'UPDATE ON CLIENT')
+ equal(screen.getByTestId('map-test').textContent, 'UPDATE ON CLIENT')
+})
diff --git a/test/index.test.ts b/test/use-store.test.ts
similarity index 99%
rename from test/index.test.ts
rename to test/use-store.test.ts
index 17dc937..d55457c 100644
--- a/test/index.test.ts
+++ b/test/use-store.test.ts
@@ -10,7 +10,7 @@ import React from 'react'
import { hydrateRoot } from 'react-dom/client'
import { renderToString } from 'react-dom/server'
-import { useStore } from '../index.js'
+import { useStore } from '../use-store/index.js'
let { createElement: h, useState } = React
diff --git a/use-selector/index.d.ts b/use-selector/index.d.ts
new file mode 100644
index 0000000..80428e5
--- /dev/null
+++ b/use-selector/index.d.ts
@@ -0,0 +1,37 @@
+import type { ReadableAtom } from 'nanostores'
+import type { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'
+
+type SyncExternalStoreSubscribe = Parameters<
+ typeof useSyncExternalStoreWithSelector
+>[0]
+
+export interface UseSelectorOptions {
+ /**
+ * Custom equality function (defaults to strict equality check).
+ */
+ compare?: (a: T, b: T) => boolean
+ /**
+ * Enable SSR support. Set `initial` when selector's initial value is the same on
+ * server and client, or provide a function to return the server selector state
+ * for advanced cases (per useSyncExternalStoreWithSelector's getServerSnapshot).
+ */
+ ssr?: 'initial' | (() => Snapshot) | false
+}
+
+/**
+ * React hook to subscribe to a nanostores atom with high-performance selection.
+ * Only re-renders if the selector result changes (uses strict equality by default).
+ * @param store Store to subscribe to.
+ * @param selector Function to derive data from the store state.
+ * @param options Configuration options for evaluation and server-side rendering.
+ */
+export function useSelector(
+ store: Store,
+ selector: (
+ snapshot: Store extends { get: () => infer Snapshot } ? Snapshot : undefined
+ ) => T,
+ options?: UseSelectorOptions<
+ T,
+ Store extends { get: () => infer Snapshot } ? Snapshot : undefined
+ >
+): T
diff --git a/use-selector/index.js b/use-selector/index.js
new file mode 100644
index 0000000..c10f5ee
--- /dev/null
+++ b/use-selector/index.js
@@ -0,0 +1,44 @@
+import { computed } from 'nanostores'
+import { useCallback, useMemo, useSyncExternalStore, useRef } from 'react'
+
+function defaultCompare(a, b) {
+ return a === b
+}
+
+function emit(snapshotRef, onChange, compare) {
+ return value => {
+ if (compare(snapshotRef.current, value)) return
+ snapshotRef.current = value
+ onChange()
+ }
+}
+
+export function useSelector(
+ store,
+ selector,
+ { compare = defaultCompare, ssr } = {}
+) {
+ let $computed = useMemo(() => computed(store, selector), [store, selector])
+
+ let snapshotRef = useRef()
+ snapshotRef.current = $computed.get()
+
+ let subscribe = useCallback(
+ onChange => {
+ emit(snapshotRef, onChange, compare)($computed.value)
+
+ return $computed.listen(emit(snapshotRef, onChange, compare))
+ },
+ [$computed, compare]
+ )
+
+ let get = () => snapshotRef.current
+
+ let server = get
+ if (ssr && 'init' in store) {
+ server =
+ ssr === 'initial' ? () => selector(store.init) : () => selector(ssr())
+ }
+
+ return useSyncExternalStore(subscribe, get, server)
+}
diff --git a/use-store/index.d.ts b/use-store/index.d.ts
new file mode 100644
index 0000000..da56581
--- /dev/null
+++ b/use-store/index.d.ts
@@ -0,0 +1,56 @@
+import type { Store, StoreValue } from 'nanostores'
+import type { DependencyList } from 'react'
+
+type StoreKeys = T extends { setKey: (k: infer K, v: any) => unknown }
+ ? K
+ : never
+
+export interface UseStoreOptions {
+ /**
+ * @default
+ * ```ts
+ * [store, options.keys]
+ * ```
+ */
+ deps?: DependencyList
+
+ /**
+ * Will re-render components only on specific key changes.
+ */
+ keys?: StoreKeys[]
+
+ /**
+ * Enable SSR support. Set `initial` when store's initial value is the same on
+ * server and client, or provide a function to return the server store state
+ * for advanced cases (per useSyncExternalStore's getServerSnapshot).
+ */
+ ssr?: (() => StoreValue) | 'initial' | false
+}
+
+/**
+ * Subscribe to store changes and get store’s value.
+ *
+ * Can be user with store builder too.
+ *
+ * ```js
+ * import { useStore } from 'nanostores/react'
+ *
+ * import { router } from '../store/router'
+ *
+ * export const Layout = () => {
+ * let page = useStore(router)
+ * if (page.route === 'home') {
+ * return
+ * } else {
+ * return
+ * }
+ * }
+ * ```
+ *
+ * @param store Store instance.
+ * @returns Store value.
+ */
+export function useStore(
+ store: SomeStore,
+ options?: UseStoreOptions
+): StoreValue
diff --git a/use-store/index.js b/use-store/index.js
new file mode 100644
index 0000000..652a73a
--- /dev/null
+++ b/use-store/index.js
@@ -0,0 +1,34 @@
+import { listenKeys } from 'nanostores'
+import { useCallback, useRef, useSyncExternalStore } from 'react'
+
+let emit = (snapshotRef, onChange) => value => {
+ if (snapshotRef.current === value) return
+ snapshotRef.current = value
+ onChange()
+}
+
+export function test() {
+ return 1
+}
+
+export function useStore(store, { keys, deps = [store, keys], ssr } = {}) {
+ let snapshotRef = useRef()
+ snapshotRef.current = store.get()
+
+ let subscribe = useCallback(onChange => {
+ emit(snapshotRef, onChange)(store.value)
+
+ return keys?.length > 0
+ ? listenKeys(store, keys, emit(snapshotRef, onChange))
+ : store.listen(emit(snapshotRef, onChange))
+ }, deps)
+
+ let get = () => snapshotRef.current
+
+ let server = get
+ if (ssr && 'init' in store) {
+ server = ssr === 'initial' ? () => store.init : ssr
+ }
+
+ return useSyncExternalStore(subscribe, get, server)
+}