Over the years, I’ve written and reviewed hundreds of React components for a $1.7B GenAI startup.
I’ve examined code from both junior developers and experienced ex-FAANG engineers. While this advice might seem obvious for small components, it becomes crucial as your front-end codebase grows.
By following these guidelines, you’ll ensure your code is among the most understandable and maintainable in any codebase.
How to Structure Your React Components
🛑 Don’t
The code below shows a React component where the built-in and custom hooks are not in a clear order.
export const MyReactComponent = memo(function MyReactComponent(props: Props) {
const [state1, setState1] = useState(initialState1);
const memoizedValue = useMemo(() => computeExpensiveValue(state1, props.prop1), [state1, props.prop1]);
useEffect(() => {
console.log(memoizedValue);
}, [memoizedValue]);
const ref1 = useRef(null);
const handleClick = () => {
// Handle click
};
const { customValue, customFunction } = useMyCustomHook();
const [state2, setState2] = useState(initialState2);
const memoizedCallback = useCallback(() => doSomething(state1, props.prop2), [state1, props.prop2]);
useEffect(() => {
// Another effect
document.title = `${state1} ${state2}`;
}, [state1, state2]);
return (
<div>
<button onClick={handleClick} ref={ref1}>
My button ({state1}, {state2})
</button>
<SomeOtherComponent
value={memoizedValue}
callback={memoizedCallback}
customValue={customValue}
/>
</div>
);
});
You might think that related code should be close together, like memoizedValue
and the useEffect
at the beginning of the component.
However, I've found that having a clear order instead in your React component is more effective. As your codebase grows, it becomes hard to keep related code close together, and other engineers are less likely to follow this style.
While it is important to have related code close by, it is better to prefer a clear structure in this case.
✅ Do
You want to have a clear structure in components so you can navigate through them quickly and have a pattern that is obvious to other engineers:
State declarations
Ref declarations
Memoized values
Memoized callbacks
Custom hooks
Effects
Event handler
JSX
export const MyReactComponent = memo(function MyReactComponent(props: Props) {
// 1. State declarations
const [state1, setState1] = useState(initialState1);
// 2. Refs
const ref1 = useRef(null);
// 3. Memoized values
const memoizedValue = useMemo(() => computeExpensiveValue(state1, prop1), [state1, prop1]);
// 4. Memoized callbacks
const memoizedCallback = useCallback(() => doSomething(state1, prop2), [state1, prop2]);
// 5. Custom hooks
const { customValue, customFunction } = useMyCustomHook();
// 6. Effects
useEffect(() => {
// This effect uses the memoized value
console.log(memoizedValue);
}, [memoizedValue]);
// 7. Event handlers and other functions
const handleClick = useCallback(() => {
// Handle click
}), []);
// 8. JSX
return (
<button onClick={handleClick}>
My button
</button>
);
});
Keep in mind this is just a general guideline. This structure might not always be possible. For example, you might need the result of a custom hook as a dependency for a memoized value.
When you encounter exceptions like this, consider extracting the related code into its own custom hook.
When to use a custom hook
🛑 Don’t
Too often, I see components with hundreds of lines of code and everything cramped together. If you didn’t write the component, it’s hard to understand what’s happening.
Take a look at the component below. It has a useEffect
dependent on a useMemo
, and all the code is in the component, making it hard to get a quick overview.
import React, { useState, useMemo, useEffect } from 'react';
const WeatherDisplay = React.memo(function WeatherDisplay({ cityId }) {
const [temperatureUnit, setTemperatureUnit] = useState('celsius');
const weatherData = useMemo(() => fetchWeatherData(cityId), [cityId]);
const convertedTemperature = useMemo(() => {
if (temperatureUnit === 'fahrenheit') {
return (weatherData.temperature * 9/5) + 32;
}
return weatherData.temperature;
}, [weatherData.temperature, temperatureUnit]);
useEffect(() => {
localStorage.setItem('preferredTempUnit', temperatureUnit);
}, [temperatureUnit]);
const toggleTemperatureUnit = () => {
setTemperatureUnit(prev => prev === 'celsius' ? 'fahrenheit' : 'celsius');
};
return (
<div>
<h2>Weather in {weatherData.cityName}</h2>
<p>Temperature: {convertedTemperature}°{temperatureUnit === 'celsius' ? 'C' : 'F'}</p>
<button onClick={toggleTemperatureUnit}>
Switch to {temperatureUnit === 'celsius' ? 'Fahrenheit' : 'Celsius'}
</button>
</div>
);
});
✅ Do
According to the official React documentation:
… whenever you write an Effect, consider whether it would be clearer to also wrap it in a custom Hook.
Take a look at the component below where we extract the useEffect
and the dependent useMemo
into a custom hook:
import React, { useState, useMemo, useEffect, useCallback } from 'react';
function useTemperatureConversion(initialTemperature) {
const [unit, setUnit] = useState(() => {
return localStorage.getItem('preferredTempUnit') || 'celsius';
});
const convertedTemperature = useMemo(() => {
if (unit === 'fahrenheit') {
return (initialTemperature * 9/5) + 32;
}
return initialTemperature;
}, [initialTemperature, unit]);
useEffect(() => {
localStorage.setItem('preferredTempUnit', unit);
}, [unit]);
const toggleUnit = useCallback(() => {
setUnit(prev => prev === 'celsius' ? 'fahrenheit' : 'celsius');
}, []);
return { convertedTemperature, unit, toggleUnit };
}
const WeatherDisplay = React.memo(function WeatherDisplay({ cityId }) {
const weatherData = useMemo(() => fetchWeatherData(cityId), [cityId]);
const { convertedTemperature, unit, toggleUnit } = useTemperatureConversion(weatherData.temperature);
return (
<div>
<h2>Weather in {weatherData.cityName}</h2>
<p>Temperature: {convertedTemperature}°{unit === 'celsius' ? 'C' : 'F'}</p>
<button onClick={toggleUnit}>
Switch to {unit === 'celsius' ? 'Fahrenheit' : 'Celsius'}
</button>
</div>
);
});
The example above is simple, but as your front-end application scales, you’ll encounter components with a lot more happening. Using custom hooks to encapsulate logic can help keep your components clean and understandable.
Personally, I write a custom hook when:
I have a couple of memoized values or callbacks that depend on each other. These can be extracted into a hook.
I have a
useEffect
in my component. By putting theuseEffect
and its dependencies into a hook, I can give it a more declarative name, making the code easier to understand.Data fetching.
Duplicated logic.
So, the summary is: Put related code into a custom hook and give this hook a descriptive name.
This may seem like simple advice, but after reviewing hundreds of React components from both experienced and inexperienced engineers, I can assure you that following this practice will set you apart.
Learn more: