UseEffectReducer
useReducer + useEffect = useEffectReducer
Install / Use
/learn @davidkpiano/UseEffectReducerREADME
useEffectReducer
A React hook for managing side-effects in your reducers.
Inspired a looooong time ago by the useReducerWithEmitEffect hook idea by Sophie Alpert.
If you know how to useReducer, you already know how to useEffectReducer.
Table of Contents
- Installation
- Quick Start
- Named Effects
- Effect Implementations
- Initial Effects
- Effect Entities
- Effect Cleanup
- Replacing Effects
- String Events
- API
- TypeScript
Installation
npm install use-effect-reducer
import { useEffectReducer } from 'use-effect-reducer';
Quick Start
An effect reducer takes 3 arguments:
state- the current stateevent- the event that was dispatchedexec- a function that captures effects to be executed and returns an effect entity
import { useEffectReducer } from 'use-effect-reducer';
const countReducer = (state, event, exec) => {
switch (event.type) {
case 'INC':
exec(() => {
console.log('Going up!');
});
return {
...state,
count: state.count + 1,
};
default:
return state;
}
};
const App = () => {
const [state, dispatch] = useEffectReducer(countReducer, { count: 0 });
return (
<div>
<output>Count: {state.count}</output>
<button onClick={() => dispatch('INC')}>Increment</button>
</div>
);
};
How It Works
Internally, useEffectReducer abstracts this pattern:
const myReducer = ([state], event) => {
const effects = [];
const exec = (effect) => effects.push(effect);
const nextState = // calculate next state
return [nextState, effects];
};
const [[state, effects], dispatch] = useReducer(myReducer);
useEffect(() => {
effects.forEach((effect) => {
// execute the effect
});
}, [effects]);
Instead of being implicit about which effects are executed and when, you make this explicit in the effect reducer with exec. The hook then properly executes pending effects within useEffect().
Named Effects
A better way to make reusable effect reducers is to use named, parameterized effects. Pass an effect object to exec(...) and specify the implementation as the 3rd argument to useEffectReducer:
const fetchEffectReducer = (state, event, exec) => {
switch (event.type) {
case 'FETCH':
exec({ type: 'fetchFromAPI', user: event.user });
return { ...state, status: 'fetching' };
case 'RESOLVE':
return { status: 'fulfilled', user: event.data };
default:
return state;
}
};
const Fetcher = () => {
const [state, dispatch] = useEffectReducer(
fetchEffectReducer,
{ status: 'idle', user: undefined },
{
fetchFromAPI: (_, effect, dispatch) => {
fetch(`/api/users/${effect.user}`)
.then((res) => res.json())
.then((data) => {
dispatch({ type: 'RESOLVE', data });
});
},
}
);
// ...
};
Effect Implementations
An effect implementation receives 3 arguments:
state- the state at the timeexec(effect)was calledeffect- the effect objectdispatch- the dispatch function for sending events back to the reducer
Return a cleanup function to dispose of the effect:
exec(() => {
const id = setTimeout(() => {
// do some delayed side-effect
}, 1000);
return () => {
clearTimeout(id);
};
});
Initial Effects
The 2nd argument can be a function that receives exec and returns initial state:
const getInitialState = (exec) => {
exec({ type: 'fetchData', query: '*' });
return { data: null };
};
const [state, dispatch] = useEffectReducer(fetchReducer, getInitialState, {
fetchData(_, { query }, dispatch) {
fetch(`/api?${query}`)
.then((res) => res.json())
.then((data) => dispatch({ type: 'RESOLVE', data }));
},
});
Effect Entities
exec(effect) returns an effect entity representing the running effect. Store it in state to control the effect later:
const someReducer = (state, event, exec) => {
return {
...state,
someEffect: exec(() => {
/* ... */
}),
};
};
Effect Cleanup
Effects can be explicitly stopped using exec.stop(entity):
const timerReducer = (state, event, exec) => {
if (event.type === 'START') {
return {
...state,
timer: exec(() => {
const id = setTimeout(() => {
// delayed effect
}, 1000);
return () => clearTimeout(id);
}),
};
} else if (event.type === 'STOP') {
exec.stop(state.timer);
return state;
}
return state;
};
All running effects are automatically cleaned up when the component unmounts.
Replacing Effects
Use exec.replace(entity, effect) to stop an existing effect and start a new one:
const timerReducer = (state, event, exec) => {
if (event.type === 'START') {
return {
...state,
timer: exec(() => doSomeDelay()),
};
} else if (event.type === 'LAP') {
return {
...state,
timer: exec.replace(state.timer, () => doSomeDelay()),
};
} else if (event.type === 'STOP') {
exec.stop(state.timer);
return state;
}
return state;
};
String Events
For events without payload, you can dispatch the type string directly:
// Same as dispatch({ type: 'INC' })
dispatch('INC');
API
useEffectReducer hook
const [state, dispatch] = useEffectReducer(effectReducer, initialState);
const [state, dispatch] = useEffectReducer(effectReducer, initialState, effectsMap);
const [state, dispatch] = useEffectReducer(effectReducer, initFunction, effectsMap);
exec(effect)
Queues an effect for execution. Returns an effect entity.
const entity = exec({ type: 'alert', message: 'hello' });
// or inline:
const entity = exec(() => alert('hello'));
exec.stop(entity)
Stops the effect represented by the entity and runs its cleanup function.
exec.stop(someEntity);
exec.replace(entity, effect)
Stops the existing entity and queues a new effect. Returns a new effect entity.
const newEntity = exec.replace(oldEntity, newEffect);
TypeScript
The effect reducer can be typed with EffectReducer<TState, TEvent, TEffect>:
import { useEffectReducer, EffectReducer } from 'use-effect-reducer';
interface User {
name: string;
}
type FetchState =
| { status: 'idle'; user: undefined }
| { status: 'fetching'; user: User | undefined }
| { status: 'fulfilled'; user: User };
type FetchEvent =
| { type: 'FETCH'; user: string }
| { type: 'RESOLVE'; data: User };
type FetchEffect = {
type: 'fetchFromAPI';
user: string;
};
const fetchEffectReducer: EffectReducer<
FetchState,
FetchEvent,
FetchEffect
> = (state, event, exec) => {
switch (event.type) {
case 'FETCH':
exec({ type: 'fetchFromAPI', user: event.user });
return { ...state, status: 'fetching' };
case 'RESOLVE':
return { status: 'fulfilled', user: event.data };
default:
return state;
}
};
Related Skills
bluebubbles
341.2kUse when you need to send or manage iMessages via BlueBubbles (recommended iMessage integration). Calls go through the generic message tool with channel="bluebubbles".
node-connect
341.2kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
slack
341.2kUse when you need to control Slack from OpenClaw via the slack tool, including reacting to messages or pinning/unpinning items in Slack channels or DMs.
frontend-design
84.5kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
