React 18 更新 state 中的对象

这篇具有很好参考价值的文章主要介绍了React 18 更新 state 中的对象。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

参考文章

更新 state 中的对象

state 中可以保存任意类型的 JavaScript 值,包括对象。但是,不应该直接修改存放在 React state 中的对象。相反,当想要更新一个对象时,需要创建一个新的对象(或者将其拷贝一份),然后将 state 更新为此对象。

什么是 mutation?

可以在 state 中存放任意类型的 JavaScript 值。

const [x, setX] = useState(0);

在 state 中存放数字、字符串和布尔值,这些类型的值在 JavaScript 中是不可变(immutable)的,这意味着它们不能被改变或是只读的。可以通过替换它们的值以触发一次重新渲染。

setX(5);

state x0 变为 5,但是数字 0 本身并没有发生改变。在 JavaScript 中,无法对内置的原始值,如数字、字符串和布尔值,进行任何更改。

现在考虑 state 中存放对象的情况:

const [position, setPosition] = useState({ x: 0, y: 0 });

从技术上来讲,可以改变对象自身的内容。当这样做时,就制造了一个 mutation

position.x = 5;

然而,虽然严格来说 React state 中存放的对象是可变的,但应该像处理数字、布尔值、字符串一样将它们视为不可变的。因此应该替换它们的值,而不是对它们进行修改。

将 state 视为只读的

换句话说,应该 把所有存放在 state 中的 JavaScript 对象都视为只读的

在下面的例子中,用一个存放在 state 中的对象来表示指针当前的位置。当在预览区触摸或移动光标时,红色的点本应移动。但是实际上红点仍停留在原处:

import { useState } from 'react';
export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        position.x = e.clientX;
        position.y = e.clientY;
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  );
}

问题出在下面这段代码中。

onPointerMove={e => {
  position.x = e.clientX;
  position.y = e.clientY;
}}

这段代码直接修改了 上一次渲染中 分配给 position 的对象。但是因为并没有使用 state 的设置函数,React 并不知道对象已更改。所以 React 没有做出任何响应。虽然在一些情况下,直接修改 state 可能是有效的,但并不推荐这么做。应该把在渲染过程中可以访问到的 state 视为只读的。

在这种情况下,为了真正地 触发一次重新渲染,需要创建一个新对象并把它传递给 state 的设置函数

onPointerMove={e => {
  setPosition({
    x: e.clientX,
    y: e.clientY
  });
}}

通过使用 setPosition,在告诉 React:

  • 使用这个新的对象替换 position 的值
  • 然后再次渲染这个组件

现在可以看到,当在预览区触摸或移动光标时,红点会跟随着指针移动:

import { useState } from 'react';
export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        setPosition({
          x: e.clientX,
          y: e.clientY
        });
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  );
}

使用展开语法复制对象

在之前的例子中,始终会根据当前指针的位置创建出一个新的 position 对象。但是通常,会希望把 现有 数据作为所创建的新对象的一部分。例如,可能只想要更新表单中的一个字段,其他的字段仍然使用之前的值。

