Dave Ceddia
1 Nov 2018
•
6 min read
Up until now, if you started writing a function component and then ran into a situation where you needed to add state, you’d have to convert the component to a class.
Write out class Thing extends React.Component
, copy the function body into the render()
method, fix the indentation, and finally add your state.
Today, you can get that same functionality with a hook, and save yourself the work.
What’s a “hook”? Good question. Learn about Hooks here.
In this article, we’re looking specifically at the useState
hook.
Quick note: Hooks is currently in alpha and not yet ready for production use. The API could still change. I don't recommend rewriting any production apps with hooks at this stage. Leave comments at the Open RFC and check out the official docs and FAQ too.
The useState
hook lets you add state to function components. (I’m going to keep calling these “hooks” but they’re actually functions, and they come bundled with React 16.7 alpha). By calling useState
inside a function component, you’re creating a single piece of state.
In classes, the state is always an object. You can store properties on that object.
With hooks, the state doesn’t have to be an object. It can be any type you want – an array, a number, a boolean, a string, etc. Each call to useState
creates a single piece of state, holding a single value.
This will probably make more sense with an example.
This example is a component that displays some text with a “read more” link at the end, and will expand to show the rest of the text when the link is clicked.
Or if you’re more the video type, watch me build a similar component here:
Read through the comments to see what’s happening here:
// First: import useState, which is a named export from 'react'
// We could alternatively skip this step, and write React.useState
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
// This component expects 2 props:
// text - the text to display
// maxLength - how many characters to show before "read more"
function LessText({ text, maxLength }) {
// Create a piece of state, and initialize it to `true`
// `hidden` will hold the current value of the state,
// and `setHidden` will let us change it
const [hidden, setHidden] = useState(true);
// If the text is short enough, don't bother with the
// buttons
if (text <= maxLength) {
return <span>{text}</span>;
}
// Render the text (shortened or full-length) followed by
// a link to expand/collapse it.
// When a link is clicked, update the value of `hidden`,
// which will trigger a re-render
return (
<span>
{hidden ? `${text.substr(0, maxLength).trim()} ...` : text}
{hidden ? (
<a onClick={() => setHidden(false)}> read more</a>
) : (
<a onClick={() => setHidden(true)}> read less</a>
)}
</span>
);
}
ReactDOM.render(
<LessText
text={`Focused, hard work is the real key
to success. Keep your eyes on the goal,
and just keep taking the next step
towards completing it.`}
maxLength={35}
/>,
document.querySelector(# root')
);
Try out the working example in this CodeSandbox!
With just one line of code, we’ve made this function stateful:
const [hidden, setHidden] = useState(true);
Once that’s done, the “read more” / “read less” links just need to call setHidden
when they’re clicked.
useState
returns an array with 2 elements, and we’re using ES6 destructuring to assign names to them. The first element is the current value of the state, and the second element is a state setter function – just call it with a new value, and the state will be set and the component will re-render.
const [hidden, setHidden] = useState(true);
But what is this function doing, really? If it gets called every render (and it does!), how can it retain state?
The “magic” here is that React maintains an object behind the scenes for each component, and in this persistent object, there’s an array of “state cells.” When you call useState
, React stores that state in the next available cell, and increments the pointer (the array index).
Assuming that your hooks are always called in the same order (which they will be, if you’re following the [Rules of Hooks](https://daveceddia.com/intro-to-hooks# rules-of-hooks)), React is able to look up the previous value for that particular useState
call. The first call to useState
is stored in the first array element, the second call in the second element, and so on.
It’s not magic, but it relies on a truth you may not have thought about: React is the one calling your component, so it can set things up beforehand. And moreover, the act of rendering a component is not just a function call. JSX like <Thing/>
gets compiled to React.createElement(Thing)
– so React is clearly in control of how and when it is called.
For a play-by-play of how this “call order” magic works, see my [Intro to Hooks post](https://daveceddia.com/intro-to-hooks# the-magic-of-hooks).
Let’s look at another example: updating the value of state based on the previous value.
We’ll build a, uh, “step tracker.” Very easy to use. Just like a Fitbit. Every time you take a step, simply click the button. At the end of the day, it will tell you how many steps you took. I’m working on securing my first round of funding as you read this.
function StepTracker() {
const [steps, setSteps] = useState(0);
function increment() {
setSteps(steps => steps + 1);
}
return (
<div>
Today you've taken {steps} steps!
<br />
<button onClick={increment}>I took another step</button>
</div>
);
}
This example looks a lot like the last one. First, we’re creating a new piece of state with useState
, initializing it to 0. It returns the current value of steps
(0) and a function for updating it. We have an increment
function to increase the step counter.
You’ll notice we’re using the functional or “updater” form of setSteps
here. We could just call setSteps(steps + 1)
and it would work the same in this example, but I wanted to show you the updater form, because it’ll be useful in case your update is happening in a closure which has closed over the old (stale) value of the state. Using the updater form ensures you are operating on the latest value of state.
Another thing we’ve done here is to extract the increment
function, instead of inlining the arrow function on the button’s onClick
prop. We could have written button this way and it would’ve worked just the same:
<button onClick={() => setSteps(steps => steps + 1)}>
I took another step
</button>
Remember that state can hold any value you want! Here’s an example of a list of random numbers. Clicking the button adds a new random number to the list:
function RandomList() {
const [items, setItems] = useState([]);
const addItem = () => {
setItems([
...items,
{
id: items.length,
value: Math.random() * 100
}
]);
};
return (
<>
<button onClick={addItem}>Add a number</button>
<ul>
{items.map(item => (
<li key={item.id}>{item.value}</li>
))}
</ul>
</>
);
}
Notice we’re initializing the state to an empty array []
, and take a look at the addItem
function.
The state updater function (setItems
in this case) doesn’t “merge” new values with old – it overwrites the state with the new value. So in order to add an item to the array, we’re using the ES6 spread operator ...
to copy the existing items into the new array, and inserting the new item at the end.
Let’s look at an example where state is an object. We’ll make a login form with 2 fields: username and password.
I’ll show you how to store multiple values in one state object, and how to update individual values.
function LoginForm() {
const [form, setValues] = useState({
username: '',
password: ''
});
const printValues = e => {
e.preventDefault();
console.log(form.username, form.password);
};
const updateField = e => {
setValues({
...form,
[e.target.name]: e.target.value
});
};
return (
<form onSubmit={printValues}>
<label>
Username:
<input
value={form.username}
name="username"
onChange={updateField}
/>
</label>
<br />
<label>
Password:
<input
value={form.password}
name="password"
type="password"
onChange={updateField}
/>
</label>
<br />
<button>Submit</button>
</form>
);
}
Try it out in this CodeSandbox.
First up, we’re creating a piece of state and initializing it with an object:
const [form, setValues] = useState({
username: '',
password: ''
});
This looks a lot like how you might initialize state in a class.
Then we have a function to handle the submission, which does a preventDefault
to avoid a page refresh and prints out the form values.
The updateField
function is more interesting. It uses setValues
(which is what we called the state updater) and passes an object, but it must be sure to include the existing state with ...form
if it doesn’t want to overwrite it. Try taking out the ...form
line and see how the form behaves.
At the bottom we have a pretty standard-looking chunk of JSX to render the form and its inputs. Since we’ve passed a name
prop to the inputs, the updateField
function can use it to update the appropriate state. This way you can avoid having to write a handler function for each field.
There’s another hook called useReducer
which is more suited to managing state with multiple values, and we’ll be looking at that in the next post.
Be sure to sign up so you don’t miss it! This week is Hooks Week and you’ll get an article about a new hook each day.
A hook a day keeps the falling behind at bay.
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!