Master React hooks: Introduction with examples and best practices

React Hooks explained with examples and best practices.

Vishesh Dhawan
Vishesh Dhawan
January 11, 2023
LumBytes - React Hooks explained.

    Share

Introduction

State management is a core capability of React which allows us to share data across component re-renders. This capability was reserved for class components, while the functional components were only used for presentational purposes. However, it was after the introduction of React hooks that state management became possible in functional components. The ironic part is that this new way of state management became so popular among the developers rendering the class components redundant.

Hooks were released in React version 16.8 in February 2019. React framework has been around since 29 May 2019. For 6 years React has powered many web applications and state management via class components remained the de facto standard. So, the real question is, If State management was already possible, what was the motivation behind the invention of hooks? To answer this question let's look at some of the major benefits react hooks offers.

Benefits of React Hooks.

1. Simpler Syntax - Using hooks with functional components makes the code simpler and shorter. Class components introduce their own concepts such as this keyword, function bindings, constructors, super functions, etc. We don't have to deal with these complexities with functional components.

2. Improves Reusability - Hooks allow us to improve usability by encapsulating a shared logic by implementing a custom hook and using it across components.

3. Easier to read code - React Hooks make the code easier to read and debug. For E.g. in class components, we have to deal with this keyword and method bindings which increase complexity. In functional components, we don't have to deal with them.

Understanding React hooks.

Now we are going to explore some of the most commonly used React hooks.

1. The useState Hook

The useState hook lets us add state to a functional component. It takes an optional argument as an initial state and returns an array of 2 values i.e. the current state and the state modifier function.

The initial value if provided becomes the current state for the first render. For further updating the state we use the state modifier function returned by the useState hook. Whenever the state is updated, React re-renders the component with the latest state data, hence, maintaining the latest data on the UI. E.g -

import React, { useState } from 'react';

function Counter() {
// use state hook with initial argument and returned array
const [count, setCount] = useState(0);

const handleIncrement = () => {
setCount(count + 1);
}

return (
<div>
<h1>Count: {count}</h1>
<button onClick={handleIncrement}>Increment</button>
</div>
);
}

export default Counter;

2. The useEffect Hook

The useEffect hook is used to manage side effects in functional components. In class components, we use lifecycle methods for it. Side effects are the actions performed outside the scope of the component such as fetching data from an external source, performing actions in a setTimeout function, or updating closure-scoped variables

useEffect hook takes 1 mandatory argument i.e. a function that performs side effects and an optional argument i.e. an array of dependencies. If the arguments array is not given it performs side effects every time the component renders, otherwise, it performs side effects only if there is a change in the dependencies. E.g -

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function Data() {
const [data, setData] = useState([]);
//first use effect
useEffect(() => {
async function fetchData() {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
setData(response.data);
} catch (error) {
console.log(error);
}
}
fetchData();
}, []);
// second use effect
useEffect(()=> {
console.log("I run on every render");
});