下面的代码中,输入框并不会正常运行,因为 onChange 直接修改了 state :

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleFirstNameChange(e) {
    person.firstName = e.target.value;
  }

  function handleLastNameChange(e) {
    person.lastName = e.target.value;
  }

  function handleEmailChange(e) {
    person.email = e.target.value;
  }

  return (
    <>
      <label>
        First name:
        <input
          value={person.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:
        <input
          value={person.lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Email:
        <input
          value={person.email}
          onChange={handleEmailChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

例如,下面这行代码修改了上一次渲染中的 state:

person.firstName = e.target.value;

想要实现需求,最可靠的办法就是创建一个新的对象并将它传递给 setPerson。但是在这里,还需要 把当前的数据复制到新对象中,因为只改变了其中一个字段:

setPerson({
  firstName: e.target.value, // 从 input 中获取新的 first name
  lastName: person.lastName,
  email: person.email
});

可以使用 ... 对象展开 语法,这样就不需要单独复制每个属性。

setPerson({
  ...person, // 复制上一个 person 中的所有字段
  firstName: e.target.value // 但是覆盖 firstName 字段 
});

现在表单可以正常运行了!

可以看到,并没有为每个输入框单独声明一个 state。对于大型表单,将所有数据都存放在同一个对象中是非常方便的——前提是能够正确地更新它!

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleFirstNameChange(e) {
    setPerson({
      ...person,
      firstName: e.target.value
    });
  }

  function handleLastNameChange(e) {
    setPerson({
      ...person,
      lastName: e.target.value
    });
  }

  function handleEmailChange(e) {
    setPerson({
      ...person,
      email: e.target.value
    });
  }

  return (
    <>
      <label>
        First name:
        <input
          value={person.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:
        <input
          value={person.lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Email:
        <input
          value={person.email}
          onChange={handleEmailChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

请注意 ... 展开语法本质是“浅拷贝”——它只会复制一层。这使得它的执行速度很快,但是也意味着当想要更新一个嵌套属性时,必须得多次使用展开语法。

更新一个嵌套对象

考虑下面这种结构的嵌套对象:

const [person, setPerson] = useState({
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
    image: 'https://i.imgur.com/Sd1AgUOm.jpg',
  }
});

如果想要更新 person.artwork.city 的值,用 mutation 来实现的方法非常容易理解:

person.artwork.city = 'New Delhi';

但是在 React 中,需要将 state 视为不可变的!为了修改 city 的值,首先需要创建一个新的 artwork 对象(其中预先填充了上一个 artwork 对象中的数据),然后创建一个新的 person 对象,并使得其中的 artwork 属性指向新创建的 artwork 对象:

const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);

或者,写成一个函数调用:

setPerson({
  ...person, // 复制其它字段的数据 
  artwork: { // 替换 artwork 字段 
    ...person.artwork, // 复制之前 person.artwork 中的数据
    city: 'New Delhi' // 但是将 city 的值替换为 New Delhi!
  }
});

这虽然看起来有点冗长,但对于很多情况都能有效地解决问题:

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    setPerson({
      ...person,
      name: e.target.value
    });
  }

  function handleTitleChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        title: e.target.value
      }
    });
  }

  function handleCityChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        city: e.target.value
      }
    });
  }

  function handleImageChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        image: e.target.value
      }
    });
  }

  return (
    <>
      <label>
        Name:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Title:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        City:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Image:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {' by '}
        {person.name}
        <br />
        (located in {person.artwork.city})
      </p>
      <img 
        src={person.artwork.image} 
        alt={person.artwork.title}
      />
    </>
  );
}

使用 Immer 编写简洁的更新逻辑

如果 state 有多层的嵌套,或许应该考虑 将其扁平化。但是,如果不想改变 state 的数据结构,可以使用 Immer 来实现嵌套展开的效果。Immer 是一个非常流行的库,它可以让你使用简便但可以直接修改的语法编写代码,并会帮你处理好复制的过程。通过使用 Immer,写出的代码看起来就像是“打破了规则”而直接修改了对象:

updatePerson(draft => {
  draft.artwork.city = 'Lagos';
});

但是不同于一般的 mutation,它并不会覆盖之前的 state!

尝试使用 Immer:

  1. 运行 npm install use-immer 添加 Immer 依赖
  2. import { useImmer } from 'use-immer' 替换掉 import { useState } from 'react'

下面我们把上面的例子用 Immer 实现一下:

import { useImmer } from 'use-immer';

