In this first post of the series, I will be going over the useState
React hook. useState
is one of the more straightforward and widely used hooks, so it makes a good foundation to learn when using React hooks.
Getting Started with useState
useState
is used for local state management in a React function component. To get started with the useState
hook, you need a React function component and you need to import the useState
hook from React.
// import useState hook
import React, { useState } from "react";
// create function component
const Example = () => {
// ...
}
Now we can declare a state variable. When using the useState
hook, we declare the state variable in this format: const [value, setValue] = useState(initialValue);
, where value
is the name of the state variable (we get to name our state variables), setValue
is a function that is used throughout the component to update the value of the state variable, and initialValue
is the initial value of the state variable, if any. Note that setValue
should be written in this format, with lowercase set
followed by the variable, all in camelCase.
Note: This format we are using with square brackets is called
array destructuring
, whereuseState
returns an array with two items and we turn those items into their own variables.
If we want multiple state variables, we would follow this format multiple times, calling useState
multiple times. For example:
import React, { useState } from "react";
const Example = () => {
const [count, setCount] = useState(0);
const [whatToCount, setWhatToCount] = useState();
}
When reading state, we use curly braces in our JSX or use the variable in our JavaScript like any other variable in scope.
import React, { useState } from "react";
const Example = () => {
const [count, setCount] = useState(0);
const [whatToCount, setWhatToCount] = useState("apples");
return (
<p>There are {count} {whatToCount}.</p>
)
}
To update state, we use the updater function that was created, setCount
(or setWhatToCount
). This updater function may be called in a button event, for example, or from a function or other hook in our component.
import React, { useState } from "react";
const Example = () => {
const [count, setCount] = useState(0);
const [whatToCount, setWhatToCount] = useState();
return (
<>
<button onClick={() => setCount(count + 1)}>+</button>
<p>There are {count} {whatToCount}.</p>
</>
)
}
It's important to note that the updater function doesn't update the value right away. If you need to do something with the updated value right away, you will need to use a variable that you set to the new value before you set the state, or you will need to move the work you need to do with the new value to a useEffect
hook that runs when that piece of state changes. I don't want to go too much into useEffect
for this post, but I will touch on it briefly.
Let's take this example:
const Example = () => {
const [count, setCount] = useState(0);
const addCount = () => {
setCount(count + 1);
console.log('count', count); // 0
}
return (
<button onClick={addCount}>+</button>
)
}
In this example, the console will log 0
since it runs before setCount
has completed. If we want to print the new count (1
), we need to do the following:
const Example = () => {
const [count, setCount] = useState(0);
const addCount = () => {
const newCount = count + 1;
setCount(newCount);
console.log('count', newCount); // 1
}
return (
<button onClick={addCount}>+</button>
)
}
Or use useEffect
:
const Example = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('count', count); // 1, after count has changed
}, [count]);
const addCount = () => {
setCount(count + 1);
}
return (
<button onClick={addCount}>+</button>
)
}
Using useState
with functional updater form
The useState
hook is pretty straightforward, but there are cases you will run into as you do more advanced work where using something like setCount(count + 1)
is not going to work well, and you will need to use the functional updater form of the updater function. I will review this more when we get to useEffect
and useCallback
, as that's really when the need to use this will come up, but I wanted to mention it here as well. This also comes up when working with async functions.
In some cases, using something like setCount(count + 1)
will cause infinite re-rendering of your React app, causing it to crash. If you're using this in useEffect
, for example, every time count
changes the app may re-render. If setCount
is running every time count
changes, and count
changes every time setCount
runs, then you will get an infinite looping issue.
This is where the functional updater form of setState
comes in handy.
If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.
So now, if we need to use the previous value of count
when updating it, instead of using setCount(count + 1)
, we would use: setCount(prevCount => prevCount + 1)
. This way there is no dependency on count
. Again, if you're not familiar with useEffect
and useCallback
yet, this will make more sense later in the series.
This functional updater form is also useful when performing async operations. Take the following for example:
const Example = () => {
const [count, setCount] = useState(0);
const handleClickAsync = () => {
setTimeout(function delay() {
setCount(count + 1);
}, 3000);
}
return (
<div>
{count}
<button onClick={handleClickAsync}>+</button>
</div>
);
}
In this example, because of the delay introduced with setTimeout
, the value of count
will be stale by the time setCount
runs, giving us unexpected outcomes. What we want is to use the most recent value of count
rather than the value that count
was when setTimeout
was queued. Using the functional updater form, we can change the example to this:
const Example = () => {
const [count, setCount] = useState(0);
const handleClickAsync = () => {
setTimeout(function delay() {
setCount(prevCount => prevCount + 1);
}, 3000);
}
return (
<div>
{count}
<button onClick={handleClickAsync}>+</button>
</div>
);
}
Working with objects as state values
useState
can hold any kind of value, including arrays and objects. However, because of the way JavaScript handles arrays and objects (by value vs by reference), you may run into issues where you're trying to update an array or object state value and the component doesn't re-render and display the new state as expected. This becomes especially apparent when you're working with an array of objects.
The state updater doesn't merge new values with old values, it overwrites the state with the new value. React uses Object.is
to decide if an object (or array of objects) is different and whether it should re-render. If you try to update an existing object, it is considered the same object, which means React won't re-render. You have to pass a brand new object to change the state.
There are a couple of ways you can update a state value that is an object and ensure that React recognizes the change and re-renders the component. One is to use Object.assign
to create a new object and set the state to use this value. The other is to use the ES6 spread operator to create a new object with the values of the old object and any changes.
For example, the following won't trigger a re-render since the existing state object is being mutated and to React / JavaScript, it's the same object.
const Example = () => {
const [item, setItem] = useState({id: 1, value: ''});
const editItem = () => {
item.value = Math.random() * 100;
setItem(item);
}
return (
<button onClick={editItem}>Change the number</button>
)
}
To make this work, a new object must be created.
Using Object.assign
to create a new object:
const Example = () => {
const [item, setItem] = useState({id: 1, value: ''});
const editItem = () => {
const newItem = {
id: prevValue.id,
value: Math.random() * 100
}
const updatedItem = Object.assign({}, newItem);
setItem(updatedItem);
}
return (
<button onClick={editItem}>Change the number</button>
)
}
Using the ES6 spread operator to create a new object:
const Example = () => {
const [item, setItem] = useState({id: 1, value: ''});
const editItem = () => {
setItem({
...item,
value: value: Math.random() * 100
})
}
return (
<button onClick={editItem}>Change the number</button>
)
}
Working with arrays as state values
To add an item to a state variable that is an array, we want to create a new array and add the item, using the ES6 spread operator. We'll want to do something similar when changing existing items.
For example, don't try to push Array.push
to add new items or directly modify the current array to change values in the array.
Instead, use the spread operator to create a new array using the value of the old array and add the items to the new array:
const Example = () => {
const [items, setItems] = useState([]);
const addItem = () => {
setItems([
...items,
{
id: items.length,
value: Math.random() * 100
}
]);
}
return (
<button onClick={addItem}>Add a number</button>
)
}
We can also do something like this if we want to change the value of an existing item in the array:
const Example = () => {
const [items, setItems] = useState([]);
const editItem = (id) => {
setItems(prevValue => {
const updatedItems = prevValue; // create a new array using the previous value
// modify our new array
updatedItems.map((item) => {
if (item.id === id) {
item.value = Math.random() * 100;
}
return item;
}
return [...updatedItems]; // return our new array with modified values using the spread operator
}
}
return (
<button onClick={() => editItem(3)}>Change a number</button>
)
}
Lazy initialization
From the React docs:
The
initialState
argument is the state used during the initial render. In subsequent renders, it is disregarded. If the initial state is the result of an expensive computation, you may provide a function instead, which will be executed only on the initial render
What does this mean? Normally, a value (which could be returned from a function) can be passed to useState
to set the initial state of the state variable:
const [value, setValue] = useState(someFunction());
This initial state is used during the initial render but not in re-renders (rather whatever the state is set to using setValue
, in this example). useState
is called on every render, but the initial state value is disregarded after the initial render. someFunction()
would still be called on subsequent renders, it's value is just going to be disregarded. If someFunction()
is computationally expensive (maybe it deals with a large amount of data), having it called on re-renders would be bad for performance.
This is where we can use lazy initialization, so the function runs on the initial render only. We can do that by passing a function to useState
that returns the result of the computationally expensive function. For example:
const [value, setValue] = useState(() => someFunction());
Updating state in an unmounted component (dealing with async updates)
Sometimes you'll be wanting to update your state after some async operation completes. However, if the component unmounts before the async operation is complete and the state update has had a chance to complete, you'll get a warning from React about updating state in an unmounted component.
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
Again, this is more of a topic for the useEffect
and useRef
hooks, but I wanted to briefly mention it here. Assuming you have currently cancelled other async operations, like API calls and timers, you can use useRef
to create a variable that will tell you if the component is mounted, and only update your state if it is. For example:
import React, { useState, useRef, useEffect } from "react";
const Example = () => {
const _isMounted = useRef(true);
const [apiRes, setApiRes] = useState();
// Don't worry about this too much yet, but essentially _isMounted.current is set to `false` when this component unmounts
useEffect(() => {
return () => {
_isMounted.current = false;
}
}
// Again, don't worry about `useEffect` too much yet, but here we are performing some async operation and setting state after it's done. Using our `_isMounted` variable, we check if the component is mounted before we perform our state update.
useEffect(() => {
const someAsyncFunction = async () => {
const res = await API.get("/api/some-api-endpoint");
// Here is where we check that our component is still mounted before we make the state change
if (_isMounted.current) {
setApiRes(res.data);
}
}
}, []);
}
Conclusion
That's it for this first post in the All the Hooks Series! Feel free to leave a comment if anything is confusing to you or if you find an error or want to start a conversation about any of the topics covered. We can all learn from each other! π€ Stay tuned for the next post in the series where I will be covering useEffect
.
Top comments (0)