Skip to content

Commit 1b78cc7

Browse files
committed
Add support for lazy initialization (resolves #10)
1 parent 9b15aac commit 1b78cc7

File tree

2 files changed

+83
-2
lines changed

2 files changed

+83
-2
lines changed

src/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ export type ActionByType<A, T> = A extends { type: infer T2 } ? (T extends T2 ?
2929
export default function useMethods<S, R extends MethodRecordBase<S>>(
3030
methods: Methods<S, R>,
3131
initialState: S,
32+
): StateAndCallbacksFor<typeof methods>;
33+
export default function useMethods<S, R extends MethodRecordBase<S>, I>(
34+
methods: Methods<S, R>,
35+
initializerArg: I,
36+
initializer: (arg: I) => S,
37+
): StateAndCallbacksFor<typeof methods>;
38+
export default function useMethods<S, R extends MethodRecordBase<S>, I = S>(
39+
methods: Methods<S, R>,
40+
initialState: any,
41+
initializer?: any,
3242
): StateAndCallbacksFor<typeof methods> {
3343
const reducer = useMemo<Reducer<S, ActionUnion<R>>>(
3444
() =>
@@ -37,8 +47,8 @@ export default function useMethods<S, R extends MethodRecordBase<S>>(
3747
),
3848
[methods],
3949
);
40-
const [state, dispatch] = useReducer(reducer, initialState);
41-
const actionTypes: ActionUnion<R>['type'][] = Object.keys(methods(initialState));
50+
const [state, dispatch] = useReducer(reducer, initialState, initializer);
51+
const actionTypes: ActionUnion<R>['type'][] = Object.keys(methods(state));
4252
const callbacks = useMemo(
4353
() =>
4454
actionTypes.reduce(

test/index.test.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,74 @@ it('avoids invoking methods more than necessary', () => {
120120

121121
expect(invocations).toBe(2);
122122
});
123+
124+
it('allows lazy initialization', () => {
125+
// Adapted from https://reactjs.org/docs/hooks-reference.html#lazy-initialization
126+
127+
interface State {
128+
count: number;
129+
}
130+
131+
const init = (count: number): State => ({ count });
132+
133+
const methods = (state: State) => ({
134+
increment() {
135+
state.count++;
136+
},
137+
decrement() {
138+
state.count--;
139+
},
140+
reset(newCount: number) {
141+
return init(newCount);
142+
},
143+
});
144+
145+
interface CounterProps {
146+
initialCount: number;
147+
}
148+
149+
const testId = 'counter-testid';
150+
151+
function Counter({ initialCount }: CounterProps) {
152+
const [state, { increment, decrement, reset }] = useMethods(methods, initialCount, init);
153+
return (
154+
<>
155+
Count: <span data-testid={testId}>{state.count}</span>
156+
<button onClick={increment}>+</button>
157+
<button onClick={decrement}>-</button>
158+
<button onClick={() => reset(initialCount)}>Reset</button>
159+
</>
160+
);
161+
}
162+
163+
const $ = render(<Counter initialCount={0} />);
164+
165+
const expectCount = (count: number) =>
166+
expect(Number.parseInt($.getByTestId(testId).textContent!, 10)).toBe(count);
167+
168+
expectCount(0);
169+
170+
fireEvent.click($.getByText('+'));
171+
172+
expectCount(1);
173+
174+
fireEvent.click($.getByText('+'));
175+
176+
expectCount(2);
177+
178+
fireEvent.click($.getByText(/reset/i));
179+
180+
expectCount(0);
181+
182+
fireEvent.click($.getByText('-'));
183+
184+
expectCount(-1);
185+
186+
$.rerender(<Counter initialCount={3} />);
187+
188+
expectCount(-1);
189+
190+
fireEvent.click($.getByText(/reset/i));
191+
192+
expectCount(3);
193+
});

0 commit comments

Comments
 (0)