return (
<div>
<ul>
{data.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}

export default Data;

In the above example, we have used axios library to fetch data from an API and store it in a state variable. The UI then re-renders to display the latest data. The empty dependency array `[]` indicates that there are no dependencies, therefore, the side effect will happen only once and not on every render. The second useEffect will execute on every render because the dependency array is not provided. We can also have more than one useEffect hook in a component to manage different side effects.

3. The useRef Hook

The useRef hook is used to create a direct reference to a DOM node (element). It takes an argument as the initial value and returns an object that has only one property - 'current'. This property holds the reference of the DOM node.

We can use this reference object to get the value of the element, set value of the element etc. This ref object also persists across re-renders. E.g -

import React, { useRef } from 'react';

function InputWithFocusButton() {
const ref = useRef(null);

function logValue() {
console.log(ref.current.value);
}

return (
<div>
<input type="text" ref={ref} />
<button onClick={logValue}>Log Value</button>
</div>
);
}

export default InputWithFocusButton;

In the above example, we have passed null as the initial value to the ref object. It is then again passed to the ref attribute of the input tag which assigns the input tag as the DOM reference to the ref object. The input tag is now accessible via the ref object.

When we click the 'Log Value' button it logs the value of the input field on the console.

4. The useContext Hook

This hook provides a way to share data among functional components without having to pass data as props at every level.

To use this hook we have to first create a using React.createContext. We pass an optional parameter as the initial value e.g -

// filename - context.js
const MyContext = React.createContext(10);
export default MyContext;

To use the context in a component we wrap that component with the MyContext.provider component. -

<MyContext.Provider value={/* some value */}>
// child components
<FirstComponent />
<SecondComponent />
</MyContext.Provider>

Now, we will use the useContext hook in the child component to retrieve the context value.

import React, { useContext } from 'react';
import context from "./context.js"

function FirstComponent() {
const value = useContext(context);
// do something with the value
}

5. The useMemo Hook

The useMemo Hook is used to memoize expensive computations. This hook avoids the recomputation of the function across component re-renders by caching the result of the function with respect to the dependencies (generally the function's arguments) provided.

It takes 2 arguments i.e. a function that needs to be memoized and an array of dependencies.

const result = useMemo(function, [dependencies]);

The function is executed again only when there is a change in the dependencies. It helps in increasing the performance of the application by avoiding unnecessary computations. E.g -

import React, { useState, useMemo } from 'react';

function FactorialCalculator() {
const [number, setNumber] = useState(0);

function handleChange(event) {
setNumber(event.target.value);
}

const factorial = useMemo(() => {
function calculateFactorial(n) {
if (n <= 1) {
return 1;
}
return n * calculateFactorial(n - 1);
}
return calculateFactorial(number);
}, [number]);

return (
<div>
<input type="number" value={number} onChange={handleChange} />
<p>{`Factorial of ${number} is ${factorial}`}</p>
</div>
);
}

export default FactorialCalculator;

In the above example, the result of the calculateFactorial function is memoized with the number variable as the dependency. Now, only when there is a change in the number variable the calculateFacrtorial function will be re-evaluated.

6. Custom hooks

Custom hooks are self-defined functions that extract reusable logic that can be used across multiple components. All the rules that apply to react hooks also apply to custom hooks.

We can encapsulate multiple official react hooks and perform complex complex tasks inside custom hooks. E.g -

import { useState, useEffect } from 'react';

// custom hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
async function fetchData() {
try {
const jsonData = await fetch(url).json();
setData(jsonData);
setLoading(false);
} catch (error) {
setError(error);
setLoading(false);
}
}

fetchData();
}, [url]);

return { data, loading, error };
}

// functional component
function UserList() {
const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/users');

if (loading) {
return <p>Loading...</p>;
}

if (error) {
return <p>Error: {error.message}</p>;
}

return (
<div>
<h1>User List</h1>
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}

export default UserList;

In the above example, we have created a custom hook 'useFetch'. We are using this hook to fetch data from an API as well as display loading and error states. Notice how we have encapsulated multiple useState hooks and an useEffect hook inside one single custom hook.

Best practices for using react hooks.

React hooks do help us make our code concise and cleaner but there are some practices we must follow to make out code maintainable.

  • Always use the hooks on the top level in functional components. Do not use them inside, loops, conditionals etc.
  • Always use hooks inside function components. Do not use them inside functions that don't return JSX.
  • Use useContext hook to share data among components that are nested across many levels. Avoid props drilling on more than 3-4 levels.
  • Use useContext hook to share global data that is needed across the entire application. E.g. - the current theme of the app, the role of the authenticated user, etc.
  • When using custom hooks keep the implementation more generalized which is used in multiple components. Do not use custom hooks for specific use cases.
  • Use the useMemo hook to cache the results of complex calculations.
  • Always pass dependency arrays to useEffect and useCallback Hooks until strictly unintended.

Conclusion

Overall, React hooks are a relatively new feature in React but it has changed the way we write React code today completely. Hooks allow us to write more clean code which is also concise and more readable. By using different kinds of hooks you can create fully capable and performant react applications without having to deal with concepts of class components.

I highly encourage you to use hooks in your own applications to become a master React developer.

Happy Coding 🚀.