Hey guys. In this lesson, I want to talk about ways of managing more complex state. And what do I mean
by complex? I mean managing the state of JavaScript objects where you might have to retrieve the previous
value of the object. For example, I added an input to the website
and I want to be able to greet the user using both of these pieces of information so that when they
type their first name, it says, "Hello", and then adds the first name to the h1
and then when they add their last name, the first name is still there and it's still stateful.
And the last name also updates and is independent from the first name.
Let's think about how we might do this.
And I want you to go ahead and fork the starting sandbox. And notice how we've got a very simple form
with two input's and a Submit button.
I want you to have a think about how you might, using the knowledge that you currently have, achieve this
result.
Pause the video, give it a go and then come back and we'll go through it together. And then I'll show you
some other ways of doing it too.
All right.
So I hope you took this opportunity as a little bit of extra practice because this is the only way that
you going to get familiar with it, and start turning it into your own muscle memory.
The first thing I need is some sort of variable that can hold the value that's being typed into both
of these inputs.
So I'm going to create a stateful const which I'll call fName for first name. And then I'm going
to name a method that I'm going to use to set this first name const,
so call it setFName. And then of course I have to call the useState method in order to make this const
change and update and be stateful.
And remember this of course comes from the React modules
so we have to import it too.
And then when I call this useState method I get to provide an initial state.
So I'm going to set it to just be an empty string because I'm going to use that as the value of my input,
and this is of course going to correspond to that constant fName.
And I'm also going to add it to the end of my h1,
so fName is also going to be used here. And then I'm just going to repeat this process for my last name.
So change this to lName, setLName, and the initial states also are going to be an empty string.
And then we're going to use that to control these inputs so that the value matches up with our state.
And I'm also going to add it to the end of my h1.
So now all I have to do is to call setFName and setLName when they change.
So let's go ahead and add the onChange prop to our inputs and we're going to make it call a function
and we call this updateFName(), and get hold of the event and then call it here.
And when this gets triggered we get past the event,
so whatever happened to this input. And we can create a new const which is going to be called firstName,
and we're going to set it to the event.target.value. And then we're going to update our
fName const using the setFName() method, and we're going to pass it this new value.
So now when I go ahead and write my first name in here, you can see it update.
And if I copy this and just update it for my last name, change it in all the places and also add the
onChange to my input, now you can see that when I type my first name it adds the first name.
When I type my last name it has the last name. And that's the solution to the challenge.
But you might have noticed through this experience while it's great practice, it's a little bit painful,
right?
We have so many functions and we have two separate const's.
Even though, if you think about it, when you create a Contact app or when you create some sort of input
form, these pieces of information probably should be associated with each other,
right?
The first name and the last name probably should belong inside the same JavaScript object.
So how can we do that?
How do we manage a more complex piece of state?
Well we can simply use useState but instead of storing a simple value, we can actually get it to store
an object as well.
Let's go ahead and delete all of the duplicated bits of code for the last name.
And then I'm going to change this constant to just be called fullName. And then we're going to change
this to setFullName and then we get to specify the initial state. The initial state is going to be
an object,
and the first key is going to be fName with the value being nothing, an empty string.
The second key is going to be the lName with
again nothing as the value.
So now our full name is storing an object.
And when we call setFullName ideally, we want to set this to a new object.
So let's get rid of this function and also update the code in our return statement.
So instead of fName, this is going to become a fullName.fName and the same is going to happen
with our last name because we're fetching it out of this object now. And then we're going to do the same
for values here as well.
And then on the onChange, instead of calling two separate methods, I'm going to get it call the same
method which we'll call, handleChange(). And I'm going to call that both here and here.
So the idea is that when either of these inputs are changed, they're going to call the same function
passing over the event that calls this change. And then inside this function, we're going to get hold
of the new value which we'll just call newValue, and I'm going to set it to event.target.value,
but then, I want to somehow be able to get hold of the previous value of the full name,
so this object essentially, so that I can add to it the parts that have been changed.
So if this input changes, then I only want to update the value of the fName, and if this input changes
I only want to update the lName.
The other part should stay as it was. So how could we know which input actually triggered the handleChange
?
Well, notice how inside our inputs, we've got the property or attribute called name
and this sets it to a particular value that we can check for when we get hold of the events in our handleChange
.
So I could create a const called inputName and set it to event.target.name.
So now if we go ahead and log these things, the newValue and also log our inputName,
and now if I start typing in my first name, you can see that the input name is logged as fName which
corresponds to the name of the input that triggered the event, and the new value is equal to whatever
it is that I typed inside.
But if I type something in the last name
notice how the input name changes
and it also logs what I typed.
But notice how these inputs are not actually showing what I'm typing,
even though you can hear that I'm definitely typing in earnest. What's going on here?
Well, remember that these are controlled components. So their value,
so what they're showing inside here, is set to the fullName.fName,
and we haven't actually got a way of setting that full name yet.
So it's always showing the initial value which is just the empty string.
So while we're testing, I'm just going to go ahead and comment out these two lines so that we turn this into
an uncontrolled component.
And if you're really curious, you can actually Google for what the difference is with React controlled
and uncontrolled components.
In this case in the end we definitely want a controlled component.
We want the value and the state to all be equal to the same thing. But just for now, while we're testing,
notice how now I get to actually write stuff and the thing that I type in here, say Angela, is going to
be the final new value
once I'm done typing. And the fName corresponds the input where it came from. So I can use these two
pieces of information to create my new object,
right? And you might think that it's as simple as saying well, if the inputName is equal to fName, well,
in this case, then this new value is going to correspond to what the user typed in here.
So in that case, I'm going to call setFullName and inside here, I'm going to pass over the key as
fName and the value as the newValue. And then else-if the input name was equal to lName, well then
that means they must have typed something in here, and in that case, I'm going to setFullName to equal
to a new object with lName
and then the value being the newValue. And then we're just missing one equal sign there.
So you might think that this would work, but some of you might realize what will happen if I do this.
Let's see what happens, if I start typing in the first name field,
you can see that this fName is being rendered inside here in our h1,
but watch what happens when I start typing in my lName.
You can see that it's deleted the previous value of the first name in my object.
So what's actually going on here?
Well this is another good opportunity to use the React Dev Tools. The Dev Tools inside CodeSandbox does
work but it can take a little while to load, and sometimes it doesn't want to,
so what I recommend to do instead, is to pop it out in a new window and open up your Chrome Developer
Tools, and then go to the React section.
So now if we select the app, you can see in addition to props we can also see our Hooks. And if we expand
the state of our object, you can see it starts out with fName equal empty string, lName equal empty
string,
but when I start typing here, you can see that it's deleted the lName key and value out of this object
and it's only kept the fName.
And then when I do the same with my last name, you can see it's deleted the first name property and it's
only kept the last name.
Essentially, what we're doing is each time we're calling setFullName, we're replacing this entire object
with an object that only has one property, either the fName or the lName,
and this is not what we want. What we want to do instead, is we want to get hold of the previous value
of this fullName object and then only add to it the parts which have been changed. In order to do this,
instead of just calling setFullName, or whatever it is that you decided to name the function that's
going to update or change this full name, instead of simply just adding the new value inside these parentheses,
we can also pass it a function. And I'm going to use an arrow function in this case.
And when setFullName is called, it can get access to the prevValue.
And if I simply just log this prevValue in here, you'll see what it's actually doing.
So whenever I start editing my first name input, it's going to call handleChange(),
It's going to pass the event that led to this change.
I'm going to have a newValue and I'm going to have an inputName.
Now when this happens I call setFullName and when setFullName gets called, I'm passing in a function.
That function is going to get hold of the prevValue of fullName.
So as soon as I do this, you can see that it prints an object with fName empty, lName empty.
And this is really the key. As we type into our inputs, React will re-render our App component, but our
app has state, right? In the form of the fullName JavaScript object. And React remembers the value of
this object.
This means that we can make use of this remembered value as we're updating our first name or our last
name.
So then the question really becomes, well how can we use a function that gives a different output depending
on the previous value?
Well, we could say if the inputName, so the name of the input element, was equal to fName,
well in this case, we're going to return a new object where the fName is corresponding to the newValue
and then the lName is corresponding to the prevValue's lName.
And this way we create a new object and only update the part that has been updated by the user.
And we can complete this by adding an else-if, input name is equal to lName, and make sure that we actually
spell this exactly the same as the names in our inputs here,
well in this case, that means the user must have edited this field and I'm going to return a different
object. The fName in this case is going to be using the prevValues.fName, and the lName is going
to be using the newValue.
So now if we go over to our app and I start typing my first name, and we look at our App component, you
can see this object has its first name property replaced with whatever I typed here.
And if I edit my last name, it's now replacing the previous value of last name with my new value,
but it's using the previous value for the first name. The last thing to remember to do is just do uncomment
these lines so that the values of our inputs can correspond to the latest state, so that we control this
input using our state. Now I've tried to keep this code as expressive as possible so that you can actually
see what's going on,
but in reality, we could probably write this code in a much simpler way. To start with, instead of using
our const newValue equals event.target.value, inputName being event.target.name, we could
in fact use our object destructuring. So we could create a new const, open up a set of curly braces
to create a new object literal where we tap into the value property and the name property and we set
it to equal event.target. And this will replace both of these things and we now get to use name, name,
value, and value.
And that makes it already a lot simpler looking, right? Now
just as a word of warning
make sure that in the future when you're creating your own apps, or in any of the exercises or challenges,
don't try to access the event or anything related to the event, inside a stateful setter.
So when we set fullName we're trying to update the state of this constant fullName. And inside here,
if you try to access event, let's say instead of getting the name we actually try to get event.target.name
even though this seems like valid code, what's actually going to happen is you're going to get
an error inside the console and it's going to warn you about synthetic events being reused.
Now if you're curious, I'd recommend heading over to the React docs and having a read about these synthetic
events. But essentially, what it boils down to, is the fact that when these inputs are passing an event
through these event listeners, the event that you actually get hold of is not a real event, it's a synthetic
event that React has created. And you must never try to access those events when you're trying to use
one of these stateful setters.
So inside here, you should not ever use event,
you should always have it outside, somewhere over here for example, and I recommend just getting used
to using destructuring like this when you're trying to access the new value or the name of the inputs.
Now we've covered quite a lot of things in this one lesson and there's a lot of complex topics being
thrown around here.
So if it's at all confusing, I really recommend to just delete everything
go back to the starting version of the sandbox and try to see if you can get this behavior created by
yourself.
That way, you might encounter some problems and you might have to debug some of the issues, but at least you'll
be able to make this knowledge your own.
Now in the next lesson, I've got a challenge for you.
So once you're ready and you're happy that you understand what's going on in this lesson, then head over
there and complete the challenge.