Best Practices with React Hooks

Nathan Sebhastian
Photo by Vishal Jadhav on Unsplash

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:

It might seem obvious, but both newbie and seasoned React developers tend to forget to follow the rules of React hooks, which are:

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.

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.

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.

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 = defaultProps
export 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.

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.

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.

Author: admin

Leave a Reply

Your email address will not be published.