Introduction
In this article, we are looking into the basics of hooks and their types. Also, there are a few real-time use cases and examples.
Agenda
- What are Hooks?
- Why Hooks?
- Types of Hooks
- Examples of Hooks
Prerequisites
- NPM
- React JS
- VS Code
What are Hooks?
Hooks are special functions in React that let you use state and other React features without writing a class.
Why Hooks?
Hooks allow functional components to manage state and lifecycle events, which were previously only possible in class components.
Hooks simplify the way React developers manage state and side effects, making functional components more powerful and flexible.
Types of Hooks
1. useState
Allows you to add state to functional components.
Example
import React, { useState } from 'react';
function UseStateDemo() {
// Declare a state variable called `count` with an initial value of 0
const [count, setCount] = useState(0);
// Function to increment the count
const increment = () => {
setCount(count + 1);
};
// Function to decrement the count
const decrement = () => {
set count(count - 1);
};
return (
<div className="UseStateDemo">
<h1>Counter App</h1>
<p>Current Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
export default UseStateDemo;
Explanation
- Importing useState: useState is imported from React to allow the functional component to manage the state.
- Declaring State
const [count, setCount] = useState(0);
- count is the current state variable, initialized to 0.
- setCount is the function used to update the count state.
- Increment Function
const increment = () => {
setCount(count + 1);
};
- This function increases the count value by 1 whenever it's called.
- Decrement Function
const decrement = () => {
setCount(count - 1);
};
- This function decreases the count value by 1 whenever its called.
- Returning JSX: The component returns JSX that displays the current count value and two buttons that trigger the increment and decrement functions when clicked.
Example Usage
When you click the “Increment” button, the count will increase by 1.
When you click the “Decrement” button, the count will decrease by 1.
This example clearly shows how useState can be used to manage local states within a functional component, allowing you to create interactive and dynamic components in React.
2. useEffect
Allows you to perform side effects in your function components, such as data fetching or manually updating the DOM.
Example
import React, { useState, useEffect } from 'react';
function UseEffectDemo() {
// State to store the list of product
const [product, setProducts] = useState([]);
// State to handle loading status
const [loading, setLoading] = useState(true);
// State to handle errors
const [error, setError] = useState(null);
// Fetch products when the component mounts
useEffect(() => {
// Define an async function to fetch data
const fetchProducts = async () => {
try {
// Fetch data from API
const response = await fetch('https://fakestoreapi.com/products');
// Check if response is ok
if (!response.ok) {
throw new Error('Network response was not ok');
}
// Parse the JSON data
const data = await response.json();
// Update state with the fetched data
setProducts(data);
} catch (error) {
// Update state with the error
setError(error);
} finally {
// Update loading state
setLoading(false);
}
};
// Call the async function
fetchProducts();
}, []); // Empty dependency array means this effect runs once when the component mounts
// Render loading, error, or product data
if (loading) {
return <p>Loading...</p>;
}
if (error) {
return <p>Error: {error.message}</p>;
}
return (
<div className="ProductList">
<h1>Product List</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{product.map(product => (
<tr key={product.id}>
<td>{product.id}</td>
<td>{product.title}</td>
<td>${product.price.toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export default UseEffectDemo;
Explanation
- State Management
- product: Holds the list of products fetched from the API.
- loading: Tracks whether the data is still being fetched.
- error: Stores any error that occurs during the fetch process.
- Fetching Data with useEffect
- The useEffect Hook is used to perform side effects — in this case, fetching data from the API.
- The effect runs only once when the component mounts because the dependency array is empty ([]).
- Inside the useEffect, an asynchronous function fetchProducts is defined and immediately invoked to fetch product data from https://fakestoreapi.com/products.
- The function handles
- Successful Fetching: The fetched data is stored in the product state.
- Error Handling: Any errors encountered during the fetch are stored in the error state.
- Loading State: The loading state is set to false once the data fetch is complete (regardless of success or failure).
- Rendering Logic
- Loading State: While the data is being fetched, the component renders a loading message: <p>Loading…</p>.
- Error State: If an error occurs, an error message is displayed: <p>Error: {error.message}</p>.
- Data Display: Once the data is fetched successfully, the component renders a table listing the products with their IDs, names, and prices.
Example Usage
When the UseEffectDemo component mounts, it automatically fetches the list of products from the provided API.
Depending on the fetch result, it will either show the products, an error message, or a loading indicator.
This component showcases how to effectively use useEffect to perform side effects like data fetching in a React functional component, alongside managing the various states related to the fetch process (loading, error, and data).
3. useReducer
This hook is an alternative to useState for managing complex state logic.
Example
import React, { useReducer } from 'react';
// Define the initial state
const initialState = {
count: 0,
name: '',
isEditing: false,
};
// Define the reducer function
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'TOGGLE_EDIT':
return { ...state, isEditing: !state.isEditing };
case 'SET_NAME':
return { ...state, name: action.payload };
default:
return state;
}
};
// Component
const UseReducerDemo = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const handleIncrement = () => dispatch({ type: 'INCREMENT' });
const handleDecrement = () => dispatch({ type: 'DECREMENT' });
const handleToggleEdit = () => dispatch({ type: 'TOGGLE_EDIT' });
const handleNameChange = (e) => dispatch({ type: 'SET_NAME', payload: e.target.value });
return (
<div>
<h1>UseReducer Demo</h1>
<div>
<h2>Count: {state.count}</h2>
<button onClick={handleIncrement}>Increment</button>
<button onClick={handleDecrement}>Decrement</button>
</div>
<div>
<h2>Name: {state.name}</h2>
<button onClick={handleToggleEdit}>
{state.isEditing ? 'Stop Editing' : 'Edit Name'}
</button>
{state.isEditing && (
<input
type="text"
value={state.name}
onChange={handleNameChange}
placeholder="Enter your name"
/>
)}
</div>
</div>
);
};
export default UseReducerDemo;
Explanation
- Initial State: The initialState object defines the initial values for.
- count: A numeric counter, starting at 0.
- name: A string, initially empty, to store the user’s name.
- editing: A boolean that tracks whether the name is being edited, initially set to false.
- Reducer Function: The reducer function determines how the state should change based on the dispatched action.
- INCREMENT: Increases the count by 1.
- DECREMENT: Decreases the count by 1.
- TOGGLE_EDIT: Toggles the isEditing state between true and false.
- SET_NAME: Updates the name state with the value provided in action. payload.
- Default Case: Returns the current state if the action type doesn’t match any case.
- Using useReducer
- The useReducer Hook is used to manage the component’s state. It takes the reducer function and initialState as arguments and returns the current state and a dispatch function.
- dispatch is used to send actions to the reducer, triggering state changes based on the action type.
- Event Handlers
- handleIncrement: Dispatches the INCREMENT action to increase the count.
- handleDecrement: Dispatches the DECREMENT action to decrease the count.
- handleToggleEdit: Dispatches the TOGGLE_EDIT action to toggle the editing mode.
- handleNameChange: Dispatches the SET_NAME action with the new name value whenever the input field changes.
- Rendering the Component
- The component renders.
- The current count with buttons to increment and decrement it.
- The current name with a button to toggle the editing mode.
- An input field to edit the name if editing is true.
Example Usage
- Increment/Decrement Count: Users can click the buttons to adjust the count.
- Toggle Edit Mode: Users can toggle the editing mode for the name input.
- Update Name: If editing mode is active, users can type in the input field to update the name state.
Benefits of useReducer in this Component
- Centralized State Management: The reducer function manages all state changes in one place, making the logic easy to follow and maintain.
- Scalability: As the state management logic grows more complex, useReducer can handle it more gracefully than useState would with multiple state variables.
- Readability: By using actions, the intent of each state change is clearly communicated, improving the readability of the code.
This component effectively demonstrates how useReducer can be used to manage state in more complex scenarios, providing a clear structure and making it easier to scale and maintain your application.
4. useContext
Allows you to access context values in a functional component.
Example
import React, { createContext, useState, useContext } from 'react';
// Create a Context with default values
const ThemeContext = createContext();
const ThemeProvider = ({ children }) => {
// State to manage the theme
const [theme, setTheme] = useState('light');
// Function to toggle the theme
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
const ThemeSwitcher = () => {
// Use the context in a component
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<div style={{ padding: '20px', backgroundColor: theme === 'light' ? '#f0f0f0' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
<h1>Current Theme: {theme}</h1>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
};
// Main component using the ThemeProvider
const UseContextDemo = () => {
return (
<ThemeProvider>
<ThemeSwitcher />
</ThemeProvider>
);
};
export default UseContextDemo;
Explanation
- Creating the Context
- ThemeContext: Created using createContext(). It acts as a container to hold the state and functions related to the theme.
- The context is initialized without a default value since it’s set within the ThemeProvider.
- ThemeProvider Component
- State Management: The theme state is managed using useState, with “light” as the initial value.
- toggleTheme Function: This function switches the theme between “light” and “dark” using the previous state.
- ThemeContext.Provider: The provider wraps around children (any components passed to ThemeProvider), allowing them to access the theme state and toggle the theme function via context.
- ThemeSwitcher Component
- Using the Context: The useContext(ThemeContext) hook is used to consume the context, giving access to the theme and toggle the theme.
- Rendering
- The background color and text color of the div change based on the current theme.
- A button is provided to toggle the theme, invoking the toggleTheme function when clicked.
- UseContextDemo Component
- ThemeProvider: Wraps the ThemeSwitcher component, ensuring it and any nested components have access to the ThemeContext.
- This structure allows the ThemeSwitcher to receive and use the theme and toggle the theme without needing props passed down through multiple components.
Benefits of Using Context
- Avoiding Prop Drilling: The ThemeContext allows the theme state and functions to be shared directly with components that need them, without passing them down as props through multiple layers of components.
- Centralized State Management: The ThemeProvider centralizes the logic for managing and updating the theme, making the application easier to maintain and extend.
- Reusability: Any component within the ThemeProvider can access the theme context, making it easy to build a consistent UI across different parts of the application.
Example Use Case
Theming Across an Application: This pattern is commonly used in applications to provide global themes, language settings, user authentication, or any other shared state that needs to be accessed by multiple components.
Your implementation is clean, and the use of use context in combination with a context provider demonstrates an effective way to manage the global state in a React application.
5. useMemo
Memorizes a value, re-computing it only when its dependencies change.
Example
import React, { useState, useMemo } from 'react';
// Expensive calculation function
const calculateFactorial = (num) => {
console.log('Calculating factorial...');
let result = 1;
for (let i = 1; i <= num; i++) {
result *= i;
}
return result;
};
const UseMemoDemo = () => {
const [count, setCount] = useState(0);
const [number, setNumber] = useState(1);
// Memoized factorial calculation
const factorial = useMemo(() => calculateFactorial(number), [number]);
return (
<div>
<h1>UseMemo Demo</h1>
<div>
<h2>Counter: {count}</h2>
<button onClick={() => setCount(count + 1)}>Increment Counter</button>
</div>
<div>
<h2>Factorial of {number}: {factorial}</h2>
<input
type="number"
value={number}
onChange={(e) => setNumber(Number(e.target.value))}
/>
</div>
</div>
);
};
export default UseMemoDemo;
Explanation
- State Variables
- count: Tracks the count of button clicks, initialized to 0.
- number: Stores the number for which the factorial will be calculated, initialized to 1.
- Expensive Calculation Function
- calculate factorial (num): This function calculates the factorial of a given number num. The calculation is performed in a loop, which could become expensive for larger numbers.
- The function logs “Calculating factorial…” to the console each time it runs, which helps to see when the calculation is actually being performed.
- Memoizing the Factorial Calculation
useMemo(() => calculateFactorial(number), [number]);
- useMemo is used to memoize the result of calculating factorial (number).
- The calculation is only re-executed when the number value changes. If the number stays the same, the cached result is returned, avoiding unnecessary recalculations.
- This optimization is particularly useful when the calculation is computationally expensive, as it prevents the factorial calculation from being re-executed on every render if a number hasn’t changed.
- Rendering the Component
- Counter Section
- Displays the current count and includes a button to increment the count.
- The counter is independent of the factorial calculation, so clicking the button will not re-trigger the factorial calculation due to useMemo.
- Factorial Section
- Displays the factorial of the current number.
- Includes an input field to change the number, which will trigger the recalculation of the factorial if the number is modified.
Benefits of useMemo in This Component
- Performance Optimization: By memoizing the factorial calculation, the component avoids unnecessary recomputation, improving performance, especially when dealing with large numbers or complex calculations.
- Efficient Rendering: The component only recalculates the factorial when necessary (i.e., when number changes), ensuring that unrelated state changes (like count) do not trigger expensive operations.
Example Use Case
Heavy Computations: useMemo is ideal in scenarios where you have computationally intensive operations that depend on specific inputs. It ensures that these operations are only performed when absolutely necessary, preventing performance bottlenecks in your application.
This component demonstrates the practical use of useMemo to optimize React applications by controlling when expensive computations are performed, ensuring smooth and efficient rendering.
6. Custom hooks
A custom hook in React is a JavaScript function that allows you to reuse stateful logic across multiple components. Custom hooks enable you to extract and share logic that would otherwise be duplicated across components, making your code more modular and easier to maintain.
Example
import React from 'react';
import useLocalStorage from './useLocalStorage';
const CustomHookDemo = () => {
// Use the custom hook to manage a count value in localStorage
const [count, setCount] = useLocalStorage('count', 0);
return (
<div style={{ padding: '20px' }}>
<h1>Custom Hook Demo</h1>
<h2>Count: {count}</h2>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
};
export default CustomHookDemo;
import { useState, useEffect } from 'react';
// Custom hook to manage localStorage
const useLocalStorage = (key, initialValue) => {
// Get initial value from localStorage or use initialValue
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// Save value to localStorage whenever it changes
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
};
export default useLocalStorage;
Explanation
This component demonstrates the use of the useLocalStorage custom hook.
- Hook Usage
- The use local storage hook is used to manage the count state, which is persisted in localStorage under the key ‘count’.
- setCount is a function that updates both the state and local storage.
- Component Structure: The component displays the current count and provides three buttons.
- Increment: Increases the count by 1.
- Decrement: Decreases the count by 1.
- Reset: Resets the count to 0.
use local storage Custom Hook
This custom hook encapsulates the logic to synchronize state with localStorage.
- State Initialization
- The hook first checks if a value exists in localStorage under the provided key.
- If a value exists, it is parsed from JSON and used as the initial state.
- If no value exists, the initialValue is used as the initial state.
- Effect for Updating localStorage
- The useEffect hook monitors changes to storedValue or key.
- Whenever storedValue changes, the new value is stored in localStorage.
- Error Handling: Both the initialization and the effect include error handling via try-catch blocks to prevent the application from crashing if there’s an issue with local storage.
How Does It Work?
- State Persistence: The count state is stored in localStorage, meaning it will persist even if the user refreshes the page or closes and reopens the browser.
- Automatic Sync: Any time the count changes, the new value is automatically saved in localStorage.
- Reusability: The useLocalStorage hook can be reused in any component that needs to sync state with localStorage.
This pattern is especially useful in scenarios where you want user data to persist between sessions, such as saving form inputs, user preferences, or any other application state that needs to survive page reloads.
GitHub: https://github.com/Jaydeep-007/React-Hooks
Conclusion
In this article, we looked into the basics of hooks, types of hooks, and examples with step-by-step implementation.