In this and the following posts, I want to give a brief and very practical overview of the core React hooks. React hooks are simply functions that you use in functional React components to compensate for the lack of state and lifecycle methods. If you haven't read my explanation of React functional components, start there. But otherwise, let's just dive in.

useState

The most basic and obvious need for functional components to be able to compete with class components is to have access to React's state API. The useState hook provides that access. It accepts a single argument, initialState, that tells React the starting point for the state variable you'll be using. One thing to know up front about useState is that it's often better to call it multiple times for multiple state parameters than it is to pass a single, larger state object (as you would in a class component). React tracks the order of calls to useState (as well as other hooks), so you can call it more than once with different values, and it will keep them straight for you (as long as you don't do so in conditionals). Here's a simple example that allows you to filter a list of names based on user input.

import React, { useState } from 'react';  
const people = ['Ted', 'Albert', 'Mary', 'Jennifer'];

const Filter = () => {  
  // We pass an empty string as our initial filter
  // and we get back a state variable "filter" as
  // well as a function to update that state.
  const [filter, setFilter] = useState('');

  return (
    <div>
      <input type="text" value={filter} onChange={(e) => setFilter(e.target.value)} />
      {people.filter((person) => !filter || person.includes(filter)).map((person) => <div key={person}>{person}</div>}
    </div>
  );
};

export default Filter;  

The useState hooks returns an array of two elements, where the first element is the state variable and the second is a function you can call to update that state variable. Why an array? Because that let's you use whatever variable names you want. If it was an object, destructuring would be less useful because you'd have to rename them inline to avoid name clashes. As in:

const { state: filter, setState: setFilter } = useState('');  
const { state: sort, setState: setSort } = useState('desc');  

That would be a nuisance to type all the time (and in hook-based functional React, you'll be using these methods all the time).

As I mentioned, you can pass non-primitive constructs to useState, and sometimes this really feels like a good idea, but most of the times I've done it, I later regret it. It helps to keep things simple. But as this is a tutorial, here's what that would look like:

const [{ filter, sort }, setState] = useState({ filter: '', sort: 'desc' });  

One problem with this approach is that, unlike setState in class components, useState does not automatically merge new state and old state together. So in that example above, you may be tempted to call setState({ sort: 'asc' }), but if you do, the filter property will be wiped out. It's fairly trivial to write your own hook that does state merging, but I'm saving custom hooks for another time.

Another problem is that the initial state isn't falsy. I find it useful when the initial state is a sort of sentinel value that you can use to prevent unwanted rendering. For example, you may want to augment that first example to include a button that clears the filter. But you don't really need it to show all the time: only when there's something typed in the input. Because the initial filter value is falsy, we can toggle visibility off of it.

import React, { useState } from 'react';  
const people = ['Ted', 'Albert', 'Mary', 'Jennifer'];

const Filter = () => {  
  const [filter, setFilter] = useState('');

  return (
    <div>
      <input type="text" value={filter} onChange={(e) => setFilter(e.target.value) />
      {people.filter((person) => !person || person.includes(filter)).map((person) => <div key={person}>{person}</div>}
      {/* If filter is truthy, render the clear button. If it's falsey, this won't show. */}
      {filter && <button onClick={() => setFilter('')}>Clear</button>}
    </div>
  );
};

export default Filter;  

If you're unfamiliar with React idioms, the { thing && <div /> } pattern is a common way of implementing conditional logic. Because javascript short-circuits Boolean expressions, if thing is false, <div /> won't be rendered.

So how does useState work? When you call it the first time, the piece of state you're initializing doesn't have a value, so it uses the value you pass to it and returns it, along with an update function. When you call the update function, React will store the new value you pass and trigger (enqueue) a re-render of the component. When the component rerenders (when your function reruns), it calls useState again, passing the same initial value, but this time, React has already seen that state parameter and has a value saved for it, so it returns that instead of the initial value. That's why the order of calls to useState matters. That's how React tracks what should be returned for a particular piece of state. Henceforth after that, it will return the most recent value passed to the update function. That is, until the component is destroyed. If you toggle some piece of state elsewhere that causes the component to be removed from the DOM, it will render with the initial value the next time it's added to the DOM again.

And that's really all there is to useState. Besides that last paragraph, it's pretty simple and straight-forward, so it's nice that React abstracts those implementation details away for us. We can focus on what state our components need to maintain and update in order to render useful, stateful information. Come back next time for a look at useEffect.