@cdd-team/zustand 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -2
- package/dist/index-DaTXGjg2.d.mts +27 -0
- package/dist/index.d.mts +20 -0
- package/dist/index.mjs +50 -0
- package/dist/vanilla-uZsAZUZ1.mjs +43 -0
- package/dist/vanilla.d.mts +2 -0
- package/dist/vanilla.mjs +1 -0
- package/package.json +52 -5
package/README.md
CHANGED
|
@@ -1,3 +1,90 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @cdd-team/zustand
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@cdd-team/zustand)
|
|
4
|
+
|
|
5
|
+
Учебный аналог [Zustand](https://github.com/pmndrs/zustand) для [`@cdd-team/react`](https://www.npmjs.com/package/@cdd-team/react). Названия и сигнатуры совпадают с Zustand 5, поэтому код переносится заменой импорта.
|
|
6
|
+
|
|
7
|
+
npm: https://www.npmjs.com/package/@cdd-team/zustand
|
|
8
|
+
|
|
9
|
+
## Установка
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bun add @cdd-team/zustand @cdd-team/react
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## React
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { create, useShallow } from "@cdd-team/zustand";
|
|
19
|
+
|
|
20
|
+
type CounterState = {
|
|
21
|
+
count: number;
|
|
22
|
+
step: number;
|
|
23
|
+
inc: () => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const useCounter = create<CounterState>()((set) => ({
|
|
27
|
+
count: 0,
|
|
28
|
+
step: 1,
|
|
29
|
+
inc: () => set((state) => ({ count: state.count + state.step })),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
function Counter() {
|
|
33
|
+
// Перерисовка только при изменении count
|
|
34
|
+
const count = useCounter((state) => state.count);
|
|
35
|
+
const inc = useCounter((state) => state.inc);
|
|
36
|
+
return <button onClick={inc}>{count}</button>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function Summary() {
|
|
40
|
+
// Селектор возвращает новый объект: сравниваем поверхностно
|
|
41
|
+
const { count, step } = useCounter(useShallow((state) => ({ count: state.count, step: state.step })));
|
|
42
|
+
return <p>{count} / {step}</p>;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Хук `useCounter` одновременно является стором: `useCounter.getState()`, `useCounter.setState(...)`, `useCounter.subscribe(...)`. Для стора, созданного через `createStore`, есть `useStore(store, selector?)`.
|
|
47
|
+
|
|
48
|
+
## Без React
|
|
49
|
+
|
|
50
|
+
Точка входа `/vanilla` не импортирует React.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { createStore } from "@cdd-team/zustand/vanilla";
|
|
54
|
+
|
|
55
|
+
const store = createStore<{ count: number }>()(() => ({ count: 0 }));
|
|
56
|
+
|
|
57
|
+
const unsubscribe = store.subscribe((state, prevState) => {
|
|
58
|
+
console.log(prevState.count, "→", state.count);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
store.setState({ count: 1 }); // поверхностное слияние
|
|
62
|
+
store.setState((state) => ({ count: state.count + 1 }));
|
|
63
|
+
store.setState(store.getInitialState(), true); // полная замена
|
|
64
|
+
unsubscribe();
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API
|
|
68
|
+
|
|
69
|
+
| Экспорт | Что делает |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `createStore(initializer)` | Стор с `getState`, `getInitialState`, `setState(partial, replace?)`, `subscribe` |
|
|
72
|
+
| `create(initializer)` | То же, плюс хук `useBoundStore(selector?)` |
|
|
73
|
+
| `useStore(store, selector?)` | Подписка компонента на произвольный стор |
|
|
74
|
+
| `useShallow(selector)` | Не перерисовывает, если результат селектора поверхностно равен прошлому |
|
|
75
|
+
| `shallow(a, b)` | Поверхностное сравнение |
|
|
76
|
+
|
|
77
|
+
## Отличия от Zustand
|
|
78
|
+
|
|
79
|
+
- Хуки построены на `useState` + `useLayoutEffect`, без `useSyncExternalStore`. В конкурентном режиме теоретически возможен «tearing».
|
|
80
|
+
- Нет middleware (`persist`, `devtools`, `immer`) и сторов через Context.
|
|
81
|
+
|
|
82
|
+
## Разработка
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
bun install
|
|
86
|
+
bun run test # vanilla без DOM + хуки на happy-dom
|
|
87
|
+
bun run typecheck
|
|
88
|
+
bun run lint
|
|
89
|
+
bun run build
|
|
90
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/vanilla/types.d.ts
|
|
2
|
+
type SetState<T> = {
|
|
3
|
+
(partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: false): void;
|
|
4
|
+
(state: T | ((state: T) => T), replace: true): void;
|
|
5
|
+
};
|
|
6
|
+
interface StoreApi<T> {
|
|
7
|
+
setState: SetState<T>;
|
|
8
|
+
getState: () => T;
|
|
9
|
+
getInitialState: () => T;
|
|
10
|
+
subscribe: (listener: (state: T, prevState: T) => void) => () => void;
|
|
11
|
+
}
|
|
12
|
+
type ExtractState<S> = S extends {
|
|
13
|
+
getState: () => infer T;
|
|
14
|
+
} ? T : never;
|
|
15
|
+
type StateCreator<T> = (setState: StoreApi<T>["setState"], getState: StoreApi<T>["getState"], store: StoreApi<T>) => T;
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/vanilla/create-store.d.ts
|
|
18
|
+
type CreateStore = {
|
|
19
|
+
<T>(initializer: StateCreator<T>): StoreApi<T>;
|
|
20
|
+
<T>(): (initializer: StateCreator<T>) => StoreApi<T>;
|
|
21
|
+
};
|
|
22
|
+
declare const createStore: CreateStore;
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/vanilla/shallow.d.ts
|
|
25
|
+
declare function shallow<T>(a: T, b: T): boolean;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { StoreApi as a, StateCreator as i, createStore as n, ExtractState as r, shallow as t };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { a as StoreApi, i as StateCreator, n as createStore, r as ExtractState, t as shallow } from "./index-DaTXGjg2.mjs";
|
|
2
|
+
//#region src/react/create.d.ts
|
|
3
|
+
type UseBoundStore<T> = {
|
|
4
|
+
(): T;
|
|
5
|
+
<U>(selector: (state: T) => U): U;
|
|
6
|
+
} & StoreApi<T>;
|
|
7
|
+
type Create = {
|
|
8
|
+
<T>(initializer: StateCreator<T>): UseBoundStore<T>;
|
|
9
|
+
<T>(): (initializer: StateCreator<T>) => UseBoundStore<T>;
|
|
10
|
+
};
|
|
11
|
+
export declare const create: Create;
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/react/use-shallow.d.ts
|
|
14
|
+
export declare function useShallow<S, U>(selector: (state: S) => U): (state: S) => U;
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/react/use-store.d.ts
|
|
17
|
+
export declare function useStore<T>(api: StoreApi<T>): T;
|
|
18
|
+
export declare function useStore<T, U>(api: StoreApi<T>, selector: (state: T) => U): U;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { type ExtractState, type StateCreator, type StoreApi, type UseBoundStore, createStore, shallow };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { n as createStore, t as shallow } from "./vanilla-uZsAZUZ1.mjs";
|
|
2
|
+
import { useLayoutEffect, useMemo, useState } from "@cdd-team/react";
|
|
3
|
+
//#region src/react/use-store.ts
|
|
4
|
+
const identity = (value) => value;
|
|
5
|
+
function useStore(api, selector = identity) {
|
|
6
|
+
const state = api.getState();
|
|
7
|
+
const slice = selector(state);
|
|
8
|
+
const [, forceRender] = useState(0);
|
|
9
|
+
useLayoutEffect(() => {
|
|
10
|
+
const check = () => {
|
|
11
|
+
const nextState = api.getState();
|
|
12
|
+
if (Object.is(nextState, state)) return;
|
|
13
|
+
if (Object.is(selector(nextState), slice)) return;
|
|
14
|
+
forceRender((count) => count + 1);
|
|
15
|
+
};
|
|
16
|
+
const unsubscribe = api.subscribe(check);
|
|
17
|
+
check();
|
|
18
|
+
return unsubscribe;
|
|
19
|
+
}, [
|
|
20
|
+
api,
|
|
21
|
+
state,
|
|
22
|
+
selector,
|
|
23
|
+
slice
|
|
24
|
+
]);
|
|
25
|
+
return slice;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/react/create.ts
|
|
29
|
+
const createImpl = (createState) => {
|
|
30
|
+
const api = createStore(createState);
|
|
31
|
+
const useBoundStore = (selector) => useStore(api, selector);
|
|
32
|
+
return Object.assign(useBoundStore, api);
|
|
33
|
+
};
|
|
34
|
+
const create = ((createState) => createState ? createImpl(createState) : createImpl);
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/react/use-shallow.ts
|
|
37
|
+
function createShallowMemo() {
|
|
38
|
+
let previous;
|
|
39
|
+
return (next) => {
|
|
40
|
+
if (shallow(previous, next)) return previous;
|
|
41
|
+
previous = next;
|
|
42
|
+
return next;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function useShallow(selector) {
|
|
46
|
+
const memo = useMemo(() => createShallowMemo(), []);
|
|
47
|
+
return (state) => memo(selector(state));
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { create, createStore, shallow, useShallow, useStore };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/vanilla/create-store.ts
|
|
2
|
+
const createStoreImpl = (createState) => {
|
|
3
|
+
let state;
|
|
4
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
5
|
+
const setState = (partial, replace) => {
|
|
6
|
+
const nextState = typeof partial === "function" ? partial(state) : partial;
|
|
7
|
+
if (Object.is(nextState, state)) return;
|
|
8
|
+
const previousState = state;
|
|
9
|
+
state = replace ?? (typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
|
|
10
|
+
listeners.forEach((listener) => listener(state, previousState));
|
|
11
|
+
};
|
|
12
|
+
const getState = () => state;
|
|
13
|
+
const getInitialState = () => initialState;
|
|
14
|
+
const subscribe = (listener) => {
|
|
15
|
+
listeners.add(listener);
|
|
16
|
+
return () => {
|
|
17
|
+
listeners.delete(listener);
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
const api = {
|
|
21
|
+
setState,
|
|
22
|
+
getState,
|
|
23
|
+
getInitialState,
|
|
24
|
+
subscribe
|
|
25
|
+
};
|
|
26
|
+
const initialState = state = createState(setState, getState, api);
|
|
27
|
+
return api;
|
|
28
|
+
};
|
|
29
|
+
const createStore = ((createState) => createState ? createStoreImpl(createState) : createStoreImpl);
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/vanilla/shallow.ts
|
|
32
|
+
function shallow(a, b) {
|
|
33
|
+
if (Object.is(a, b)) return true;
|
|
34
|
+
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false;
|
|
35
|
+
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
|
|
36
|
+
const keysA = Object.keys(a);
|
|
37
|
+
if (keysA.length !== Object.keys(b).length) return false;
|
|
38
|
+
const recordA = a;
|
|
39
|
+
const recordB = b;
|
|
40
|
+
return keysA.every((key) => Object.hasOwn(recordB, key) && Object.is(recordA[key], recordB[key]));
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { createStore as n, shallow as t };
|
package/dist/vanilla.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { n as createStore, t as shallow } from "./vanilla-uZsAZUZ1.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@cdd-team/zustand",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Учебный стейт-менеджер",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Cringe-Driven-Development-Team/frontend-packages.git",
|
|
9
|
+
"directory": "packages/zustand"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"engines": {
|
|
14
|
+
"bun": ">=1.4.0"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.mts",
|
|
19
|
+
"import": "./dist/index.mjs"
|
|
20
|
+
},
|
|
21
|
+
"./vanilla": {
|
|
22
|
+
"types": "./dist/vanilla.d.mts",
|
|
23
|
+
"import": "./dist/vanilla.mjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"lint": "oxlint -c ../../oxlint.config.ts src test",
|
|
31
|
+
"fmt:check": "oxfmt -c ../../oxfmt.config.ts --check src test",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"test": "bun run test:vanilla && bun run test:react",
|
|
34
|
+
"test:vanilla": "bun test test/vanilla",
|
|
35
|
+
"test:react": "bun test --preload ./test/setup-dom.ts test/react",
|
|
36
|
+
"build": "tsdown"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@cdd-team/react": "^1.2.1"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"@cdd-team/react": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@cdd-team/react": "^1.2.1",
|
|
48
|
+
"@happy-dom/global-registrator": "^20.14.5"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|