Custom Hooks in React
In the React ecosystem, Hooks serve as the core mechanism for managing UI state and side effects. While built-in Hooks like useState and useEffect cover most common scenarios, business logic in actual development often exhibits high reusability and complexity. When we need to extract a set of related logic (such as form validation, animation control, or data subscription) for use across multiple components, writing duplicate code not only reduces maintainability but also wastes resources. In such cases, Custom Hooks become the optimal solution.
Custom Hooks are essentially functions that accept React-provided Hooks as parameters and return a value or side effects (such as void). Unlike regular JavaScript functions, Custom Hooks follow specific rules: they must be named with a use prefix, and internally they can only call other Hooks (including custom ones), without relying on external variables. These restrictions ensure that Hook behavior is entirely based on React's state and DOM tree, guaranteeing predictability for components during every render. By encapsulating existing logic, we can abstract complex business rules into independent units, achieving "write once, use everywhere."
The process of building a Custom Hook is relatively straightforward; the key lies in understanding its input-output mechanism. First, define a function starting with use, which internally can call any number of React built-in Hooks or recursively call itself or other custom Hooks. Since Hook execution order is linear and independent of the component tree, as long as the rules are followed, Custom Hooks can be safely used anywhere.
Below is a typical example of a Custom Hook, demonstrating how to encapsulate counter logic with an initial value:
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
In this example, useCounter accepts an optional parameter initialValue and returns an object containing the current count value and three operation methods. The caller does not need to care about the internal state management mechanism; they simply call the returned methods directly. This design pattern greatly simplifies component complexity, making the code structure clearer.
Using Custom Hooks in Components
Once a Custom Hook is created, it can be called in any React component just like built-in Hooks. Whether the component is a functional component or a class component (though class components are no longer recommended for using Hooks), as long as the rules are met, new logic can be seamlessly integrated.
Assume we have a list display component that frequently performs counting operations; we can directly reuse the aforementioned useCounter Hook without repeating state management code in each list:
import React from 'react';
import { useCounter } from './hooks/useCounter';
function ItemList() {
// Instantiate a counter for each item instance
const { count, increment, reset } = useCounter(0);
return (
<div className="item-card">
<p>Current count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={reset}>Reset</button>
</div>
);
}
In this scenario, the ItemList component no longer directly manipulates useState; it relies entirely on useCounter. If we need to modify the counter's behavior in the future (e.g., changing to a decrement logic), we only need to modify the definition of useCounter, and all components calling it will automatically benefit. This is the core value brought by code reusability.
Advanced Techniques: Using useReducer and Custom Logic
When business logic becomes complex enough that simple useState cannot meet requirements, we can incorporate useReducer into Custom Hooks to handle more complex state update flows. Additionally, combined with useEffect, we can build composite Hooks capable of handling side effects, such as automatically loading data or listening to external changes.
Below is an example combining useReducer and useEffect to simulate an asynchronous data fetching feature with a loading state:
function useFetchData(url, initialState) {
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'LOADING':
return { ...state, loading: true };
case 'SUCCESS':
return { ...state, data: action.payload, loading: false };
case 'ERROR':
return { ...state, error: action.message, loading: false };
default:
return state;
}
}, initialState);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => dispatch({ type: 'SUCCESS', payload: data }))
.catch(err => dispatch({ type: 'ERROR', message: err.message }));
// Simulate loading delay to demonstrate state transitions
setTimeout(() => dispatch({ type: 'LOADING' }), 1000);
}, [url]);
return { ...state, fetchData: () => dispatch({ type: 'LOADING' }) };
}
In this way, we not only encapsulate the asynchronous flow of data fetching but also unify the types of state changes. The component side only needs to focus on these three properties: data, loading, and error to build robust UI interaction logic. This abstraction capability ensures that React applications maintain high maintainability and extensibility even when facing complex business scenarios.