The Basics of React Hooks: A Comprehensive Guide - Part 2

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