The Basics of React Hooks: A Comprehensive Guide - Part 2
Bonus- React.memo ,infinite Rendering in react
hooks used in this blog- useMemo, useCallback
React hooks part 1 featuring useState, useEffect , useRef can be accessed here
TheuseMemo
Hook
In React, rendering refers to the process of converting your components into DOM (Document Object Model) nodes that the browser can understand and display. Whenever the state or props of a component change, React re-renders the component to reflect these updates. This process is crucial for ensuring that the user interface stays consistent with the underlying data.
Imagine a scenario where your component includes a complex calculation - maybe it's filtering a large dataset, performing mathematical computations, or transforming data in some significant way. If this calculation is tied directly to your component's render method or is part of a functional component's body, it will be recalculated every time the component re-renders. This can be a significant performance bottleneck, especially if the calculation is resource-intensive and the component re-renders frequently.
React's useMemo
hook is a tool specifically designed to tackle this problem. It memoizes (caches) the result of a function and only recalculates the value when one of its dependencies changes. This means that if you use useMemo
it for an expensive calculation, React will only recompute the calculation when it's necessary, rather than on every render. This can lead to a substantial performance improvement.
import React, { useState, useMemo } from 'react';
const ExpensiveComponent = () => {
const [count, setCount] = useState(0);
const [inputValue, setInputValue] = useState('');
// Expensive calculation
const calculateExpensiveValue = (val) => {
console.log('Calculating expensive value...');
// Simulate an expensive operation
return val * 2; // This is just a placeholder for a more complex calculation
};
// Memoize the expensive calculation
const expensiveValue = useMemo(() => calculateExpensiveValue(count), [count]);
return (
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<p>Expensive Value: {expensiveValue}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
In this example, calculateExpensiveValue
is an expensive calculation. By using useMemo
, we ensure that this function is only called when count
changes. If the user types into the input field, causing re-renders, the expensive calculation won't be recalculated, as it's not dependent on inputValue
.
TheuseCallback
Hook
In React, when a component re-renders, all its inline functions are re-created. This isn't usually a problem for most components. However, if a function is passed as a prop to a child component, these re-created functions can cause the child components to re-render unnecessarily, even if the actual functionality of the function hasn't changed. This is where useCallback
comes in handy.
By wrapping a function with useCallback
, you tell React to memoize the function and only recreate it if its dependencies change. This can improve performance, especially in cases with complex components or large applications.
import React, { useState, useCallback } from 'react';
const ChildComponent = React.memo(({ handleClick }) => {
console.log('Child component rendered');
return (
<button onClick={handleClick}>Click me</button>
);
});
const ParentComponent = () => {
const [count, setCount] = useState(0);
const [inputValue, setInputValue] = useState('');
// Memoized function using useCallback
const incrementCount = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<p>Count: {count}</p>
<ChildComponent handleClick={incrementCount} />
</div>
);
};
export default ParentComponent;
In this example, the ChildComponent
only re-renders when the handleClick
function changes. The useCallback
hook in ParentComponent
ensures that incrementCount
is the same function instance across renders as long as count
doesn't change. This means typing in the input field won't cause ChildComponent
to re-render, as the incrementCount
function isn't being recreated each time.
TheReact.memo
Here a new thing comes up React.memo , what is it?
React.memo
is a higher-order component in React that helps optimize performance by preventing unnecessary re-renders of components. It does this by memoizing the component, which means the component only updates if its props change. This is particularly useful for functional components to avoid redundant rendering when the parent component re-renders.
In the above code snippet, ChildComponent
is wrapped with React.memo
, which means it will only re-render if its props, specifically handleClick
, change. The useCallback
hook in ParentComponent
ensures that the incrementCount
function, passed as handleClick
to ChildComponent
, doesn’t change across re-renders unless its dependency (count
) changes. This combination prevents ChildComponent
from re-rendering when you type in the input field, as this action doesn't affect the count
state, thus optimizing the application's performance
Infinite Rendering in React
In React, components can sometimes end up in an infinite rendering loop. This usually happens when a component's state or props update in a way that causes the component to re-render, and this re-render triggers another state or props update, creating an endless cycle. Let's first look at an example where this issue might occur.
import React, { useState, useEffect } from 'react';
const ProblematicComponent = () => {
const [data, setData] = useState('Initial data');
useEffect(() => {
// Imagine fetchData is an API call that returns updated data
const fetchData = () => 'Updated data';
setData(fetchData());
}, [data]); // Dependency on 'data' causes the useEffect to run every time 'data' changes
return (
<div>{data}</div>
);
};
export default ProblematicComponent;
In this example, the useEffect
hook is set up to run every time data
changes. However, inside useEffect
, we update data
, which triggers the effect again, leading to an infinite loop.
Resolving Infinite Rendering with useMemo
and useCallback
To solve this, we can use useMemo
and useCallback
to control when certain parts of our component should update or rerun.
import React, { useState, useEffect, useMemo, useCallback } from 'react';
const OptimizedComponent = () => {
const [data, setData] = useState('Initial data');
// Simulating a function that fetches data
const fetchData = useCallback(() => {
return 'Updated data';
}, []); // Empty dependency array ensures this is created once
// Memoized data update logic
const updatedData = useMemo(() => fetchData(), [fetchData]);
useEffect(() => {
setData(updatedData);
}, [updatedData]); // Dependency on 'updatedData' to control the effect
return (
<div>{data}</div>
);
};
export default OptimizedComponent;
In this revised example, fetchData
is wrapped with useCallback
to ensure it's only created once and doesn't change on every render. Then, useMemo
is used to recalculate updatedData
only when fetchData
changes, which, in this case, never happens after the initial render. Finally, useEffect
depends on updatedData
, effectively breaking the infinite loop.
That's the end of these hooks discussion.
Stay tuned for our next blog, where we will delve into the concepts of prop-drilling and how the Context API can be a game-changer in managing state across your React applications