Compare commits

...

7 Commits

Author SHA1 Message Date
daishi
32a3698e95 5.0.9 2025-11-30 09:59:13 +09:00
Daishi Kato
350d9ec6bc
chore(deps): update dev dependencies (#3309)
* chore(deps): update dev dependencies

* fix devtools test

* update old ts test

* update older ts test

* possible fix

* testing old ts

* move it to older ts
2025-11-30 09:45:31 +09:00
Daishi Kato
2cfb6a72d2
chore: publish.yml (#3307)
* chore: publish.yml

* Update .github/workflows/publish.yml

Co-authored-by: Danilo Britto <dbritto.dev@gmail.com>

---------

Co-authored-by: Danilo Britto <dbritto.dev@gmail.com>
2025-11-30 08:59:06 +09:00
Triumph-light
ddfc158f87
feat: add zustand playground (#3209)
* feat: add zustand playground

* use another playground

* fix styles

---------

Co-authored-by: daishi <daishi@axlight.com>
2025-11-30 08:35:40 +09:00
Daishi Kato
f99902226a
experimental: unstable ssrSafe middleware (#3308)
* unstable ssrSafe

* add todo comment
2025-11-30 07:59:05 +09:00
dependabot[bot]
82806501c4
chore(deps): bump actions/checkout from 5 to 6 (#3306)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
2025-11-29 21:58:17 +09:00
Daishi Kato
957bf89509
Update README 2025-11-25 22:44:18 +09:00
16 changed files with 941 additions and 905 deletions

View File

@ -6,7 +6,7 @@ jobs:
compressed_size:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:

View File

@ -6,7 +6,7 @@ jobs:
preview_release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:

View File

@ -4,11 +4,15 @@ on:
release:
types: [published]
permissions:
id-token: write
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
@ -18,6 +22,4 @@ jobs:
- run: pnpm install
- run: pnpm run build
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
working-directory: dist

View File

@ -15,7 +15,7 @@ jobs:
build: [cjs, esm]
env: [development] # [development, production]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
@ -41,7 +41,7 @@ jobs:
if: ${{ matrix.build == 'esm' }}
run: |
sed -i~ "s/resolve('\.\/src\(.*\)\.ts')/resolve('\.\/dist\/esm\1.mjs')/" vitest.config.mts
sed -i~ "1s/^/import.meta.env=import.meta.env||{};import.meta.env.MODE='${NODE_ENV}';/" tests/*.tsx
sed -i~ "1s/^/import.meta.env.MODE='${NODE_ENV}';/" tests/*.tsx
env:
NODE_ENV: ${{ matrix.env }}
- name: Test ${{ matrix.build }} ${{ matrix.env }}

View File

@ -22,7 +22,7 @@ jobs:
- 19.2.0-canary-0bdb9206-20250818
- 0.0.0-experimental-0bdb9206-20250818
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:

View File

@ -13,7 +13,7 @@ jobs:
fail-fast: false
matrix:
typescript:
- 5.9.2
- 5.9.3
- 5.8.3
- 5.7.3
- 5.6.3
@ -29,7 +29,7 @@ jobs:
- 4.6.4
- 4.5.5
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
@ -45,14 +45,19 @@ jobs:
run: |
sed -i~ 's/"verbatimModuleSyntax": true,//' tsconfig.json
- name: Patch for Old TS
if: ${{ matrix.typescript == '5.3.3' || matrix.typescript == '5.2.2' || matrix.typescript == '5.1.6' || matrix.typescript == '5.0.4' || matrix.typescript == '4.9.5' || matrix.typescript == '4.8.4' || matrix.typescript == '4.7.4' || matrix.typescript == '4.6.4' || matrix.typescript == '4.5.5' }}
run: |
sed -i~ 's/"moduleResolution": "bundler",/"moduleResolution": "node",/' tsconfig.json
sed -i~ 's/"allowImportingTsExtensions": true,//' tsconfig.json
sed -i~ 's/"zustand": \["\.\/src\/index\.ts"\],/"zustand": [".\/dist\/index.d.ts"],/' tsconfig.json
sed -i~ 's/"zustand\/\*": \["\.\/src\/\*\.ts"\]/"zustand\/*": [".\/dist\/*.d.ts"]/' tsconfig.json
sed -i~ 's/"include": .*/"include": ["src\/types.d.ts", "dist\/**\/*", "tests\/**\/*"],/' tsconfig.json
- name: Patch for Older TS
if: ${{ matrix.typescript == '4.7.4' || matrix.typescript == '4.6.4' || matrix.typescript == '4.5.5' }}
run: |
pnpm json -I -f package.json -e "this.resolutions={}; this.resolutions['@types/node']='18.13.0';"
pnpm add -D @types/node@18.13.0
pnpm add -D vitest@3.2.4 @vitest/coverage-v8@3.2.4 @vitest/ui@3.2.4
- name: Install old TypeScript
run: pnpm add -D typescript@${{ matrix.typescript }}
- name: Test ${{ matrix.typescript }}

View File

@ -10,7 +10,7 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:

View File

@ -8,6 +8,12 @@
[![Downloads](https://img.shields.io/npm/dt/zustand.svg?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/zustand)
[![Discord Shield](https://img.shields.io/discord/740090768164651008?style=flat&colorA=000000&colorB=000000&label=discord&logo=discord&logoColor=ffffff)](https://discord.gg/poimandres)
<a href="https://dai-shi.github.io/zustand-banner-sponsorship/sponsors/" target="_blank" rel="noopener">
<p align="center">
<img src="https://dai-shi.github.io/zustand-banner-sponsorship/api/banner.png" />
</p>
</a>
A small, fast and scalable bearbones state-management solution using simplified flux principles. Has a comfy API based on hooks, isn't boilerplatey or opinionated.
Don't disregard it because it's cute. It has quite the claws, lots of time was spent dealing with common pitfalls, like the dreaded [zombie child problem](https://react-redux.js.org/api/hooks#stale-props-and-zombie-children), [react concurrency](https://github.com/bvaughn/rfcs/blob/useMutableSource/text/0000-use-mutable-source.md), and [context loss](https://github.com/facebook/react/issues/13332) between mixed renderers. It may be the one state-manager in the React space that gets all of these right.

View File

@ -6,18 +6,17 @@ export default function Details() {
<a href="https://github.com/pmndrs/zustand">Github</a>
</nav>
<div className="bottom">
<a
href="https://github.com/pmndrs/zustand/tree/main/examples/demo"
className="bottom-right"
>
{'<Source />'}
</a>
<a
href="https://www.instagram.com/tina.henschel/"
className="bottom-left"
>
<a href="https://www.instagram.com/tina.henschel/">
Illustrations @ Tina Henschel
</a>
<div className="bottom-links">
<a href="https://github.com/pmndrs/zustand/tree/main/examples/demo">
{'<Source />'}
</a>
<a href="https://stackblitz.com/github/pmndrs/zustand/tree/main/examples/starter?file=src%2Findex.tsx">
{'<Playground />'}
</a>
</div>
</div>
<span className="header-left">Zustand</span>
</>

View File

@ -158,14 +158,26 @@ a {
flex: 0 0 auto;
}
a.bottom-left {
.bottom {
position: fixed;
bottom: 40px;
left: 40px;
right: 40px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
}
a.bottom-right {
bottom: 40px;
right: 40px;
.bottom a {
position: static;
}
.bottom-links {
display: flex;
gap: 16px;
align-items: center;
}
.snippet-container {

View File

@ -3,7 +3,7 @@
"description": "🐻 Bear necessities for state management in React",
"private": true,
"type": "commonjs",
"version": "5.0.8",
"version": "5.0.9",
"main": "./index.js",
"types": "./index.d.ts",
"typesVersions": {
@ -116,7 +116,7 @@
"homepage": "https://github.com/pmndrs/zustand",
"packageManager": "pnpm@10.18.3",
"devDependencies": {
"@eslint/js": "^9.38.0",
"@eslint/js": "^9.39.1",
"@redux-devtools/extension": "^3.3.0",
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-node-resolve": "^16.0.3",
@ -124,37 +124,37 @@
"@rollup/plugin-typescript": "12.3.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@types/node": "^24.9.2",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@types/node": "^24.10.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/use-sync-external-store": "^1.5.0",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/eslint-plugin": "^1.3.26",
"@vitest/ui": "^3.2.4",
"esbuild": "^0.25.11",
"eslint": "9.38.0",
"@vitest/coverage-v8": "^4.0.14",
"@vitest/eslint-plugin": "^1.5.0",
"@vitest/ui": "^4.0.14",
"esbuild": "^0.27.0",
"eslint": "9.39.1",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest-dom": "^5.5.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-testing-library": "^7.13.3",
"immer": "^10.2.0",
"jsdom": "^27.0.1",
"eslint-plugin-testing-library": "^7.13.5",
"immer": "^11.0.1",
"jsdom": "^27.2.0",
"json": "^11.0.0",
"prettier": "^3.6.2",
"prettier": "^3.7.2",
"react": "19.2.0",
"react-dom": "19.2.0",
"redux": "^5.0.1",
"rollup": "^4.52.5",
"rollup": "^4.53.3",
"rollup-plugin-esbuild": "^6.2.1",
"shelljs": "^0.10.0",
"shx": "^0.4.0",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.2",
"typescript-eslint": "^8.48.0",
"use-sync-external-store": "^1.6.0",
"vitest": "^3.2.4"
"vitest": "^4.0.14"
},
"peerDependencies": {
"@types/react": ">=18.0.0",

1699
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -14,3 +14,4 @@ export {
type PersistStorage,
type PersistOptions,
} from './middleware/persist.ts'
export { ssrSafe as unstable_ssrSafe } from './middleware/ssrSafe.ts'

25
src/middleware/ssrSafe.ts Normal file
View File

@ -0,0 +1,25 @@
import type { StateCreator, StoreMutatorIdentifier } from '../vanilla.ts'
// This is experimental middleware. It will be changed before finalizing it.
// https://github.com/pmndrs/zustand/discussions/2740
// TODO Not very happy with the middleware name. Will revisit it later.
export function ssrSafe<
T extends object,
U extends object,
Mps extends [StoreMutatorIdentifier, unknown][] = [],
Mcs extends [StoreMutatorIdentifier, unknown][] = [],
>(
config: StateCreator<T, Mps, Mcs, U>,
isSSR: boolean = typeof window === 'undefined',
): StateCreator<T, Mps, Mcs, U> {
return (set, get, api) => {
if (!isSSR) {
return config(set, get, api)
}
const ssrSet = () => {
throw new Error('Cannot set state of Zustand store in SSR')
}
api.setState = ssrSet
return config(ssrSet as never, get, api)
}
}

View File

@ -18,7 +18,7 @@ type TupleOfEqualLength<Arr extends unknown[], T> = number extends Arr['length']
type Connection = {
subscribers: ((message: unknown) => void)[]
api: {
subscribe: Mock<any>
subscribe: Mock<(f: (message: unknown) => void) => () => void>
unsubscribe: Mock<any>
send: Mock<any>
init: Mock<any>
@ -93,7 +93,7 @@ const extensionConnector = {
? unnamedConnections
: namedConnections
const subscribers: Connection['subscribers'] = []
const api = {
const api: Connection['api'] = {
subscribe: vi.fn((f: (m: unknown) => void) => {
subscribers.push(f)
return () => {}

View File

@ -185,14 +185,16 @@ it('state is covariant', () => {
foo: '',
}))
const _testIsCovariant: StoreApi<{ count: number }> = store
const testIsCovariant: StoreApi<{ count: number }> = store
expect(testIsCovariant).toBeDefined()
// @ts-expect-error should not compile
const _testIsNotContravariant: StoreApi<{
const testIsNotContravariant: StoreApi<{
count: number
foo: string
baz: string
}> = store
expect(testIsNotContravariant).toBeDefined()
})
it('StateCreator<T, [StoreMutatorIdentfier, unknown][]> is StateCreator<T, []>', () => {
@ -229,8 +231,9 @@ it('StateCreator subtyping', () => {
create<State>()(persist(foo(), { name: 'prefix' }))
const _testSubtyping: StateCreator<State, [['zustand/persist', unknown]]> =
const testSubtyping: StateCreator<State, [['zustand/persist', unknown]]> =
{} as StateCreator<State, []>
expect(testSubtyping).toBeDefined()
})
it('set state exists on store with readonly store', () => {