Improve the usability of Component State Reducers with state change types

Kent C. Dodds
InstructorKent C. Dodds

Share this video with your friends

Send Tweet
Published 4 years ago
Updated a year ago

Users of our component can make custom modifications to the state whenever it changes, but in more complex components they may only want to change the state updates for certain types of changes. Let's add a type property to our changes object so people providing a state reducer have more insight into the source of the changes and can reduce the changes based on that information.

Instructor: [00:00] With a state reducer, users of the toggle component are able to prevent state changes based off of their own state. Not every state change is equal and there could be some state changes that are acceptable and others that are not.

[00:12] In this case we can click and once we get to too many then our state reducer prevents any further toggling, but we want to allow users to force the toggle if they click on this button, and then reset works exactly as it had before.

[00:25] Users of our state reducer need to know what type of change is happening, so they know how to differentiate different types of changes. In the API of the toggle state reducer the changes object can have a type property.

[00:37] Because we're rendering the force toggle button we're going to specify that type when we call the toggle function. It would be useful for each time we call this internal setstate that a type be provided so that the toggle reducer can determine whether or not to allow the state, based on the changes type.

[00:54] In order to make this API work every time the internal setstate is called it should be called with the changes. That changes object should have a type on it. Here for a reset I'm going to take this and turn it into an object. We'll spread that, and we'll provide a type that is reset.

[01:10] We'll do the same thing for toggle. We'll provide a type that is toggle. Here in our use case the toggle can be called with a specific type. We'll allow that API here in our toggle. We'll accept an object that accepts a type.

[01:24] The toggle function is called with the event if we're using get toggler props. Here we're going to just call it with an arrow function so it's not called with the event. We'll default this to an empty object.

[01:36] We'll also default the type to be toggle rather than prescribing it here. Now the type can be specified. With that, our new API is supported.

[01:50] Let's go ahead and refactor a few things. First of all, because the changes now has a type associated with it, the reduced changes is going to include that type. What we return from setstate will be an object that includes a type property that's adding the type to our state, which is not optimal.

[02:06] If nothing else changes but the type we're still going to get a re-render, and that could be unnecessary. What we're going to do is, I'm going to click off the type. I'll just call that ignored type. Then we'll get the only changes from reduced changes. Then we'll return only the changes.

[02:25] The next bit of refactoring I'm going to do is, I don't want to have these random strings in my code. If somebody wanted to reference these types in their state reducer I wouldn't want them to have to hard code these streams.

[02:37] I'm going to create static state change types, and we'll have a reset that is reset and a toggle that is toggle. Then I can reference the state change types instead of these strings. Let's say toggled on state change types.reset. Then toggle.state change types.toggle.

[03:00] Anyone who wants to reference these could reference toggle.statechangetypes.toggle or reset, or whatever else they support. If they want to know what's available they can simply console log toggle state change types.

[03:13] In review, the problem we're trying to solve is to further enable people to have control over the internal state of this toggle component by exposing a state reducer, which gives people a hook into how we manage our state in the toggle component.

[03:26] Specifically, here, we want to allow them to know what type of change is taking place so they can know whether or not they want to allow for that change. In our example we have this toggle state reducer that checks the type to see if it's forced, because we provide that type in our forced toggle on click.

[03:42] To support this API, we had to add a type to every time we call Internet setstate. Then we pluck off that type from the reduced changes to avoid an unnecessary re-render. As a convenience, we also added the static property called state change types so we can reference that in our own code, and users of the toggle component can reference that in their state reducer.

[04:01] That's the state reducer with types.

spencer
spencer
~ 4 years ago

Really cool stuff so far Kent. I'm trying to understand when you say that we are calling this.toggle with the event and you change it so getTogglerProps onClick property uses callAll(onClick, () => this.toggle()) instead. I've tried logging out what the callAll is outputting, or what args are sent to this.toggle, but I cant figure out why we changed it or how to see what you mean by "this.toggle is being called with the event". Would you mind elaborating on what exactly is going on? Thank you!

Kent C. Dodds
Kent C. Doddsinstructor
~ 4 years ago

Hi Spencer,

What's going on is the onClick will be called with the event just because that's the way React works. We pass onClick the function which is returned by callAll which will call the given functions with all the arguments it's called. So when that function's called with the event, it will then call toggle with the event. So we provide a function which calls toggle without any arguments which will cause the default argument to be picked up instead.

I hope that helps.

spencer
spencer
~ 4 years ago

Thank you so much for the explanation. Makes total sense now. Congrats on DownShift 2.0 as well 🏎

Viktor Soroka
Viktor Soroka
~ 4 years ago

Hi Kent, Thanks for the material. As I understand you assigned a type for every action for consistency even though in that particular example it is enough just to provide the 'force' one. 'toggle' and 'reset' may be used later if needed, right? And another point I have is about a bit wrong behavior in the case when there are too many clicks and the force button was hit once, there is the ability to toggle state to the off state by clicking on the Toggler itself which should not be the case. Not a big deal though.

Bogdan Bivolaru
Bogdan Bivolaru
~ 3 years ago

Hi Kent, In internalSetState => setState you say we need to pluck the type to avoid necessary rerenders. Shouldn't we need to pluck the timesClicked too to avoid rerenders? Why not?

Indeed, I've seen the Usage component does not rerender, but I don't understand why! To verify this I've put an input field inside the Usage component - it does not lose its value when we toggle / force / reset.

A̶l̶s̶o̶,̶ ̶i̶n̶s̶i̶d̶e̶ ̶̶̶h̶a̶n̶d̶l̶e̶T̶o̶g̶g̶l̶e̶̶̶,̶ ̶i̶n̶s̶i̶d̶e̶ ̶̶̶s̶e̶t̶S̶t̶a̶t̶e̶̶̶ ̶I̶ ̶w̶o̶u̶l̶d̶ ̶e̶x̶p̶e̶c̶t̶ ̶t̶o̶ ̶b̶e̶ ̶a̶b̶l̶e̶ ̶t̶o̶ ̶o̶v̶e̶r̶w̶r̶i̶t̶e̶ ̶t̶h̶e̶ ̶o̶n̶ ̶k̶e̶y̶ ̶ ̶w̶i̶t̶h̶ ̶a̶ ̶s̶t̶r̶i̶n̶g̶,̶ ̶w̶h̶i̶c̶h̶ ̶I̶ ̶e̶x̶p̶e̶c̶t̶e̶d̶ ̶t̶o̶ ̶b̶r̶e̶a̶k̶ ̶t̶h̶e̶ ̶c̶o̶m̶p̶o̶n̶e̶n̶t̶.̶ ̶I̶t̶ ̶d̶o̶e̶s̶n̶'̶t̶ ̶-̶ ̶b̶u̶t̶ ̶w̶h̶y̶?̶

Oh, my sandbox: https://codesandbox.io/s/rwp5z49qo4

Kent C. Dodds
Kent C. Doddsinstructor
~ 3 years ago

Bogdan, timesClicked is state that's in the Usage component, not the Toggle. That's why not :)

Antonino Manno
Antonino Manno
~ 3 years ago

Hi Kent, I'm following this course, and I appreciate your work. In my opinion in this lecture when we insert the "Force toggle" function with type, we need to fix the toggleStateReducer

Now when you reach 4 timesClicked you will be able to "force toggle" once and then you can click on the switch and it will toggle even if we clicked more than 4 times :D So in this case we need to get "on" from the state and use that value in the return statement inside timesClicked > 4 condition

Paweł Waszczyński
Paweł Waszczyński
~ 3 years ago

Quick questions: does the optimization with onlyChanges makes sense? I will return new object anyway (might be empty), but according to docs:

setState() will always lead to a re-render unless shouldComponentUpdate() returns false

Kent C. Dodds
Kent C. Doddsinstructor
~ 3 years ago

If you return null from your state updater (or simply pass null) then react will not trigger a rerender. If you return an empty object (or simply pass an empty object) then react will trigger a rerender.

See this codesandbox for an example: https://codesandbox.io/s/5wlvrkp5jn