Kent C. Dodds: Here we have a very simple app that's simply rendering a button and renders out the mode that we're getting from useDarkMode. When you click on it, we toggle that mode and so as we click it goes dark and then light. We're applying different styles based on the current mode.
Here we have useDarkMode. We're initializing our state based on whether or not the user prefers dark mode or if they've already set it in local storage, then we'll prefer that. Then we also register this side effect to add a listener for that mediaQuery so that we can update the dark or light mode based on what the user selection is. Then we have a React.useEffect for updating localStorage every time we change the mode.
Here we're using useState, but let's go ahead and try using useReducer instead. I'm going to say state, dispatch = React.useReducer(). We're going to make a typical reducer. We'll make a function called darkModeReducer() that'll take a state and an action here. Then we'll switch on that action.type and we'll have a case for when the media query changes.
In that case, we'll return all the state and we'll specify the mode to be action.mode. We'll receive that mode from the action, and we'll have another case SET_MODE for when someone calls set directly. Return a spread of the state and the mode is going to be action.mode here as well. We'll need a default just in case we end up in a typo and we'll throw a new Error('Unhandled action type: $(action.type).
That's our reducer for this useReducer. Then our initial state value is going to be mode: 'light' but we'll want to do some lazy initialization, so we have that same thing that we're doing here. Let's make an initialization function, so function init(). This is going to do basically all the same logic, so I'll just copy this, and we'll initialize this to an object that has a mode and that will be set to this.
Looks like we're going to need to pull this preferDarkQuery out. I'll just stick it up here at the top and then we'll pass this init as the last argument to our useReducer call.
Then let's go ahead and grab the mode from that state and we'll get rid of all of this. We're going to want to return the exact same API that we returned before. We'll just have that mode right there and then we're going to need to make a setMode function. We'll need that function to be memorized so people can add it to a dependency list.
We'll say setMode = React.useCallback(). Our callback will take a new mode and that we'll just call dispatch where the type is SET_MODE and the mode is the newMode. Then we'll need an empty array list here because we have no dependencies in there.
Then for this one, instead of calling it SET_MODE we're going to do dispatch and we'll have our type be MEDIA_CHANGE and our mode is going to be that mediaQuery.matches ternary there. We'll save that and everything should be working.
I don't know about you, but this seems like a lot more complicated and a lot more boilerplate than what we had before. I don't see a lot of benefits of using useReducer in this way over useState, but you can use useReducer in a little bit simpler way. Let's go ahead and re-factor to that. We don't need all this nonsense here.
Let's go ahead and instead of having this darkModeReducer and having state as an object, what if we just had the state as a string like we had before? We'll say this is our mode and this is our setMode. If we write the reducer in a way to make our dispatch behave in the same way setMode did, then this should work just fine.
Let's go ahead and take our prevMod and our nextMode and that'll be function where we take the typeof the nextMode. If that's a function, then we'll call nextMode with the prevMod. Otherwise, we'll just assume that they gave us the nextMode.
Then for our initialization, we don't want to initialize that to an object, we're going to initialize it to the value that we get from localStorage or from this ternary here. Then from here, we no longer need to pluck anything off of state, we just get it that way. Instead of calling dispatch, we'll call setMode just like we were before. Then we no longer need to create this setMode function.
If we save that, everything is working just fine, and we no longer need this reducer.
That's considerably simpler than our first attempt, but it's still not simpler than the regular useState that we had before. When you have a single element of state that you're managing, you're almost certain to be better off using useState rather than useReducer. I'm going to restore what we had before and leave it at that.