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

Commit 8c75e99

Browse files
authored
Merge pull request #10 from davidkpiano/davidkpiano/initial-effects
2 parents bee8583 + b1c53b2 commit 8c75e99

File tree

4 files changed

+245
-27
lines changed

4 files changed

+245
-27
lines changed

.changeset/three-chefs-play.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
'use-effect-reducer': minor
3+
---
4+
5+
Added support for initial effects, via the 2nd argument to `useEffectReducer`, which can either be a static `initialState` or a function that takes in `exec` and returns an initial state as well as executing initial effects:
6+
7+
```js
8+
const fetchReducer = (state, event) => {
9+
if (event.type === 'RESOLVE') {
10+
return {
11+
...state,
12+
data: event.data,
13+
};
14+
}
15+
16+
return state;
17+
};
18+
19+
const getInitialState = exec => {
20+
exec({ type: 'fetchData', someQuery: '*' });
21+
22+
return { data: null };
23+
};
24+
25+
// (in the component)
26+
const [state, dispatch] = useEffectReducer(fetchReducer, getInitialState, {
27+
fetchData(_, { someQuery }) {
28+
fetch(`/some/api?${someQuery}`)
29+
.then(res => res.json())
30+
.then(data => {
31+
dispatch({
32+
type: 'RESOLVE',
33+
data,
34+
});
35+
});
36+
},
37+
});
38+
```

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ If you know how to [`useReducer`](https://reactjs.org/docs/hooks-reference.html#
1616
- [Quick Start](#quick-start)
1717
- [Named Effects](#named-effects)
1818
- [Effect Implementations](#effect-implementations)
19+
- [Initial Effects](#initial-effects)
1920
- [Effect Entities](#effect-entities)
2021
- [Effect Cleanup](#effect-cleanup)
2122
- [Replacing Effects](#replacing-effects)
@@ -216,6 +217,43 @@ const [state, dispatch] = useEffectReducer(someReducer, initialState, {
216217
});
217218
```
218219

220+
## Initial Effects
221+
222+
The 2nd argument to `useEffectReducer(state, initialState)` can either be a static `initialState` or a function that takes in an effect `exec` function and returns the `initialState`:
223+
224+
```js
225+
const fetchReducer = (state, event) => {
226+
if (event.type === 'RESOLVE') {
227+
return {
228+
...state,
229+
data: event.data,
230+
};
231+
}
232+
233+
return state;
234+
};
235+
236+
const getInitialState = exec => {
237+
exec({ type: 'fetchData', someQuery: '*' });
238+
239+
return { data: null };
240+
};
241+
242+
// (in the component)
243+
const [state, dispatch] = useEffectReducer(fetchReducer, getInitialState, {
244+
fetchData(_, { someQuery }) {
245+
fetch(`/some/api?${someQuery}`)
246+
.then(res => res.json())
247+
.then(data => {
248+
dispatch({
249+
type: 'RESOLVE',
250+
data,
251+
});
252+
});
253+
},
254+
});
255+
```
256+
219257
## Effect Entities
220258

221259
The `exec(effect)` function returns an **effect entity**, which is a special object that represents the running effect. These objects can be stored directly in the reducer's state:
@@ -348,6 +386,27 @@ const SomeComponent = () => {
348386
};
349387
```
350388

389+
The 2nd argument to `useEffectReducer(...)` can either be a static `initialState` or a function that takes in `exec` and returns an `initialState` (with executed initial effects). See [Initial Effects](#initial-effects) for more information.
390+
391+
```js
392+
const SomeComponent = () => {
393+
const [state, dispatch] = useEffectReducer(
394+
someEffectReducer,
395+
exec => {
396+
exec({ type: 'someEffect' });
397+
return someInitialState;
398+
},
399+
{
400+
someEffect(state, effect) {
401+
// ...
402+
},
403+
}
404+
);
405+
406+
// ...
407+
};
408+
```
409+
351410
Additionally, the `useEffectReducer` hook takes a 3rd argument, which is the implementation details for [named effects](#named-effects):
352411

353412
```js

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

0 commit comments

Comments
 (0)