- Notifications
You must be signed in to change notification settings - Fork24
useReducer + useEffect = useEffectReducer
License
davidkpiano/useEffectReducer
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
AReact hook for managing side-effects in your reducers.
Inspired by theuseReducerWithEmitEffect hook idea bySophie Alpert.
If you know how touseReducer, you already know how touseEffectReducer.
💻 CodeSandbox example: Dog Fetcher withuseEffectReducer
- Installation
- Quick Start
- Named Effects
- Effect Implementations
- Initial Effects
- Effect Entities
- Effect Cleanup
- Replacing Effects
- String Events
- API
exec.replace(entity, effect)- TypeScript
Install it:
npm install use-effect-reducer
Import it:
import{useEffectReducer}from'use-effect-reducer';
Create an effect reducer:
constsomeEffectReducer=(state,event,exec)=>{// execute effects like this:exec(()=>{/* ... */});// or parameterized (better):exec({type:'fetchUser',user:event.user});// and treat this like a normal reducer!// ...returnstate;};
// ...const[state,dispatch]=useEffectReducer(someEffectReducer,initialState,{// implementation of effects});// Just like useReducer:dispatch({type:'FETCH',user:'Sophie'});
No - internally,useEffectReducer (as the name implies) is abstracting this pattern:
// pseudocodeconstmyReducer=([state],event)=>{consteffects=[];constexec=(effect)=>effects.push(effect);constnextState=// calculate next statereturn[nextState,effects];}// in your componentconst[[state,effects],dispatch]=useReducer(myReducer);useEffect(()=>{effects.forEach(effect=>{// execute the effect});},[effects]);
Instead of being implicit about which effects are executed andwhen they are executed, you make this explicit in the "effect reducer" with the helperexec function. Then, theuseEffectReducer hook will take the pending effects and properly execute them within auseEffect() 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 aneffect entity that allows you to control the effect
import{useEffectReducer}from'use-effect-reducer';// I know, I know, yet another counter exampleconstcountReducer=(state,event,exec)=>{switch(event.type){case'INC':exec(()=>{// "Execute" a side-effect hereconsole.log('Going up!');});return{ ...state,count:state.count+1,};default:returnstate;}};constApp=()=>{const[state,dispatch]=useEffectReducer(countReducer,{count:0});return(<div><output>Count:{state.count}</output><buttononClick={()=>dispatch('INC')}>Increment</button></div>);};
A better way to make reusable effect reducers is to have effects that arenamed andparameterized. This is done by runningexec(...) an effect object (instead of a function) and specifying that named effect's implementation as the 3rd argument touseEffectReducer(reducer, initial, effectMap).
constfetchEffectReducer=(state,event,exec)=>{switch(event.type){case'FETCH':// Capture a named effect to be executedexec({type:'fetchFromAPI',user:event.user});return{ ...state,status:'fetching',};case'RESOLVE':return{status:'fulfilled',user:event.data,};default:returnstate;}};constinitialState={status:'idle',user:undefined};constfetchFromAPIEffect=(_,effect,dispatch)=>{fetch(`/api/users/${effect.user}`).then(res=>res.json()).then(data=>{dispatch({type:'RESOLVE', data,});});};constFetcher=()=>{const[state,dispatch]=useEffectReducer(fetchEffectReducer,initialState,{// Specify how effects are implementedfetchFromAPI:fetchFromAPIEffect,});return(<buttononClick={()=>{dispatch({type:'FETCH',user:42});}}> Fetch user</div>);};
An effect implementation is a function that takes 3 arguments:
- The
stateat the time the effect was executed withexec(effect) - The
eventobject that triggered the effect - The effect reducer's
dispatchfunction to dispatch events back to it. This enables dispatching within effects in theeffectMapif 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 youreffectMapthere.
The effect implementation should return a disposal function that cleans up the effect:
// Effect defined inlineexec(()=>{constid=setTimeout(()=>{// do some delayed side-effect},1000);// disposal functionreturn()=>{clearTimeout(id);};});
// Parameterized effect implementation// (in the effect reducer)exec({type:'doDelayedEffect'});// ...// (in the component)const[state,dispatch]=useEffectReducer(someReducer,initialState,{doDelayedEffect:()=>{constid=setTimeout(()=>{// do some delayed side-effect},1000);// disposal functionreturn()=>{clearTimeout(id);};},});
The 2nd argument touseEffectReducer(state, initialState) can either be a staticinitialState or a function that takes in an effectexec function and returns theinitialState:
constfetchReducer=(state,event)=>{if(event.type==='RESOLVE'){return{ ...state,data:event.data,};}returnstate;};constgetInitialState=exec=>{exec({type:'fetchData',someQuery:'*'});return{data:null};};// (in the component)const[state,dispatch]=useEffectReducer(fetchReducer,getInitialState,{fetchData(_,{ someQuery}){fetch(`/some/api?${someQuery}`).then(res=>res.json()).then(data=>{dispatch({type:'RESOLVE', data,});});},});
Theexec(effect) function returns aneffect entity, which is a special object that represents the running effect. These objects can be stored directly in the reducer's state:
constsomeReducer=(state,event,exec)=>{// ...return{ ...state,// state.someEffect is now an effect entitysomeEffect:exec(()=>{/* ... */}),};};
The advantage of having a reference to the effect (via the returned effectentity) is that you can explicitlystop those effects:
constsomeReducer=(state,event,exec)=>{// ...// Stop an effect entityexec.stop(state.someEffect);return{ ...state,// state.someEffect is no longer neededsomeEffect:undefined,};};
Instead of implicitly relying on arbitrary values in a dependency array changing to stop an effect (as you would withuseEffect), effects can be explicitly stopped usingexec.stop(entity), whereentity is the effect entity returned from initially callingexec(effect):
consttimerReducer=(state,event,exec)=>{if(event.type==='START'){return{ ...state,timer:exec(()=>{constid=setTimeout(()=>{// Do some delayed effect},1000);// Disposal function - will be called when// effect entity is stoppedreturn()=>{clearTimeout(id);};}),};}elseif(event.type==='STOP'){// Stop the effect entityexec.stop(state.timer);returnstate;}returnstate;};
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 callingexec.stop(entity) and callingexec(effect) to manually replace an effect, you can callexec.replace(entity, effect) as a shorthand:
constdoSomeDelay=()=>{constid=setTimeout(()=>{// do some delayed effect},delay);return()=>{clearTimeout(id);};};consttimerReducer=(state,event,exec)=>{if(event.type==='START'){return{ ...state,timer:exec(()=>doSomeDelay()),};}elseif(event.type==='LAP'){// Replace the currently running effect represented by `state.timer`// with a new effectreturn{ ...state,timer:exec.replace(state.timer,()=>doSomeDelay()),};}elseif(event.type==='STOP'){// Stop the effect entityexec.stop(state.timer);returnstate;}returnstate;};
The events handled by the effect reducers are intended to be event objects with atype 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:
// dispatched as `{ type: 'INC' }`// and is the same as `dispatch({ type: 'INC' })`dispatch('INC');
TheuseEffectReducer hook takes the same first 2 arguments as the built-inuseReducer hook, and returns the currentstate returned from the effect reducer, as well as adispatch function for sending events to the reducer.
constSomeComponent=()=>{const[state,dispatch]=useEffectReducer(someEffectReducer,initialState);// ...};
The 2nd argument touseEffectReducer(...) can either be a staticinitialState or a function that takes inexec and returns aninitialState (with executed initial effects). SeeInitial Effects for more information.
constSomeComponent=()=>{const[state,dispatch]=useEffectReducer(someEffectReducer,exec=>{exec({type:'someEffect'});returnsomeInitialState;},{someEffect(state,effect){// ...},});// ...};
Additionally, theuseEffectReducer hook takes a 3rd argument, which is the implementation details fornamed effects:
constSomeComponent=()=>{const[state,dispatch]=useEffectReducer(someEffectReducer,initialState,{log:(state,effect,dispatch)=>{console.log(state);},});// ...};
Used in an effect reducer,exec(effect) queues theeffect for execution and returns aneffect entity.
Theeffect can either be an effect object:
// ...constentity=exec({type:'alert',message:'hello',});
Or it can be an inline effect implementation:
// ...constentity=exec(()=>{alert('hello');});
Used in an effect reducer,exec.stop(entity) stops the effect represented by theentity. Returnsvoid.
// Queues the effect entity for disposalexec.stop(someEntity);
Used in an effect reducer,exec.replace(entity, effect) does two things:
- Queues the
entityfor disposal (same as callingexec.stop(entity)) - Returns a neweffect entity that represents the
effectthat replaces the previousentity.
The effect reducer can be specified as anEffectReducer<TState, TEvent, TEffect>, where the generic types are:
- The
statetype returned from the reducer - The
eventobject type that can be dispatched to the reducer - The
effectobject type that can be executed
import{useEffectReducer,EffectReducer}from'use-effect-reducer';interfaceUser{name:string;}typeFetchState=|{status:'idle';user:undefined;}|{status:'fetching';user:User|undefined;}|{status:'fulfilled';user:User;};typeFetchEvent=|{type:'FETCH';user:string;}|{type:'RESOLVE';data:User;};typeFetchEffect={type:'fetchFromAPI';user:string;};constfetchEffectReducer:EffectReducer<FetchState,FetchEvent,FetchEffect>=(state,event,exec)=>{switch(event.type){case'FETCH':// State, event, and effect types will be inferred!// Also you should probably switch on// `state.status` first ;-)// ...default:returnstate;}};
About
useReducer + useEffect = useEffectReducer
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors4
Uh oh!
There was an error while loading.Please reload this page.