Why Hooks Exist
Before React 16.8, class-based components were the only option for projects requiring state and lifecycle methods. These components created three serious problems for developers: deeply nested component trees, bloated components mixing unrelated logic, and verbose class syntax that tripped up even experienced developers.
React introduced hooks at React Conf 2018 to solve these issues. Hooks are functions that let you access React features directly from functional components. They eliminate wrapper hell by making logic easy to share without higher-order components or render props. They let you organize side effects by functionality rather than by lifecycle method, keeping components manageable as they grow. And they remove the ceremony of classes—no more binding event handlers, no more confusing this context.
Consider the difference between these two equivalent components. A class-based version requires boilerplate and syntax overhead:
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>
);
}
}
The functional version with hooks achieves the exact same result with far less code:
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>
);
}
Hook Conventions
Hooks follow three simple rules:
- Hook names must begin with the prefix
use, likeuseStateoruseEffect. Modern editors with the ESLint React hooks plugin can warn you when you break this convention. - Call hooks only at the top level of a component, before the
returnstatement. You cannot invoke them inside conditionals, loops, or nested functions. - Call hooks only from React functions—either components or custom hooks—never from plain JavaScript functions.
Working with useState
useState is the fundamental React hook for managing state in functional components. Import it from react:
import {useState} from 'react'
You declare a state variable and its updater function by destructuring the array or value pair that useState returns:
const [state, updaterFn] = useState('')
The naming follows a clear convention. The first array element is the state; the second is the updater function, commonly prefixed with set and the state name in camel case. For example:
const [count, setCount] = useState(0)
State can hold any JavaScript data type—numbers, strings, booleans, arrays, objects, even BigInt. The hook accepts an initial value as its argument.
Updates and Re-renders
Calling the updater function triggers a component re-render, but only when React detects an actual difference between old and new state. React uses the JavaScript Object.is algorithm for this comparison. You can pass a new value directly:
setCount(newValue)
When the new state depends on the previous value, pass a function instead:
setCount(prevCount => prevCount + 5)
This is critical for asynchronous operations and rapid successive updates, as the functional form always uses the latest state.
Updating Arrays and Objects
Setting state for arrays and objects works the same way, but preserving existing values requires the ES6 spread operator. Without it, the new value replaces the entire state:
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
Notice how both setArr and setObj spread the existing values first. React compares old and new state using Object.is; if a reference is replaced without retaining prior properties, those properties are lost.
Asynchronous Updates
useState updates are asynchronous. When you call an updater function, React queues the new value but does not immediately apply it. Reading the state variable right after calling the updater returns the old value:
const [count, setCount] = useState(0)
function handleClick() {
setCount(count + 1)
console.log(count) // logs the old value, not the new one
}
To access the new value immediately, store it in a local variable before updating:
function handleClick() {
const newCountValue = count + 1
setCount(newCountValue)
console.log(newCountValue) // logs the correct new value
}
This queued update mechanism requires care with rapid sequential updates. Using the functional form of the updater ensures you always work with the latest state, avoiding stale closures and out-of-sync values.
Scheduling Side Effects With useEffect
Almost every real React project relies on useEffect at some point. The hook covers ground similar to the class-based lifecycle methods (componentDidMount, componentWillUnmount, and componentDidUpdate), but with a single unified API. It is the place to run imperative code that has side effects on the application: logging, subscriptions, mutations, and the like.
When no second argument is supplied, the effect runs after every render. This basic example simply logs the current count value on each render:
import {useState, useEffect} from 'react'
const App = () =>{
const [count, setCount] = useState(0)
useEffect(() =>{
console.log(count)
})
return(
<div>
...
</div>
)
}
That behavior is often not what you want. useEffect accepts a second argument: an array of dependencies. Providing an empty array instructs React to run the effect only once, on mount. In the following example, the component mounts, the count updates from 0 to 1, and the effect logs the initial value:
import {useState, useEffect} from 'react'
const App = () =>{
const [count, setCount] = useState(0)
useEffect(() =>{
setCount(count + 1)
}, [])
return(
<div>
<h1>{count}</h1>
...
</div>
)
}
To run the side effect when specific values change, list those values in the dependency array:
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;
This effect runs under two conditions:
- On mount, after the component's first render.
- Whenever the
countvalue changes.
Once you start supplying dependencies, be thorough. If an effect relies on a value that is not declared in the dependency array, the side effect may behave unexpectedly. Linters such as ESLint will typically flag missing dependencies.
Cleaning Up the Effect
An effect can clean up resources before the component unmounts by returning a cleanup function. This helps avoid memory leaks and keeps the app efficient. A cleanup function logging on unmount would look like this:
useEffect(() => {
console.log('mounted')
return () => console.log('unmounting... clean up here')
})
The general cleanup structure is of the form:
useEffect(() => {
//The effect we intend to make
effect
//We then return the clean up
return () => the cleanup/unsubscription
})
Cleanup is particularly relevant when dealing with subscriptions and timers. For example, when working with web sockets, you likely want to unsubscribe when the component leaves the screen to avoid wasting resources.
Data Fetching With useEffect
Fetching data from an API is one of the most common use cases for useEffect. The following example pulls a list of users from JSONPlaceholder using Axios and renders it. The empty dependency array keeps the request to a single call on mount:
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>
);
}
Data can also be refetched when certain conditions change. This example uses two effects: one to fetch the full user list (so we know how many users exist), and another that fetches a single user by id. Because id is in the second effect's dependency list, the request fires again whenever its value changes:
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>
);
}
Buttons update the id, which in turn triggers the effect. If you want to go a step further, Axios allows you to cancel an in-flight request during cleanup. The token is passed as a second parameter to axios.get, and the subscription is canceled when the component unmounts:
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]);
Complex State Logic With useReducer
For state management logic that goes beyond simple value replacement, useReducer is a suitable alternative to useState. The React documentation recommends it for more complex state logic, and internally useState is actually implemented on top of useReducer.
The hook accepts a reducer function, and optionally an initial state and an init function:
const [state, dispatch] = useReducer(reducer, initialState, init)
The init function is used for lazy creation of the initial state.
A simple to-do app shows the pattern in action:
First, define the reducer to hold the state transitions:
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;
Action types are declared as constants to avoid typos. Following the Redux convention, the reducer function takes the current state and an action object. Inside, a switch statement checks the action type: ADD_TODO appends a new item, REMOVE_TODO filters out the item matching the given id, and COMPLETE_TODO toggles the corresponding item's completion flag.
The component wires it together:
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>
);
}
A form collects input and dispatches an ADD_TODO action with a new id and the to-do text. The id is generated by incrementing the previous id value. Delete and complete buttons dispatch their respective actions, which are handled in the reducer.
The useReducer hook takes the reducer and the initial state, returning the current state and a dispatch function. Dispatch plays the same role as the setter from useState, but is named as we like. To display the list, we map over the to-dos held in the state object.
This functionality could be built with useState, but the reducer keeps complex updates in one pure JavaScript function — easier to test in isolation and easier to reason about as component logic grows. For a larger app, a useReducer paired with dispatch exposed via context allows different parts of the tree to fire actions, update state, and react to changes.
The third argument to useReducer enables lazy initial-state creation. If an init function is supplied, it overrides the provided initialState and computes it lazily:
const initFunc = () => [
{
id: id,
text: "First Item",
completed: false
}
]
const [state, dispatch] = useReducer(reducer, initialState, initFunc)
Sharing State With useContext
The Context API, stable since React 16.3.0, lets components share data anywhere in the tree without prop drilling. A context is created with React.createContext and then consumed with the useContext hook. It works application-wide or for just a slice of the tree.
To see it in action, consider a tiny app that adjusts the global font size. First, we define the context and give it a default value of 16:
import { createContext } from "react";
//Here, we set the initial fontSize as 16.
const fontSizeContext = createContext(16);
export default fontSizeContext;
The context is then attached to the component tree. The Provider receives a value prop, here the size state created with useState, so the value stays in sync with state changes:
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;
Any component under the Provider can read that value. For example, PageOne calls useContext and applies the returned font size directly to a style:
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;
This works identically in PageTwo. Changing the size from the buttons in App.js immediately re-renders both pages. Contexts fit well for themes or other app-level configuration.
Building Global State With useReducer
Combining useContext with useReducer produces a lightweight global state system. Returning to the to-do example, we first set up a context with an empty array as its default:
import { createContext } from "react";
const initialState = [];
export default createContext(initialState);
In App.js, the reducer's state and dispatch function are passed into the TodoContext.Provider, making both available throughout the tree:
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>
);
}
List components consume the context without receiving props. The TodoList component pulls the state out with useContext and maps over it:
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;
The individual Todo component grabs the dispatch function the same way, then uses it for completeTodo and removeTodo, just as in the useReducer example:
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;
Multiple providers can also be nested at the root for unrelated concerns, like adding theming to the same to-do app.
Create the theme context, with colors.light as its starting value:
import { createContext } from "react";
import colors from "./colors";
export default createContext(colors.light);
The color palette stores backgroundColor and color for each mode:
const colors = {
light: {
backgroundColor: "#fff",
color: "#000"
},
dark: {
backgroundColor: "#000",
color: "#fff"
}
};
export default colors;
A themeReducer switches between the two palettes based on action type. Either useState or useReducer works here; the reducer better shows how an app-wide state stays organized:
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;
Integrating it into App.js means nesting the theme provider around the existing to-do components. The theme state gets spread onto the wrapping JSX so its style properties apply directly:
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>
);
}
Actual toggling happens in the separate ThemeToggler component, which reads the theme state and dispatch functions from context via useContext. A small showLight state controls the button label based on the active theme:
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;
Optimizing Expensive Work With useMemo
The useMemo hook caches the result of an expensive computation and returns the cached value as long as its dependencies remain unchanged. Use it like this:
const memoizedResult = useMemo(() => expensiveComputation(a, b), [a, b])
Behavior breaks down into three cases:
- Unchanged dependencies: the hook returns the memoized value without recomputing.
- Changed dependencies: the hook recalculates.
- No dependency array: the hook recalculates on every render.
A realistic use is payroll tax calculation. After fetching employee records from JSONPlaceholder in a set of useEffect hooks, the example computes tax reliefs and net pay. Both API calls use axios, but one uses async/await while the other uses promise chaining — the result is the same:
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]);
Applying the Nigerian personal income tax rules requires several relief variables first. That derivation is wrapped in useMemo so revisiting the same employee does not redo the math:
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]);
The PAYE and take-home pay depend on those reliefs, so they are calculated in a second memoized block:
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]);
A few notes on when to use useMemo:
- Apply it only for genuinely expensive recomputations.
- Write the calculation plainly first; add memoization only if profiling shows a slowdown.
- Indiscriminate memoization can make performance worse, not better.
- An excess of memoization can itself tax the runtime.
Memoizing Functions With useCallback
useCallback corresponds directly to useMemo, except it memoizes a function instance rather than a computed value. The two can look interchangeable:
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 that snippet both memoResult and callbackResult equal 12. But while useMemo is returning the evaluated result, useCallback in its more typical form returns a stable function reference — useful when passing callbacks to memoized child components:
...
const callbackResult = useCallback(() => a + b, [a, b])
...
That callback can run in a useEffect or on demand:
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
Here the callback fires once on mount because of its empty dependency array, and again on every button click. The guidance about dependencies — and about avoiding over-optimization — that applies to useMemo applies equally to useCallback.
Reading and Mutating the DOM With useRef
useRef creates a persistent object with a single current property. Passing an initial value is straightforward:
const newRef = useRef('')
This hook serves two main purposes: accessing DOM elements directly and storing mutable values without causing a re-render when those values change.
Direct DOM Access
When attached to a JSX element via the ref attribute, the ref's current property points to that DOM node, giving you access to its properties. The following example demonstrates basic assignment:
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
With headingRef pointing to an h1, logging the ref yields {current: h1}, which can then be manipulated like any DOM element retrieved via document.querySelector. For instance, you could make the text italic on mount, alter its content, or change the background color of its container:
useEffect(() => {
headingRef.current.style.fontStyle = "italic";
}, []);
...
headingRef.current.innerHTML = "A Changed H1 Element";
...
...
headingRef.current.parentNode.style.backgroundColor = "red";
...
A particularly common use case is programmatically focusing an input element:
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
Here, inputRef.current.focus() places the cursor in the field both when the component mounts and when a button is clicked.
Refs defined in a parent can be passed to child components by wrapping the child with React.forwardRef. This is required because regular function components don't accept a ref prop. A child component receives the forwarded ref as its second argument:
import { useRef, forwardRef } from "react";
const NewInput = forwardRef((props, ref) => {
return <input placeholder={props.val} ref={ref} />;
});
export default NewInput;
The parent can then attach its own ref to the child and gain access to the underlying DOM element:
...
<NewInput val="Just an example" ref={inputRef} />
...
Storing Mutable Values
Beyond DOM access, useRef can hold any mutable value. The key distinction from useState is that updating a ref does not trigger a re-render:
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 this example, the click count increments and logs correctly, but the component never re-renders. Also note that while useState updates asynchronously, refs are synchronous: the updated value is available immediately.
Working With useLayoutEffect
useLayoutEffect runs synchronously after the component mounts and the DOM has been mutated. Its signature and usage match useEffect, but it is intended specifically for DOM mutations or measurements that could cause visible flickering if deferred. Use useEffect for everything else.
One practical demonstration tracks window dimensions 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
This sets windowSize state whenever the window resizes and cleans up the event listener on unmount. Cleanup is as important here as it is with useEffect.
Another example blurs a paragraph when it is clicked:
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>
);
}
Here, paragraphRef identifies the target, a click handler applies CSS blur styles, and the listener is removed during cleanup.
Connecting Redux With useDispatch and useSelector
These two hooks come from the react-redux package, not the core Redux library. useDispatch returns the store's dispatch function for triggering actions, replacing mapDispatchToProps. useSelector extracts state from the Redux store with a selector function, replacing mapStateToProps.
After wrapping the app in a Redux Provider, dispatching an action and reading the state requires minimal setup:
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
A fuller example pulls employee data from a remote API. Start by installing Redux Toolkit and React Redux:
npm i redux @reduxjs/toolkit react-redux axios
The slice file uses createAsyncThunk to handle the async request and createSlice to define the reducer, which returns loading, error, and employee data states:
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;
The store configuration combines the reducers with configureStore:
import { configureStore } from "@reduxjs/toolkit";
import { combineReducers } from "redux";
import employeesReducer from "./employeesSlice";
const reducer = combineReducers({
employees: employeesReducer
});
export default configureStore({ reducer });;
Connect the store to the app at the root level by importing Provider from react-redux and wrapping the application:
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
);
Finally, within a component, dispatch the async thunk inside a useEffect and retrieve the result with useSelector:
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>
);
}
Routing With useHistory
React Router's hooks provide a straightforward way to handle navigation. Install the package and wrap the app in BrowserRouter:
npm install react-router-dom
Routes are then declared inside the router. The root route should come after child routes unless it includes the exact keyword:
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>
);
}
<Route path='/' exact >
<Employees />
</Route>
The useHistory hook exposes a history object whose properties include push, replace, goBack, and goForward. Navigating programmatically is handled by history.push:
import {useHistory} from 'history'
import {useHistory} from 'react-router-dom'
const Employees = () =>{
const history = useHistory()
...
}
In a list of employees, clicking a name could trigger a navigation to a detail page, passing the employee ID along:
function moveToPage = (id) =>{
history.push(`/employees/${id}`)
}
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>
);
}
Inspecting Routes With useLocation
The useLocation hook also ships with React Router DOM and acts similarly to the browser's window.location. It returns an object containing the current pathname, search query string, hash, and any associated state:
import {useLocation} from 'react'
const LocationExample = () =>{
const location = useLocation()
return (
...
)
}
export default LocationExample
For a URL like http://mywebsite.com/employee/?id=1, the hook returns a pathname of /employee and a search value of ?id=1. Query-string values can then be parsed with a library such as query-string or by writing custom parsing logic.
Route Parameters with useParams
When a Route includes a URL parameter in its path — for example a dynamic :id segment — React Router makes that value available as a key/value pair. The useParams hook returns an object containing those parameters, letting you read the value the user navigated with.
<Route path='/employees/:id' >
<Employees />
</Route>
Given that route definition, navigating to a matching URL such as one produces via history.push supplies the id value:
function goToPage = () => {
history.push(`/employee/3`)
}
Inside the routed component, useParams exposes that parameter directly:
import {useParams} from 'react-router-dom'
const ParamsExample = () =>{
const params = useParams()
console.log(params)
return(
<div>
...
</div>
)
}
export default ParamsExample
Logging the resulting params object shows {id: "3"}, reflecting the value from the URL.
The useRouteMatch Hook
The useRouteMatch hook returns the match object for the current route. Called without arguments, it resolves to the closest match for the enclosing component. The match object contains the route's path, the current URL, the params object, and the isExact flag.
This is useful for conditionally rendering components based on the matched route, without repeating route configuration:
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;
The hook can define the path for a nested route declaration, which still needs to be registered in the app's main router file.
...
<Route>
<CustomRoute />
</Route>
...
Writing a Custom Hook
As described in the React documentation, a custom hook extracts reusable logic into a function. Such functions must obey every rule that applies to built-in hooks — they cannot be called conditionally and must follow the Rules of Hooks. Done correctly, custom hooks give you a clean way to write logic once and reuse it across the app, staying in line with the DRY principle.
A practical example is tracking the page's scroll position:
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;
};
The hook declares a scrollPos state to store the current position. Since this touches the DOM, useLayoutEffect is the appropriate effect — the scroll listener it registers captures the x and y coordinates and is cleaned up afterward. Finally, the hook returns the scroll position so callers can use it as normal state.
Using it elsewhere in the application is no different from using any other hook:
import {useScrollPos} from './Scroll'
const App = () =>{
const scrollPos = useScrollPos()
console.log(scrollPos.x, scrollPos.y)
return (
...
)
}
export default App
Importing and initializing useScrollPos gives you live scroll coordinates in the component's state, logging each update as you scroll.
The same pattern applies broadly: combine built-in hooks (or hooks from third-party libraries, which you would need to install) to encapsulate any custom behavior your app needs.
Further Resources
For deeper detail, the official references cover the essentials:
- Hooks FAQ
- Using the State Hook
- Using the Effect Hook
- Hooks API Reference
- React Redux Hooks
- React Router Hooks
- Redux Toolkit
For related reading on state management and frontend patterns:



