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>