Gathering detailed insights and metrics for use-effect-reducer
Gathering detailed insights and metrics for use-effect-reducer
Gathering detailed insights and metrics for use-effect-reducer
Gathering detailed insights and metrics for use-effect-reducer
npm install use-effect-reducer
Typescript
Module System
Min. Node Version
Node Version
NPM Version
TypeScript (100%)
Total Downloads
0
Last Day
0
Last Week
0
Last Month
0
Last Year
0
MIT License
790 Stars
70 Commits
24 Forks
7 Watchers
17 Branches
4 Contributors
Updated on Jun 17, 2025
Latest Version
0.7.0
Package Id
use-effect-reducer@0.7.0
Unpacked Size
81.63 kB
Size
15.31 kB
File Count
13
NPM Version
6.14.5
Node Version
14.5.0
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
1
A React hook for managing side-effects in your reducers.
Inspired by the useReducerWithEmitEffect
hook idea by Sophie Alpert.
If you know how to useReducer
, you already know how to useEffectReducer
.
💻 CodeSandbox example: Dog Fetcher with useEffectReducer
exec.replace(entity, effect)
Install it:
1npm install use-effect-reducer
Import it:
1import { useEffectReducer } from 'use-effect-reducer';
Create an effect reducer:
1const someEffectReducer = (state, event, exec) => { 2 // execute effects like this: 3 exec(() => {/* ... */}); 4 5 // or parameterized (better): 6 exec({ type: 'fetchUser', user: event.user }); 7 8 // and treat this like a normal reducer! 9 // ... 10 11 return state; 12});
1// ... 2const [state, dispatch] = useEffectReducer(someEffectReducer, initialState, { 3 // implementation of effects 4}); 5 6// Just like useReducer: 7dispatch({ type: 'FETCH', user: 'Sophie' });
No - internally, useEffectReducer
(as the name implies) is abstracting this pattern:
1// pseudocode 2const myReducer = ([state], event) => { 3 const effects = []; 4 const exec = (effect) => effects.push(effect); 5 6 const nextState = // calculate next state 7 8 return [nextState, effects]; 9} 10 11// in your component 12const [allState, dispatch] = useReducer(myReducer); 13 14useEffect(() => { 15 allState.effects.forEach(effect => { 16 // execute the effect 17 }); 18}, [allState.effects]);
Instead of being implicit about which effects are executed and when they are executed, you make this explicit in the "effect reducer" with the helper exec
function. Then, the useEffectReducer
hook will take the pending effects and properly execute them within a useEffect()
hook.
An "effect reducer" takes 3 arguments:
state
- the current stateevent
- the event that was dispatched to the reducerexec
- a function that captures effects to be executed and returns an effect entity that allows you to control the effect1import { useEffectReducer } from 'use-effect-reducer'; 2 3// I know, I know, yet another counter example 4const countReducer = (state, event, exec) => { 5 switch (event.type) { 6 case 'INC': 7 exec(() => { 8 // "Execute" a side-effect here 9 console.log('Going up!'); 10 }); 11 12 return { 13 ...state, 14 count: state.count + 1, 15 }; 16 17 default: 18 return state; 19 } 20}; 21 22const App = () => { 23 const [state, dispatch] = useEffectReducer(countReducer, { count: 0 }); 24 25 return ( 26 <div> 27 <output>Count: {state.count}</output> 28 <button onClick={() => dispatch('INC')}>Increment</button> 29 </div> 30 ); 31};
A better way to make reusable effect reducers is to have effects that are named and parameterized. This is done by running exec(...)
an effect object (instead of a function) and specifying that named effect's implementation as the 3rd argument to useEffectReducer(reducer, initial, effectMap)
.
1const fetchEffectReducer = (state, event, exec) => { 2 switch (event.type) { 3 case 'FETCH': 4 // Capture a named effect to be executed 5 exec({ type: 'fetchFromAPI', user: event.user }); 6 7 return { 8 ...state, 9 status: 'fetching', 10 }; 11 case 'RESOLVE': 12 return { 13 status: 'fulfilled', 14 user: event.data, 15 }; 16 default: 17 return state; 18 } 19}; 20 21const initialState = { status: 'idle', user: undefined }; 22 23const fetchFromAPIEffect = (_, effect, dispatch) => { 24 fetch(`/api/users/${effect.user}`) 25 .then(res => res.json()) 26 .then(data => { 27 dispatch({ 28 type: 'RESOLVE', 29 data, 30 }); 31 }); 32}; 33 34const Fetcher = () => { 35 const [state, dispatch] = useEffectReducer(fetchEffectReducer, initialState, { 36 // Specify how effects are implemented 37 fetchFromAPI: fetchFromAPIEffect, 38 }); 39 40 return ( 41 <button 42 onClick={() => { 43 dispatch({ type: 'FETCH', user: 42 }); 44 }} 45 > 46 Fetch user 47 </div> 48 ); 49};
An effect implementation is a function that takes 3 arguments:
state
at the time the effect was executed with exec(effect)
event
object that triggered the effectdispatch
function to dispatch events back to it. This enables dispatching within effects in the effectMap
if it is written outside of the scope of your component. If your effects require access to variables and functions in the scope of your component, write your effectMap
there.The effect implementation should return a disposal function that cleans up the effect:
1// Effect defined inline 2exec(() => { 3 const id = setTimeout(() => { 4 // do some delayed side-effect 5 }, 1000); 6 7 // disposal function 8 return () => { 9 clearTimeout(id); 10 }; 11});
1// Parameterized effect implementation 2// (in the effect reducer) 3exec({ type: 'doDelayedEffect' }); 4 5// ... 6 7// (in the component) 8const [state, dispatch] = useEffectReducer(someReducer, initialState, { 9 doDelayedEffect: () => { 10 const id = setTimeout(() => { 11 // do some delayed side-effect 12 }, 1000); 13 14 // disposal function 15 return () => { 16 clearTimeout(id); 17 }; 18 }, 19});
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
:
1const fetchReducer = (state, event) => { 2 if (event.type === 'RESOLVE') { 3 return { 4 ...state, 5 data: event.data, 6 }; 7 } 8 9 return state; 10}; 11 12const getInitialState = exec => { 13 exec({ type: 'fetchData', someQuery: '*' }); 14 15 return { data: null }; 16}; 17 18// (in the component) 19const [state, dispatch] = useEffectReducer(fetchReducer, getInitialState, { 20 fetchData(_, { someQuery }) { 21 fetch(`/some/api?${someQuery}`) 22 .then(res => res.json()) 23 .then(data => { 24 dispatch({ 25 type: 'RESOLVE', 26 data, 27 }); 28 }); 29 }, 30});
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:
1const someReducer = (state, event, exec) => { 2 // ... 3 4 return { 5 ...state, 6 // state.someEffect is now an effect entity 7 someEffect: exec(() => { 8 /* ... */ 9 }), 10 }; 11};
The advantage of having a reference to the effect (via the returned effect entity
) is that you can explicitly stop those effects:
1const someReducer = (state, event, exec) => { 2 // ... 3 4 // Stop an effect entity 5 exec.stop(state.someEffect); 6 7 return { 8 ...state, 9 // state.someEffect is no longer needed 10 someEffect: undefined, 11 }; 12};
Instead of implicitly relying on arbitrary values in a dependency array changing to stop an effect (as you would with useEffect
), effects can be explicitly stopped using exec.stop(entity)
, where entity
is the effect entity returned from initially calling exec(effect)
:
1const timerReducer = (state, event, exec) => { 2 if (event.type === 'START') { 3 return { 4 ...state, 5 timer: exec(() => { 6 const id = setTimeout(() => { 7 // Do some delayed effect 8 }, 1000); 9 10 // Disposal function - will be called when 11 // effect entity is stopped 12 return () => { 13 clearTimeout(id); 14 }; 15 }), 16 }; 17 } else if (event.type === 'STOP') { 18 // Stop the effect entity 19 exec.stop(state.timer); 20 21 return state; 22 } 23 24 return state; 25};
All running effect entities will automatically be stopped when the component unmounts.
If you want to replace an effect with another (likely similar) effect, instead of calling exec.stop(entity)
and calling exec(effect)
to manually replace an effect, you can call exec.replace(entity, effect)
as a shorthand:
1const doSomeDelay = () => { 2 const id = setTimeout(() => { 3 // do some delayed effect 4 }, delay); 5 6 return () => { 7 clearTimeout(id); 8 }; 9}; 10 11const timerReducer = (state, event, exec) => { 12 if (event.type === 'START') { 13 return { 14 ...state, 15 timer: exec(() => doSomeDelay()), 16 }; 17 } else if (event.type === 'LAP') { 18 // Replace the currently running effect represented by `state.timer` 19 // with a new effect 20 return { 21 ...state, 22 timer: exec.replace(state.timer, () => doSomeDelay()), 23 }; 24 } else if (event.type === 'STOP') { 25 // Stop the effect entity 26 exec.stop(state.timer); 27 28 return state; 29 } 30 31 return state; 32};
The events handled by the effect reducers are intended to be event objects with a type
property; e.g., { type: 'FETCH', other: 'data' }
. For events without payload, you can dispatch the event type alone, which will be converted to an event object inside the effect reducer:
1// dispatched as `{ type: 'INC' }` 2// and is the same as `dispatch({ type: 'INC' })` 3dispatch('INC');
useEffectReducer
hookThe useEffectReducer
hook takes the same first 2 arguments as the built-in useReducer
hook, and returns the current state
returned from the effect reducer, as well as a dispatch
function for sending events to the reducer.
1const SomeComponent = () => { 2 const [state, dispatch] = useEffectReducer(someEffectReducer, initialState); 3 4 // ... 5};
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 for more information.
1const SomeComponent = () => { 2 const [state, dispatch] = useEffectReducer( 3 someEffectReducer, 4 exec => { 5 exec({ type: 'someEffect' }); 6 return someInitialState; 7 }, 8 { 9 someEffect(state, effect) { 10 // ... 11 }, 12 } 13 ); 14 15 // ... 16};
Additionally, the useEffectReducer
hook takes a 3rd argument, which is the implementation details for named effects:
1const SomeComponent = () => { 2 const [state, dispatch] = useEffectReducer(someEffectReducer, initialState, { 3 log: (state, effect, dispatch) => { 4 console.log(state); 5 }, 6 }); 7 8 // ... 9};
exec(effect)
Used in an effect reducer, exec(effect)
queues the effect
for execution and returns an effect entity.
The effect
can either be an effect object:
1// ... 2const entity = exec({ 3 type: 'alert', 4 message: 'hello', 5});
Or it can be an inline effect implementation:
1// ... 2const entity = exec(() => { 3 alert('hello'); 4});
exec.stop(entity)
Used in an effect reducer, exec.stop(entity)
stops the effect represented by the entity
. Returns void
.
1// Queues the effect entity for disposal 2exec.stop(someEntity);
exec.replace(entity, effect)
Used in an effect reducer, exec.replace(entity, effect)
does two things:
entity
for disposal (same as calling exec.stop(entity)
)effect
that replaces the previous entity
.The effect reducer can be specified as an EffectReducer<TState, TEvent, TEffect>
, where the generic types are:
state
type returned from the reducerevent
object type that can be dispatched to the reducereffect
object type that can be executed1import { useEffectReducer, EffectReducer } from 'use-effect-reducer'; 2 3interface User { 4 name: string; 5} 6 7type FetchState = 8 | { 9 status: 'idle'; 10 user: undefined; 11 } 12 | { 13 status: 'fetching'; 14 user: User | undefined; 15 } 16 | { 17 status: 'fulfilled'; 18 user: User; 19 }; 20 21type FetchEvent = 22 | { 23 type: 'FETCH'; 24 user: string; 25 } 26 | { 27 type: 'RESOLVE'; 28 data: User; 29 }; 30 31type FetchEffect = { 32 type: 'fetchFromAPI'; 33 user: string; 34}; 35 36const fetchEffectReducer: EffectReducer<FetchState, FetchEvent, FetchEffect> = ( 37 state, 38 event, 39 exec 40) => { 41 switch (event.type) { 42 case 'FETCH': 43 // State, event, and effect types will be inferred! 44 45 // Also you should probably switch on 46 // `state.status` first ;-) 47 48 // ... 49 50 default: 51 return state; 52 } 53};
No vulnerabilities found.
Reason
no dangerous workflow patterns detected
Reason
no binaries found in the repo
Reason
license file detected
Details
Reason
Found 4/14 approved changesets -- score normalized to 2
Reason
project is archived
Details
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
dependency not pinned by hash detected -- score normalized to 0
Details
Reason
detected GitHub workflow tokens with excessive permissions
Details
Reason
security policy file not detected
Details
Reason
project is not fuzzed
Details
Reason
branch protection not enabled on development/release branches
Details
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
Reason
43 existing vulnerabilities detected
Details
Score
Last Scanned on 2025-07-07
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn More