Reducing useStates in React
How to reduce the amount of useState hooks in React for cleaner components
Albin Groen
Posted 2021-05-18
I've written a lot of React.js code lately. It's my go-to framework for building frontend-heavy web applications and has been for a couple of years. After the release of hooks, I, and probably you too, have been using the useState frequently for keeping track of state in your application.
Around 6 months ago I started to try out using Redux again, specifically for some projects at work. I'd been using it at a previous company and didn't have particularly good memories of it. I thought I'd give it a shot though. Although we encountered the fact that Redux requires a large amount of boilerplate, we stuck with it, and it helped us in our projects.
Nowadays though, I'm not that big of fan of Redux anymore due to its large amount of setup required when wanting to implement new functionality, as well as its high barrier of entry for new developers. I also tend to stay away from using the useState hook all that much. I thought I would go through a couple of methods that you can use when building relatively straightforward CRUD applications that allow you to leave the useState hook at the shelve.
Data fetching
When it comes to data fetching, instead of using multiple useState hooks to keep track of data, errors, and loading states, I tend to use libraries like SWR or react-query for this.
With useState hooks
const [isLoading, setIsLoading] = useState<boolean>(false)
const [error, setError] = useState<string>(undefined)
const [todos, setTodos] = useState<Todo[]>([])
useEffect(() => {
setIsLoading(true)
axios.get("/api/todos").then((res) => {
setIsLoading(false)
setError(undefined)
setData(res.data)
}).catch((err) => {
setError(err.response.data || err.message)
setIsLoading(fasle)
})
}, [])
Without useState hooks
const { data, isValidating, error } = useSWR('/api/todos')
As you can see, using something like SWR or react-query is a great improvement here. You don't have to manually manage all these different states. It's really nice not having to think about when to should reset errors or loading states.
Data mutation
When it comes to data mutation I'm not entirely convinced that reducing the amount of useState hooks and replacing it with something like Formik is a good trade-off, but it's something that I've been doing more lately, and I quite like it.
With useState hooks
const [isLoading, setIsLoading] = useState<boolean>(false)
const [error, setError] = useState<string>(undefined)
<Button
loading={isLoading}
onClick={() => {
setIsLoading(true)
axios.delete('/api/users/1').then(() => {
setIsLoading(false)
refetchTodos()
}).catch((err) => {
setError(err.response.data || err.message)
})
}}
>
Delete todo
</Button>
Without useState hooks
<Formik
onSubmit={() => {
return axios
.delete("/api/users/1")
.then(refetchTodos)
.catch((err) => {
alert(err.response.data || err.message);
});
}}
>
{({ handleSubmit, isSubmitting }) => (
<form onSubmit={handleSubmit}>
<Button loading={isSubmitting} type="submit">
Delete user
</Button>
</form>
)}
</Formik>
As you can see, here everything pretty much gets off-set to a single group of elements, which I really like. I also appreciate the method of working with forms here, since that's how it originally used to be done back in the days.
Forms
The place where you can save the most amount of code is when it comes to forms. I've also thought the way we use useState hooks with forms is a little bit excessive. Take a look for yourself at how much more maintainable we can make this component by using Formik.
With useState hooks
const [isLoading, setIsLoading] = useState<boolean>(false)
const [description, setDescription] = useState<string>()
const [isDone, setIsDone] = useState<boolean>(false)
const [title, setTitle] = useState<string>()
const [error, setError] = useState<string>()
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
axios
.post("/api/todos", {
description,
isDone,
title,
})
.then(() => {
setIsSubmitting(false);
refetchTodos();
})
.catch((err) => {
setError(err.response.data || err.message);
setIsSubmitting(false);
});
}}
>
<input value={title} onChange={(e) => setTitle(e.currentTarget.value)} required />
<textarea value={title} onChange={(e) => setTitle(e.currentTarget.value)} required />
<input type="checkbox" onChange={(e) => setIsDone(e.target.checked)} checked={isDone} />
<Button type="submit" loading={isLoading}>Submit</Button>
</form>
Without useState hooks
<Formik
onSubmit={(values) => {
return axios
.post("/api/todos", values)
.then(refetchTodos)
.catch((err) => {
alert(err.response.data || err.message);
});
}}
>
{({ handleSubmit, handleChange, values, isSubmitting }) => (
<form onSubmit={handleSubmit}>
<input name="title" value={values.title} onChange={handleChange} required />
<textarea name="description" value={values.description} onChange={handleChange} required />
<input name="isDone" type="checkbox" onChange={handleChange} checked={isDone} />
<Button type="submit" loading={isSubmitting}>Submit</Button>
</form>
)}
</Formik>
This is where it really shines to me. It's really nice having Formik handling everything from the submitting state automatically by returning a promise from onSubmit function, to it handling the updating of the field values automatically. I really enjoy this approach and will probably keep using it for a long time.
Overall, I think reducing the amount of useState hooks we use in React can be to a pretty great advantage. It's easier to build cleaner, and especially more maintanable components that way. Although, sometimes it might now be worth the trade off, but that's up to you to decide.
If you want to more content like this, you can follow me on Twitter @albingroen.