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;
+}