My Intuition on When to Use Custom React Hooks


Imagine that you’re building an app with a contact form. To start out, you may decide to store the form’s state within the Form component itself, using the useState() hook.

let [name, setName] = useState(defaultValue.name || '')
let [email, setEmail] = useState(defaultValue.email || '')

To ensure reusability, you put this state management code into a useContactModel() custom hook, that can be used whenever a contact form is required within your app.

function useContactModel({ defaultValue }) { let [name, setName] = useState(defaultValue.name || '') let [email, setEmail] = useState(defaultValue.email || '') return { error: name === '' ? 'Please enter a name' : undefined, inputProps: { name: { value: name, onChange: e => setName(e.target.value), }, email: { value: email, onChange: e => setEmail(e.target.value), }, } }
}

Simple, huh? Hooks make this kind of thing super-duper easy. But what if the customer then asks you why you only allow for entry of a single contact, as opposed to a list of contacts? No worries, Contact is a component. Turning it into a list is as simple as creating a ContactList component and rendering Contact a few times.

So far, so good. There’s just one problem: you need to be able to save the contacts. And given that your API requires that you send the entire list at once, you’re going to need to have access to an array containing all that data.

If the state is all stored in child components, then you can’t just access the data from the parent. But no problem — useContactModel() is a hook, so you can just lift the whole thing into the parent component, right?

Waaait a minute… in the above example, useContactModel() is called from a loop. You can’t see the loop, because it’s happening inside of the call to map(). But I can assure you that there are looping shenanigans taking place.

Now as you know, hooks aren’t like normal functions. You can’t use them just anywhere in your code. The above code shouldn’t work, and indeed it doesn’t — just try adding or removing a contact…

#Hooks can’t (always) be lifted up

At times, you can think of custom hooks as components for state management. But this mental model breaks down after a point: while components can be rendered within loops and conditions, hooks can’t. And this has an important side effect:

Custom hooks can’t be lifted out of components that are rendered inside of conditions or loops.

Hooks aren’t like reducers; you can’t just compose them together into one big hook that holds the entire application’s state. Instead, you’ll need to call any useState() hooks closer to where the state is actually rendered — and this is by design:

While hooks do let you reuse stateful and effectful code across components, they still force you to associate that code with a component — and its associated position in the DOM. As a result, hooks come with many of the same constraints as class components:

#Hooks are mixins

Once upon a time, React had a feature called mixins that let you reuse small groups of lifecycle methods and state across multiple components. React’s mixins had a number of issues which caused them to be deprecated, but the need for a way to share functionality between components is as a strong as ever. And that’s what hooks are: a way to share stateful and side-effectful functionality between components.

Ryan Florence actually made this connection before hooks were even announced. But personally speaking, it wasn’t until I started running into the limitations of hooks that the connection really struck.

Ok, so hooks are mixins. It’s a fun connection to make — but does this mean anything in practice? Actually, I’ve found this to be a good way to intuitively understand whether something can should be done with hooks:

  • Should I try and create a Redux-like store that holds my entire app’s state by mixing things into a component? No? Then it doesn’t make sense with hooks either.

  • Should I try to create models that store state for a large form by mixing things into a component? No? Then hooks probably aren’t a great way to do this either.

  • Would mixins be an appropriate way to make a component subscribe to a global store? Yes? Then hooks will work for that too!

And we’re not done yet. There’s one more thing that hooks-are-mixins means in practice, and it’s a biggie…

#Don’t throw out your state manager.

In order to render any global state, you’ll first need to subscribe to it and store it in component state — and hooks are a great way to do this. For an example of how to do this, check out Daishi Kato’s recent guide to creating hooks to subscribe to a Redux store (with Proxies).

So here’s the thing: hooks are an incredible tool. I feel like they’re the missing piece that React has needed since Mixins were deprecated. But like any tool, hooks don’t suit every situation, and the key to getting the most out of them is to understand when to use them — and when to use something else.

Thanks for reading all the way to the end! If you liked this article and want to read more like it, then now’s a great time to join Frontend Armory’s weekly newsletter. It’s free to join — just click → here ←.

#More Reading