React.StrictMode 似乎在不同的 React 版本中工作不同(18.1.0 与 17.0.1)
React.StrictMode seems to work differently in different React version (18.1.0 vs 17.0.1)
让我简要地告诉你我想做什么。因为我正在学习 React,所以按照教程并制作了一个 Contact Manager
,它将暂时输入姓名和电子邮件地址并将其保存在本地存储中。当我重新加载页面时,它会从本地存储中检索数据并显示出来。由于我处于学习阶段,我只是在探索状态如何工作以及如何将数据保存在本地存储中,稍后我会将这些数据保存到数据库中。
我使用的 React 版本是:
react: "18.1.0"
react-dom: "18.1.0"
我的节点版本是:
17.1.0
我通过这样做将表单输入数据设置到本地存储:
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts));
}, [contacts]);
并使用以下代码从本地存储中检索这些数据:
useEffect(() => {
const retriveContacts = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
if (retriveContacts) {
setContacts(retriveContacts);
}
}, []);
我可以从 Chrome 开发工具看到数据存储在本地存储中,但每当我重新加载页面时,我的目标是从本地存储中检索数据并显示列表。但是重新加载后,这些数据在页面上不再可见。我尝试调试,然后我注意到,当我重新加载页面时,它两次点击 app.js
组件,第一次它获取数据,但第二次数据丢失。
我得到了一个解决方案 here,它说从 index.js
中删除 React.StrictMode
并且在这样做之后它工作正常。
然后我尝试将当前的index.js
替换为之前的react版本(17.0.1)index.js
。这是我试过的代码:
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
使用此代码,即使使用 React.StrictMode
这似乎也能正常工作,我的意思是我的数据在重新加载后不会从本地存储中清除。
我的问题是,背后的原因是什么?我是漏掉了什么还是背后有某种逻辑?我提供的解决方案link是4年前的,React 18也是前段时间发布的,想不通到底哪里做错了!
如有任何帮助或建议,我们将不胜感激。
以下是重新生成场景的组件。
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;
}
根据您当前在单个渲染中使用 2 useEffect
的设置,我猜其中一个可能的问题是 automatic batching in React v18 导致所有状态更新并在一个渲染中 side-effects。
关于你的问题React.StrictMode
在React v17和React v18中有什么不同,你可以找到有非常详细的解释。
进一步解释您的情况下可能的修复方法
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts));
}, [contacts]);
您对上述 useEffect
的期望是 “您希望在 contacts
状态更新时调用它”
但实际上,它在没有contacts
更新的情况下也被调用了第一次加载(当contacts
为空时)。
对于模拟,您可以查看下面的代码并进行一些解释(我使用react: 17.0.1
,但如果我使用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>
一个潜在的修复应该是添加一个条件来检查 contacts
在任何更新之前 useEffect
中的状态可用性 - 这将有助于避免再次更新本地存储为空 contacts
没想到
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>
让我简要地告诉你我想做什么。因为我正在学习 React,所以按照教程并制作了一个 Contact Manager
,它将暂时输入姓名和电子邮件地址并将其保存在本地存储中。当我重新加载页面时,它会从本地存储中检索数据并显示出来。由于我处于学习阶段,我只是在探索状态如何工作以及如何将数据保存在本地存储中,稍后我会将这些数据保存到数据库中。
我使用的 React 版本是:
react: "18.1.0"
react-dom: "18.1.0"
我的节点版本是:
17.1.0
我通过这样做将表单输入数据设置到本地存储:
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts));
}, [contacts]);
并使用以下代码从本地存储中检索这些数据:
useEffect(() => {
const retriveContacts = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
if (retriveContacts) {
setContacts(retriveContacts);
}
}, []);
我可以从 Chrome 开发工具看到数据存储在本地存储中,但每当我重新加载页面时,我的目标是从本地存储中检索数据并显示列表。但是重新加载后,这些数据在页面上不再可见。我尝试调试,然后我注意到,当我重新加载页面时,它两次点击 app.js
组件,第一次它获取数据,但第二次数据丢失。
我得到了一个解决方案 here,它说从 index.js
中删除 React.StrictMode
并且在这样做之后它工作正常。
然后我尝试将当前的index.js
替换为之前的react版本(17.0.1)index.js
。这是我试过的代码:
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
使用此代码,即使使用 React.StrictMode
这似乎也能正常工作,我的意思是我的数据在重新加载后不会从本地存储中清除。
我的问题是,背后的原因是什么?我是漏掉了什么还是背后有某种逻辑?我提供的解决方案link是4年前的,React 18也是前段时间发布的,想不通到底哪里做错了!
如有任何帮助或建议,我们将不胜感激。
以下是重新生成场景的组件。
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;
}
根据您当前在单个渲染中使用 2 useEffect
的设置,我猜其中一个可能的问题是 automatic batching in React v18 导致所有状态更新并在一个渲染中 side-effects。
关于你的问题React.StrictMode
在React v17和React v18中有什么不同,你可以找到
进一步解释您的情况下可能的修复方法
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(contacts));
}, [contacts]);
您对上述 useEffect
的期望是 “您希望在 contacts
状态更新时调用它”
但实际上,它在没有contacts
更新的情况下也被调用了第一次加载(当contacts
为空时)。
对于模拟,您可以查看下面的代码并进行一些解释(我使用react: 17.0.1
,但如果我使用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>
一个潜在的修复应该是添加一个条件来检查 contacts
在任何更新之前 useEffect
中的状态可用性 - 这将有助于避免再次更新本地存储为空 contacts
没想到
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>