10,000 Hours
of |

Another dev blog

logo
JavaScriptReactBest PracticesStrict ModeDebugging

React Strict Mode and the useEffect double rerender

In the ever-evolving world of web development, keeping your code clean and error-free is paramount. That's where React's Strict Mode comes into play, quietly safeguarding your app from potential pitfalls. Today, we dive deep into the essence of Strict Mode, exploring its significance and how it can drastically improve your development process. Let's walk through practical examples, uncovering the common mistakes it helps you avoid and how to correct them. This is Strict Mode in React, demystified.

What is React's Strict Mode?

React's Strict Mode assists development by highlighting problems like unsafe lifecycles, deprecated API usage, and unexpected side effects, Strict Mode prepares you to tackle these issues head-on, ensuring your app is robust and efficient.

Example 1: The Right Way to Handle Socket Connections

First on our list is correctly closing socket connections. Resource cleanup is crucial, and neglecting it will cause chaos into your codebase. Consider the following example where we implement a socket API:

const socketApi = {
  connect: () => { console.log("✅ connected") },
  disconnect: () => { console.log("❌ disconnected") },
};

export { socketApi };

And here's how we correctly manage the connection in our component:

import React from 'react';
import { socketApi } from './fake-api';

export function App(props) {
  React.useEffect(() => {
    socketApi.connect();
    return () => socketApi.disconnect();
  }, []);

  return (
    <div className='App'>
      <h1>This is a Strict mode example</h1>
      <p>Check the console</p>
    </div>
  );
}

By ensuring the cleanup of our socket connection, we avoid memory leaks and unexpected behaviors. This keeps our application's performant and reliable.

Example 2: Mastering API Request Management

Handling API requests with care is essential. The following example demonstrates a rest API interaction:

const restApi = {
  requestData: async () => {
    return new Promise((resolve) => {
      setTimeout(() => resolve({ data: { item: "data" } }), 500);
    });
  },
};

export { restApi };

And here's how we handle the request in our component:

import React from 'react';
import { restApi } from './fake-api';

export function App(props) {
  const [restData, setRestData] = React.useState();

  React.useEffect(() => {
    let ignoreResult = false;

    const fetchData = async () => {
      console.log('Data requested');
      const { data } = await restApi.requestData();
      if (!ignoreResult) {
        console.log('Data set');
        setRestData(data);
      }
    };

    fetchData();

    return () => {
      ignoreResult = true;
    };
  }, []);

  return (
    <div className='App'>
      <h1>This is a Strict mode example</h1>
      <p>Check the console</p>
    </div>
  );
}

This example highlights the importance of ignoring API responses when the component unmounts, preventing attempts to set state on an unmounted component—a classic mistake that leads to memory leaks and error-prone code.

Example 3: Handling Scroll Events Gracefully

Handling event listeners in React, especially for actions like scrolling, requires careful consideration to avoid performance issues and memory leaks. Here's how you can set up a scroll listener in a way that Strict Mode would approve:

import React, { useEffect } from 'react';

export function ScrollComponent(props) {
  useEffect(() => {
    const handleScroll = () => {
      console.log(window.scrollY);
    };

    // Adding scroll listener
    window.addEventListener('scroll', handleScroll);

    // Cleanup function to remove scroll listener
    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);

  return (
    <div className='ScrollComponent' style={{ height: '200vh' }}>
      <h1>Scroll down to see the listener in action!</h1>
    </div>
  );
}

In this example, we attach a scroll event listener to the window when the component mounts and importantly, remove it when the component unmounts. This cleanup is crucial to prevent the listener from firing even after the component is no longer in the DOM, ensuring our app remains efficient and leak-free.

Example 4: Ensuring Animations Play Nice

Animations can enhance user experience, but they can also lead to user frustration if not managed correctly, especially when components unmount before an animation completes. Let’s see how we can safely implement animations:

import React, { useEffect, useState } from 'react';

export function AnimatedComponent(props) {
  const [isAnimating, setAnimating] = useState(false);

  useEffect(() => {
    setAnimating(true);
    const animationId = window.requestAnimationFrame(() => {
      // Animation logic here
      console.log('Animating...');
      // Reset animation state if needed
      setAnimating(false);
    });

    // Cleanup function to cancel the animation frame request
    return () => {
      window.cancelAnimationFrame(animationId);
    };
  }, []);

  return (
    <div className='AnimatedComponent'>
      {isAnimating ? <p>Animating...</p> : <h1>Animation Complete</h1>}
    </div>
  );
}

This snippet demonstrates setting up an animation with requestAnimationFrame and ensuring that if the component unmounts, we cancel the animation request. This step is essential to prevent the animation callback from executing on an unmounted component, which could lead to errors or unexpected behaviors.

Wrapping Up

Strict Mode in React is more than just a tool; it's a mindset. It encourages developers to write cleaner, more reliable code by exposing potential issues early in the development process. Through the examples discussed, we've seen how adhering to best practices and leveraging Strict Mode can help prevent common mistakes, leading to a more stable and efficient application.

So, as you embark on your next React project, remember to enable Strict Mode and let it guide you towards writing better, more resilient code.