Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Stephen Charles Weiss
Stephen Charles Weiss

Posted on • Originally published atstephencharlesweiss.com on

     

UseReducer With Typescript

When does it make sense to use a reducer vs a simple state value with React’s Hooks? There’s no hard-and-fast rule, but the React team suggestsuseReducer when “managing state objects that contains multiple sub-values”

That described my use case well. I had a series of steps, each one with multiple sub values, and depending on what the user did, the step(s) would be affected in a variety of ways (to start with - only two, but the number of interactions may grow in the future).

Before jumping into theuseReducer, a few thoughts:

  • I could have stored the entire object in a singleuseState and then managed the updates manually. For example, from the docs:1
setState(prevState=>{// Object.assign would also workreturn{prevState,updatedValues};})
  • I may have benefitted from using React’s Context API to avoid some of the prop-drilling and passing around that occurred later on. I opted tonot use it in this case, however, because the reducer was used immediately in the child components (i.e. I would not benefit from avoiding prop drilling).

Getting Started - Adding theUseReducer

To actuallyuse this hook, I needed to invoke it within a component. A simplified look at how that was done:

interfaceStep{id:string;hidden?:boolean;status?:StepStatusTypes;}constMyComponent=()=>{/* ... */constinitialArg:Step[]=[...]const[steps,stepsDispatch]=useReducer(stepsReducer,initialArg);/* ... */return(/* ... */<MyChildComponentstepsDispatch={stepsDispatch}/>/* ... */)}
Enter fullscreen modeExit fullscreen mode

Defining The Reducer

Defining the reducer, the first argument foruseReducer was actually the trickiest part. Not because reducers themselves are actually that complicated, but because of Typescript.

For example, here’s the basic reducer that I came up with:

functionstepsReducer(steps,action){switch(action.type){case'SHOW-ALL':returnsteps.map(step=>({...step,hidden:action.payload}))case'SET-STATUS':steps.splice(action.payload.index,1,{...steps[action.payload.index],status:action.payload.status,})returnstepsdefault:returnsteps}}
Enter fullscreen modeExit fullscreen mode

If it received aSHOW-ALL action, each step would take the value of the payload and apply it to thehidden attribute. On the other hand, if it received theSET-STATUS action, onlythat step would have its status updated. In all other cases, the steps object was simply returned.

In this project, Typescript is configured to yell if anything has a type ofany - implicitly or otherwise. As a result, I needed to type the Actions. And, given the differentshape of the actions, this proved to be the most challenging part of the entire exercise.

My first approach

interfaceAction{type:stringpayload:boolean|{index:number;status:string}}
Enter fullscreen modeExit fullscreen mode

I thought this was going to work the compiler started yelling thatindex doesn’t exist on typefalse. Sure, that makes sense - except that I was trying to say it’d be a boolean valueor an objectwith anindex property.

Oh well.

So, I started digging for examples of folks usingredux withtypescript. (While I was using a hook, since they’re still relatively new and the principles are the same, I figured whatever I found, I’d be able to apply.)

I found this thread onhow to type Redux actions and Redux reducers in TypeScript? on Stack Overflow helpful and got me going in the right direction.

The first attempt I made was splitting up the types like the first answer suggests:

interfaceShowAllAction{type:stringpayload:boolean}interfaceAction{type:stringpayload:{index:number;status:string}}typeAction=ShowAllAction|SetStatusAction
Enter fullscreen modeExit fullscreen mode

Same story. Typescript yelled becauseindex didn’t exist on typefalse. This wasn’t the answer.

Then, I found an answer which usedType Guards.2 Type Guards have always made more sense to me than Generics. A Type Guard is a function that allows Typescript to determine which type is being used while a Generic is a type that receives another to define it (actuallyDispatch is a Generic if I’m not mistaken).

Effectively,before I used an action, I needed to determine which type of action I’d be using. Enter Type Guards:

exportinterfaceAction{type:string}exportinterfaceShowAllActionextendsAction{payload:boolean}exportinterfaceSetStatusActionextendsAction{payload:{index:numberstatus:string}}// The Type Guard FunctionsfunctionisShowAllAction(action:Action):actionisShowAllAction{returnaction.type==='SHOW-ALL'}functionisSetStatusAction(action:Action):actionisSetStatusAction{returnaction.type==='SET-STATUS'}functionstepsReducer(steps:IFormStep[],action:Action){if(isShowAllAction(action)){returnsteps.map((step:IFormStep)=>({...step,disabled:action.payload,hidden:action.payload,}))}if(isSetStatusAction(action)){steps.splice(action.payload.index,1,{...steps[action.payload.index],status:action.payload.status,})returnsteps}returnsteps}
Enter fullscreen modeExit fullscreen mode

With the type guards in place, I was ready to actually start dispatching actions.

Invoking (And Typing) The Dispatch Function

If I had only ever tried to dispatch actions from within the component in which theuseReducer was defined, Typescript probably would have been able to tell what was happening. However, I wanted different parts of my app to be able to dispatch actions and not have to repeat logic. That was why I wanted a shared state in the first place.

That meant I needed to pass the dispatch function around and define it as part of the other component’s interfaces.

So, how do you type a dispatch so that Typescript doesn’t yell? It turns out React exports aDispatch type which takes anAction (note, however, that theAction is the one defined by you).

Use React’sDispatch like so:

importReact,{Dispatch}from"react";import{Action,ShowAllAction}from"../index";constMyChildComponent=({stepsDispatch:Dispatch<Action&ShowAllAction>})=>{/* ... */}
Enter fullscreen modeExit fullscreen mode

Conclusion

UsinguseReducer with Typescript is not that challenging once the basics are understood. The hardest part about the entire process was getting the typing right, and Type Guards proved to be up to the challenge!

I’m excited to keep exploring other ways to use reducers and dispatch actions.

Maybe my next step will be to explore Action Factories so I don’t have to keep creating these objects!

Footnotes

  • 1 WhileuseState predominately is updated declaratively. It can use the old syntax of usingprevState in a functional update:Hooks API Reference – React.
  • 2 I’d read the typescript documentation on Type Guards several times in the past without it clicking. As with so many topics, things fell in place once I had a reason to be there.

Top comments(5)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
torbenrahbekkoch profile image
Torben Rahbek Koch
Passionate developer, usually looking for solutions to the wrong problems ;)
  • Location
    Denmark
  • Work
    Developer at Copenhagen
  • Joined

You may want to look into discriminated unions :typescriptlang.org/docs/handbook/a...

The syntax is, I think, somewhat clunky, but the idea is that you start out with an enum with your action types:

enum Kind {    ShowAll,    SetStatus}
Enter fullscreen modeExit fullscreen mode

Then the actual discriminated union with whatever data each action type needs:

type Action = {    kind : Kind.ShowAll    payload : boolean}| {    kind : Kind.SetStatus    index : number    status : string}
Enter fullscreen modeExit fullscreen mode

In your reducer you can now switch on kind:

function stepsReducer(steps: IFormStep[], action: Action) {    switch (action.Kind)    {        case Kind.ShowAll:            // You can now access action.payload  and do whatever...            break;         case Kind.SetStatus:             // You can now access action.index and action.status             break;          default:              // This mostly seems like black magic to me, but it has the compiler              // warn when you have NOT switched on ALL action types:              const _exhaustiveCheck: never = action;        }}
Enter fullscreen modeExit fullscreen mode

It is fairly elegant, although perhaps a bit convoluted, but you have the compiler help you out quite bit :)

CollapseExpand
 
stephencweiss profile image
Stephen Charles Weiss
Engineer | Lover of dogs, books, and learning | Dos XX can be most interesting man in the world, I'm happy being the luckiest. | I write about what I learn @ code-comments.com
  • Location
    Chicago, IL
  • Work
    Software Engineer at Olo
  • Joined

Very nice! Thank you!

CollapseExpand
 
nikican profile image
niki
  • Joined

It's an old post but I guess it's never too late to say thank you.
Thank you for your post! It set me on a right track to solve my issue.

I also need to include generics so let me add a SO post, in case somebody needs that too:
stackoverflow.com/questions/553964...

CollapseExpand
 
scorpian555 profile image
Ian
independent web dev, React, Mongo, lately lotta lambdas
  • Location
    Near Earth
  • Work
    dev at on the internet
  • Joined
• Edited on• Edited

Thank you so f***ing much for this! Pretty much nailed my exact problem, as well as my approach to solving it so I feel a lot better.

I also have been reading a lot of Redux/Typescript docs b/c I am experienced w/ Redux, but am using the Context API w/ Graphql in a NextJS project...

First major attempt at a TS project, love the tooling with VSCode, but it's a lot of extra work up front (though I can see how it pays off just in how forward it makes you think...)

Great stuff. Thanks again.

CollapseExpand
 
stephencweiss profile image
Stephen Charles Weiss
Engineer | Lover of dogs, books, and learning | Dos XX can be most interesting man in the world, I'm happy being the luckiest. | I write about what I learn @ code-comments.com
  • Location
    Chicago, IL
  • Work
    Software Engineer at Olo
  • Joined

So happy to hear it was helpful!

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Engineer | Lover of dogs, books, and learning | Dos XX can be most interesting man in the world, I'm happy being the luckiest. | I write about what I learn @ code-comments.com
  • Location
    Chicago, IL
  • Work
    Software Engineer at Olo
  • Joined

More fromStephen Charles Weiss

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp