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
5 changes: 5 additions & 0 deletions .changeset/lucky-mangos-make.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/react-store': patch
---

Mark React hooks and context modules as client boundaries so RSC bundlers do not link them against React's server-only entry. Core Store reexports remain available to Server Components, and ordinary SSR continues using the real hooks.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ title: createStoreContext
function createStoreContext<TValue>(): object;
```

Defined in: [packages/react-store/src/createStoreContext.tsx:40](https://github.com/TanStack/store/blob/main/packages/react-store/src/createStoreContext.tsx#L40)
Defined in: [packages/react-store/src/createStoreContext.tsx:42](https://github.com/TanStack/store/blob/main/packages/react-store/src/createStoreContext.tsx#L42)

Creates a typed React context for sharing a bundle of atoms and stores with a subtree.

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/react/reference/functions/useAtom.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ title: useAtom
function useAtom<TValue>(atom, options?): [TValue, (fn) => void & (value) => void];
```

Defined in: [packages/react-store/src/useAtom.ts:16](https://github.com/TanStack/store/blob/main/packages/react-store/src/useAtom.ts#L16)
Defined in: [packages/react-store/src/useAtom.ts:18](https://github.com/TanStack/store/blob/main/packages/react-store/src/useAtom.ts#L18)

Returns the current atom value together with a stable setter.

Expand Down
4 changes: 2 additions & 2 deletions docs/framework/react/reference/functions/useCreateAtom.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ title: useCreateAtom
function useCreateAtom<T>(getValue, options?): ReadonlyAtom<T>;
```

Defined in: [packages/react-store/src/useCreateAtom.ts:17](https://github.com/TanStack/store/blob/main/packages/react-store/src/useCreateAtom.ts#L17)
Defined in: [packages/react-store/src/useCreateAtom.ts:19](https://github.com/TanStack/store/blob/main/packages/react-store/src/useCreateAtom.ts#L19)

Creates a stable atom instance for the lifetime of the component.

Expand Down Expand Up @@ -49,7 +49,7 @@ const countAtom = useCreateAtom(0)
function useCreateAtom<T>(initialValue, options?): Atom<T>;
```

Defined in: [packages/react-store/src/useCreateAtom.ts:21](https://github.com/TanStack/store/blob/main/packages/react-store/src/useCreateAtom.ts#L21)
Defined in: [packages/react-store/src/useCreateAtom.ts:23](https://github.com/TanStack/store/blob/main/packages/react-store/src/useCreateAtom.ts#L23)

Creates a stable atom instance for the lifetime of the component.

Expand Down
6 changes: 3 additions & 3 deletions docs/framework/react/reference/functions/useCreateStore.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ title: useCreateStore
function useCreateStore<T>(getValue): ReadonlyStore<T>;
```

Defined in: [packages/react-store/src/useCreateStore.ts:24](https://github.com/TanStack/store/blob/main/packages/react-store/src/useCreateStore.ts#L24)
Defined in: [packages/react-store/src/useCreateStore.ts:26](https://github.com/TanStack/store/blob/main/packages/react-store/src/useCreateStore.ts#L26)

Creates a stable store instance for the lifetime of the component.

Expand Down Expand Up @@ -45,7 +45,7 @@ const counterStore = useCreateStore({ count: 0 })
function useCreateStore<T>(initialValue): Store<T>;
```

Defined in: [packages/react-store/src/useCreateStore.ts:27](https://github.com/TanStack/store/blob/main/packages/react-store/src/useCreateStore.ts#L27)
Defined in: [packages/react-store/src/useCreateStore.ts:29](https://github.com/TanStack/store/blob/main/packages/react-store/src/useCreateStore.ts#L29)

Creates a stable store instance for the lifetime of the component.

Expand Down Expand Up @@ -81,7 +81,7 @@ const counterStore = useCreateStore({ count: 0 })
function useCreateStore<T, TActions>(initialValue, actions): Store<T, TActions>;
```

Defined in: [packages/react-store/src/useCreateStore.ts:28](https://github.com/TanStack/store/blob/main/packages/react-store/src/useCreateStore.ts#L28)
Defined in: [packages/react-store/src/useCreateStore.ts:30](https://github.com/TanStack/store/blob/main/packages/react-store/src/useCreateStore.ts#L30)

Creates a stable store instance for the lifetime of the component.

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/react/reference/functions/useSelector.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function useSelector<TSource, TSelected>(
options?): TSelected;
```

Defined in: [packages/react-store/src/useSelector.ts:58](https://github.com/TanStack/store/blob/main/packages/react-store/src/useSelector.ts#L58)
Defined in: [packages/react-store/src/useSelector.ts:60](https://github.com/TanStack/store/blob/main/packages/react-store/src/useSelector.ts#L60)

Selects a slice of state from an atom or store and subscribes the component
to that selection.
Expand Down
2 changes: 1 addition & 1 deletion docs/framework/react/reference/functions/useStore-1.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function useStore<TSource, TSelected>(
compare?): TSelected;
```

Defined in: [packages/react-store/src/useStore.ts:13](https://github.com/TanStack/store/blob/main/packages/react-store/src/useStore.ts#L13)
Defined in: [packages/react-store/src/useStore.ts:15](https://github.com/TanStack/store/blob/main/packages/react-store/src/useStore.ts#L15)

Deprecated alias for [useSelector](useSelector.md).

Expand Down
2 changes: 1 addition & 1 deletion docs/framework/react/reference/functions/useStore.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function _useStore<TState, TActions, TSelected>(
options?): [TSelected, [TActions] extends [never] ? (updater) => void : TActions];
```

Defined in: [packages/react-store/src/\_useStore.ts:24](https://github.com/TanStack/store/blob/main/packages/react-store/src/_useStore.ts#L24)
Defined in: [packages/react-store/src/\_useStore.ts:26](https://github.com/TanStack/store/blob/main/packages/react-store/src/_useStore.ts#L26)

Experimental combined read+write hook for stores, mirroring useAtom's tuple
pattern.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ id: UseSelectorOptions
title: UseSelectorOptions
---

Defined in: [packages/react-store/src/useSelector.ts:3](https://github.com/TanStack/store/blob/main/packages/react-store/src/useSelector.ts#L3)
Defined in: [packages/react-store/src/useSelector.ts:5](https://github.com/TanStack/store/blob/main/packages/react-store/src/useSelector.ts#L5)

## Type Parameters

Expand All @@ -19,7 +19,7 @@ Defined in: [packages/react-store/src/useSelector.ts:3](https://github.com/TanSt
optional compare?: (a, b) => boolean;
```

Defined in: [packages/react-store/src/useSelector.ts:4](https://github.com/TanStack/store/blob/main/packages/react-store/src/useSelector.ts#L4)
Defined in: [packages/react-store/src/useSelector.ts:6](https://github.com/TanStack/store/blob/main/packages/react-store/src/useSelector.ts#L6)

#### Parameters

Expand Down
3 changes: 3 additions & 0 deletions knip.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"workspaces": {
"packages/react-store": {
"entry": ["src/index.ts", "tests/fixtures/rsc-loader.mjs"]
},
"packages/store": {
"entry": ["src/signal.ts"]
}
Expand Down
13 changes: 12 additions & 1 deletion packages/react-store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,18 @@
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"react": "^19.2.5",
"react-dom": "^19.2.5"
"react-dom": "^19.2.5",
"react-server-dom-webpack": "19.2.5"
},
"nx": {
"targets": {
"test:lib": {
"dependsOn": [
"build",
"^build"
]
}
}
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/react-store/src/_useStore.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client'

import { useMemo } from 'react'
import { useSelector } from './useSelector'
import type { Store, StoreActionMap } from '@tanstack/store'
Expand Down
2 changes: 2 additions & 0 deletions packages/react-store/src/createStoreContext.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client'

import { createContext, useContext } from 'react'
import type { PropsWithChildren, ReactElement } from 'react'

Expand Down
2 changes: 2 additions & 0 deletions packages/react-store/src/useAtom.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client'

import { useSelector } from './useSelector'
import type { Atom } from '@tanstack/store'
import type { UseSelectorOptions } from './useSelector'
Expand Down
2 changes: 2 additions & 0 deletions packages/react-store/src/useCreateAtom.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client'

import { useState } from 'react'
import { createAtom } from '@tanstack/store'
import type { Atom, AtomOptions, ReadonlyAtom } from '@tanstack/store'
Expand Down
2 changes: 2 additions & 0 deletions packages/react-store/src/useCreateStore.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client'

import { useState } from 'react'
import { createStore } from '@tanstack/store'
import type {
Expand Down
2 changes: 2 additions & 0 deletions packages/react-store/src/useSelector.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client'

import { useRef, useSyncExternalStore } from 'react'

export interface UseSelectorOptions<TSelected> {
Expand Down
2 changes: 2 additions & 0 deletions packages/react-store/src/useStore.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client'

import { useSelector } from './useSelector'

/**
Expand Down
6 changes: 6 additions & 0 deletions packages/react-store/tests/fixtures/rsc-loader.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare module 'react-server-dom-webpack/node-loader' {
import type { LoadHook, ResolveHook } from 'node:module'

export const load: LoadHook
export const resolve: ResolveHook
}
18 changes: 18 additions & 0 deletions packages/react-store/tests/fixtures/rsc-loader.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { load as loadRsc } from 'react-server-dom-webpack/node-loader'

export { resolve } from 'react-server-dom-webpack/node-loader'

/** @type {import('node:module').LoadHook} */
export async function load(url, context, nextLoad) {
return loadRsc(url, context, async (specifier, nextContext) => {
// React also loads relative source-map URLs through this callback.
const result = await nextLoad(new URL(specifier, url).href, nextContext)

// React's loader accepts text; Node's default loader can return bytes.
if (result.format === 'module' && typeof result.source !== 'string') {
return { ...result, source: new TextDecoder().decode(result.source) }
}

return result
})
}
75 changes: 75 additions & 0 deletions packages/react-store/tests/rsc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { execFileSync } from 'node:child_process'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'

const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const assertions = `
const atom = store.createAtom(1)
const counter = store.createStore({ count: 2 })
store.batch(() => {
atom.set(3)
counter.setState(() => ({ count: 4 }))
})
assert.equal(atom.get(), 3)
assert.deepEqual(counter.get(), { count: 4 })

for (const [name, call] of [
['createStoreContext', () => store.createStoreContext()],
['useCreateAtom', () => store.useCreateAtom(1)],
['useCreateStore', () => store.useCreateStore({ count: 2 })],
['useSelector', () => store.useSelector(atom)],
['useAtom', () => store.useAtom(atom)],
['_useStore', () => store._useStore(counter, (state) => state.count)],
['useStore', () => store.useStore(atom)],
]) {
assert.throws(
call,
/Attempted to call .* from the server.*on the client/,
name,
)
}
console.log('server core exports and client boundaries verified')
`

describe('published React Server Component boundaries', () => {
it.each(['import', 'require'])(
'%s keeps core exports usable and React APIs client-only',
(format) => {
const args = ['--conditions=react-server']
let entry: string

if (format === 'import') {
args.push(
'--loader=./tests/fixtures/rsc-loader.mjs',
'--input-type=module',
)
entry = `
import assert from 'node:assert/strict'
import * as store from '@tanstack/react-store'
`
} else {
entry = `
const assert = require('node:assert/strict')
require('react-server-dom-webpack/node-register')()
const store = require('@tanstack/react-store')
`
}

const output = execFileSync(
process.execPath,
[...args, '--eval', entry + assertions],
{
cwd: packageDir,
env: { ...process.env, NODE_ENV: 'development' },
encoding: 'utf8',
timeout: 15_000,
},
)

expect(output).toContain(
'server core exports and client boundaries verified',
)
},
)
})
57 changes: 57 additions & 0 deletions packages/react-store/tests/ssr.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { renderToString } from 'react-dom/server'
import { expect, it, vi } from 'vitest'
import {
_useStore,
createAtom,
createStore,
createStoreContext,
useAtom,
useCreateAtom,
useCreateStore,
useSelector,
useStore,
} from '../src/index'

it('renders every React API during ordinary SSR without subscribing', () => {
const atom = createAtom(2)
const store = createStore({ count: 3 })
const subscribeAtom = vi.spyOn(atom, 'subscribe')
const subscribeStore = vi.spyOn(store, 'subscribe')
const { StoreProvider, useStoreContext } = createStoreContext<{
value: number
}>()

function Values() {
const createdAtom = useCreateAtom(4)
const createdStore = useCreateStore({ value: 5 })
const [atomValue] = useAtom(atom)
const selected = useSelector(store, (state) => state.count)
const alias = useStore(atom)
const [tuple] = _useStore(store, (state) => state.count)
const context = useStoreContext()

return (
<output>
{[
atomValue,
selected,
createdAtom.get(),
createdStore.get().value,
alias,
tuple,
context.value,
].join(',')}
</output>
)
}

expect(
renderToString(
<StoreProvider value={{ value: 7 }}>
<Values />
</StoreProvider>,
),
).toBe('<output>2,3,4,5,2,3,7</output>')
expect(subscribeAtom).not.toHaveBeenCalled()
expect(subscribeStore).not.toHaveBeenCalled()
})
Loading
Loading