Skip to content
This repository was archived by the owner on Apr 3, 2024. It is now read-only.

Commit 8332fb9

Browse files
committed
Add support for initial effects
1 parent bee8583 commit 8332fb9

File tree

2 files changed

+148
-27
lines changed

2 files changed

+148
-27
lines changed

src/index.tsx

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
1-
import { useReducer, useEffect, useCallback, useRef } from 'react';
1+
import { useReducer, useEffect, useCallback, useRef, useMemo } from 'react';
22

33
type CleanupFunction = () => void;
44

5-
export type EffectFunction<TState, TEvent> = (
5+
export type EffectFunction<
6+
TState,
7+
TEvent extends EventObject,
8+
TEffect extends EffectObject<TState, TEvent>
9+
> = (
610
state: TState,
7-
effect: EffectObject<TState, TEvent>,
11+
effect: TEffect,
812
dispatch: React.Dispatch<TEvent>
913
) => CleanupFunction | void;
1014

11-
export interface EffectObject<TState, TEvent> {
15+
export interface EffectObject<TState, TEvent extends EventObject> {
1216
[key: string]: any;
1317
type: string;
14-
exec?: EffectFunction<TState, TEvent>;
18+
exec?: EffectFunction<TState, TEvent, any>;
1519
}
1620

1721
export type Effect<
1822
TState,
19-
TEvent,
23+
TEvent extends EventObject,
2024
TEffect extends EffectObject<TState, TEvent>
21-
> = TEffect | EffectFunction<TState, TEvent>;
25+
> = TEffect | EffectFunction<TState, TEvent, TEffect>;
2226

2327
type EntityTuple<TState, TEvent extends EventObject> = [
2428
TState,
@@ -81,14 +85,14 @@ export interface EffectReducerExec<
8185
TEvent extends EventObject,
8286
TEffect extends EffectObject<TState, TEvent>
8387
> {
84-
(effect: TEffect | EffectFunction<TState, TEvent>): EffectEntity<
88+
(effect: TEffect | EffectFunction<TState, TEvent, TEffect>): EffectEntity<
8589
TState,
8690
TEvent
8791
>;
8892
stop: (entity: EffectEntity<TState, TEvent>) => void;
8993
replace: (
9094
entity: EffectEntity<TState, TEvent> | undefined,
91-
effect: TEffect | EffectFunction<TState, TEvent>
95+
effect: TEffect | EffectFunction<TState, TEvent, TEffect>
9296
) => EffectEntity<TState, TEvent>;
9397
}
9498

@@ -110,18 +114,26 @@ interface FlushEvent {
110114
count: number;
111115
}
112116

113-
export function toEffect<TState, TEvent>(
114-
exec: EffectFunction<TState, TEvent>
117+
export function toEffect<TState, TEvent extends EventObject>(
118+
exec: EffectFunction<TState, TEvent, any>
115119
): Effect<TState, TEvent, any> {
116120
return {
117121
type: exec.name,
118122
exec,
119123
};
120124
}
121125

122-
interface EffectsMap<TState, TEvent> {
123-
[key: string]: EffectFunction<TState, TEvent>;
124-
}
126+
export type EffectsMap<
127+
TState,
128+
TEvent extends EventObject,
129+
TEffect extends EffectObject<TState, TEvent>
130+
> = {
131+
[key in TEffect['type']]: EffectFunction<
132+
TState,
133+
TEvent,
134+
TEffect & { type: key }
135+
>;
136+
};
125137

126138
const toEventObject = <TEvent extends EventObject>(
127139
event: TEvent['type'] | TEvent
@@ -138,26 +150,35 @@ const toEffectObject = <
138150
TEvent extends EventObject,
139151
TEffect extends EffectObject<TState, TEvent>
140152
>(
141-
effect: TEffect | EffectFunction<TState, TEvent>,
142-
effectsMap?: EffectsMap<TState, TEvent>
153+
effect: TEffect | EffectFunction<TState, TEvent, TEffect>,
154+
effectsMap?: EffectsMap<TState, TEvent, TEffect>
143155
): TEffect => {
144156
const type = typeof effect === 'function' ? effect.name : effect.type;
145-
const customExec = effectsMap ? effectsMap[type] : undefined;
157+
const customExec = effectsMap
158+
? effectsMap[type as TEffect['type']]
159+
: undefined;
146160
const exec =
147161
customExec || (typeof effect === 'function' ? effect : effect.exec);
148162
const other = typeof effect === 'function' ? {} : effect;
149163

150164
return { ...other, type, exec } as TEffect;
151165
};
152166

167+
export type InitialEffectStateGetter<
168+
TState,
169+
TEffect extends EffectObject<TState, any>
170+
> = (
171+
exec: (effect: TEffect | EffectFunction<TState, any, TEffect>) => void
172+
) => TState;
173+
153174
export function useEffectReducer<
154175
TState,
155176
TEvent extends EventObject,
156177
TEffect extends EffectObject<TState, TEvent> = EffectObject<TState, TEvent>
157178
>(
158179
effectReducer: EffectReducer<TState, TEvent, TEffect>,
159-
initialState: TState,
160-
effectsMap?: EffectsMap<TState, TEvent>
180+
initialState: TState | InitialEffectStateGetter<TState, TEffect>,
181+
effectsMap?: EffectsMap<TState, TEvent, TEffect>
161182
): [TState, React.Dispatch<TEvent | TEvent['type']>] {
162183
const entitiesRef = useRef<Set<EffectEntity<TState, TEvent>>>(new Set());
163184
const wrappedReducer = (
@@ -175,7 +196,9 @@ export function useEffectReducer<
175196
return [state, stateEffectTuples.slice(event.count), nextEntitiesToStop];
176197
}
177198

178-
const exec = (effect: TEffect | EffectFunction<TState, TEvent>) => {
199+
const exec = (
200+
effect: TEffect | EffectFunction<TState, TEvent, TEffect>
201+
) => {
179202
const effectObject = toEffectObject(effect, effectsMap);
180203
const effectEntity = createEffectEntity<TState, TEvent, TEffect>(
181204
effectObject
@@ -191,7 +214,7 @@ export function useEffectReducer<
191214

192215
exec.replace = (
193216
entity: EffectEntity<TState, TEvent>,
194-
effect: TEffect | EffectFunction<TState, TEvent>
217+
effect: TEffect | EffectFunction<TState, TEvent, TEffect>
195218
) => {
196219
if (entity) {
197220
nextEntitiesToStop.push(entity);
@@ -216,10 +239,39 @@ export function useEffectReducer<
216239
];
217240
};
218241

242+
const initialStateAndEffects: AggregatedEffectsState<
243+
TState,
244+
TEvent
245+
> = useMemo(() => {
246+
if (typeof initialState === 'function') {
247+
const initialEffectEntities: Array<EffectEntity<TState, TEvent>> = [];
248+
249+
const resolvedInitialState = (initialState as InitialEffectStateGetter<
250+
TState,
251+
TEffect
252+
>)(effect => {
253+
const effectObject = toEffectObject(effect, effectsMap);
254+
const effectEntity = createEffectEntity<TState, TEvent, TEffect>(
255+
effectObject
256+
);
257+
258+
initialEffectEntities.push(effectEntity);
259+
});
260+
261+
return [
262+
resolvedInitialState,
263+
[[resolvedInitialState, initialEffectEntities]],
264+
[],
265+
];
266+
}
267+
268+
return [initialState, [], []];
269+
}, []);
270+
219271
const [
220272
[state, effectStateEntityTuples, entitiesToStop],
221273
dispatch,
222-
] = useReducer(wrappedReducer, [initialState, [], []]);
274+
] = useReducer(wrappedReducer, initialStateAndEffects);
223275

224276
const wrappedDispatch = useCallback((event: TEvent | TEvent['type']) => {
225277
dispatch(toEventObject(event));

test/useEffectReducer.test.tsx

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { useEffect } from 'react';
33

44
import { render, cleanup, fireEvent, waitFor } from '@testing-library/react';
55

6-
import { useEffectReducer, EffectReducer, EffectEntity } from '../src';
6+
import {
7+
useEffectReducer,
8+
EffectReducer,
9+
EffectEntity,
10+
InitialEffectStateGetter,
11+
} from '../src';
712

813
// have to add this because someone made a breaking change somewhere...
914
class MutationObserver {
@@ -80,7 +85,7 @@ describe('useEffectReducer', () => {
8085
setTimeout(() => {
8186
dispatch({
8287
type: 'RESOLVE',
83-
data: effect.user,
88+
data: { name: effect.user },
8489
});
8590
}, 100);
8691
},
@@ -92,7 +97,7 @@ describe('useEffectReducer', () => {
9297
onClick={() => dispatch({ type: 'FETCH', user: '42' })}
9398
data-testid="result"
9499
>
95-
{state.user ? state.user : '--'}
100+
{state.user ? state.user.name : '--'}
96101
</div>
97102
);
98103
};
@@ -175,7 +180,7 @@ describe('useEffectReducer', () => {
175180
setTimeout(() => {
176181
_dispatch({
177182
type: 'RESOLVE',
178-
data: effect.user,
183+
data: { name: effect.user },
179184
});
180185
}, 100);
181186
},
@@ -187,7 +192,7 @@ describe('useEffectReducer', () => {
187192
onClick={() => dispatch({ type: 'FETCH', user: '42' })}
188193
data-testid="result"
189194
>
190-
{state.user ? state.user : '--'}
195+
{state.user ? state.user.name : '--'}
191196
</div>
192197
);
193198
};
@@ -514,4 +519,68 @@ describe('useEffectReducer', () => {
514519
expect(delayedResults).toEqual(['goodbye']);
515520
});
516521
});
522+
523+
it('should allow for initial effects', async () => {
524+
interface FetchState {
525+
data: null | string;
526+
}
527+
528+
type FetchEvent = {
529+
type: 'RESOLVE';
530+
data: string;
531+
};
532+
533+
type FetchEffects = {
534+
type: 'fetchData';
535+
data: string;
536+
};
537+
538+
const fetchReducer: EffectReducer<FetchState, FetchEvent, FetchEffects> = (
539+
state,
540+
event
541+
) => {
542+
if (event.type === 'RESOLVE') {
543+
return {
544+
...state,
545+
data: event.data,
546+
};
547+
}
548+
549+
return state;
550+
};
551+
552+
const getInitialState: InitialEffectStateGetter<
553+
FetchState,
554+
FetchEffects
555+
> = exec => {
556+
exec({ type: 'fetchData', data: 'secret' });
557+
558+
return { data: null };
559+
};
560+
561+
const App = () => {
562+
const [state, dispatch] = useEffectReducer(
563+
fetchReducer,
564+
getInitialState,
565+
{
566+
fetchData(_, { data }) {
567+
setTimeout(() => {
568+
dispatch({ type: 'RESOLVE', data: data.toUpperCase() });
569+
}, 20);
570+
},
571+
}
572+
);
573+
574+
return <div data-testid="result">{state.data || '--'}</div>;
575+
};
576+
577+
const { getByTestId } = render(<App />);
578+
const result = getByTestId('result');
579+
580+
expect(result.textContent).toEqual('--');
581+
582+
await waitFor(() => {
583+
expect(result.textContent).toEqual('SECRET');
584+
});
585+
});
517586
});

0 commit comments

Comments
 (0)