Hooks!! Again?
Hello there fellow readers. We are continuing our learning to Day 4 where we will see more commonly used hooks other than useState hook. The reason is plain as clear chicken soup, we have explored useState in deep in Day 3 blog of this series. So let us "hook" up for further learning.
Lets check if you havent forgotten the basics.
Hooks are functions that let you "hook into" React state and lifecycle features from function components.
Why we use hooks ? Because they allow you to use state and other React features without writing a class component.
Rules to use Hooks:
Only call hooks at the top level: Don’t call hooks inside loops, conditions, or nested functions.
Only call hooks from React functions: Call hooks from React functional components or custom hooks.
Thats enough for the basics part. We will look into other hooks with some practical examples discussed in this blog post. We are going to practically explore the following hooks today:
useEffect Hook
useContext Hook
useReducer Hook
useRef Hook
useMemo Hook
useCallback Hook
useEffect Hook:
The useEffect
hook allows you to perform side effects in function components. It runs after every render by default, but you can control when it runs by passing a second argument, known as the dependency array.
import React, { useState, useEffect } from 'react';
function TitleUpdater() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
export default TitleUpdater;
In our example, we are updating our document's title every time the Increment button is hit. We have passed the [count]
as a dependency array, which ensures that the effect only runs when count
updates.
document.title = ...
is the line that changes the title of the browser tab to show the current value of count
. So, when count
is 3
, the tab's title will say "Count: 3". The square brackets [count]
at the end of useEffect
tell React to only run the effect (i.e., update the title) when count
changes. If count
stays the same, React won’t rerun the effect. When you click the button, setCount(count + 1)
runs, which increases count
by 1
. This causes the component to re-render, and the document title updates with the new count
.
If the above example seem a bit complex, let me break it down with much simpler example:
import React, { useState, useEffect } from 'react';
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000); // Side effect
return () => clearInterval(interval); // Cleanup
}, []);
return <p>Timer: {count} seconds</p>;
}
export default Timer;
Let us look into what is the concept of side-effects
. The side effect here is setting up a timer that increments count
every second. In our case you might be seeing 1000
after the useEffect hook. The 1000 stands for to update the Count value after every 1000 milliseconds.
There is another thing when it comes to useEffect Hook and that is Cleanup
. The return () => clearInterval(interval);
part cleans up the interval when the component is unmounted, preventing memory leaks.
useContext Hook
This hook returns you the context value for the component which we are calling. For example, in below example, this hook allows you to consume context values in a functional component without needing to use any other component in the code.
Lets do some revamped changes in our App.js:
import React, { createContext, useContext, useState } from 'react';
// Step 1: Create a Context for the theme with a default value of "light"
const ThemeContext = createContext('light');
function App() {
// Step 2: Manage the theme state with useState
const [theme, setTheme] = useState('light');
// Function to toggle between "light" and "dark" themes
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
// Step 3: Provide the current theme value to all child components
<ThemeContext.Provider value={theme}>
{/* Button to toggle the theme */}
<button onClick={toggleTheme}>
Toggle Theme
</button>
{/* Render the component that uses the theme */}
<ThemedComponent />
</ThemeContext.Provider>
);
}
function ThemedComponent() {
// Step 4: Access the current theme value from the ThemeContext
const theme = useContext(ThemeContext);
// Apply styles based on the current theme and display the theme
return (
<div style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff',
padding: '10px',
borderRadius: '5px',
textAlign: 'center',
marginTop: '20px',
border: '5px solid crimson'
}}>
The current theme is {theme}.
</div>
);
}
export default App;
Each time I click the above "Toggle Theme" button, this happens:
Indeed helpful for creating applications with dark mode and light mode. As far as I am concerned, I prefer the latter.
Now I believe that that useContext hooks is a bit difficult to grab up for knowledge. So let us look into the above example, drilled down to bottom.
createContext
is a function in React that creates a Context object. A Context object is used to share data (like themes, user information, settings, etc.) across different components in a React application without having to pass props manually at every level of the component tree.
Before diving more deep in this useContext hook, it is important to understand what the Context is in React terms. We will be looking into explainations, so let’s walk through the same theme example to understand createContext
more clearly.
When you call createContext
, it returns an object with two main properties:
Provider
: This component allows you to supply or "provide" a value to be shared with any components that consume this context.Consumer
: This component is used to "consume" or use the context value within a component. However, with the introduction of theuseContext
hook in React, using theConsumer
component is less common.
How does it work then?
You create a context using createContext
. You can optionally provide a default value for the context. Later, you use the Provider
component to make the context value available to all components within its subtree. This value can be a primitive (like a string or number), an object, or even a function. Using this approach, components within the provider’s subtree can access the context value using the useContext
hook or the Consumer
component.
Okay let's start. We will break down our own code in smaller chunks below:
import React, { createContext, useContext, useState } from 'react';
// Step 1: Create a Context for the theme with a default value of "light"
const ThemeContext = createContext('light');
Here, createContext('light')
creates a context with a default value of "light"
. This means that if a component consumes the ThemeContext
but no provider is above it in the component tree, it will receive "light"
as the default value.
function App() {
const theme = 'dark'; // In a real app, this might come from state
return (
<ThemeContext.Provider value={theme}>
<ThemedComponent />
</ThemeContext.Provider>
);
}
The ThemeContext.Provider
is used here to pass the theme
value ('dark'
in this case) down to any components wrapped inside it. It is also important to note that any component inside ThemeContext.Provider
can access the theme
value.
function ThemedComponent() {
const theme = React.useContext(ThemeContext); // Access the current theme
return <div>The current theme is {theme}</div>;
}
Inside ThemedComponent
, useContext(ThemeContext)
is used to read the current theme
value provided by the nearest ThemeContext.Provider
.
To sum up, createContext
is a way to create a global value that can be shared across different components without passing props manually.
useReducer Hook
useReducer
is a hook that is an alternative to useState
. It’s particularly useful when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.
Well there are some things which you should know before knowing about this hook. These are, a Reducer Function and a Dispatch function.
A Reducer function is a function that determines how the state should change based on an action. It takes two arguments:
state
: The current state.action
: An object describing what happened, usually containing atype
field and possibly some additional data.
A Dispatch function is provided by useReducer
to send actions to the reducer. This triggers the state transition.
Here is how the basic syntax of useReducer hook should look like:
const [state, dispatch] = useReducer(reducer, initialState);
What's in the syntax:
reducer
: The function that defines how state transitions should occur.initialState
: The initial state value.state
: The current state.dispatch
: A function to trigger state changes by dispatching actions.
To know in depth, let us look at a simple example. Write a reducer function and see how it goes.
function counterReducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
As we know that reducer function is basically an alternative to useState with complex logic. By seeing above code, it should have cleared your doubt. The counterReducer
function receives the current state and an action. It uses a switch
statement to determine how to update the state based on the action's type
. In the end, it returns a new state object based on the action.
Now it is time to use the useReducer in a component. We will name it as CounterReducer
.
import React, { useReducer } from 'react';
function CounterReducer() {
const initialState = { count: 0 };
// useReducer hook to manage state
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}
export default CounterReducer;
We start with initialState = { count: 0 }
. Next, we call useReducer(counterReducer, initialState)
, which returns the current state
and the dispatch
function. We use dispatch({ type: 'increment' })
to trigger state changes based on the action type. This triggers the counterReducer
function with the current state and the action { type: 'increment' }
. The reducer
returns a new state { count: state.count + 1 }
, updating the count.
So here is how your full code should look like:
import React, { useReducer } from 'react';
// Step 1: Define the reducer function
function counterReducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
throw new Error(`Unknown action type: ${action.type}`);
}
}
// Step 2: Create the Counter component
function CounterReducer() {
// Initial state
const initialState = { count: 0 };
// Step 3: Use the useReducer hook
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
{/* Step 4: Dispatch actions to update the state */}
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}
// Step 5: Export the Counter component
export default CounterReducer;
Each button dispatches a different action (increment
, decrement
, or reset
) to update the state.
And Yeah dont forget to make changes in your App.js file:
import React from 'react';
import Counter from '.components/CounterReducer';
function App() {
return (
<div className="App">
<h1>React Counter with useReducer</h1>
<CounterReducer />
</div>
);
}
export default App;
useRef Hook
The useRef
hook is one of the most commonly used hooks in React, and it's very useful when you need to access and interact with DOM elements directly, or when you need to persist some data across renders without causing re-renders.
useRef
is a hook that returns a mutable ref object. This ref object is like a “box” that can hold a value in its .current
property. The main feature of a ref object is that it doesn’t cause the component to re-render when the value changes.
Few things you should know before proceeding further:
Ref Object: The object returned by
useRef
has a single property calledcurrent
, which you can read or modify. This object persists across renders, so it’s a good place to store mutable values that you don’t want to trigger a re-render.DOM Manipulation:
useRef
is often used to directly interact with DOM elements. For example, you can store a reference to a DOM element (like an input field) in the ref object and then interact with it (like focusing the input) when needed.
Basic syntax:
const myRef = useRef(initialValue);
initialValue
is the initial value you want to store in the current
property of the ref object. myRef.current
is the property that holds the value you want to persist across renders.
Let's start with a simple example where we want to focus an input field when a button is clicked.
import React, { useRef } from 'react';
function FocusInput() {
// Create a ref object
const inputRef = useRef(null);
// Function to focus the input field
const handleFocus = () => {
inputRef.current.focus(); // Focus the input element
};
return (
<div>
<input ref={inputRef} type="text" placeholder="Focus me!" />
<button onClick={handleFocus}>Focus the input</button>
</div>
);
}
export default FocusInput;
const inputRef = useRef(null);
is the line that creates a ref object with an initial value of null
. This ref will be attached to the input element. In the JSX, we attach the ref to the input field with ref={inputRef}
. This tells React to store the reference to this input field in inputRef.current
. When the "Focus the input" button is clicked, the handleFocus
function is called, which triggers inputRef.current.focus()
. This causes the input field to gain focus. useRef
is handy for interacting directly with DOM elements, like focusing inputs, scrolling to elements, or measuring element dimensions.
We can also store Mutable values without Re-Rendering in useRef Hook.
Besides interacting with the DOM, useRef
can also be used to store mutable values that persist across renders without causing re-renders. Lets look at the code example below:
import React, { useRef, useState } from 'react';
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
const start = () => {
if (intervalRef.current !== null) return; // Prevent multiple intervals
intervalRef.current = setInterval(() => {
setSeconds((prevSeconds) => prevSeconds + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
intervalRef.current = null;
};
const reset = () => {
stop();
setSeconds(0);
};
return (
<div>
<p>{seconds} seconds</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
<button onClick={reset}>Reset</button>
</div>
);
}
export default Stopwatch;
Here, seconds
is a piece of state that tracks how many seconds have passed. The setSeconds
function updates this state, causing the component to re-render with the new value. intervalRef = useRef(null);
is used to store a reference to the interval ID. This value does not change across renders unless explicitly set.
When the "Start" button is clicked, start()
is called. It checks if the timer is already running (intervalRef.current !== null
); if not, it starts a new interval that updates the seconds
state every second. Clicking "Stop" clears the interval using clearInterval(intervalRef.current)
and resets intervalRef.current
to null
. Clicking "Reset" stops the timer and resets the seconds
state to 0
.
As far as this case is concerned, useRef
is used to store the interval ID. Since the ID doesn’t need to trigger a re-render, it’s ideal to store it in a ref instead of state.
useMemo Hook:
Imagine you have a function that performs an expensive calculation—something that takes a lot of processing power or time. If this function is called on every render, it could slow down your app. useMemo
helps by storing the result of that function and only recalculating it if the inputs to the function (dependencies) change.
The useMemo
hook in React is used to optimize performance by memoizing (i.e., caching) the result of a calculation or function so that it doesn’t need to be recalculated on every render, but only when its dependencies change.
Basic Syntax:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Where, computeExpensiveValue(a, b)
is the function that performs the expensive calculation. [a, b]
are the dependencies. useMemo
will only recompute the memoized value when one of these dependencies changes.
Let us understand this hook with a simple example. Let's say you have a component that renders a list of numbers and allows the user to filter the list. The filtering operation is expensive because it involves a large dataset.
So typically with useMemo hook, the code for sorting should look something like this:
import React, { useState, useMemo } from 'react';
function FilterList() {
const [filter, setFilter] = useState('');
const [count, setCount] = useState(0);
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const filteredNumbers = useMemo(() => {
console.log('Filtering...');
return numbers.filter((num) => num > parseInt(filter, 10));
}, [filter]);
return (
<div>
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter numbers greater than..."
/>
<ul>
{filteredNumbers.map((num) => (
<li key={num}>{num}</li>
))}
</ul>
<button onClick={() => setCount(count + 1)}>Re-render</button>
<p>Render Count: {count}</p>
</div>
);
}
export default FilterList;
Here, the useMemo
hook wraps the filtering logic. The filtered result is only recalculated if the filter
value changes.
[filter]
is the dependency array. If the filter
value doesn’t change, useMemo
returns the memoized (cached) result from the last render instead of recalculating it. If you click the "Re-render" button, the component re-renders, but the filtering only happens if the filter
value has changed. This can significantly improve performance, especially with large datasets.
Lets have a bit more understanding of useMemo hook with another example. Anyone remember the program to calculate Factorials??
Suppose you have a function that performs a complex calculation i.e. calculating factorials based on user input. Without useMemo
, this calculation would be repeated on every render, potentially slowing down the app.
So your code should be looking like this:
import React, { useState, useMemo } from 'react';
function ComplexCalculation() {
const [input, setInput] = useState(1);
const factorial = (n) => {
console.log('Calculating factorial...');
return n <= 1 ? 1 : n * factorial(n - 1);
};
const result = useMemo(() => factorial(input), [input]);
return (
<div>
<input
type="number"
value={input}
onChange={(e) => setInput(parseInt(e.target.value))}
/>
<p>Factorial of {input} is {result}</p>
</div>
);
}
export default ComplexCalculation;
factorial(n)
is a recursive function that calculates the factorial of a number. Without useMemo
, this function would run on every render, even if the input hasn’t changed. In our case, useMemo
caches the result of the factorial calculation. The calculation is only performed again if input
changes.
To make things simple, if you change the input, the factorial is recalculated. However, if the input doesn’t change, the cached result is used, making the component more efficient.
Using useMemo
effectively can help make your React applications faster and more efficient, especially in situations where certain calculations don’t need to happen on every single render. In simpler words, this hook is handy for improving performance when dealing with expensive or complex calculations that don’t need to be repeated on every render.
useCallback Hook
The useCallback
hook in React is similar to useMemo
, but instead of memoizing a value, it memoizes a function. It’s used to optimize performance by ensuring that the same function reference is returned across renders unless its dependencies change.
In React, whenever a component re-renders, all the functions inside it are recreated. In most cases, this is not a problem. However, if these functions are passed as props to child components, it can cause unnecessary re-renders of those child components. useCallback
helps prevent this by memoizing the function so that the same function reference is returned across renders unless the dependencies change.
Basic Syntax:
const memoizedCallback = useCallback(() => {
// Function code here
}, [dependencies]);
memoizedCallback
is the memoized function that useCallback
returns. We also have the dependencies
array; which contains values that the function depends on. The function will only be re-created if one of these values changes.
Time to see this hook in action:
Suppose, you have a component that has a button to increment a counter, and this button is passed as a prop to a child component. With the above useCallback hook, you would do it this way:
import React, { useState, useCallback } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div>
<h1>Count: {count}</h1>
<ChildComponent onIncrement={increment} />
</div>
);
}
function ChildComponent({ onIncrement }) {
console.log('ChildComponent rendered');
return <button onClick={onIncrement}>Increment</button>;
}
export default ParentComponent;
In our example, The increment
function is wrapped in useCallback
. Now, the increment
function is only recreated if count
changes. If count
doesn’t change, the same function reference is used, preventing unnecessary re-renders of ChildComponent
.
[count]
is the dependency array. If count
changes, useCallback
will return a new function reference. This optimization ensures that ChildComponent
only re-renders when it needs to, not every time ParentComponent
re-renders.
Tough to understand? I see you require one more example then:
Suppose you have an event listener that needs to be added and removed depending on certain conditions. Using useCallback
ensures that the event listener function reference doesn’t change unless necessary.
import React, { useState, useCallback, useEffect } from 'react';
function EventListenerComponent() {
const [isListening, setIsListening] = useState(false);
const handleResize = useCallback(() => {
console.log('Window resized');
}, []);
useEffect(() => {
if (isListening) {
window.addEventListener('resize', handleResize);
} else {
window.removeEventListener('resize', handleResize);
}
// Cleanup on component unmount
return () => {
window.removeEventListener('resize', handleResize);
};
}, [isListening, handleResize]);
return (
<div>
<button onClick={() => setIsListening(!isListening)}>
{isListening ? 'Stop Listening' : 'Start Listening'}
</button>
</div>
);
}
export default EventListenerComponent;
In the above code example, isListening
is a piece of state that determines whether the event listener is active. Next we have the handleResize
function logs a message whenever the window is resized. handleResize
is memoized using useCallback
to ensure that the same function reference is used unless necessary. This prevents the event listener from being removed and re-added unnecessarily.
We have also used useEffect
to add and remove the event listener based on the isListening
state. The event listener is only updated when isListening
or handleResize
changes.
useCallback
is a hook that memoizes a function so that it retains the same reference across renders unless its dependencies change. It’s especially useful for optimizing performance in scenarios where functions are passed as props to child components or used in event listeners. By using useCallback
, you can prevent unnecessary re-renders and make your React applications more efficient.
Wrapping Up:
So class, today, you've delved into the power of React hooks beyond useState
. These hooks allow you to manage side effects, complex state logic, context, references to DOM elements, and performance optimizations in functional components.
We will be exploring more concepts in next day blog. As Hooks are one of the crucial parts in React ecosystem, I had to cover 2 days entirely on this. Make sure you practice by remodifying the examples we discussed in the blog. Meanwhile I will continue my studying for Day 5 topic.
Until then Ciao!!