TBA
TBA

Reputation: 2027

React.StrictMode seems to work differently in different React version (18.1.0 vs 17.0.1)

Let me tell you in brief what I am trying to do. As I am learning React, so following a tutorial and making a Contact Manager, where it will take input of name and email address for now and will save it in local storage. When I will reload the page, it will retrieve data from local storage and display it. As I am in a learning phase, I am just exploring how the state works and how I can save data in local storage, Later I will save these data to a DB.

React version I am using is:

 react: "18.1.0"
 react-dom: "18.1.0"

and my node version is:

17.1.0

I am setting the form input data to local storage by doing this:

   useEffect(() => {
    localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts));
  }, [contacts]);

and retriving these data from local storage with the below code:

  useEffect(() => {
    const retriveContacts = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
    if (retriveContacts) {
      setContacts(retriveContacts);
    }
  }, []);

I can see data is being stored in local storage from the Chrome Dev tool but whenever I am reloading the page, my goal is to retrieve the data from local storage and show the list. But after reloading, these data are not any longer visible on the page. I tried to debug, then I noticed, that when I am reloading the page, it's hitting the app.js component twice, the first time it's getting the data, but the second time the data is being lost.

I got a solution here, where it's saying to remove the React.StrictMode from index.js and after doing so it's working fine.

Then I tried to replace the current index.js with the previous react version (17.0.1) index.js. This is the code I tried:

import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);

With this code, even with React.StrictMode this seems to work fine, I mean my data does not clear from local storage after reloading.

My question is, what's the reason behind it? Am I missing something or there is some logic behind it? The solution link I have provided is 4 years ago, and React 18 was released some time ago, can't figure out what actually I am doing wrong!

Any help or suggestions will be appreciated.

Below are the components to regenerate the scenario.

Current index.js (version 18.1.0)

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./components/App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

App.jsx

import React, { useState, useEffect } from "react";
import "../style/App.css";
import Header from "./Header";
import AddContact from "./AddContact";
import ContactList from "./ContactList";

function App() {
  const LOCAL_STORAGE_KEY = "contacts";
  const [contacts, setContacts] = useState([]);

  const addContactHandler = (contact) => {
    console.log(contact);
    setContacts([...contacts, contact]);
  };

  useEffect(() => {
    const retriveContacts = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
    if (retriveContacts) {
      setContacts(retriveContacts);
      console.log(retriveContacts);
    }
  }, []);

  useEffect(() => {
    localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts));
  }, [contacts]);

  return (
    <div className="ui container">
      <Header />
      <AddContact addContactHandler={addContactHandler} />
      <ContactList contacts={contacts} />
    </div>
  );
}

export default App;

Header.jsx

import React from 'react';

const Header = () => {
    return(
        <div className='ui fixed menu'>
            <div className='ui center container'>
                <h2>
                    Contact Manager
                </h2>
            </div>
        </div>
    );
};

export default Header;

ContactList.jsx

import React from "react";
import ContactCard from "./ContactCard";

const ContactList = (props) => {
  const listOfContact = props.contacts.map((contact) => {
    return <ContactCard contact={contact}></ContactCard>;
  });

  return <div className="ui celled list">{listOfContact}</div>;
};

export default ContactList;

AddContact.jsx

import React from "react";

class AddContact extends React.Component {
  state = {
    name: "",
    email: "",
  };

  add = (e) => {
    e.preventDefault();
    if (this.state.name === "" || this.state.email === "") {
      alert("ALl the fields are mandatory!");
      return;
    }
    this.props.addContactHandler(this.state);
    this.setState({ name: "", email: "" });
  };
  render() {
    return (
      <div className="ui main">
        <h2>Add Contact</h2>
        <form className="ui form" onSubmit={this.add}>
          <div className="field">
            <label>Name</label>
            <input
              type="text"
              name="name"
              placeholder="Name"
              value={this.state.name}
              onChange={(e) => this.setState({ name: e.target.value })}
            />
          </div>
          <div className="field">
            <label>Email</label>
            <input
              type="text"
              name="email"
              placeholder="Email"
              value={this.state.email}
              onChange={(e) => this.setState({ email: e.target.value })}
            />
          </div>
          <button className="ui button blue">Add</button>
        </form>
      </div>
    );
  }
}

export default AddContact;

ContactCard.jsx

import React from "react";
import user from '../images/user.png'

const ContactCard = (props) => {
    const {id, name, email} = props.contact;
    return(
        <div className="item">
        <img className="ui avatar image" src={user} alt="user" />
        <div className="content">
            <div className="header">{name}</div>
            <div>{email}</div>
        </div>
        <i className="trash alternate outline icon"
            style={{color:"red"}}></i>
    </div>
    );
};

export default ContactCard;

App.css

.main {
  margin-top: 5em;
}

.center {
  justify-content: center;
  padding: 10px;
}

.ui.search input {
  width: 100%;
  border-radius: 0 !important;
}

.item {
  padding: 15px 0px !important;
}

i.icon {
  float: right;
  font-size: 20px;
  cursor: pointer;
}

Upvotes: 1

Views: 2472

Answers (1)

Nick Vu
Nick Vu

Reputation: 15520

With your current setup with 2 useEffect in a single rendering, I guess one of the possible problems is automatic batching in React v18 that causes all state updates and side-effects in one rendering.

As for your question about what's different between React.StrictMode in React v17 and React v18, you can find this useful answer with a very detailed explanation.


For further explanation of a possible fix in your case

useEffect(() => {
    localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts));
}, [contacts]);

Your expectation for the above useEffect is "You want it to be called when contacts state gets updated"

But in fact, it has been called for the first load without contacts update too (when contacts is empty).

For the simulation, you can check the below code with some explanations (I'm using react: 17.0.1, but no differences if I use react 18.1.0)

const App = () => {
   const [contacts, setContacts] = React.useState()
   
   React.useEffect(() => {
      console.log('initial useEffect')
      //simulate to set contacts from local storage
      //it should have data, but after this, it's overriden by the 2nd useEffect
      setContacts([{name: "testing"}])
   }, [])
   
   React.useEffect(() => {
      console.log('useEffect with dependency', { contacts })
      //you're expecting this should not be called initially
      //but it's called and updated your `contacts` back to no data
      //means it's completely wiped out all your local storage data unexpectedly
      setContacts(contacts) 
   }, [contacts])
   
   //trace contact values
   //it should have values `{name: "testing"}` after `useEffect`, but now it's rendering with nothing
   console.log({ contacts })
   
   return <div></div>
}

ReactDOM.render(
  <React.StrictMode>
     <App/>
  </React.StrictMode>,
  document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>

A potential fix for it should be to add a condition to check contacts state availability within that useEffect before any updates - That would help to avoid updating local storage again with empty contacts unexpectedly.

const App = () => {
  const [contacts, setContacts] = React.useState()

  React.useEffect(() => {
    console.log('initial useEffect')
    //simulate to set contacts from local storage
    //it should have data, but after this, it's overriden by the 2nd useEffect
    setContacts([{
      name: "testing"
    }])
  }, [])

  React.useEffect(() => {
    if (contacts) {
      //now you're able to set contacts from this!
      console.log('useEffect with dependency', {
        contacts
      })
      setContacts(contacts)
    }
  }, [contacts])

  console.log({
    contacts
  }) //trace contact values

  return <div></div>
}

ReactDOM.render(<React.StrictMode>
     <App/>
  </React.StrictMode>,
  document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Upvotes: 4

Related Questions