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;
}
TBA
  • 1,921
  • 4
  • 13
  • 26

1 Answers1

4

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>
Nick Vu
  • 14,512
  • 4
  • 21
  • 31
  • Yes, I can understand what you're saying and I will implement that. But the surprising fact for me is, suppose, suppose, If I don't make any change in any of my components, just replace index.js with version 17 index.js, then it works exactly like how I want! – TBA May 08 '22 at 14:40
  • Hmm interesting, the only thing I can see is React.Strictmode makes double renderings (as the answer youbshared). By the way, let me try to run the code locally – Nick Vu May 09 '22 at 00:45
  • Sure, if you want I can give you the full code – TBA May 09 '22 at 03:03
  • Could you put it somewhere that I can fork your code from it? By the way, did you try to modify the part I mentioned in the answer? That potentially fixes your problem. @TBA – Nick Vu May 09 '22 at 03:08
  • I have made the changes as you suggested and working fine. You can clone from [here](https://github.com/TahirAnny/Contact-Manager). I would request you to run the project with the current `index.js` file, then you will find I have commented out the previous version `index.js` code, uncomment that and run the project, I hope you will be able to regenerate the issue. – TBA May 09 '22 at 03:55
  • 1
    Before I start checking, I think [automatic batching in React v18](https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching) is another possible cause for your case. It groups all state effects to a single rendering which is not ideal for your setup with 2 `useEffect` for `setContacts` @TBA – Nick Vu May 09 '22 at 04:02
  • 1
    I think [this answer](https://stackoverflow.com/a/72112129/9201587) is good for your case @TBA – Nick Vu May 10 '22 at 09:52
  • 1
    Yu, Thank you so much, your last two comments were really helpful, you better update your answer with these references, I will mark your answer as Accepted. – TBA May 11 '22 at 04:22
  • Thanks for your feedback, @TBA! I updated the answer for the references :D – Nick Vu May 11 '22 at 05:12