Skip to content

Commit

Permalink
breaking: avoid action merging (#32)
Browse files Browse the repository at this point in the history
* feat: avoid action merging

* revert #30

* support bail-out

* update CHANGELOG
  • Loading branch information
dai-shi authored Oct 11, 2024
1 parent 07b8fe7 commit ce66041
Show file tree
Hide file tree
Showing 11 changed files with 91 additions and 99 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

- breaking: avoid action merging #32

## [0.3.0] - 2024-07-11

### Added
Expand Down
2 changes: 0 additions & 2 deletions docs/getting-started/01_introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 10 additions & 5 deletions examples/01_counter/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ const countSlice = createSlice({
name: 'count',
value: 0,
actions: {
inc: () => (prev) => prev + 1,
reset: () => () => 0,
incCount: () => (prev) => prev + 1,
resetCount: () => () => 0,
},
});

Expand All @@ -15,7 +15,7 @@ const textSlice = createSlice({
value: 'Hello',
actions: {
updateText: (newText: string) => () => newText,
reset: () => () => 'Hello',
resetText: () => () => 'Hello',
},
});

Expand All @@ -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 (
<>
<p>
Count: {count}
<button type="button" onClick={inc}>
<button type="button" onClick={incCount}>
+1
</button>
</p>
Expand Down
15 changes: 10 additions & 5 deletions examples/02_async/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});

Expand All @@ -18,7 +18,7 @@ const textSlice = createSlice({
value: 'Hello',
actions: {
updateText: (newText: string) => () => newText,
reset: () => () => 'Hello',
resetText: () => () => 'Hello',
},
});

Expand All @@ -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 (
<>
<p>
Count: {count}
<button type="button" onClick={inc}>
<button type="button" onClick={incCount}>
+1
</button>
</p>
Expand Down
26 changes: 13 additions & 13 deletions examples/03_actions/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,43 @@ 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,
},
});

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);
},
}),
);

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 (
<>
<p>
Count: {count}
<button type="button" onClick={inc}>
<button type="button" onClick={incCount}>
+1
</button>
</p>
Expand Down
15 changes: 10 additions & 5 deletions examples/04_immer/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const countSlice = createSliceWithImmer({
count: 0,
},
actions: {
inc: () => (state) => {
incCount: () => (state) => {
state.count += 1;
},
reset: () => () => ({ count: 0 }),
resetCount: () => () => ({ count: 0 }),
},
});

Expand All @@ -20,7 +20,7 @@ const textSlice = createSliceWithImmer({
value: 'Hello',
actions: {
updateText: (newText: string) => () => newText,
reset: () => () => 'Hello',
resetText: () => () => 'Hello',
},
});

Expand All @@ -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 (
<>
<p>
Count: {count}
<button type="button" onClick={inc}>
<button type="button" onClick={incCount}>
+1
</button>
</p>
Expand Down
65 changes: 17 additions & 48 deletions src/with-slices.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import type { SliceConfig } from './create-slice.js';

type ParametersIf<T> = T extends (...args: infer Args) => unknown
? Args
: never;

type InferState<Configs> = Configs extends [
SliceConfig<infer Name, infer Value, infer Actions>,
...infer Rest,
Expand All @@ -15,37 +11,17 @@ type InferState<Configs> = Configs extends [
} & InferState<Rest>
: unknown;

type HasDuplicatedNames<
Configs,
Names extends string[] = [],
> = Configs extends [
type HasDuplicatedNames<Configs, Names = never> = Configs extends [
SliceConfig<infer Name, infer _Value, infer Actions>,
...infer Rest,
]
? Extract<Name | keyof Actions, Names[number]> extends never
? HasDuplicatedNames<Rest, [Name, ...Names]>
: true
: false;

type HasDuplicatedArgs<Configs, State> = Configs extends [
SliceConfig<infer _Name, infer _Value, infer Actions>,
...infer Rest,
]
? {
[actionName in keyof State]: ParametersIf<State[actionName]>;
} extends {
[actionName in keyof Actions]: Parameters<Actions[actionName]>;
}
? HasDuplicatedArgs<Rest, State>
? Extract<Name | keyof Actions, Names> extends never
? HasDuplicatedNames<Rest, Name | keyof Actions | Names>
: true
: false;

type ValidConfigs<Configs> =
HasDuplicatedNames<Configs> extends true
? never
: HasDuplicatedArgs<Configs, InferState<Configs>> extends true
? never
: Configs;
HasDuplicatedNames<Configs> extends true ? never : Configs;

export function withSlices<
Configs extends SliceConfig<string, unknown, NonNullable<unknown>>[],
Expand All @@ -63,29 +39,22 @@ export function withSlices<
) => {
const state: Record<string, unknown> = {};
type ActionFn = (...args: unknown[]) => (prev: unknown) => unknown;
const sliceMapsByAction = new Map<string, Map<string, ActionFn>>();
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<string, unknown>) => {
const nextState: Record<string, unknown> = {};
for (const [sliceName, actionFn] of actionsBySlice) {
const prevSlice = prevState[sliceName];
for (const [actionName, actionFn] of Object.entries<ActionFn>(
config.actions,
)) {
state[actionName] = (...args: unknown[]) => {
set(((prevState: Record<string, unknown>) => {
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;
Expand Down
11 changes: 6 additions & 5 deletions tests/01_basic.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@ test('withSlices', () => {
name: 'count',
value: 0,
actions: {
inc: () => (prev) => prev + 1,
reset: () => () => 0,
incCount: () => (prev) => prev + 1,
resetCount: () => () => 0,
},
});
const textSlice = createSlice({
name: 'text',
value: 'Hello',
actions: {
updateText: (newText: string) => () => newText,
reset: () => () => 'Hello',
resetText: () => () => 'Hello',
},
});
const combinedConfig = withSlices(countSlice, textSlice);
Expand All @@ -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);
});
11 changes: 6 additions & 5 deletions tests/02_type.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ describe('withSlices', () => {
name: 'count',
value: 0,
actions: {
inc: () => (prev) => prev + 1,
reset: () => () => 0,
incCount: () => (prev) => prev + 1,
resetCount: () => () => 0,
},
});

Expand All @@ -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);
Expand Down
Loading

0 comments on commit ce66041

Please sign in to comment.