export default function Form() {
  const [person, updatePerson] = useImmer({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    updatePerson(draft => {
      draft.name = e.target.value;
    });
  }

  function handleTitleChange(e) {
    updatePerson(draft => {
      draft.artwork.title = e.target.value;
    });
  }

  function handleCityChange(e) {
    updatePerson(draft => {
      draft.artwork.city = e.target.value;
    });
  }

  function handleImageChange(e) {
    updatePerson(draft => {
      draft.artwork.image = e.target.value;
    });
  }

  return (
    <>
      <label>
        Name:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Title:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        City:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Image:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {' by '}
        {person.name}
        <br />
        (located in {person.artwork.city})
      </p>
      <img 
        src={person.artwork.image} 
        alt={person.artwork.title}
      />
    </>
  );
}


package.json:

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

可以看到,事件处理函数变得更简洁了。可以随意在一个组件中同时使用 useStateuseImmer。如果想要写出更简洁的更新处理函数,Immer 会是一个不错的选择,尤其是当 state 中有嵌套,并且复制对象会带来重复的代码时。文章来源地址https://www.toymoban.com/news/detail-648206.html

摘要

  • 将 React 中所有的 state 都视为不可直接修改的。
  • 当在 state 中存放对象时,直接修改对象并不会触发重渲染,并会改变前一次渲染“快照”中 state 的值。
  • 不要直接修改一个对象,而要为它创建一个 版本,并通过把 state 设置成这个新版本来触发重新渲染。
  • 可以使用这样的 {...obj, something: 'newValue'} 对象展开语法来创建对象的拷贝。
  • 对象的展开语法是浅层的:它的复制深度只有一层。
  • 想要更新嵌套对象,需要从更新的位置开始自底向上为每一层都创建新的拷贝。
  • 想要减少重复的拷贝代码,可以使用 Immer。

到了这里,关于React 18 更新 state 中的对象的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • React 18 state 如同一张快照

    参考文章 也许 state 变量看起来和一般的可读写的 JavaScript 变量类似。但 state 在其表现出的特性上更像是一张快照。设置它不会更改已有的 state 变量,但会触发重新渲染。 可能会认为用户界面会直接对点击之类的用户输入做出响应并发生变化。在 React 中,它的工作方式与这

    2024年02月13日
    浏览(46)
  • 区分react中的state和 props

    在 React 中, state 和 props 是两个不同的概念,用于处理组件的数据和属性。它们具有以下区别: 数据来源: state (状态):是组件内部自己管理和维护的数据,用于表示组件的内部状态。可以通过 setState() 方法来更新和改变组件的状态。 props (属性):是从组件外部传递给

    2024年02月04日
    浏览(34)
  • React18原理: React核心对象之ReactElement对象和Fiber对象

    React中的核心对象 在React应用中,有很多特定的对象或数据结构.了解这些内部的设计,可以更容易理解react运行原理 列举从react启动到渲染过程出现频率较高,影响范围较大的对象,它们贯穿整个react运行时 如 ReactElement 对象 如 Fiber 对象 其他过程的重要对象 如事件对象(位

    2024年02月19日
    浏览(35)
  • React16源码: React中的PortalComponent创建, 调和, 更新的源码实现

    PortalComponent 1 )概述 React Portal之所以叫Portal,因为做的就是和“传送门”一样的事情 render到一个组件里面去,实际改变的是网页上另一处的DOM结构 主要关注 portal的创建, 调和, 更新过程 2 )源码 定位到 packages/react-dom/src/client/ReactDOM.js#L576 这里调用的是 ReactPortal.createPortal ,

    2024年01月21日
    浏览(43)
  • React突变状态,更新数据(对象或者数组),页面数据不刷新的问题

    ​ 刚开始开发时,我们可能会遇到这样的问题,使用useState创建对象或者数组时,setState时,页面未进行更新。这里其实是一个突变状态的问题 什么是突变状态? ​ 当我们给 setState 一个基本数据类型时, state 值将会是 一个不可变的值 ​ 更新时, state 的 原始值也不会被更

    2024年02月11日
    浏览(51)
  • React16源码: React中的completeWork对HostText处理含更新的源码实现

    HostText 1 )概述 在 completeWork 中 对 HostText 的处理 在第一次挂载和后续更新的不同条件下进行操作 第一次挂载主要是创建实例 后续更新其实也是重新创建实例 2 )源码 定位到 packages/react-reconciler/src/ReactFiberCompleteWork.js#L663 到 case HostText 这里 进入 updateHostText 进入 createTextInst

    2024年01月24日
    浏览(51)
  • 前端react入门day02-React中的事件绑定与组件

    (创作不易,感谢有你,你的支持,就是我前行的最大动力,如果看完对你有帮助,请留下您的足迹) 目录 React中的事件绑定 React 基础事件绑定 使用事件对象参数  传递自定义参数  同时传递事件对象和自定义参数  React中的组件  组件是什么 React组件 useState  修改状态的规

    2024年02月06日
    浏览(86)
  • 步入React正殿 - State进阶

    目录 扩展学习资料 State进阶知识点 状态更新扩展 shouldComponentUpdate PureComponent 为何使用不变数据【保证数据引用不会出错】  单一数据源  @/src/App.js @/src/components/listItem.jsx 状态提升  @/src/components/navbar.jsx @/src/components/listPage.jsx @src/App.js 有状态组件无状态组件 Stateful【有状态

    2024年02月12日
    浏览(36)
  • React三属性之:state

    作用: state是用于在组件中存储数据,称之为\\\"状态机\\\" 类似于vue2中的data属性,不过操作和vue中data差别很大. 使用: this.state的值不能直接进行赋值操作 ,如:this.state.value_str = \\\'修改的值\\\',需要使用 this.setState 方法 this.setState({修改的key:修改的value},数值发生改变后的函数),只会改变修改

    2024年02月09日
    浏览(36)
  • React 如何获取上一次 state 的值

    一、用 ref 存储上一次的 state 类似 usePrevious 二、通过 setState 的入参改为函数获取

    2024年02月10日
    浏览(83)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包