Hooks are simply functions that allow you to hook into or make use of React features. They were introduced at the React Conf 2018 to address three major problems of class components: wrapper hell, huge components, and confusing classes. Hooks give power to React functional components, making it possible to develop an entire application with it.
The aforementioned problems of class components are connected and solving one without the other could introduce further problems. Thankfully, hooks solved all the problems simply and efficiently while creating room for more interesting features in React. Hooks do not replace already existing React concepts and classes, they merely provide an API to access them directly.
The React team introduced several hooks in React 16.8. However, you could also use hooks from third-party providers in your application or even create a custom hook. In this tutorial, we’ll take a look at some useful hooks in React and how to use them. We’ll go through several code examples of each hook and also explore how you’d create a custom hook.
Note: This tutorial requires a basic understanding of Javascript (ES6+) and React.
Motivation Behind Hooks
As stated earlier, hooks were created to solve three problems: wrapper hell, huge components, and confusing classes. Let’s take a look at each of these in more detail.
Wrapper Hell
Complex applications built with class components easily run into wrapper hell. If you examine the application in the React Dev Tools, you will notice deeply nested components. This makes it very difficult to work with the components or debug them. While these problems could be solved with higher-order components and render props, they require you to modify your code a bit. This could lead to confusion in a complex application.
Hooks are easy to share, you don’t have to modify your components before reusing the logic.
A good example of this is the use of the Redux connect
Higher Order Component (HOC) to subscribe to the Redux store. Like all HOCs, to use the connect HOC, you have to export the component alongside the defined higher-order functions. In the case of connect
, we’ll have something of this form.
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
Where mapStateToProps
and mapDispatchToProps
are functions to be defined.
Whereas in the Hooks era, one can easily achieve the same result neatly and succinctly by using the Redux useSelector
and useDispatch
hooks.
Huge Components
Class components usually contain side effects and stateful logic. As the application grows in complexity, it is common for the component to become messy and confusing. This is because the side effects are expected to be organized by lifecycle methods rather than functionality. While it is possible to split the components and make them simpler, this often introduces a higher level of abstraction.
Hooks organize side effects by functionality and it is possible to split a component into pieces based on the functionality.
Confusing Classes
Classes are generally a more difficult concept than functions. React class-based components are verbose and a bit difficult for beginners. If you are new to Javascript, you could find functions easier to get started with because of their lightweight syntax as compared to classes. The syntax could be confusing; sometimes, it is possible to forget binding an event handler which could break the code.
React solves this problem with functional components and hooks, allowing developers to focus on the project rather than code syntax.
For instance, the following two React components will yield exactly the same result.
import React, { Component } from "react";
export default class App extends Component { constructor(props) { super(props); this.state = { num: 0 }; this.incrementNumber = this.incrementNumber.bind(this); } incrementNumber() { this.setState({ num: this.state.num + 1 }); } render() { return ( <div> <h1>{this.state.num}</h1> <button onClick={this.incrementNumber}>Increment</button> </div> ); }
}
import React, { useState } from "react";
export default function App() { const [num, setNum] = useState(0); function incrementNumber() { setNum(num + 1); } return ( <div> <h1>{num}</h1> <button onClick={incrementNumber}>Increment</button> </div> );
}
The first example is a class-based component while the second is a functional component. Although this is a simple example, notice how bogus the first example is compared to the second.
The Hooks Convention And Rules
Before delving into the various hooks, it could be helpful to take a look at the convention and rules that apply to them. Here are some of the rules that apply to hooks.
- The naming convention of hooks should start with the prefix
use
. So, we can haveuseState
,useEffect
, etc. If you are using modern code editors like Atom and VSCode, the ESLint plugin could be a very useful feature for React hooks. The plugin provides useful warnings and hints on the best practices. - Hooks must be called at the top level of a component, before the return statement. They can’t be called inside a conditional statement, loop, or nested functions.
- Hooks must be called from a React function (inside a React component or another hook). It shouldn’t be called from a Vanilla JS function.
The useState
Hook
The useState
hook is the most basic and useful React hook. Like other built-in hooks, this hook must be imported from react
to be used in our application.
import {useState} from 'react'
To initialize the state, we must declare both the state and its updater function and pass an initial value.
const [state, updaterFn] = useState('')
We are free to call our state and updater function whatever we want but by convention, the first element of the array will be our state while the second element will be the updater function. It is a common practice to prefix our updater function with the prefix set followed by the name of our state in camel case form.
For instance, let’s set a state to hold count values.
const [count, setCount] = useState(0)
Notice that the initial value of our count
state is set to 0
and not an empty string. In other words, we can initialize our state to any kind of JavaScript variables, namely number, string, boolean, array, object, and even BigInt. There is a clear difference between setting states with the useState
hook and class-based component states. It is noteworthy that the useState
hook returns an array, also known as state variables and in the example above, we destructured the array into state
and the updater
function.
Rerendering Components
Setting states with the useState
hook causes the corresponding component to rerender. However, this only happens if React detects a difference between the previous or old state and the new state. React does the state comparison using the Javascript Object.is
algorithm.
Setting States With useState
Our count
state can be set to new state values by simply passing the new value to the setCount
updater function as follows setCount(newValue)
.
This method works when we don’t want to reference the previous state value. If we wish to do that, we need to pass a function to the setCount
function.
Assuming we want to add 5 to our count
variable anytime a button is clicked, we could do the following.
import {useState} from 'react' const CountExample = () => { // initialize our count state const [count, setCount] = useState(0) // add 5 to to the count previous state const handleClick = () =>{ setCount(prevCount => prevCount + 5) } return( <div> <h1>{count} </h1> <button onClick={handleClick}>Add Five</button> </div> )
} export default CountExample
In the code above, we first imported the useState
hook from react
and then initialized the count
state with a default value of 0. We created an onClick
handler to increment the value of count
by 5 whenever the button is clicked. Then we displayed the result in an h1
tag.
Setting Arrays And Object States
States for arrays and objects can be set in much the same way as other data types. However, if we wish to retain already existing values, we need to use the ES6 spread operator when setting states.
The spread operator in Javascript is used to create a new object from an already existing object. This is useful here because React
compares the states with the Object.is
operation and then rerender accordingly.
Let’s consider the code below for setting states on button click.
import {useState} from 'react' const StateExample = () => { //initialize our array and object states const [arr, setArr] = useState([2, 4]) const [obj, setObj] = useState({num: 1, name: 'Desmond'}) // set arr to the new array values const handleArrClick = () =>{ const newArr = [1, 5, 7] setArr([...arr, ...newArr]) } // set obj to the new object values const handleObjClick = () =>{ const newObj = {name: 'Ifeanyi', age: 25} setObj({...obj, ...newObj}) } return( <div> <button onClick ={handleArrClick}>Set Array State</button> <button onClick ={handleObjClick}>Set Object State</button> </div> )
} export default StateExample
In the above code, we created two states arr
and obj
, and initialized them to some array and object values respectively. We then created onClick
handlers called handleArrClick
and handleObjClick
to set the states of the array and object respectively. When handleArrClick
fires, we call setArr
and use the ES6 spread operator to spread already existing array values and add newArr
to it.
We did the same thing for handleObjClick
handler. Here we called setObj
, spread the existing object values using the ES6 spread operator, and updated the values of name
and age
.
Async Nature Of useState
As we have already seen, we set states with useState
by passing a new value to the updater function. If the updater is called multiple times, the new values will be added to a queue and re-rendering is done accordingly using the JavaScript Object.is
comparison.
The states are updated asynchronously. This means that the new state is first added to a pending state and thereafter, the state is updated. So, you may still get the old state value if you access the state immediately it is set.
Let’s consider the following example to observe this behavior.
In the code above, we created a count
state using the useState
hook. We then created an onClick
handler to increment the count
state whenever the button is clicked.
Observe that although the count
state increased, as displayed in the h2
tag, the previous state is still logged in the console. This is due to the async nature of the hook.
If we wish to get the new state, we can handle it in a similar way we would handle async functions. Here is one way to do that.
Here, we stored created newCountValue
to store the updated count value and then set the count
state with the updated value. Then, we logged the updated count value in the console.
The useEffect
Hook
useEffect
is another important React hook used in most projects. It does a similar thing to the class-based component’s componentDidMount
, componentWillUnmount
, and componentDidUpdate
lifecycle methods. useEffect
provides us an opportunity to write imperative codes that may have side effects on the application. Examples of such effects include logging, subscriptions, mutations, etc.
The user can decide when the useEffect
will run, however, if it is not set, the side effects will run on every rendering or rerendering.
Consider the example below.
import {useState, useEffect} from 'react' const App = () =>{ const [count, setCount] = useState(0) useEffect(() =>{ console.log(count) }) return( <div> ... </div> )
}
In the code above, we simply logged count
in the useEffect
. This will run after every render of the component.
Sometimes, we may want to run the hook once (on the mount) in our component. We can achieve this by providing a second parameter to useEffect
hook.
import {useState, useEffect} from 'react' const App = () =>{ const [count, setCount] = useState(0) useEffect(() =>{ setCount(count + 1) }, []) return( <div> <h1>{count}</h1> ... </div> )
}
The useEffect
hook has two parameters, the first parameter is the function we want to run while the second parameter is an array of dependencies. If the second parameter is not provided, the hook will run continuously.
By passing an empty square bracket to the hook’s second parameter, we instruct React to run the useEffect
hook only once, on the mount. This will display the value 1
in the h1
tag because the count will be updated once, from 0 to 1, when the component mounts.
We could also make our side effect run whenever some dependent values change. This can be done by passing these values in the list of dependencies.
For instance, we could make the useEffect
to run whenever count
changes as follows.
import { useState, useEffect } from "react";
const App = () => { const [count, setCount] = useState(0); useEffect(() => { console.log(count); }, [count]); return ( <div> <button onClick={() => setCount(count + 1)}>Increment</button> </div> );
};
export default App;
The useEffect
above will run when either of these two conditions is met.
- On mount — after the component is rendered.
- When the value of
count
changes.
On mount, the console.log
expression will run and log count
to 0. Once the count
is updated, the second condition is met, so the useEffect
runs again, this will continue whenever the button is clicked.
Once we provide the second argument to useEffect
, it is expected that we pass all the dependencies to it. If you have ESLINT
installed, it will show a lint error if any dependency is not passed to the parameter list. This could also make the side effect behave unexpectedly, especially if it depends on the parameters that are not passed.
Cleaning Up The Effect
useEffect
also allows us to clean up resources before the component unmounts. This may be necessary to prevent memory leaks and make the application more efficient. To do this, we’d return the clean-up function at the end of the hook.
useEffect(() => { console.log('mounted') return () => console.log('unmounting... clean up here')
})
The useEffect
hook above will log mounted
when the component is mounted. Unmounting… clean up here will be logged when the component unmounts. This can happen when the component is removed from the UI.
The clean-up process typically follows the form below.
useEffect(() => { //The effect we intend to make effect //We then return the clean up return () => the cleanup/unsubscription
})
While you may not find so many use cases for useEffect
subscriptions, it is useful when dealing with subscriptions and timers. Particularly, when dealing with web sockets, you may need to unsubscribe from the network to save resources and improve performance when the component unmounts.
Fetching And Refetching Data With useEffect
One of the commonest use cases of the useEffect
hook is fetching and prefetching data from an API.
To illustrate this, we’ll use fake user data I created from JSONPlaceholder
to fetch data with the useEffect
hook.
import { useEffect, useState } from "react";
import axios from "axios"; export default function App() { const [users, setUsers] = useState([]); const endPoint = "https://my-json-server.typicode.com/ifeanyidike/jsondata/users"; useEffect(() => { const fetchUsers = async () => { const { data } = await axios.get(endPoint); setUsers(data); }; fetchUsers(); }, []); return ( <div className="App"> {users.map((user) => ( <div> <h2>{user.name}</h2> <p>Occupation: {user.job}</p> <p>Sex: {user.sex}</p> </div> ))} </div> );
}
In the code above, we created a users
state using the useState
hook. Then we fetched data from an API using Axios. This is an asynchronous process, and so we used the async/await function, we could have also used the dot then the syntax. Since we fetched a list of users, we simply mapped through it to display the data.
Notice that we passed an empty parameter to the hook. This ensures that it is called just once when the component mounts.
We can also refetch the data when some conditions change. We’ll show this in the code below.
import { useEffect, useState } from "react";
import axios from "axios"; export default function App() { const [userIDs, setUserIDs] = useState([]); const [user, setUser] = useState({}); const [currentID, setCurrentID] = useState(1); const endPoint = "https://my-json-server.typicode.com/ifeanyidike/userdata/users"; useEffect(() => { axios.get(endPoint).then(({ data }) => setUserIDs(data)); }, []); useEffect(() => { const fetchUserIDs = async () => { const { data } = await axios.get(`${endPoint}/${currentID}`}); setUser(data); }; fetchUserIDs(); }, [currentID]); const moveToNextUser = () => { setCurrentID((prevId) => (prevId < userIDs.length ? prevId + 1 : prevId)); }; const moveToPrevUser = () => { setCurrentID((prevId) => (prevId === 1 ? prevId : prevId - 1)); }; return ( <div className="App"> <div> <h2>{user.name}</h2> <p>Occupation: {user.job}</p> <p>Sex: {user.sex}</p> </div> <button onClick={moveToPrevUser}>Prev</button> <button onClick={moveToNextUser}>Next</button> </div> );
}
Here we created two useEffect
hooks. In the first one, we used the dot then syntax to get all users from our API. This is necessary to determine the number of users.
We then created another useEffect
hook to get a user based on the id
. This useEffect
will refetch the data whenever the id changes. To ensure this, we passed the id
in the dependency list.
Next, we created functions to update the value of our id
whenever the buttons are clicked. Once the value of the id
changes, the useEffect
will run again and refetch the data.
If we want, we can even clean up or cancel the promise-based token in Axios, we could do that with the clean-up method discussed above.
useEffect(() => { const source = axios.CancelToken.source(); const fetchUsers = async () => { const { data } = await axios.get(`${endPoint}/${num}`, { cancelToken: source.token }); setUser(data); }; fetchUsers(); return () => source.cancel(); }, [num]);
Here, we passed the Axios’ token as a second parameter to axios.get
. When the component unmounts we then canceled the subscription by calling the cancel method of the source object.
The useReducer
Hook
The useReducer
hook is a very useful React hook that does a similar thing to the useState
hook. According to the React documentation, this hook should be used to handle more complex logic than the useState
hook. It’s worthy of note that the useState
hook is internally implemented with the useReducer hook.
The hook takes a reducer as an argument and can optionally take the initial state and an init function as arguments.
const [state, dispatch] = useReducer(reducer, initialState, init)
Here, init
is a function and it is used whenever we want to create the initial state lazily.
Let’s look at how to implement the useReducer
hook by creating a simple to-do app as shown in the sandbox below.
First off, we should create our reducer to hold the states.
export const ADD_TODO = "ADD_TODO";
export const REMOVE_TODO = "REMOVE_TODO";
export const COMPLETE_TODO = "COMPLETE_TODO"; const reducer = (state, action) => { switch (action.type) { case ADD_TODO: const newTodo = { id: action.id, text: action.text, completed: false }; return [...state, newTodo]; case REMOVE_TODO: return state.filter((todo) => todo.id !== action.id); case COMPLETE_TODO: const completeTodo = state.map((todo) => { if (todo.id === action.id) { return { ...todo, completed: !todo.completed }; } else { return todo; } }); return completeTodo; default: return state; }
};
export default reducer;
We created three constants corresponding to our action types. We could have used strings directly but this method is preferable to avoid typos.
Then we created our reducer function. Like in Redux
, the reducer must take the state and the action object. But unlike Redux, we don’t need to initialize our reducer here.
Furthermore, for a lot of state management use-cases, a useReducer
along with the dispatch
exposed via context can enable a larger application to fire actions, update state
and listen to it.
Then we used the switch
statements to check the action type passed by the user. If the action type is ADD_TODO
, we want to pass a new to-do and if it is REMOVE_TODO
, we want to filter the to-dos and remove the one that corresponds to the id
passed by the user. If it is COMPLETE_TODO
, we want to map through the to-dos and toggle the one with the id
passed by the user.
Here is the App.js
file where we implemented the reducer
.
import { useReducer, useState } from "react";
import "./styles.css";
import reducer, { ADD_TODO, REMOVE_TODO, COMPLETE_TODO } from "./reducer";
export default function App() { const [id, setId] = useState(0); const [text, setText] = useState(""); const initialState = [ { id: id, text: "First Item", completed: false } ]; //We could also pass an empty array as the initial state //const initialState = [] const [state, dispatch] = useReducer(reducer, initialState); const addTodoItem = (e) => { e.preventDefault(); const newId = id + 1; setId(newId); dispatch({ type: ADD_TODO, id: newId, text: text }); setText(""); }; const removeTodo = (id) => { dispatch({ type: REMOVE_TODO, id }); }; const completeTodo = (id) => { dispatch({ type: COMPLETE_TODO, id }); }; return ( <div className="App"> <h1>Todo Example</h1> <form className="input" onSubmit={addTodoItem}> <input value={text} onChange={(e) => setText(e.target.value)} /> <button disabled={text.length === 0} type="submit">+</button> </form> <div className="todos"> {state.map((todo) => ( <div key={todo.id} className="todoItem"> <p className={todo.completed && "strikethrough"}>{todo.text}</p> <span onClick={() => removeTodo(todo.id)}>✕</span> <span onClick={() => completeTodo(todo.id)}>✓</span> </div> ))} </div> </div> );
}
Here, we created a form containing an input element, to collect the user’s input, and a button to trigger the action. When the form is submitted, we dispatched an action of type ADD_TODO
, passing a new id and to-do text. We created a new id by incrementing the previous id value by 1. We then cleared the input text box. To delete and complete to-do, we simply dispatched the appropriate actions. These have already been implemented in the reducer as shown above.
However, the magic happens because we are using the useReducer
hook. This hook accepts the reducer and the initial state and returns the state and the dispatch function. Here, the dispatch function serves the same purpose as the setter function for the useState
hook and we can call it anything we want instead of dispatch
.
To display the to-do items, we simply mapped through the list of to-dos returned in our state object as shown in the code above.
This shows the power of the useReducer
hook. We could also achieve this functionality with the useState
hook but as you can see from the example above, the useReducer
hook helped us to keep things neater. useReducer
is often beneficial when the state object is a complex structure and is updated in different ways as against a simple value-replace. Also, once these update functions get more complicated, useReducer
makes it easy to hold all that complexity in a reducer function (which is a pure JS function) making it very easy to write tests for the reducer function alone.
We could have also passed the third argument to the useReducer
hook to create the initial state lazily. This means that we could calculate the initial state in an init
function.
For instance, we could create an init
function as follows:
const initFunc = () => [ { id: id, text: "First Item", completed: false }
]
and then pass it to our useReducer
hook.
const [state, dispatch] = useReducer(reducer, initialState, initFunc)
If we do this, the initFunc
will override the initialState
we provided and the initial state will be calculated lazily.
The useContext
Hook
The React Context API provides a way to share states or data throughout the React component tree. The API has been available in React, as an experimental feature, for a while but it became safe to use in React 16.3.0. The API makes data sharing between components easy while eliminating prop drilling.
While you can apply the React Context to your entire application, it is also possible to apply it to part of the application.
To use the hook, you need to first create a context using React.createContext
and this context can then be passed to the hook.
To demonstrate the use of the useContext
hook, let’s create a simple app that will increase font size throughout our application.
Let’s create our context in context.js
file.
import { createContext } from "react"; //Here, we set the initial fontSize as 16.
const fontSizeContext = createContext(16);
export default fontSizeContext;
Here, we created a context and passed an initial value of 16
to it, and then exported the context. Next, let’s connect our context to our application.
import FontSizeContext from "./context";
import { useState } from "react";
import PageOne from "./PageOne";
import PageTwo from "./PageTwo";
const App = () => { const [size, setSize] = useState(16); return ( <FontSizeContext.Provider value={size}> <PageOne /> <PageTwo /> <button onClick={() => setSize(size + 5)}>Increase font</button> <button onClick={() => setSize((prevSize) => Math.min(11, prevSize - 5)) } > Decrease font </button> </FontSizeContext.Provider> );
};
export default App;
In the above code, we wrapped our entire component tree with FontSizeContext.Provider
and passed size
to its value prop. Here, size
is a state-created with the useState
hook. This allows us to change the value prop whenever the size
state changes. By wrapping the entire component with the Provider
, we can access the context anywhere in our application.
For instance, we accessed the context in <PageOne />
and <PageTwo />
. As a result of this, the font size will increase across these two components when we increase it from the App.js
file. We can increase or decrease the font size from the buttons as shown above and once we do, the font size changes throughout the application.
import { useContext } from "react";
import context from "./context";
const PageOne = () => { const size = useContext(context); return <p style={{ fontSize: `${size}px` }}>Content from the first page</p>;
};
export default PageOne;
Here, we accessed the context using the useContext
hook from our PageOne
component. We then used this context to set our font-size property. A similar procedure applies to the PageTwo.js
file.
Themes or other higher-order app-level configurations are good candidates for contexts.
Using useContext
And useReducer
When used with the useReducer
hook, useContext
allows us to create our own state management system. We can create global states and easily manage them in our application.
Let’s improve our to-do application using the context API.
As usual, we need to create a todoContext
in the todoContext.js
file.
import { createContext } from "react";
const initialState = [];
export default createContext(initialState);
Here we created the context, passing an initial value of an empty array. Then we exported the context.
Let’s refactor our App.js
file by separating the to-do list and items.
import { useReducer, useState } from "react";
import "./styles.css";
import todoReducer, { ADD_TODO } from "./todoReducer";
import TodoContext from "./todoContext";
import TodoList from "./TodoList"; export default function App() { const [id, setId] = useState(0); const [text, setText] = useState(""); const initialState = []; const [todoState, todoDispatch] = useReducer(todoReducer, initialState); const addTodoItem = (e) => { e.preventDefault(); const newId = id + 1; setId(newId); todoDispatch({ type: ADD_TODO, id: newId, text: text }); setText(""); }; return ( <TodoContext.Provider value={[todoState, todoDispatch]}> <div className="app"> <h1>Todo Example</h1> <form className="input" onSubmit={addTodoItem}> <input value={text} onChange={(e) => setText(e.target.value)} /> <button disabled={text.length === 0} type="submit"> + </button> </form> <TodoList /> </div> </TodoContext.Provider> );
}
Here, we wrapped our App.js
file with the TodoContext.Provider
then we passed the return values of our todoReducer
to it. This makes the reducer’s state and dispatch
function to be accessible throughout our application.
We then separated the to-do display into a component TodoList
. We did this without prop drilling, thanks to the Context API. Let’s take a look at the TodoList.js
file.
import React, { useContext } from "react";
import TodoContext from "./todoContext";
import Todo from "./Todo";
const TodoList = () => { const [state] = useContext(TodoContext); return ( <div className="todos"> {state.map((todo) => ( <Todo key={todo.id} todo={todo} /> ))} </div> );
};
export default TodoList;
Using array destructuring, we can access the state (leaving the dispatch function) from the context using the useContext
hook. We can then map through the state and display the to-do items. We still extracted this in a Todo
component. The ES6+ map function requires us to pass a unique key and since we need the specific to-do, we pass it alongside as well.
Let’s take a look at the Todo
component.
import React, { useContext } from "react";
import TodoContext from "./todoContext";
import { REMOVE_TODO, COMPLETE_TODO } from "./todoReducer";
const Todo = ({ todo }) => { const [, dispatch] = useContext(TodoContext); const removeTodo = (id) => { dispatch({ type: REMOVE_TODO, id }); }; const completeTodo = (id) => { dispatch({ type: COMPLETE_TODO, id }); }; return ( <div className="todoItem"> <p className={todo.completed ? "strikethrough" : "nostrikes"}> {todo.text} </p> <span onClick={() => removeTodo(todo.id)}>✕</span> <span onClick={() => completeTodo(todo.id)}>✓</span> </div> );
};
export default Todo;
Again using array destructuring, we accessed the dispatch function from the context. This allows us to define the completeTodo
and removeTodo
function as already discussed in the useReducer
section. With the todo
prop passed from todoList.js
we can display a to-do item. We can also mark it as completed and remove the to-do as we deem fit.
It is also possible to nest more than one context provider in the root of our application. This means that we can use more than one context to perform different functions in an application.
To demonstrate this, let’s add theming to the to-do example.
Here’s what we’ll be building.
Again, we have to create themeContext
. To do this, create a themeContext.js
file and add the following codes.
import { createContext } from "react";
import colors from "./colors";
export default createContext(colors.light);
Here, we created a context and passed colors.light
as the initial value. Let’s define the colors with this property in the colors.js
file.
const colors = { light: { backgroundColor: "#fff", color: "#000" }, dark: { backgroundColor: "#000", color: "#fff" }
};
export default colors;
In the code above, we created a colors
object containing light and dark properties. Each property has backgroundColor
and color
object.
Next, we create the themeReducer
to handle the theme states.
import Colors from "./colors";
export const LIGHT = "LIGHT";
export const DARK = "DARK";
const themeReducer = (state, action) => { switch (action.type) { case LIGHT: return { ...Colors.light }; case DARK: return { ...Colors.dark }; default: return state; }
};
export default themeReducer;
Like all reducers, the themeReducer
takes the state and the action. It then uses the switch
statement to determine the current action. If it’s of type LIGHT
, we simply assign Colors.light
props and if it’s of type DARK
, we display Colors.dark
props. We could have easily done this with the useState
hook but we choose useReducer
to drive the point home.
Having set up the themeReducer
, we can then integrate it in our App.js
file.
import { useReducer, useState, useCallback } from "react";
import "./styles.css";
import todoReducer, { ADD_TODO } from "./todoReducer";
import TodoContext from "./todoContext";
import ThemeContext from "./themeContext";
import TodoList from "./TodoList";
import themeReducer, { DARK, LIGHT } from "./themeReducer";
import Colors from "./colors";
import ThemeToggler from "./ThemeToggler"; const themeSetter = useCallback( theme => themeDispatch({type: theme}, [themeDispatch]); export default function App() { const [id, setId] = useState(0); const [text, setText] = useState(""); const initialState = []; const [todoState, todoDispatch] = useReducer(todoReducer, initialState); const [themeState, themeDispatch] = useReducer(themeReducer, Colors.light); const themeSetter = useCallback( (theme) => { themeDispatch({ type: theme }); }, [themeDispatch] ); const addTodoItem = (e) => { e.preventDefault(); const newId = id + 1; setId(newId); todoDispatch({ type: ADD_TODO, id: newId, text: text }); setText(""); }; return ( <TodoContext.Provider value={[todoState, todoDispatch]}> <ThemeContext.Provider value={[ themeState, themeSetter ]} > <div className="app" style={{ ...themeState }}> <ThemeToggler /> <h1>Todo Example</h1> <form className="input" onSubmit={addTodoItem}> <input value={text} onChange={(e) => setText(e.target.value)} /> <button disabled={text.length === 0} type="submit"> + </button> </form> <TodoList /> </div> </ThemeContext.Provider> </TodoContext.Provider> );
}
In the above code, we added a few things to our already existing to-do application. We began by importing the ThemeContext
, themeReducer
, ThemeToggler
, and Colors
. We created a reducer using the useReducer
hook, passing the themeReducer
and an initial value of Colors.light
to it. This returned the themeState
and themeDispatch
to us.
We then nested our component with the provider function from the ThemeContext
, passing the themeState
and the dispatch
functions to it. We also added theme styles to it by spreading out the themeStates
. This works because the colors
object already defined properties similar to what the JSX styles will accept.
However, the actual theme toggling happens in the ThemeToggler
component. Let’s take a look at it.
import ThemeContext from "./themeContext";
import { useContext, useState } from "react";
import { DARK, LIGHT } from "./themeReducer";
const ThemeToggler = () => { const [showLight, setShowLight] = useState(true); const [themeState, themeSetter] = useContext(ThemeContext); const dispatchDarkTheme = () => themeSetter(DARK); const dispatchLightTheme = () => themeSetter(LIGHT); const toggleTheme = () => { showLight ? dispatchDarkTheme() : dispatchLightTheme(); setShowLight(!showLight); }; console.log(themeState); return ( <div> <button onClick={toggleTheme}> {showLight ? "Change to Dark Theme" : "Change to Light Theme"} </button> </div> );
};
export default ThemeToggler;
In this component, we used the useContext
hook to retrieve the values we passed to the ThemeContext.Provider
from our App.js
file. As shown above, these values include the ThemeState
, dispatch function for the light theme, and dispatch function for the dark theme. Thereafter, we simply called the dispatch functions to toggle the themes. We also created a state showLight
to determine the current theme. This allows us to easily change the button text depending on the current theme.
The useMemo
Hook
The useMemo
hook is designed to memoize expensive computations. Memoization simply means caching. It caches the computation result with respect to the dependency values so that when the same values are passed, useMemo
will just spit out the already computed value without recomputing it again. This can significantly improve performance when done correctly.
The hook can be used as follows:
const memoizedResult = useMemo(() => expensiveComputation(a, b), [a, b])
Let’s consider three cases of the useMemo
hook.
- When the dependency values, a and b remain the same.
TheuseMemo
hook will return the already computed memoized value without recomputation. - When the dependency values, a and b change.
The hook will recompute the value. - When no dependency value is passed.
The hook will recompute the value.
Let’s take a look at an example to demonstrate this concept.
In the example below, we’ll be computing the PAYE and Income after PAYE of a company’s employees with fake data from JSONPlaceholder.
The calculation will be based on the personal income tax calculation procedure for Nigeria providers by PricewaterhouseCoopers available here.
This is shown in the sandbox below.
First, we queried the API to get the employees’ data. We also get data for each employee (with respect to their employee id).
const [employee, setEmployee] = useState({}); const [employees, setEmployees] = useState([]); const [num, setNum] = useState(1); const endPoint = "https://my-json-server.typicode.com/ifeanyidike/jsondata/employees"; useEffect(() => { const getEmployee = async () => { const { data } = await axios.get(`${endPoint}/${num}`); setEmployee(data); }; getEmployee(); }, [num]); useEffect(() => { axios.get(endPoint).then(({ data }) => setEmployees(data)); }, [num]);
We used axios
and the async/await
method in the first useEffect
and then the dot then syntax in the second. These two approaches work in the same way.
Next, using the employee data we got from above, let’s calculate the relief variables:
const taxVariablesCompute = useMemo(() => { const { income, noOfChildren, noOfDependentRelatives } = employee; //supposedly complex calculation //tax relief computations for relief Allowance, children relief, // relatives relief and pension relief const reliefs = reliefAllowance1 + reliefAllowance2 + childrenRelief + relativesRelief + pensionRelief; return reliefs; }, [employee]);
This is a fairly complex calculation and so we had to wrap it in a useMemo
hook to memoize or optimize it. Memoizing it this way will ensure that the calculation will not be recomputed if we tried to access the same employee again.
Furthermore, using the tax relief values obtained above, we’d like to calculate the PAYE and income after PAYE.
const taxCalculation = useMemo(() => { const { income } = employee; let taxableIncome = income - taxVariablesCompute; let PAYE = 0; //supposedly complex calculation //computation to compute the PAYE based on the taxable income and tax endpoints const netIncome = income - PAYE; return { PAYE, netIncome }; }, [employee, taxVariablesCompute]);
We performed tax calculation (a fairly complex calculation) using the above-computed tax variables and then memoized it with the useMemo
hook.
The complete code is available on here.
This follows the tax calculation procedure given here. We first computed the tax relief considering income, number of children, and number of dependent relatives. Then, we multiplied the taxable income by the PIT rates in steps. While the calculation in question is not entirely necessary for this tutorial, it is provided to show us why useMemo
may be necessary. This is also a fairly complex calculation and so we may need to memorize it with useMemo
as shown above.
After calculating the values, we simply displayed the result.
Note the following about the useMemo
hook.
useMemo
should be used only when it is necessary to optimize the computation. In other words, when recomputation is expensive.- It is advisable to first write the calculation without memorization and only memorize it if it is causing performance issues.
- Unnecessary and irrelevant use of the
useMemo
hook may even compound the performance issues. - Sometimes, too much memoization can also cause performance issues.
The useCallback
Hook
useCallback
serves the same purpose as useMemo
but it returns a memoized callback instead of a memoized value. In other words, useCallback
is the same as passing useMemo
without a function call.
For instance, consider the following codes below.
import React, {useCallback, useMemo} from 'react' const MemoizationExample = () => { const a = 5 const b = 7 const memoResult = useMemo(() => a + b, [a, b]) const callbackResult = useCallback(a + b, [a, b]) console.log(memoResult) console.log(callbackResult) return( <div> ... </div> ) } export default MemoizationExample
In the above example, both memoResult
and callbackResult
will give the same value of 12
. Here, useCallback
will return a memoized value. However, we could also make it return a memoized callback by passing it as a function.
The useCallback
below will return a memoized callback.
... const callbackResult = useCallback(() => a + b, [a, b])
...
We can then trigger the callback when an action is performed or in a useEffect
hook.
import {useCallback, useEffect} from 'react'
const memoizationExample = () => { const a = 5 const b = 7 const callbackResult = useCallback(() => a + b, [a, b]) useEffect(() => { const callback = callbackResult() console.log(callback) }) return ( <div> <button onClick= {() => console.log(callbackResult())}> Trigger Callback </button> </div> )
} export default memoizationExample
In the above code, we defined a callback function using the useCallback
hook. We then called the callback in a useEffect
hook when the component mounts and also when a button is clicked.
Both the useEffect
and the button click yield the same result.
Note that the concepts, do’s, and don’ts that apply to the useMemo
hook also apply to the useCallback
hook. We can recreate the useMemo
example with useCallback
.
The useRef
Hook
useRef
returns an object that can persist in an application. The hook has only one property, current
, and we can easily pass an argument to it.
It serves the same purpose a createRef
used in class-based components. We can create a reference with this hook as follows:
const newRef = useRef('')
Here we created a new ref called newRef
and passed an empty string to it.
This hook is used mainly for two purposes:
- Accessing or manipulating the DOM, and
- Storing mutable states — this is useful when we don’t want the component to rerender when a value change.
Manipulating the DOM
When passed to a DOM element, the ref object points to that element and can be used to access its DOM attributes and properties.
Here is a very simple example to demonstrate this concept.
import React, {useRef, useEffect} from 'react' const RefExample = () => { const headingRef = useRef('') console.log(headingRef) return( <div> <h1 className='topheading' ref={headingRef}>This is a h1 element</h1> </div> )
}
export default RefExample
In the example above, we defined headingRef
using the useRef
hook passing an empty string. We then set the ref in the h1
tag by passing ref = {headingRef}
. By setting this ref, we have asked the headingRef
to point to our h1
element. This means that we can access the properties of our h1
element from the ref.
To see this, if we check the value of console.log(headingRef)
, we’ll get {current: HTMLHeadingElement}
or {current: h1}
and we can assess all the properties or attributes of the element. A similar thing applies to any other HTML element.
For instance, we could make the text italic when the component mounts.
useEffect(() => { headingRef.current.style.fontStyle = "italic";
}, []);
We can even change the text to something else.
... headingRef.current.innerHTML = "A Changed H1 Element";
...
We can even change the background color of the parent container as well.
... headingRef.current.parentNode.style.backgroundColor = "red";
...
Any kind of DOM manipulation can be done here. Observe that headingRef.current
can be read in the same way as document.querySelector('.topheading')
.
One interesting use case of the useRef
hook in manipulating the DOM element is to focus the cursor on the input element. Let’s quickly run through it.
import {useRef, useEffect} from 'react' const inputRefExample = () => { const inputRef = useRef(null) useEffect(() => { inputRef.current.focus() }, []) return( <div> <input ref={inputRef} /> <button onClick = {() => inputRef.current.focus()}>Focus on Input </button> </div> )
}
export default inputRefExample
In the above code, we created inputRef
using the useRef
hook and then asked it to point to the input element. We then made the cursor focus on the input ref when the component loads and when the button is clicked using inputRef.current.focus()
. This is possible because focus()
is an attribute of input elements and so the ref will be able to assess the methods.
Refs created in a parent component can be assessed at the child component by forwarding it using React.forwardRef()
. Let’s take a look at it.
Let’s first create another component NewInput.js
and add the following codes to it.
import { useRef, forwardRef } from "react";
const NewInput = forwardRef((props, ref) => { return <input placeholder={props.val} ref={ref} />;
});
export default NewInput;
This component accepts props
and ref
. We passed the ref to its ref prop and props.val
to its placeholder prop. Regular React components do not take a ref
attribute. This attribute is available only when we wrap it with React.forwardRef
as shown above.
We can then easily call this in the parent component.
...
<NewInput val="Just an example" ref={inputRef} />
...
Storing The Mutable States
Refs are not just used to manipulate DOM elements, they can also be used to store mutable values without re-rendering the entire component.
The following example will detect the number of times a button is clicked without re-rendering the component.
import { useRef } from "react"; export default function App() { const countRef = useRef(0); const increment = () => { countRef.current++; console.log(countRef); }; return ( <div className="App"> <button onClick={increment}>Increment </button> </div> );
}
In the code above, we incremented the countRef
when the button is clicked and then logged it to the console. Although the value is incremented as shown in the console, we won’t be able to see any change if we try to assess it directly in our component. It will only update in the component when it re-renders.
Note that while useState
is asynchronous, useRef
is synchronous. In other words, the value is available immediately after it is updated.
The useLayoutEffect
Hook
Like the useEffect
hook, useLayoutEffect
is called after the component is mounted and rendered. This hook fires after DOM mutation and it does so synchronously. Apart from getting called synchronously after DOM mutation, useLayoutEffect
does the same thing as useEffect
.
useLayoutEffect
should only be used for performing DOM mutation or DOM-related measurement, otherwise, you should use the useEffect
hook. Using the useEffect
hook for DOM mutation functions may cause some performance issues such as flickering but useLayoutEffect
handles them perfectly as it runs after the mutations have occurred.
Let’s take a look at some examples to demonstrate this concept.
- We’ll be getting the width and height of the window on resize.
import {useState, useLayoutEffect} from 'react' const ResizeExample = () =>{ const [windowSize, setWindowSize] = useState({width: 0, height: 0}) useLayoutEffect(() => { const resizeWindow = () => setWindowSize({ width: window.innerWidth, height: window.innerHeight }) window.addEventListener('resize', resizeWindow) return () => window.removeEventListener('resize', resizeWindow) }, []) return ( <div> <p>width: {windowSize.width}</p> <p>height: {windowSize.height}</p> </div> )
}
export default ResizeExample
In the above code, we created a state windowSize
with width and height properties. Then we set the state to the current window’s width and height respectively when the window is resized. We also cleaned up the code when it unmounts. The clean-up process is essential in useLayoutEffect
to clean up the DOM manipulation and improve efficiency.
- Let’s blur a text with
useLayoutEffect
.
import { useRef, useState, useLayoutEffect } from "react"; export default function App() { const paragraphRef = useRef(""); useLayoutEffect(() => { const { current } = paragraphRef; const blurredEffect = () => { current.style.color = "transparent"; current.style.textShadow = "0 0 5px rgba(0,0,0,0.5)"; }; current.addEventListener("click", blurredEffect); return () => current.removeEventListener("click", blurredEffect); }, []); return ( <div className="App"> <p ref={paragraphRef}>This is the text to blur</p> </div> );
}
We used useRef
and useLayoutEffect
together in the above code. We first created a ref, paragraphRef
to point to our paragraph. Then we created an on-click event listener to monitor when the paragraph is clicked and then blurred it using the style properties we defined. Finally, we cleaned up the event listener using removeEventListener
.
The useDispatch
And useSelector
Hooks
useDispatch
is a Redux hook for dispatching (triggering) actions in an application. It takes an action object as an argument and invokes the action. useDispatch
is the hook’s equivalence to mapDispatchToProps
.
On the other hand, useSelector
is a Redux hook for assessing Redux states. It takes a function to select the exact Redux reducer from the store and then returns the corresponding states.
Once our Redux store is connected to a React application through the Redux provider, we can invoke the actions with useDispatch
and access the states with useSelector
. Every Redux action and state can be assessed with these two hooks.
Note that these states ship with React Redux (a package that makes assessing the Redux store easy in a React application). They are not available in the core Redux library.
These hooks are very simple to use. First, we have to declare the dispatch function and then trigger it.
import {useDispatch, useSelector} from 'react-redux'
import {useEffect} from 'react'
const myaction from '...' const ReduxHooksExample = () =>{ const dispatch = useDispatch() useEffect(() => { dispatch(myaction()); //alternatively, we can do this dispatch({type: 'MY_ACTION_TYPE'}) }, []) const mystate = useSelector(state => state.myReducerstate) return( ... )
}
export default ReduxHooksExample
In the above code, we imported useDispatch
and useSelector
from react-redux
. Then, in a useEffect
hook, we dispatched the action. We could define the action in another file and then call it here or we could define it directly as shown in the useEffect
call.
Once we have dispatched the actions, our states will be available. We can then retrieve the state using the useSelector
hook as shown. The states can be used in the same way we would use states from the useState
hook.
Let’s take a look at an example to demonstrate these two hooks.
To demonstrate this concept, we have to create a Redux store, reducer, and actions. To simplify things here, we’ll be using the Redux Toolkit library with our fake database from JSONPlaceholder.
We need to install the following packages to get started. Run the following bash commands.
npm i redux @reduxjs/toolkit react-redux axios
First, let’s create the employeesSlice.js
to handle the reducer and action for our employees’ API.
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
const endPoint = "https://my-json-server.typicode.com/ifeanyidike/jsondata/employees"; export const fetchEmployees = createAsyncThunk("employees/fetchAll", async () => { const { data } = await axios.get(endPoint); return data;
}); const employeesSlice = createSlice({ name: "employees", initialState: { employees: [], loading: false, error: "" }, reducers: {}, extraReducers: { [fetchEmployees.pending]: (state, action) => { state.status = "loading"; }, [fetchEmployees.fulfilled]: (state, action) => { state.status = "success"; state.employees = action.payload; }, [fetchEmployees.rejected]: (state, action) => { state.status = "error"; state.error = action.error.message; } }
});
export default employeesSlice.reducer;
This is the standard setup for the Redux toolkit. We used the createAsyncThunk
to access the Thunk
middleware to perform async actions. This allowed us to fetch the list of employees from the API. We then created the employeesSlice
and returned, “loading”, “error”, and the employees’ data depending on the action types.
Redux toolkit also makes setting up the store easy. Here is the store.
import { configureStore } from "@reduxjs/toolkit";
import { combineReducers } from "redux";
import employeesReducer from "./employeesSlice"; const reducer = combineReducers({ employees: employeesReducer
}); export default configureStore({ reducer });;
Here, we used combineReducers
to bundle the reducers and the configureStore
function provided by Redux toolkit to set up the store.
Let’s proceed to use this in our application.
First, we need to connect Redux to our React application. Ideally, this should be done at the root of our application. I like to do it in the index.js
file.
import React, { StrictMode } from "react";
import ReactDOM from "react-dom";
import store from "./redux/store";
import { Provider } from "react-redux";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render( <Provider store={store}> <StrictMode> <App /> </StrictMode> </Provider>, rootElement
);
Here, I’ve imported the store I created above and also Provider
from react-redux
.
Then, I wrapped the entire application with the Provider
function, passing the store to it. This makes the store accessible throughout our application.
We can then proceed to use the useDispatch
and useSelector
hooks to fetch the data.
Let’s do this in our App.js
file.
import { useDispatch, useSelector } from "react-redux";
import { fetchEmployees } from "./redux/employeesSlice";
import { useEffect } from "react"; export default function App() { const dispatch = useDispatch(); useEffect(() => { dispatch(fetchEmployees()); }, [dispatch]); const employeesState = useSelector((state) => state.employees); const { employees, loading, error } = employeesState; return ( <div className="App"> {loading ? ( "Loading..." ) : error ? ( <div>{error}</div> ) : ( <> <h1>List of Employees</h1> {employees.map((employee) => ( <div key={employee.id}> <h3>{`${employee.firstName} ${employee.lastName}`}</h3> </div> ))} </> )} </div> );
}
In the above code, we used the useDispatch
hook to invoke the fetchEmployees
action created in the employeesSlice.js
file. This makes the employees state to be available in our application. Then, we used the useSelector
hook to get the states. Thereafter, we displayed the results by mapping through the employees
.
The useHistory
Hook
Navigation is very important in a React application. While you could achieve this in a couple of ways, React Router provides a simple, efficient and popular way to achieve dynamic routing in a React application. Furthermore, React Router provides a couple of hooks for assessing the state of the router and performing navigation on the browser but to use them, you need to first set up your application properly.
To use any React Router hook, we should first wrap our application with BrowserRouter
. We can then nest the routes with Switch
and Route
.
But first, we have to install the package by running the following commands.
npm install react-router-dom
Then, we need to set up our application as follows. I like to do this in my App.js
file.
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Employees from "./components/Employees";
export default function App() { return ( <div className="App"> <Router> <Switch> <Route path='/'> <Employees /> </Route> ... </Switch> </Router> </div> );
}
We could have as many Routes as possible depending on the number of components we wish to render. Here, we have rendered only the Employees
component. The path
attribute tells React Router DOM the path of the component and can be assessed with query string or various other methods.
The order matters here. The root route should be placed below the child route and so forth. To override this order, you need to include the exact
keyword on the root route.
<Route path='/' exact > <Employees />
</Route>
Now that we have set up the router, we can then use the useHistory
hook and other React Router hooks in our application.
To use the useHistory
hook, we need to first declare it as follows.
import {useHistory} from 'history'
import {useHistory} from 'react-router-dom' const Employees = () =>{ const history = useHistory() ...
}
If we log history to the console, we’ll see several properties associated with it. These include block
, createHref
, go
, goBack
, goForward
, length
, listen
, location
, push
, replace
. While all these properties are useful, you will most likely use history.push
and history.replace
more often than other properties.
Let’s use this property to move from one page to another.
Assuming we want to fetch data about a particular employee when we click on their names. We can use the useHistory
hook to navigate to the new page where the employee’s information will be displayed.
function moveToPage = (id) =>{ history.push(`/employees/${id}`)
}
We can implement this in our Employee.js
file by adding the following.
import { useEffect } from "react";
import { Link, useHistory, useLocation } from "react-router-dom"; export default function Employees() { const history = useHistory(); function pushToPage = (id) => { history.push(`/employees/${id}`) } ... return ( <div> ... <h1>List of Employees</h1> {employees.map((employee) => ( <div key={employee.id}> <span>{`${employee.firstName} ${employee.lastName} `}</span> <button onClick={pushToPage(employee.id)}> » </button> </div> ))} </div> );
}
In the pushToPage
function, we used history
from the useHistory
hook to navigate to the employee’s page and pass the employee id alongside.
The useLocation
Hook
This hook also ships with React Router DOM. It is a very popular hook used to work with the query string parameter. This hook is similar to the window.location
in the browser.
import {useLocation} from 'react' const LocationExample = () =>{ const location = useLocation() return ( ... )
}
export default LocationExample
The useLocation
hook returns the pathname
, search
parameter, hash
and state
. The most commonly used parameters include the pathname
and search
but you could equally use hash
, and state
a lot in your application.
The location pathname
property will return the path we set in our Route
set up. While search
will return the query search parameter if any. For instance, if we pass 'http://mywebsite.com/employee/?id=1'
to our query, the pathname
would be /employee
and the search
would be ?id=1
.
We can then retrieve the various search parameters using packages like query-string or by coding them.
The useParams
Hook
If we set up our Route with a URL parameter in its path attribute, we can assess those parameters as key/value pairs with the useParams
hook.
For instance, let’s assume that we have the following Route.
<Route path='/employees/:id' > <Employees />
</Route>
The Route will be expecting a dynamic id in place of :id
.
With the useParams
hook, we can assess the id passed by the user, if any.
For instance, assuming the user passes the following in function with history.push
,
function goToPage = () => { history.push(`/employee/3`)
}
We can use the useParams
hook to access this URL parameter as follows.
import {useParams} from 'react-router-dom' const ParamsExample = () =>{ const params = useParams() console.log(params) return( <div> ... </div> )
}
export default ParamsExample
If we log params
to the console, we’ll get the following object {id: "3"}
.
The useRouteMatch
Hook
This hook provides access to the match object. It returns the closest match to a component if no argument is supplied to it.
The match object returns several parameters including the path
(the same as the path specified in Route), the URL
, params
object, and isExact
.
For instance, we can use useRouteMatch
to return components based on the route.
import { useRouteMatch } from "react-router-dom";
import Employees from "...";
import Admin from "..." const CustomRoute = () => { const match = useRouteMatch("/employees/:id"); return match ? ( <Employee /> ) : ( <Admin /> );
};
export default CustomRoute;
In the above code, we set a route’s path with useRouteMatch
and then rendered the <Employee />
or <Admin />
component depending on the route selected by the user.
For this to work, we still need to add the route to our App.js
file.
... <Route> <CustomRoute /> </Route>
...
Building A Custom Hook
According to the React documentation, building a custom hook allows us to extract a logic into a reusable function. However, you need to make sure that all the rules that apply to React hooks apply to your custom hook. Check the rules of React hook at the top of this tutorial and ensure that your custom hook complies with each of them.
Custom hooks allow us to write functions once and reuse them whenever they are needed and hence obeying the DRY principle.
For instance, we could create a custom hook to get the scroll position on our page as follows.
import { useLayoutEffect, useState } from "react"; export const useScrollPos = () => { const [scrollPos, setScrollPos] = useState({ x: 0, y: 0 }); useLayoutEffect(() => { const getScrollPos = () => setScrollPos({ x: window.pageXOffset, y: window.pageYOffset }); window.addEventListener("scroll", getScrollPos); return () => window.removeEventListener("scroll", getScrollPos); }, []); return scrollPos;
};
Here, we defined a custom hook to determine the scroll position on a page. To achieve this, we first created a state, scrollPos
, to store the scroll position. Since this will be modifying the DOM, we need to use useLayoutEffect
instead of useEffect
. We added a scroll event listener to capture the x and y scroll positions and then cleaned up the event listener. Finally, we returned to the scroll position.
We can use this custom hook anywhere in our application by calling it and using it just as we would use any other state.
import {useScrollPos} from './Scroll' const App = () =>{ const scrollPos = useScrollPos() console.log(scrollPos.x, scrollPos.y) return ( ... )
}
export default App
Here, we imported the custom hook useScrollPos
we created above. Then we initialized it and then logged the value to our console. If we scroll on the page, the hook will show us the scroll position at every step of the scroll.
We can create custom hooks to do just about anything we can imagine in our app. As you can see, we simply need to use the inbuilt React hook to perform some functions. We can also use third-party libraries to create custom hooks but if we do so, we will have to install that library to be able to use the hook.
Conclusion
In this tutorial, we took a good look at some useful React hooks you will be using in most of your applications. We examined what they present and how to use them in your application. We also looked at several code examples to help you understand these hooks and apply them to your application.
I encourage you to try these hooks in your own application to understand them more.