Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings
This repository was archived by the owner on Apr 3, 2024. It is now read-only.

useReducer + useEffect = useEffectReducer

License

NotificationsYou must be signed in to change notification settings

davidkpiano/useEffectReducer

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

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

Use it:

// ...const[state,dispatch]=useEffectReducer(someEffectReducer,initialState,{// implementation of effects});// Just like useReducer:dispatch({type:'FETCH',user:'Sophie'});

Isn't this unsafe?

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.

Quick Start

An "effect reducer" takes 3 arguments:

  1. state - the current state
  2. event - the event that was dispatched to the reducer
  3. exec - 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>);};

Named Effects

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

Effect Implementations

An effect implementation is a function that takes 3 arguments:

  1. Thestate at the time the effect was executed withexec(effect)
  2. Theevent object that triggered the effect
  3. The effect reducer'sdispatch function to dispatch events back to it. This enables dispatching within effects in theeffectMap 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 youreffectMap there.

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

Initial Effects

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

Effect Entities

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,};};

Effect Cleanup

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.

Replacing Effects

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

String Events

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

API

useEffectReducer hook

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);},});// ...};

exec(effect)

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

exec.stop(entity)

Used in an effect reducer,exec.stop(entity) stops the effect represented by theentity. Returnsvoid.

// Queues the effect entity for disposalexec.stop(someEntity);

exec.replace(entity, effect)

Used in an effect reducer,exec.replace(entity, effect) does two things:

  1. Queues theentity for disposal (same as callingexec.stop(entity))
  2. Returns a neweffect entity that represents theeffect that replaces the previousentity.

TypeScript

The effect reducer can be specified as anEffectReducer<TState, TEvent, TEffect>, where the generic types are:

  • Thestate type returned from the reducer
  • Theevent object type that can be dispatched to the reducer
  • Theeffect object 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

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors4

  •  
  •  
  •  
  •  

[8]ページ先頭

©2009-2025 Movatter.jp