Reputation: 2027
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
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