From 58a92439271b359fd833ef50cdea571d9b313246 Mon Sep 17 00:00:00 2001 From: daishi Date: Fri, 3 May 2024 22:08:51 +0900 Subject: [PATCH] feat: store actions --- examples/03_actions/package.json | 22 +++++++++ examples/03_actions/public/index.html | 8 +++ examples/03_actions/src/app.tsx | 71 +++++++++++++++++++++++++++ examples/03_actions/src/index.tsx | 13 +++++ package.json | 3 +- src/index.ts | 1 + src/with-actions.ts | 48 ++++++++++++++++++ 7 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 examples/03_actions/package.json create mode 100644 examples/03_actions/public/index.html create mode 100644 examples/03_actions/src/app.tsx create mode 100644 examples/03_actions/src/index.tsx create mode 100644 src/with-actions.ts diff --git a/examples/03_actions/package.json b/examples/03_actions/package.json new file mode 100644 index 0000000..3a958df --- /dev/null +++ b/examples/03_actions/package.json @@ -0,0 +1,22 @@ +{ + "name": "example", + "version": "0.0.0", + "private": true, + "type": "commonjs", + "dependencies": { + "@types/react": "latest", + "@types/react-dom": "latest", + "react": "latest", + "react-dom": "latest", + "react-scripts": "latest", + "typescript": "latest", + "zustand": "latest", + "zustand-slices": "latest" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + } +} diff --git a/examples/03_actions/public/index.html b/examples/03_actions/public/index.html new file mode 100644 index 0000000..ad4c782 --- /dev/null +++ b/examples/03_actions/public/index.html @@ -0,0 +1,8 @@ + + + example + + +
+ + diff --git a/examples/03_actions/src/app.tsx b/examples/03_actions/src/app.tsx new file mode 100644 index 0000000..863cb7c --- /dev/null +++ b/examples/03_actions/src/app.tsx @@ -0,0 +1,71 @@ +import { create } from 'zustand'; +import { createSlice, withSlices, withActions } from 'zustand-slices'; + +const countSlice = createSlice({ + name: 'count', + value: 0, + actions: { + 'count/inc': () => (prev) => prev + 1, + 'count/set': (newCount: number) => () => newCount, + reset: () => () => 0, + }, +}); + +const textSlice = createSlice({ + name: 'text', + value: 'Hello', + actions: { + 'text/set': (newText: string) => () => newText, + reset: () => () => 'Hello', + }, +}); + +const useCountStore = create( + withActions(withSlices(countSlice, textSlice), { + setCountWithTextLength: () => (state) => { + state['count/set'](state.text.length); + }, + }), +); + +const Counter = () => { + const count = useCountStore((state) => state.count); + const text = useCountStore((state) => state.text); + const { + 'count/inc': inc, + 'text/set': updateText, + reset, + setCountWithTextLength, + } = useCountStore.getState(); + return ( + <> +

+ Count: {count} + +

+

+ updateText(e.target.value)} /> +

+

+ +

+

+ +

+ + ); +}; + +const App = () => ( +
+ +
+); + +export default App; diff --git a/examples/03_actions/src/index.tsx b/examples/03_actions/src/index.tsx new file mode 100644 index 0000000..10774d1 --- /dev/null +++ b/examples/03_actions/src/index.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; + +import App from './app'; + +const ele = document.getElementById('app'); +if (ele) { + createRoot(ele).render( + + + , + ); +} diff --git a/package.json b/package.json index 2c22934..e54430e 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "test:types:examples": "tsc -p examples --noEmit", "test:spec": "vitest run", "examples:01_counter": "DIR=01_counter vite", - "examples:02_async": "DIR=02_async vite" + "examples:02_async": "DIR=02_async vite", + "examples:03_actions": "DIR=03_actions vite" }, "keywords": [ "react", diff --git a/src/index.ts b/src/index.ts index a087a2a..fa2f8e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export { createSlice } from './create-slice.js'; export { withSlices } from './with-slices.js'; +export { withActions } from './with-actions.js'; diff --git a/src/with-actions.ts b/src/with-actions.ts new file mode 100644 index 0000000..743e4c7 --- /dev/null +++ b/src/with-actions.ts @@ -0,0 +1,48 @@ +type InferStateActions = Actions extends { + [actionName: string]: (...args: never[]) => unknown; +} + ? { + [actionName in keyof Actions]: ( + ...args: Parameters + ) => void; + } + : unknown; + +// FIXME we should check name collisions between state and actions (help wanted) +type IsValidActions<_State, _Actions> = true; + +export function withActions< + State, + Actions extends { + [actionName: string]: (...args: never[]) => (state: State) => void; + }, +>( + config: (set: (fn: (prevState: State) => Partial) => void) => State, + actions: Actions, +): ( + set: ( + fn: ( + prevState: State & InferStateActions, + ) => Partial>, + ) => void, + get: () => State & InferStateActions, +) => IsValidActions extends true + ? State & InferStateActions + : never { + return (( + set: ( + fn: ( + prevState: State & InferStateActions, + ) => Partial>, + ) => void, + get: () => State & InferStateActions, + ) => { + const state: Record = config(set as never) as never; + for (const [actionName, actionFn] of Object.entries(actions)) { + state[actionName] = (...args: unknown[]) => { + actionFn(...(args as never[]))(get()); + }; + } + return state; + }) as never; +}