Best Practices with React Hooks
Previously, React features such as state and lifecycle functions are available only for class-based components. Function-based components are referred to as dumb, skinny, or just presentational components because they can’t have access to state and lifecycle functions.
But since the release of React Hooks, function-based components have been elevated into first-class citizens of React. It has enabled function components to compose, reuse, and share React code in new ways.
In this post, I will share 6 tips about React Hooks that will be useful to keep as a guideline as you implement hooks into your components:
1. Follow the rules of Hooks
It might seem obvious, but both newbie and seasoned React developers tend to forget to follow the rules of React hooks, which are:
Only call Hooks at the top level
Don’t call hooks inside loops, conditions, and nested functions. When you want to use some hooks conditionally, write the condition inside those hooks.
Don’t do this:
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
Instead, do this:
useEffect(function persistForm() {
if (name !== '') {
localStorage.setItem('formData', name);
}
});
This rule will ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState
and useEffect
calls.
Only call Hooks from function components
Don’t call hooks from regular JavaScript functions. Call hooks only from function components or custom hooks.
By following this rule, you make sure that all stateful logic in a component is clearly visible from the source code.
2. Use the ESLint plugin for React hooks
React team has also created an ESLint plugin named eslint-plugin-react-hooks to help developers write React Hooks the right way in their projects. This plugin will help you to catch and fix hooks errors before you even try to run your application.
It has 2 simple rules:
react-hooks/rules-of-hooks
react-hooks/exhaustive-deps
The first rule simply enforces your code to conform to the rules of hooks that I’ve explained in the first tip. The second rule, exhaustive-deps
is used to enforce the useEffect
’s rule: Every value referenced inside the effect function should also appear in the dependencies array.
For example, the following userInfo
component will trigger the exhaustive-deps
warning because the userId
variable gets referenced inside the useEffect
but it’s not passed in the dependencies array:
function UserInfo({userId}) {
const [user, setUser] = useState(null) useEffect(() => {
getUser(userId).then(user => setUser(user))
}, []) // no userId here return <div>User detail:</div>
}
Although the rule may seem annoying, exhaustive-deps
will help you to avoid bugs caused by unlisted dependencies.
3. Create your function components in the right order
When you create class components, there is a certain ordering that optimizes the way you maintain and improve your React application code.
First, you call the constructor and initiate your state. Then, you write the lifecycle functions, followed by any functions relevant to the component’s job. Finally, you write the render
method:
const propTypes = {
id: PropTypes.number.isRequired,
url: PropTypes.string.isRequired,
text: PropTypes.string,
};
const defaultProps = {
text: 'Hello World',
};class Link extends React.Component {
static methodsAreOk() {
return true;
} constructor(props) {
super(props)
this.state = {
user = null
}
}
componentDidMount() {
console.log('component did mount')
} componentDidUpdate() {
console.log('component did update')
} componentWillUnmount() {
console.log('component will unmount')
} render() {
return <a href={this.props.url}>{this.props.text}</a>
}
}Link.propTypes = propTypes
Link.defaultProps = defaultPropsexport default Link
When writing function components, there is no constructor and lifecycle function, so you might be confused because the structure isn’t as enforced as in a class component:
function App() {
const [user, setUser] = useState(null); useEffect(() => {
console.log("component is mounted");
}, []); const [name, setName] = useState(''); return <h1>React component order</h1>;
}
But just like with class components, creating a defined structure for your function components will help your project’s readability.
It’s recommended to first declare state variables with useState
hook, then write the subscriptions with useEffect
hook followed by any function relevant to the component’s job.
Then finally, you return the elements to be rendered by the browser:
function App() {
const [user, setUser] = useState(null);
const [name, setName] = useState(''); useEffect(() => {
console.log("component is mounted");
}, []); return <h1>React component order</h1>;
}
By enforcing a structure, you will keep the flow of your code consistent and familiar across many components.
4. useState can be used exactly like the class component’s state. Not just for a single value
Many useState
examples will show you how to declare multiple states by declaring multiple variables:
const [name, setName] = useState('John Doe');
const [email, setEmail] = useState('johndoe@email.com');
const [age, setAge] = useState(28);
But useState
actually can hold both arrays and objects just fine. You can still group related data into one state variable like in the following example:
const [user, setUser] = useState(
{ name: 'John Doe', email: 'john@email.com', age: 28 }
);
There is a caveat though. When using the useState
’s update function to update the state, the previous state is replaced with the new state. This is different to class component’s this.setState
where the new state is merged into the old state:
const [user, setUser] = useState(
{ name: 'John', email: 'john@email.com', age: 28 }
);setUser({ name: 'Nathan' });// result { name: 'Nathan' }
In order to preserve the previous state, you need to merge it manually by creating a callback function that passes the current state value into it. Because the above example has user
variable assigned as state value, you can pass it into setUser
function as follows:
setUser((user) = > ({ ...user, name: 'Nathan' }));// result is { name:'Nathan', email: 'john@email.com', age: 28 }
Depends on when the data change in your application’s life span, it’s recommended to split state into multiple variables when the values are independent of each other.
But for some cases like building a simple form, it might be better to group the state together so that you can handle changes and submit the data easier.
In short, have a balance between multiple useState
calls and a single useState
call.
5. Use custom hooks for sharing application logic
As you build your application, you will notice that some of your application logic will be used again and again across many components.
With the release of React Hooks, you can extract your component’s logic into reusable functions as custom hooks, as I’ve demonstrated in the following article:
You can use tools like Bit (Github) to publish your hooks to a single curated collection. This way you can install and reuse them across your applications. It doesn't require you to create a whole new “hooks library” project — you can gradually “push” new hooks, from any project, to your shared collection.

The only caveat for this approach is that you can’t use Hooks inside class components. So if you still have legacy class components in your project, you either need to convert those components into functions or use other reusable logic patterns (with HOC or Render Props)
6. Avoid prop drilling with useContext
Prop drilling is a common problem in React application where you pass data down from one parent component through the component layers until it reaches the designated child component, while other nested components don’t actually need them
Consider the following example:
As you can see in the example, the App
component passed name
props down into Greeting
component through Hello
component, even though Hello
component doesn’t need the props.
React Context is a feature that provides a way to pass data down through the component tree without needing to pass props manually between components. The value of React Context defined in the parent component can be accessed by its descendants through the useContext
hook.
In the example below, I’ve refactored the same code by passing name
data into the Context Provider instead of the props:
Any child components of App
could access the data through the useContext
hook. You can learn more about useContext
hook from the documentation
Conclusion
React Hooks is a great addition to the React library because it allows you to compose, reuse, and share React code in a way that can’t be done without it.
As Hooks change the way developers write React components, a new set of best practices for writing React Hooks are needed to make development and collaboration easier across many teams.
While it’s certainly isn’t complete, I hope the tips I’ve shared above have helped you to write React Hooks the right way in your projects.