Kent C. Dodds: 0:00 All right. Let's add this super-duper flexibility of control props to our useToggle custom hook. The first thing that we're going to need to do is let's go ahead and add support for the onChange prop because that's something that will be pretty quick and easy for us to add.
0:14 We'll have onChange. Then we need to call the onChange anytime we issue a state change. What I'm going to do is come down here, and I'm going to change these from regular dispatch to a dispatch with onChange. Here, we'll make a function dispatch with onChange. This is going to take an action, and then we'll call a dispatch with that action.
0:36 Then instead of dispatch by itself, it'll be a dispatch with onChange. That literally did nothing. That was just a regular refactor, wrap that stuff up in its own little function, but what this does is it allows us to also add an onChange call here where we can call onChange with our suggested changes, so the changes that we are going to make to the state based on the change that happened.
1:02 The way that this onChange is going to be called, if we look down here, our users want us to call handle toggle change with the change state and the action that caused that change. Specifically, we need to call this onChange with our suggested changes, not the state we currently have but actually the state that we're going to transition to, and also the action.
1:25 How do we get that state we're transitioning to? We can call the reducer with that state and that action. That's going to get us our new state.
1:35 I'm going to go ahead and pass that instead. Now we're calling onChange with a new state that we're going to be transitioning to. If I save this, pop up in my DevTools, go to the console, and then click on the uncontrolled toggle, I'm going to get the onTrue and type of toggle.
1:55 All right. The next thing that we're going to need to do is I do not want to call this dispatch if we are controlled. The reason for that is this dispatch is going to trigger a re-render of this component. If I'm being controlled, it's possible that I don't need to re-render, so that would be an unnecessary re-render.
2:13 I don't even care about state changes because I'm not controlling that state value anyway. Whatever I get back from returning this value or specifically from re-rendering this component is probably irrelevant anyway. I want just this onChange to get called so the parent can trigger a re-render if they want to. Then I just will leave this thing alone if I'm being controlled.
2:38 First, we need to figure out whether or not we're being controlled. If we come down here and see the way that this is being used, this is the controlled state where we're passing the on property. If on is neither null or undefined, then we know that we're being controlled.
2:52 Let's come up here. We'll see that on is being used to controlledOn and then being passed to our useToggle as the on option. Then we can come up here and add an on here, but I'm going to alias it to controlledOn so I don't mix up with the on variable that I'm already using.
3:09 Now, I need to determine whether we're controlled or not. I'm going to take this onIsControlled and will have controlledOn != null. That means it's not null or undefined. If it's neither no or undefined, then I know that it is being controlled. If it is being controlled, then this on variable that I create should not be from state.on, but it should be from the controlledOn.
3:36 I can say, "If on is controlled, then we want to get that value from the user," the controlledOn. Otherwise, we're managing it ourselves, and we can grab it from our state. OK, great. Now, we know whether on is controlled. We know in either case what that on value should be.
3:53 The next thing that we want to do is only call this dispatch function if on is not controlled. We're only updating this ourselves if on is not controlled. We can say, "If not on is controlled, then call the dispatch. In either case, we're going to call onChange." We can save that and click here. Those two are staying in sync.
4:17 What's going on here? I click here once, and it toggles like I expect. Then I click again, and it doesn't toggle. Let's remember that the state that we're passing here to our reducer is going to be the state that we're managing ourselves from this React useReducer, but we're not managing the state ourselves anymore.
4:38 Then onState, specifically, is being passed for an initial value, but we actually don't even care about that initial value because we're getting our onState from the controlledOn value.
4:50 When we call this onChange, we're going to call this reducer with the state that we are managing and the action. The state that we're managing never changes. We could fix this really easily by just always calling this dispatch. That will work fine.
5:06 We don't want to call the dispatch because we don't want to trigger a re-render of our own component and potentially wind up in a state synchronization problem. Instead, what we should do is make sure that we're passing our suggested changes.
5:19 That is what we suggest the state should be based on the state that we have. This state variable is not the state that we have. It is the state that we're managing ourselves. We're not just managing this ourselves. We're getting a little bit of help from this property here.
5:34 What I'm going to do is I'm going to combine this state object for any properties that it has that aren't being controlled with the properties that are being controlled, like this controlledOn.
5:45 What I'm going to do is just spread the state object here and then provide the onProperty here, so that it is whatever that on value is, whether it's controlled or not. That will still work. If we save that now, I can click and click, and everything is working swimmingly.
6:04 Let's go ahead and review what's going on here. We've got two toggles, right here and one down here. This one's totally uncontrolled. We're not providing that on value. These two are controlled. We're providing that on value. This is demonstrating that we can programmatically control that on value so that we can change the state of this toggle value here.
6:25 We forward that on to our toggle function component, and then that forwards it on to the useToggle custom hook with the on option and the onChange option up here. Then with this onChange and this on, we use that on to control it on to determine whether or not on is controlled.
6:47 If it is controlled, then we know that the user is trying to provide us with a value for that on, and that's where we're going to get the on for the rest of our custom hook here. If it's not, then we're going to manage that ourselves.
7:00 Then anywhere where we're updating that state, we're going to first determine whether we're being controlled, and if that state is controlled, then we will not update our internally managed state for that particular element of state.
7:13 Otherwise, we will because we are managing it ourselves. In either case, we want to make sure that we call this onChange handler with our suggested state change, as well as the action that triggered the state change to give the consumers of this hook and component as much information as they need to determine what they want to do with that suggested state change.
7:34 One other thing that we're missing here is what happens if somebody doesn't care about the onChange prop and they just leave that out? We're going to get an explosion. onChange is not a function right there.
7:46 I'm going to conditionally call this onChange only if it's defined. That way, people don't have to define this onChange function which is not required to use our custom hook or our function component. If I save that and then click on the uncontrolled toggle again, we're no longer getting that big error.