React Hooks: the popular ones and some custom hooks
A dive into react hooks, with examples
If you’re part of the world of React developers, the concept of Hooks must be no stranger to you. Introduced in React 16.8, Hooks have become a go-to solution for state management and side effects in functional components. This article walks you through eight of the most popular React Hooks and concludes with a discussion on creating custom Hooks.
1. useState
useState
is the simplest Hook and a great starting point to understand Hooks in general. It allows you to add state to functional components. When you call useState
, you provide the initial state, and it returns an array with the current state and a function that allows you to update it.
const [state, setState] = useState(initialState);
For example, if we were creating a simple counter:
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
};
This Hook declares a state variable count
, initialized to 0
. To update count
, you just need to call setCount()
.
2. useEffect
useEffect
lets you perform side effects in functional components, such as data fetching, setting up a subscription, or manually changing the DOM.
Consider a component that fetches user data from an API:
const UserProfile = ({userId}) => {
const [user, setUser] = useState(null);
useEffect(() => {
const fetchData = async () => {
const result = await axios(`https://api.example.com/user/${userId}`);
setUser(result.data);
};
fetchData();
}, [userId]); // The effect depends on the userId that is passed as a prop
return user ? (<h2>{user.name}</h2>) : (<p>Loading...</p>);
};
The effect runs after every render by default. However, if you pass an array of dependencies (userId int he example), the effect runs only when the dependencies change.
3. useContext
useContext
allows you to access the context without having to wrap a component in a Context Consumer. It accepts a context object (the value returned from React.createContext
) and returns the current context value, as given by the closest context provider for the given context.
Here’s a simple theme toggle component:
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function ThemeTogglerButton() {
const theme = useContext(ThemeContext);
return (
<button style={{background: theme.background, color: theme.foreground}}>
Toggle Theme
</button>
);
}
4. useReducer
useReducer
is usually preferable to useState
when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. It also lets you optimize performance for components that trigger deep updates because you can dispatch actions deep down.
An example use case is a simple counter where you can increment, decrement, or reset the value:
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return initialState;
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'reset'})}>Reset</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
Here, reducer
is a function that determines how the state changes in response to actions, and dispatch
is the method to dispatch actions to the reducer.
5. useRef
useRef
returns a mutable ref object whose .current
property is initialized with the passed argument (initialValue
). The returned object will persist for the full lifetime of the component. It can be used to access DOM elements directly.
A common use case for useRef
is to access a child imperatively:
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
6. useMemo
useMemo
will only recompute the memoized value when one of the dependencies has changed. This optimization helps to avoid expensive calculations on every render.
Consider a component that performs an expensive calculation:
function MyComponent({ a, b }) {
const result = useMemo(() => {
let output = 0;
// Imagine a really expensive computation here
for (let i = 0; i < 100000000000; i++) {
output = a + b + i;
}
return output;
}, [a, b]); // Only re-run the expensive function if `a` or `b` changes
return <h2>{result}</h2>;
}
7. useCallback
useCallback
returns a memoized version of the callback function that only changes if one of the dependencies has changed. It’s useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders.
Here’s an example with a search input that waits for the user to stop typing before executing:
function Search({ query, setQuery }) {
const debouncedSearch = useCallback(
debounce(q => {
// Make your API call here
}, 500),
[],
);
useEffect(() => {
debouncedSearch(query);
}, [query, debouncedSearch]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
8. useLayoutEffect
useLayoutEffect
has the same signature as useEffect
, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect
will be flushed synchronously, before the browser has a chance to paint.
A use case is to prevent scroll jumping:
function ScrollingList({ items, position }) {
const ref = useRef(null);
useLayoutEffect(() => {
ref.current.scrollTop = position;
}, [position]); // Only runs if position changes
return (
<div ref={ref}>
{items.map(item => <div key={item.id}>{item.name}</div>)}
</div>
);
}
The ScrollingList
component restores its scroll position right after the DOM has been updated but before it's been painted, preventing a visible jump.
Creating Custom Hooks
Custom Hooks are essentially JavaScript functions whose name starts with “use”. They can call other Hooks and even other custom Hooks.
1. useFormInput
Let’s create a custom Hook, useFormInput
, which manages form input state.
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
function handleChange(e) {
setValue(e.target.value);
}
return {
value,
onChange: handleChange
};
}
This custom Hook first initializes a state variable `value` with an initial value. It then defines a function `handleChange()` that updates `value` when the user types in the input field. Finally, it returns an object containing `value` and `handleChange`. You can now use this Hook in your functional components like this:
function MyForm() {
const name = useFormInput("");
const password = useFormInput("");
return (
<form>
<input type="text" {...name} placeholder="Name" />
<input type="password" {...password} placeholder="Password" />
</form>
);
}
Here, we’re spreading out the object returned by useFormInput
into the input elements. This is the equivalent of setting value={name.value}
and onChange={name.onChange}
.
2. useLocalStorage
Let’s take a look at another custom hook: useLocalStorage
. This hook allows you to read and write to local storage, just like you'd manage state with useState
.
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
const setValue = value => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue];
}
This custom hook first tries to get the value from local storage, and if it’s not there, it returns the initialValue
. The setValue
function is used to update both the state and the local storage value.
You can use this custom hook just like you’d use useState
:
function MyComponent() {
const [name, setName] = useLocalStorage('name', 'Anonymous');
return (
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
);
}
In this component, name
is stored in local storage. Even if you refresh the page, the input field will keep displaying the same value, thanks to the power of local storage.
In conclusion, Hooks have not only simplified the way we handle state and lifecycle methods in functional components, but they’ve also given us the flexibility to create our own custom Hooks. This allows us to write more readable and maintainable React code, bringing productivity to new heights.