diff --git a/CHANGELOG.md b/CHANGELOG.md index 63cfe1d..5ea9067 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- breaking: avoid action merging #32 + ## [0.3.0] - 2024-07-11 ### Added diff --git a/docs/getting-started/01_introduction.md b/docs/getting-started/01_introduction.md index 0075e05..8e722df 100644 --- a/docs/getting-started/01_introduction.md +++ b/docs/getting-started/01_introduction.md @@ -59,8 +59,6 @@ By utilizing `create` from Zustand, you can combine these slices into a single s const useCountStore = create(withSlices(countSlice, textSlice)); ``` -> 💡 **Note:** Actions with the same name across slices are merged into a single action in the combined store. Calling such an action executes the corresponding actions from each slice. For example, since both slices have a `reset` action, calling `reset` will reset both `count` and `text` to their initial values. - ### Easily utilize it in your components Finally, you can seamlessly integrate and access your store directly into your component logic utilizing the `useCountStore` hook. diff --git a/examples/01_counter/src/app.tsx b/examples/01_counter/src/app.tsx index ba3ea44..2ed887f 100644 --- a/examples/01_counter/src/app.tsx +++ b/examples/01_counter/src/app.tsx @@ -5,8 +5,8 @@ const countSlice = createSlice({ name: 'count', value: 0, actions: { - inc: () => (prev) => prev + 1, - reset: () => () => 0, + incCount: () => (prev) => prev + 1, + resetCount: () => () => 0, }, }); @@ -15,7 +15,7 @@ const textSlice = createSlice({ value: 'Hello', actions: { updateText: (newText: string) => () => newText, - reset: () => () => 'Hello', + resetText: () => () => 'Hello', }, }); @@ -24,12 +24,17 @@ const useCountStore = create(withSlices(countSlice, textSlice)); const Counter = () => { const count = useCountStore((state) => state.count); const text = useCountStore((state) => state.text); - const { inc, updateText, reset } = useCountStore.getState(); + const { incCount, resetCount, updateText, resetText } = + useCountStore.getState(); + const reset = () => { + resetCount(); + resetText(); + }; return ( <>

Count: {count} -

