Skip to content

Commit

Permalink
feat: create slice with immer (#24)
Browse files Browse the repository at this point in the history
* create slice with immer

* update pnpm-lock.yaml

* Support return type for both immer and non-immer slices in with-slices index signature

* Update test for create-slice-with-immer

* test immer slice action wrapping by produce

* Support immer's produce returning arbitrary data that doesn't modify draft as per official docs

* do not change with-slices type, and refactor

* prefer Object.fromEntries, and fix types with Draft

* split createSliceWithImmer test

* change void to unknown in InferArgs type

* further test on createSliceWithImmer actions

* run prettier

* update CHANGELOG

---------

Co-authored-by: daishi <daishi@axlight.com>
Ebubekir-Tas and dai-shi authored Jul 10, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent e32ef94 commit 29f11af
Showing 7 changed files with 129 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -2,6 +2,10 @@

## [Unreleased]

### Added

- feat: create slice with immer #24

## [0.2.0] - 2024-05-04

### Added
16 changes: 16 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -22,6 +22,16 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./immer": {
"require": {
"types": "./dist/cjs/immer/index.d.ts",
"default": "./dist/cjs/immer/index.js"
},
"default": {
"types": "./dist/immer/index.d.ts",
"default": "./dist/immer/index.js"
}
}
},
"sideEffects": false,
@@ -81,7 +91,13 @@
"zustand-slices": "link:."
},
"peerDependencies": {
"immer": ">=9.0.6",
"react": ">=18.0.0",
"zustand": ">=4.0.0"
},
"peerDependenciesMeta": {
"immer": {
"optional": true
}
}
}
13 changes: 11 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions src/immer/create-slice-with-immer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { produce } from 'immer';
import type { Draft } from 'immer';

// Utility type to infer the argument types of the actions
type InferArgs<T> = T extends (...args: infer A) => unknown ? A : never;

type SliceActions<Value> = {
[actionName: string]: (
...args: never[]
) => (draft: Draft<Value>) => Draft<Value> | void;
};

export type SliceConfig<
Name extends string,
Value,
Actions extends SliceActions<Value>,
> = {
name: Name;
value: Value;
actions: Actions;
};

type ImmerActions<Value, Actions extends SliceActions<Value>> = {
[K in keyof Actions]: (
...args: InferArgs<Actions[K]>
) => (prev: Value) => Value;
};

export function createSliceWithImmer<
Name extends string,
Value,
Actions extends SliceActions<Value>,
>(config: SliceConfig<Name, Value, Actions>) {
const immerActions = Object.fromEntries(
Object.entries(config.actions).map(([actionKey, action]) => [
actionKey,
(...args: InferArgs<typeof action>) =>
(prev: Value) =>
produce(prev, (draft) => action(...args)(draft)),
]),
) as unknown as ImmerActions<Value, Actions>;
return {
...config,
actions: immerActions,
};
}
1 change: 1 addition & 0 deletions src/immer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createSliceWithImmer } from './create-slice-with-immer.js';
49 changes: 49 additions & 0 deletions tests/05_createSliceWithImmer.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { expect, test } from 'vitest';
import { createSliceWithImmer } from 'zustand-slices/immer';

test('should export functions', () => {
expect(createSliceWithImmer).toBeDefined();
});

test('createSliceWithImmer', () => {
const immerSlice = createSliceWithImmer({
name: 'counter',
value: {
count: 0,
text: 'First',
},
actions: {
increment: () => (prev) => {
prev.count += 1;
},
setText: (payload: { newText: string }) => (prev) => {
prev.text = payload.newText;
},
reset: () => () => {
return {
count: 0,
text: 'First',
};
},
},
});
const result = createSliceWithImmer(immerSlice);

// should not be equal as createSliceWithImmer should wrap actions in `produce`
expect(result.actions.increment).not.toBe(immerSlice.actions.increment);

expect(result.name).toEqual(immerSlice.name);
expect(result.value).toEqual(immerSlice.value);

expect(typeof result.actions.increment).toBe('function');

const incrementedState = result.actions.increment()(result.value);
const newTextState = result.actions.setText({ newText: 'Second' })(
result.value,
);
const resetState = result.actions.reset()(result.value);

expect(incrementedState.count).toBe(1);
expect(newTextState.text).toBe('Second');
expect(resetState).toEqual({ count: 0, text: 'First' });
});
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -12,7 +12,8 @@
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"zustand-slices": ["./src/index.js"]
"zustand-slices": ["./src/index.js"],
"zustand-slices/immer": ["./src/immer/index.js"]
}
},
"exclude": ["dist", "examples"]

0 comments on commit 29f11af

Please sign in to comment.