Working With More Complex Versions Of State
#
Learning ObjectivesAfter this lesson you will be able to:
- Create multiple instances of useState
- Work with objects and arrays as state values
- Use
Object Destructuring
to copy data from a previous version state
#
FramingWe've all had to learn new skills in our lives. The fact that your sitting in the class right now is an example of just that.
At first, learning something new involves keeping track of small bits of information. Take for instance learning a skill like riding a 🚵♂️ bike. A child often starts with training wheels so that they can focus on developing the skills to peddle and steer. They might even attempt standing up to provide more downward force into the peddle.
Soon the training wheels are removed and they must learned to balance which requires that they adjust their steering and how they peddle while all the while paying attention to their surroundings.
Building our app falls along the same path. At first we might only need to keep track of a single number. But then we may need to keep track of how many times that number has been incremented/decremented or how many times the user has reset the number back to 0. We may need to keep track of what time and day each event occurred.
All of it requires a more organized approach to aggregating the data.
#
More Than One StateSo far, we've only leveraged one instance of useState
to keep track of a single number.
Several requirements has been introduced to the app that require:
- keeping track of how many times the reset button was clicked
- color coding an elements background based on which button was clicked
- keeping a record of which button was clicked and when
There are several ways that we can keep track of additional state values:
- create an additional instances of useState
- aggregate all state values into a single object
#
Multiple Instances of useStateLet's continue to work through the Counter Component and take the first approach of adding an additional instance of state.
Here is the Counter CodeSandbox Starter code with the changes from the previous lecture.
https://codesandbox.io/s/rctr-9-8-20-w02d04-counter-starter-71oc2?file=/src/Counter.js
Note: Feel free to fork this CodeSandbox if you missed last class or want a fresh codebase to start with.
Just as a quick review here is what we need to do:
- create a new instance of
useState
- update the
JSX
to include a new HTML element to show the state value - calls the
setState
function to increment that value
Let's start with creating a new instance of useState
that will keep track of how many times the reset button was clicked.
const [resetCount, setResetCount] = useState(0);
#
⏰ Activity - 2minNow that we have added a new instance of useState so let's take a look at React DevTools
and confirm state is present and accounted for.
If you highlight Counter
it should look like the following:
Try incrementing the value a 4x times and you should both values eventually update.
One thing to note here is that each instance of state is merely represented by the word State
and not which specific state it represents.
Since we will be keeping track of how many times the reset button is clicked it makes sense to call setResetCount
in handleReset
.
const handleReset = () => { setCount(0); setResetCount(resetCount + 1);};
And finally let's add some JSX to display the current value in the DOM.
<span>Current Count: {count}</span> <span>Reset Count: {resetCount}</span>
The Counter app should look like the following now:
Try clicking on +/-
a few times and then the Reset
button. Confirm that the functionality is working as expected.
#
⏰ Activity - 3minLet's try a quick activity that should help you apply some of these concepts.
- Add a third instance of state called
color
and set it's initial value towhite
- When the user increments the counter set color to
lightgreen
- When the user decrements the counter set color to
pink
- Add an inline style to the
Current Count
span that will apply the background color assigned in state.
Here is the basic syntax of writing an inline style.
<span style={{ css-property-name: value }}></span>
👍 Click on the thumbs up when you've implemented the solution
Solution
const Counter = () => { const [count, setCount] = useState(0); const [resetCount, setResetCount] = useState(0); const [color, setColor] = useState("white");
const handleIncrement = () => { count === 3 ? handleReset() : setCount(count + 1); setColor("lightgreen"); };
const handleDecrement = () => { count === -3 ? handleReset() : setCount(count - 1); setColor("pink"); };
const handleReset = () => { setCount(0); setResetCount(resetCount + 1); };
return ( <> <span style={{ background: color }}>Current Count: {count}</span> <span>Reset Count: {resetCount}</span> <section> <button onClick={handleIncrement}>+</button> <button onClick={handleDecrement}>-</button> <button onClick={handleReset}>Reset</button> </section> </> );};
#
Using an Object as the State ValueAdding another instance of useState works in this example and it seems like an intuitive way to go.
There is however one small flaw in our design but it won't become evident until we try and call setColor
in both handleIncrement/handleDecrement
and then again in handleReset
.
Let's add setColor
to handleReset
and see this in action, or better yet, not in action.
setCount(0);setResetCount(resetCount + 1);setColor('white');
#
⏰ Activity - 1min- Try incrementing the value more than three 3️⃣ times
- This should do the following:
- resets count to
0
- if clicked three 3️⃣ times increments the reset counter by
1
- reset the background color to white...
or does it?
- resets count to
As our testing would have confirmed the color is not reset to white.
The issue is that we are calling setColor
in 2
different functions in sequence and React will ignore the second update call.
In order to set the value to white React would need to first re-render
and then call setColor
. This requires the help of an additional hook called useEffect
and something that we will cover in a future lecture.
So having multiple unique instances of useState
can work but it also has it's limitations.
Also, having multiple instances starts to make the code seem more bloated.
A better approach would be to create one single instance of useState that contains all the values. The most optimal data type to use for this is an object
.
#
One Object To Rule Them AllLet's organize all our data into one object and comment out the previous instances of useState.
⭐ Name the initial state based on what it contains
For the sake of this demo I'm calling it counterObj
and only to convey that this state is now set to an object. If this was a real app we would probably call it something more like counter
or counterState
.
const [counterObj, setCounterObj] = useState({ count: 0, resetCount: 0, color: "white",});
Of course this requires some refactoring to use the new object.
Let's start with handleIncrement
. Since it's only responsible for incrementing the count and assigning the color let's make sure it updates those two values.
counterObj.count === 3 ? handleReset() : setCounterObj({ color: "lightgreen", count: counterObj.count + 1, });
And now update the JSX with the following:
<span style={{ background: counterObj.color }}>Current Count: {counterObj.count}</span><span>Reset Count: {counterObj.resetCount}</span>
#
⏰ Activity II - 2minNow that we have updated useState to use an object let's take a look at React DevTools
and see what state looks like now.
If you highlight Counter
it should look like the following:
Try incrementing the value just once and you should see state update to the following:
❓ Question: What change has occurred to our object?
#
The ...Spread OperatorSo it seems that state lost one of its keys. In this instance when we called setCounterObj
and assigned it a new object we excluded one of the keys.
This is due to the following rule:
🚔 State is never directly edited and must always be overwritten with a new value
In order to guarantee that all the previous keys are present in the new object we must use the ...spread
operator.
⭐ Always use ...spread
operator to copy object and array values to the new state
Let's update our code to use the ...spread
operator:
counterObj.count === 3 ? handleReset() : setCounterObj({ ...counterObj, color: "lightgreen", count: counterObj.count + 1, });
#
⏰ Activity III - 2minNow it's your turn.
- Update both
handleIncrement
andhandleRest
to use the new implementation of state. - Click on the thumbs up 👍 when you've implemented the solution
Solution
const Counter = () => { const [counterObj, setCounterObj] = useState({ count: 0, resetCount: 0, color: "white", });
const handleIncrement = () => { counterObj.count === 3 ? handleReset() : setCounterObj({ ...counterObj, color: "lightgreen", count: counterObj.count + 1, }); };
const handleDecrement = () => { counterObj.count === -3 ? handleReset() : setCounterObj({ ...counterObj, color: "pink", count: counterObj.count - 1, }); };
const handleReset = () => { setCounterObj({ color: "white", count: 0, resetCount: counterObj.resetCount + 1, }); };
return ( <> <span style={{ background: counterObj.color }}> Current Count: {counterObj.count} </span> <span>Reset Count: {counterObj.resetCount}</span> <section> <button onClick={handleIncrement}>+</button> <button onClick={handleDecrement}>-</button> <button onClick={handleReset}>Reset</button> </section> </> );};
#
Using an Array as the State ValueSo we were just informed that the owner of the app would like to capture, not only how many times each increment/decrement button was clicked but the time it was clicked as well.
Now we have the following two 2️⃣ options to implement this requirement:
- add a new key:value to our existing state
- create a new instance of state
Since the goal of this section is to demo how to set an array as the value for state let's opt to create a new instance of state.
const [buttonClicks, setButtonCLicks] = useState([]);
#
Update HandlersIt makes sense to call the setButtonClicks
function in both handler functions and add a new object that contains the buttonType and date.
const handleIncrement = () => { // rest of code... setButtonCLicks([{ buttonType: "increment", date: new Date() }]);};
const handleDecrement = () => { // rest of code... setButtonCLicks([{ buttonType: "decrement", date: new Date() }]);};
If we examine React DevTools
we should see the new state in place.
Let's click increment
and see state update.
And now decrement
Hmmm...It seems we've lost the previous value. This has to do with the fact that were overwriting the previous array with a new one that only contains a single object based on the last instance of a button click.
So just like the counterObj
we need to copy the previous elements in the array to a new array. For that we can use the ...spread
operator once again.
const handleIncrement = () => { // rest of code... setButtonCLicks([ ...buttonClicks, { buttonType: "increment", date: new Date() }, ]);};
Let's increment several times and see if this works.
#
⏰ Activity III - 1minWay back machine time...
Before the introduction of the ES6 ...spread
operator what other techniques did you use to create a copy of an array?
Slack you answer in a thread created by the instructor.
#
Bonus - Using A Style ObjectSo we've used inline styles to assign the background color property
<span style={{ background: counterObj.color }}>Current Count: {counterObj.count}</span>
This works best if there is only a single value to assign but if there are more properties then it will start to bloat the JSX and make it harder to read. Another way to structure the css is to use an object.
const styles = { background: counterObj.color}
Let's use the styles object.
<span style={styles}>Current Count: {counterObj.count}</span>
Let's keep in mind that background
is short for several properties in css so let's be very specific about which rule we are targeting, which is background-color
.
Let's update the property name to target that rule specifically.
const styles = { background-color: counterObj.color;}
That however won't work and we should get the following:
We must keep in mind that there are other rules, JavaScript specific, that React must follow as well. And in the case of css property names they must be written in camel case.
Let's update the previous rule and add one more to drive the point home.
const styles = { backgroundColor: counterObj.color, fontFamily: "cursive",};
We should end up with the following error:
const styles = { backgroundColor: counterObj.color, fontFamily: 'cursive'}
#
Recap#
Here is the solution code from our in-class buildhttps://codesandbox.io/s/rctr-9-8-20-w02d04-counter-startersolution-gybkx?file=/src/Counter.js
You've learned quite a bit about React state. Let's take a minute to review the rules
and best practices
🚔 - Rules
Here are some of the rules that govern the useState Hook:
- never update the state value directly
- always use the
setState
function to update state - since state is never directly edited it must always be overwritten with a new value
⭐ - Best Practices
A few best practices when assigning variable names are:
- Use
Array Destructuring
when initializing the state variables - Name the initial state based on what it contains
- Use the same name for the function but precede it with
set
- Use a the callback function version of useState if you need to reference the previous version of state
- Give thought as to what needs to be in state and how that state should be organized and structured