diff --git a/examples/02_async/src/app.tsx b/examples/02_async/src/app.tsx index 64673c1..5e25981 100644 --- a/examples/02_async/src/app.tsx +++ b/examples/02_async/src/app.tsx @@ -8,8 +8,8 @@ const countSlice = createSlice({ name: 'count', value: Promise.resolve(0), actions: { - inc: () => async (prev) => (await prev) + 1, - reset: () => async () => 0, + incCount: () => async (prev) => (await prev) + 1, + resetCount: () => async () => 0, }, }); @@ -18,7 +18,7 @@ const textSlice = createSlice({ value: 'Hello', actions: { updateText: (newText: string) => () => newText, - reset: () => () => 'Hello', + resetText: () => () => 'Hello', }, }); @@ -27,12 +27,17 @@ const useCountStore = create(withSlices(countSlice, textSlice)); const Counter = () => { const count = use(useCountStore((state) => state.count)); const text = useCountStore((state) => state.text); - const { inc, updateText, reset } = useCountStore.getState(); + const { incCount, resetCount, updateText, resetText } = + useCountStore.getState(); + const reset = () => { + resetCount(); + resetText(); + }; return ( <>

Count: {count} -

diff --git a/examples/03_actions/src/app.tsx b/examples/03_actions/src/app.tsx index 863cb7c..f7663e5 100644 --- a/examples/03_actions/src/app.tsx +++ b/examples/03_actions/src/app.tsx @@ -5,9 +5,9 @@ const countSlice = createSlice({ name: 'count', value: 0, actions: { - 'count/inc': () => (prev) => prev + 1, - 'count/set': (newCount: number) => () => newCount, - reset: () => () => 0, + incCount: () => (prev) => prev + 1, + setCount: (newCount: number) => () => newCount, + resetCount: () => () => 0, }, }); @@ -15,15 +15,19 @@ const textSlice = createSlice({ name: 'text', value: 'Hello', actions: { - 'text/set': (newText: string) => () => newText, - reset: () => () => 'Hello', + updateText: (newText: string) => () => newText, + resetText: () => () => 'Hello', }, }); const useCountStore = create( withActions(withSlices(countSlice, textSlice), { + reset: () => (state) => { + state.resetCount(); + state.resetText(); + }, setCountWithTextLength: () => (state) => { - state['count/set'](state.text.length); + state.setCount(state.text.length); }, }), ); @@ -31,17 +35,13 @@ const useCountStore = create( 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(); + const { incCount, updateText, reset, setCountWithTextLength } = + useCountStore.getState(); return ( <>

Count: {count} -

diff --git a/examples/04_immer/src/app.tsx b/examples/04_immer/src/app.tsx index 59fbdcd..a3ee13a 100644 --- a/examples/04_immer/src/app.tsx +++ b/examples/04_immer/src/app.tsx @@ -8,10 +8,10 @@ const countSlice = createSliceWithImmer({ count: 0, }, actions: { - inc: () => (state) => { + incCount: () => (state) => { state.count += 1; }, - reset: () => () => ({ count: 0 }), + resetCount: () => () => ({ count: 0 }), }, }); @@ -20,7 +20,7 @@ const textSlice = createSliceWithImmer({ value: 'Hello', actions: { updateText: (newText: string) => () => newText, - reset: () => () => 'Hello', + resetText: () => () => 'Hello', }, }); @@ -29,12 +29,17 @@ const useCountStore = create(withSlices(countSlice, textSlice)); const Counter = () => { const { count } = useCountStore((state) => state.count); const text = useCountStore((state) => state.text); - const { inc, updateText, reset } = useCountStore.getState(); + const { incCount, resetCount, updateText, resetText } = + useCountStore.getState(); + const reset = () => { + resetCount(); + resetText(); + }; return ( <>

Count: {count} -

diff --git a/src/with-slices.ts b/src/with-slices.ts index 780d6da..659cbf0 100644 --- a/src/with-slices.ts +++ b/src/with-slices.ts @@ -1,9 +1,5 @@ import type { SliceConfig } from './create-slice.js'; -type ParametersIf = T extends (...args: infer Args) => unknown - ? Args - : never; - type InferState = Configs extends [ SliceConfig, ...infer Rest, @@ -15,37 +11,17 @@ type InferState = Configs extends [ } & InferState : unknown; -type HasDuplicatedNames< - Configs, - Names extends string[] = [], -> = Configs extends [ +type HasDuplicatedNames = Configs extends [ SliceConfig, ...infer Rest, ] - ? Extract extends never - ? HasDuplicatedNames - : true - : false; - -type HasDuplicatedArgs = Configs extends [ - SliceConfig, - ...infer Rest, -] - ? { - [actionName in keyof State]: ParametersIf; - } extends { - [actionName in keyof Actions]: Parameters; - } - ? HasDuplicatedArgs + ? Extract extends never + ? HasDuplicatedNames : true : false; type ValidConfigs = - HasDuplicatedNames extends true - ? never - : HasDuplicatedArgs> extends true - ? never - : Configs; + HasDuplicatedNames extends true ? never : Configs; export function withSlices< Configs extends SliceConfig>[], @@ -63,29 +39,22 @@ export function withSlices< ) => { const state: Record = {}; type ActionFn = (...args: unknown[]) => (prev: unknown) => unknown; - const sliceMapsByAction = new Map>(); for (const config of configs) { state[config.name] = config.value; - for (const [actionName, actionFn] of Object.entries(config.actions)) { - let actionsBySlice = sliceMapsByAction.get(actionName); - if (!actionsBySlice) { - sliceMapsByAction.set(actionName, (actionsBySlice = new Map())); - } - actionsBySlice.set(config.name, actionFn as never); - } - } - for (const [actionName, actionsBySlice] of sliceMapsByAction) { - state[actionName] = (...args: unknown[]) => { - set(((prevState: Record) => { - const nextState: Record = {}; - for (const [sliceName, actionFn] of actionsBySlice) { - const prevSlice = prevState[sliceName]; + for (const [actionName, actionFn] of Object.entries( + config.actions, + )) { + state[actionName] = (...args: unknown[]) => { + set(((prevState: Record) => { + const prevSlice = prevState[config.name]; const nextSlice = actionFn(...args)(prevSlice); - nextState[sliceName] = nextSlice; - } - return nextState; - }) as never); - }; + if (Object.is(prevSlice, nextSlice)) { + return prevState; + } + return { [config.name]: nextSlice }; + }) as never); + }; + } } return state; }) as never; diff --git a/tests/01_basic.spec.tsx b/tests/01_basic.spec.tsx index 8eabf65..9b99771 100644 --- a/tests/01_basic.spec.tsx +++ b/tests/01_basic.spec.tsx @@ -24,8 +24,8 @@ test('withSlices', () => { name: 'count', value: 0, actions: { - inc: () => (prev) => prev + 1, - reset: () => () => 0, + incCount: () => (prev) => prev + 1, + resetCount: () => () => 0, }, }); const textSlice = createSlice({ @@ -33,7 +33,7 @@ test('withSlices', () => { value: 'Hello', actions: { updateText: (newText: string) => () => newText, - reset: () => () => 'Hello', + resetText: () => () => 'Hello', }, }); const combinedConfig = withSlices(countSlice, textSlice); @@ -42,7 +42,8 @@ test('withSlices', () => { const state = store.getState(); expect(state.count).toBe(countSlice.value); expect(state.text).toBe(textSlice.value); - expect(state.inc).toBeInstanceOf(Function); - expect(state.reset).toBeInstanceOf(Function); + expect(state.incCount).toBeInstanceOf(Function); + expect(state.resetCount).toBeInstanceOf(Function); expect(state.updateText).toBeInstanceOf(Function); + expect(state.resetText).toBeInstanceOf(Function); }); diff --git a/tests/02_type.spec.tsx b/tests/02_type.spec.tsx index dacb990..3a9afee 100644 --- a/tests/02_type.spec.tsx +++ b/tests/02_type.spec.tsx @@ -35,8 +35,8 @@ describe('withSlices', () => { name: 'count', value: 0, actions: { - inc: () => (prev) => prev + 1, - reset: () => () => 0, + incCount: () => (prev) => prev + 1, + resetCount: () => () => 0, }, }); @@ -45,16 +45,17 @@ describe('withSlices', () => { value: 'Hello', actions: { updateText: (newText: string) => () => newText, - reset: () => () => 'Hello', + resetText: () => () => 'Hello', }, }); type CountTextState = { count: number; - inc: () => void; + incCount: () => void; + resetCount: () => void; text: string; updateText: (newText: string) => void; - reset: () => void; + resetText: () => void; }; const slices = withSlices(countSlice, textSlice); diff --git a/tests/03_component.spec.tsx b/tests/03_component.spec.tsx index da11b40..7786c62 100644 --- a/tests/03_component.spec.tsx +++ b/tests/03_component.spec.tsx @@ -10,8 +10,8 @@ const countSlice = createSlice({ name: 'count', value: 0, actions: { - inc: () => (state) => state + 1, - reset: () => () => 0, + incCount: () => (state) => state + 1, + resetCount: () => () => 0, }, }); @@ -20,7 +20,7 @@ const textSlice = createSlice({ value: 'Hello', actions: { updateText: (text: string) => () => text, - reset: () => () => 'Hello', + resetText: () => () => 'Hello', }, }); @@ -33,11 +33,11 @@ const renderWithStoreProvider = (app: ReactNode) => const Counter = () => { const count = useSelector((state) => state.count); - const { inc } = useStoreApi().getState(); + const { incCount } = useStoreApi().getState(); return (

{count}

-
@@ -55,7 +55,11 @@ const Text = () => { }; const App = () => { - const { reset } = useStoreApi().getState(); + const { resetCount, resetText } = useStoreApi().getState(); + const reset = () => { + resetCount(); + resetText(); + }; return (
diff --git a/tests/04_componentWithActions.spec.tsx b/tests/04_componentWithActions.spec.tsx index ffdea21..55d3ffa 100644 --- a/tests/04_componentWithActions.spec.tsx +++ b/tests/04_componentWithActions.spec.tsx @@ -10,8 +10,8 @@ const countSlice = createSlice({ name: 'count', value: 0, actions: { - inc: () => (state) => state + 1, - set: (newCount: number) => () => newCount, + incCount: () => (state) => state + 1, + setCount: (newCount: number) => () => newCount, }, }); @@ -27,7 +27,7 @@ const { StoreProvider, useStoreApi, useSelector } = createZustandContext(() => create( withActions(withSlices(countSlice, textSlice), { setCountWithTextLength: () => (state) => { - state.set(state.text.length); + state.setCount(state.text.length); }, }), ), @@ -38,11 +38,11 @@ const renderWithStoreProvider = (app: ReactNode) => const Counter = () => { const count = useSelector((state) => state.count); - const { inc } = useStoreApi().getState(); + const { incCount } = useStoreApi().getState(); return (

{count}

-