Opinionated React: Use State Colocation
faraz ahmad
Posted on April 23, 2020
Intro
I’ve been working with React for over four years. During this time, I’ve formed some opinions on how I think applications should be. This is part 4 in the series of such opinionated pieces.
Why
State location helps reduce the complexity of your application. In some cases, it can actually improve performance.
What does colocate your state mean?
Simply put, it means to put your state as close to where it's being used. You should avoid global state unless it's absolutely necessary.
An example
Here's some code that I've come across several times in my career. I would avoid doing this.
import * as React from "react";
interface PersonFormState {
firstName: string;
lastName: string;
}
interface PersonFormAction {
type: "SET_VALUE";
payload: { [name: string]: string };
}
function personFormReducer(state: PersonFormState, action: PersonFormAction) {
switch (action.type) {
case "SET_VALUE": {
return { ...state, ...action.payload };
}
default: {
return state;
}
}
}
export const PersonFormContainer: React.FC = () => {
const [state, dispatch] = React.useReducer(personFormReducer, {
firstName: "",
lastName: ""
});
const handleChange = (name: string, value: string) => {
const updatedValue = { [name]: value };
return dispatch({ type: "SET_VALUE", payload: updatedValue });
};
return <PersonForm values={state} onChange={handleChange} />;
};
interface PersonFormProps {
values: {
firstName: string;
lastName: string;
};
onChange: (name: string, value: string) => void;
}
const PersonForm: React.FC<PersonFormProps> = ({ values, onChange }) => {
return (
<form>
<label htmlFor="firstName">First name</label>
<input
name="firstName"
value={values.firstName}
onChange={event => onChange(event.target.name, event.target.value)}
/>
<label htmlFor="lastName">Last name</label>
<input
name="lastName"
value={values.lastName}
onChange={event => onChange(event.target.name, event.target.value)}
/>
</form>
);
};
I find this code overcomplicated and hard to follow. It's also good to note that this will perform poorly, because we are causing a re-render of our entire state tree every time an input's value changes.
Use colocation to simplify your code.
Here's how I would write this.
import * as React from "react";
export const PersonForm: React.FC = () => {
return (
<form>
<label htmlFor="firstName">First name</label>
<PersonInput name="firstName" />
<label htmlFor="lastName">Last name</label>
<PersonInput name="lastName" />
</form>
);
};
interface PersonInputProps {
name: string;
}
const PersonInput: React.FC<PersonInputProps> = ({ name }) => {
const [value, setValue] = React.useState("");
return (
<input
name={name}
value={value}
onChange={event => setValue(event.target.value)}
/>
);
};
I would always start with state colocation and then lift state when needed. Keep things simple!
Q&A
Every post I will answer a question I received on twitter. Here's this week's question.
Best indicators for when something should be handled as application-level state vs. component-level state?
— jackgannon_ (@jackgannon_) February 28, 2020
There are very few things that I consider global. Most state should be colocated where it's being used. Here are a couple things that I consider global:
- The current user
- Language settings
- A theme
Outside of these three, I'm not sure anything else needs to be global. 👍
Wrapping Up
This is the 4th installment in a series of pieces I will be writing. If you enjoyed this, please comment below. What else would you like me to cover? As always, I’m open to feedback and recommendations.
Thanks for reading.
P.S. If you haven’t already, be sure to check out my previous posts in this series:
Posted on April 23, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.