@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 CHANGED
@@ -1,3 +1,90 @@
1
- # Temporary Holding Version
1
+ # @cdd-team/zustand
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/@cdd-team/zustand)](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 };
@@ -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 };
@@ -0,0 +1,2 @@
1
+ import { a as StoreApi, i as StateCreator, n as createStore, r as ExtractState, t as shallow } from "./index-DaTXGjg2.mjs";
2
+ export { type ExtractState, type StateCreator, type StoreApi, createStore, shallow };
@@ -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
- "name": "@cdd-team/zustand",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }