So how about more complex states. We've used useState so far. And in our example, let me get back to that example. usePerson. Here, using a useState is actually fine, because we're doing not much complex stuff with state, and there's really not much to worry about, nor no reason to make it more complex. But there are some cases where you do. And one might be that you start storing multiple related pieces of state, which are not maybe completely related, like it's still a person, object, and something else, but it is tied together. And that could be that you're storing a personal object, but you also want to store form state, like, is the form valid? Is the form dirty? Has it been mutated or not? It's not part of the person object, so it shouldn't go in there, but it kind of belongs together. A change to the person will have effects on that dirty state and potentially on that valid state as well.
Well, you could do it this way, adding multiple use state hooks. And in this case it's relatively simple, and we could get away with doing that. And we might do something like this, where the person, the set person or function we expose, is actually not the original set state person function, but one which is a bit more complex and actually changes multiple pieces of state. Works. And in this case, it's simple enough to get away with it. But if your scenario gets more complex, then it's going to be harder to maintain those different pieces of state and to work with it. Insights are a component. We still use that same use-person. This set person is a different function, quite a different implementation, but it never needs to realize. And now, all of a sudden, it gets additional information back, like in this case, is dirty, so we can disable the submit button, for instance, if the form is not dirty and potentially if it's not valid or something. So we could do something like that. That's perfectly fine. And we're actually gonna skip this part of lab because we're a little behind schema, we'll do it in a different way, and that's using useReducer.
So under the hood, useState is actually a special case of useReducer. Now, if you think useReducer, hang on, I've used Redux that uses reducers, right? Well, you're right, and useReducer is very similar to using something like Redux. It's not the same, it's not as powerful as Redux is, but it uses exactly the same principles. And it turns out, if you start looking into the React source code, useState is just a special case with a special reducer, with another hood, just calls into useReducer. So if you look at the actual implementation of hooks, there is no actual hook useStates, which is managed, it's just a little wrapper around useReducer, and useReducer is actually the only really stateful hook which manages components taken triggers re-renders. Now how do you use useReducer? It's actually pretty simple to use. You call useReducer with a reduce function. A reduce function, just like with Redux, is a function which takes two arguments. The current state and the current action being dispatched. How do you dispatch a function? Well, useReducer uses returns tuple, just like useState does, except with useState, it's the second function is a simple function which takes a new state or state mutator function. In this case, you get a dispatch function back and there you can start dispatching actions. The first argument returns, the first part of the tuple is still the current state. So whether it's useState, useReducer, doesn't matter, that's still the current state. Although we use Reducer, that state is typically more complex, but it doesn't need to be. And it's still tied to a component. Redux is similar in structure to Redux but Redux is global state management removed from your component and different components can share the same state by subscribing to it using a useSelector or something like that. But with useReducer, it's still very much tied to a single component. If you start using context and useReducer, you can actually make a sort of a Redux light. Wouldn't advise that. I would still recommend to use Redux if you want to do that. Now, if you want to do that, the first thing you have to do is create a reducer. And this is the first part of that reducer, which is just a bunch of declarations which we need. And the actual implementation is here. So we've got a function, PersonEditorReducer. It takes two parameters, the state and the action. The state is whatever it needs to be for this useReducer. The action is typically an object with hash type and optionally a payload. Usually it does have a payload, but not every action needs one. And the type usually decides what it is. So in this case, we've got a type of set initial person, which loads personal object, which is stored in the payload. And we've got action set property, which actually changes the person by using the payload, which has a name and the actual value. And all of the code in the middle here is because the person could actually be null. So if we start spreading the person as null, we would get a partial person and TypeScript doesn't like that. So this way we work around that. We're actually going to skip this because the next part, we're going to use, create another reducer. So no need to create a second. I'm just going to explain this part. Inside our use person hook or custom hook, instead of use state, we're now going to use use reducer and we pass in that reducer function, which we find. And here's one difference with Redux, you typically set up your initial state by using the first call to the reducer where state is undefined. In this case, the second argument for use reducer is actually your initial state. So we pass in the object. It has a person on that state, which defaults to null. There is a form state, which defaults to not being dirty and being valid. So this kind of sets that up. It returns the state which has this person in the form state, and it has a dispatch where we can dispatch one of those actions. Then, in order to dispatch something, we kind of do something like this. Instead of exporting a setPerson object, now we set, export a setProperty. And in the setProperty, we say, okay, we want to dispatch something in this case, a type of setProperty, and the payload with the name and the value. And the same happens, but that's a little out of screen now with the setInitialPerson dispatch. Now, if you're used to JavaScript and Redux, you might be used to having action creators here, not dispatching objects like this. If you're using JavaScript with useReducer, I would recommend you do exactly the same thing. With TypeScript, you don't really have to, because if I make a typo here in setProperty, then it will actually start complaining because the type is defined as one of these known types. If I go back for a sec here with the different types, I've set slightly lower. Two different actions. A type setInitialPerson and a type setProperty with their associated payload. And the action I'm expecting is either setPerson action or a setProperty action. So if I try to dispatch an object with type, say foo, then it's gonna say, well, foo is not a known type here. It really needs to be one of these two exact strings. And once it knows which of those two it is, it actually knows the shape of the payload. So if I try to dispatch setInitialPayload with the shape of the setProperty payload, I'll get a compile error again from TypeScript saying, well, it doesn't match. That's not a proper action. It should either be this shape or this shape, not some combination or something else that doesn't match that. So TypeScript actually simplifies cases like that. Here, you can see the other dispatch for the setInitialPerson. So like I said, we're going to skip this part. We're actually going to do this with the second part where we create the Foramic clone. Our components changed slightly. Instead of the setPerson here, we get a setProperty.
Comments