美文网首页
第二六章 添加交互-在状态中更新数组

第二六章 添加交互-在状态中更新数组

作者: 深圳都这么冷 | 来源:发表于2023-02-14 08:59 被阅读0次

在状态中更新数组

数组在 JavaScript 中是可变的,但在将它们存储在状态中时,您应该将它们视为不可变的。 就像对象一样,当你想更新存储在状态中的数组时,你需要创建一个新数组(或复制现有数组),然后设置状态以使用新数组。

你将学习

  • 如何在 React 状态下添加、删除或更改数组中的项目
  • 如何更新数组内的对象
  • 如何使用 Immer 减少数组复制的重复性

更新数组而不改变老数组

在 JavaScript 中,数组只是另一种对象。 与对象一样,您应该将处于 React 状态的数组视为只读。 这意味着你不应该像 arr[0] = 'bird' 这样重新分配数组中的项目,你也不应该使用改变数组的方法,比如 push() 和 pop()。

相反,每次你想更新一个数组时,你都需要将一个新数组传递给你的状态设置函数。 为此,您可以通过调用其非修改方法(如 filter() 和 map())从您状态下的原始数组创建一个新数组。 然后您可以将您的状态设置为生成的新数组。

这里有一个常见数组操作的参考表。 在处理 React 状态中的数组时,您需要避免使用左栏中的方法,而尽量用右栏中的方法:

避免(改变数组) 尽量(返回新数组)
添加 push,unshift concat, [...arr]展开语法
删除 pop, shift, splice filter, slice
替换 splice, arr[i] = ... 赋值 map
排序 reverse, sort 先复制数组

或者,您可以使用 Immer,它允许您使用来自两列的方法。

陷阱

不幸的是, slice 和 splice 的命名相似但有很大不同:

  • slice 允许您复制一个数组或其中的一部分。
  • splice 改变数组(插入或删除项目)。

在 React 中,你会更频繁地使用 slice(没p!),因为你不想改变状态中的对象或数组。 更新对象解释了什么是突变以及为什么不建议将其用于状态。

添加到数组

push() 会改变数组,这不是你想要的:

import { useState } from 'react';

let nextId = 0;

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState([]);

  return (
    <>
      <h1>Inspiring sculptors:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={() => {
        setName('');
        artists.push({
          id: nextId++,
          name: name,
        });
      }}>Add</button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

相反,创建一个新数组,其中包含现有项目和末尾的新项目。 有多种方法可以做到这一点,但最简单的方法是使用 ... 数组展开语法:

setArtists( // Replace the state
  [ // with a new array
    ...artists, // that contains all the old items
    { id: nextId++, name: name } // and one new item at the end
  ]
);

现在它工作正常:

import { useState } from 'react';

let nextId = 0;

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState([]);

  return (
    <>
      <h1>Inspiring sculptors:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={() => {
        setName('');
        setArtists([
          ...artists,
          { id: nextId++, name: name }
        ]);
      }}>Add</button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

数组展开语法还允许您通过将项目放在原始 ...artists 之前来预先添加项目:

setArtists([
  { id: nextId++, name: name },
  ...artists // Put old items at the end
]);

通过这种方式,展开可以通过添加到数组的末尾来完成 push() 的工作,也可以通过添加到数组的开头来完成 unshift() 的工作。

从数组移除

从数组中删除项目的最简单方法是将其过滤掉。 换句话说,您将生成一个不包含该项目的新数组。 为此,请使用 filter 方法,例如:

import { useState } from 'react';

let initialArtists = [
  { id: 0, name: 'Marta Colvin Andrade' },
  { id: 1, name: 'Lamidi Olonade Fakeye'},
  { id: 2, name: 'Louise Nevelson'},
];

export default function List() {
  const [artists, setArtists] = useState(
    initialArtists
  );

  return (
    <>
      <h1>Inspiring sculptors:</h1>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>
            {artist.name}{' '}
            <button onClick={() => {
              setArtists(
                artists.filter(a =>
                  a.id !== artist.id
                )
              );
            }}>
              Delete
            </button>
          </li>
        ))}
      </ul>
    </>
  );
}

单击“删除”按钮几次,然后查看其单击处理程序。

setArtists(
  artists.filter(a => a.id !== artist.id)
);

在这里,artists.filter(a => a.id !== artist.id) 的意思是“创建一个数组,其中包含那些 ID 与 artist.id 不同的艺术家”。 换句话说,每个艺术家的“删除”按钮都会将该艺术家从数组中过滤掉,然后请求使用结果数组重新渲染。 请注意,过滤器不会修改原始数组。

数组转换

如果要更改数组的部分或全部项目,可以使用 map() 创建一个新数组。 您将传递给 map 的函数可以根据其数据或索引(或两者)决定如何处理每个项目。

在此示例中,数组包含两个圆和一个正方形的坐标。 当您按下按钮时,它只会将圆圈向下移动 50 像素。 它通过使用 map() 生成一个新的数据数组来实现:

import { useState } from 'react';

let initialShapes = [
  { id: 0, type: 'circle', x: 50, y: 100 },
  { id: 1, type: 'square', x: 150, y: 100 },
  { id: 2, type: 'circle', x: 250, y: 100 },
];

export default function ShapeEditor() {
  const [shapes, setShapes] = useState(
    initialShapes
  );

  function handleClick() {
    const nextShapes = shapes.map(shape => {
      if (shape.type === 'square') {
        // No change
        return shape;
      } else {
        // Return a new circle 50px below
        return {
          ...shape,
          y: shape.y + 50,
        };
      }
    });
    // Re-render with the new array
    setShapes(nextShapes);
  }

  return (
    <>
      <button onClick={handleClick}>
        Move circles down!
      </button>
      {shapes.map(shape => (
        <div
          key={shape.id}
          style={{
          background: 'purple',
          position: 'absolute',
          left: shape.x,
          top: shape.y,
          borderRadius:
            shape.type === 'circle'
              ? '50%' : '',
          width: 20,
          height: 20,
        }} />
      ))}
    </>
  );
}

替换数组项

想要替换数组中的一个或多个项目是特别常见的。 像 arr[0] = 'bird' 这样的赋值会改变原始数组,因此您也需要为此使用 map。

要替换项目,请使用map创建一个新数组。 在您的map调用中,您将收到项目索引作为第二个参数。 用它来决定是否返回原始项目(第一个参数)或其他东西:

import { useState } from 'react';

let initialCounters = [
  0, 0, 0
];

export default function CounterList() {
  const [counters, setCounters] = useState(
    initialCounters
  );

  function handleIncrementClick(index) {
    const nextCounters = counters.map((c, i) => {
      if (i === index) {
        // Increment the clicked counter
        return c + 1;
      } else {
        // The rest haven't changed
        return c;
      }
    });
    setCounters(nextCounters);
  }

  return (
    <ul>
      {counters.map((counter, i) => (
        <li key={i}>
          {counter}
          <button onClick={() => {
            handleIncrementClick(i);
          }}>+1</button>
        </li>
      ))}
    </ul>
  );
}

插入数组

有时,您可能希望在既不是开头也不是结尾的特定位置插入一个项目。 为此,您可以将 ... 数组展开语法与 slice() 方法结合使用。 slice() 方法可让您切割数组的“切片”。 要插入一个项目,您将创建一个数组,该数组展开插入点之前的切片,然后是新项目,然后是原始数组的其余部分。

在此示例中,“插入”按钮始终在索引 1 处插入:

import { useState } from 'react';

let nextId = 3;
const initialArtists = [
  { id: 0, name: 'Marta Colvin Andrade' },
  { id: 1, name: 'Lamidi Olonade Fakeye'},
  { id: 2, name: 'Louise Nevelson'},
];

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState(
    initialArtists
  );

  function handleClick() {
    const insertAt = 1; // Could be any index
    const nextArtists = [
      // Items before the insertion point:
      ...artists.slice(0, insertAt),
      // New item:
      { id: nextId++, name: name },
      // Items after the insertion point:
      ...artists.slice(insertAt)
    ];
    setArtists(nextArtists);
    setName('');
  }

  return (
    <>
      <h1>Inspiring sculptors:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={handleClick}>
        Insert
      </button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

对数组进行其他更改

有些事情你不能单独使用扩展语法和非变异方法,如 map() 和 filter() 。 例如,您可能想要对数组进行反转或排序。 JavaScript reverse() 和 sort() 方法正在改变原始数组,因此您不能直接使用它们。
但是,您可以先复制数组,然后再对其进行更改。
例如:

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies' },
  { id: 1, title: 'Lunar Landscape' },
  { id: 2, title: 'Terracotta Army' },
];

export default function List() {
  const [list, setList] = useState(initialList);

  function handleClick() {
    const nextList = [...list];
    nextList.reverse();
    setList(nextList);
  }

  return (
    <>
      <button onClick={handleClick}>
        Reverse
      </button>
      <ul>
        {list.map(artwork => (
          <li key={artwork.id}>{artwork.title}</li>
        ))}
      </ul>
    </>
  );
}

在这里,您使用 [...list] 展开语法首先创建原始数组的副本。 现在您有了一个副本,您可以使用 nextList.reverse() 或 nextList.sort() 等变更方法,甚至可以使用 nextList[0] = "something" 分配单个项目。

然而,即使你复制了一个数组,你也不能直接改变其中的现有项。 这是因为复制是浅层的——新数组将包含与原始数组相同的项。 因此,如果您修改复制数组中的对象,就会改变现有状态。 例如,像这样的代码就是一个问题。

const nextList = [...list];
nextList[0].seen = true; // Problem: mutates list[0]
setList(nextList);

nextList和list虽然是两个不同的数组,但是nextList[0]和list[0]指向同一个对象。 因此,通过更改 nextList[0].seen,您也会更改 list[0].seen。 这是状态修改,您应该避免! 您可以通过与更新嵌套 JavaScript 对象类似的方式解决此问题——通过复制您想要更改的单个项目而不是改变它们。 就是这样。

更新数组中的对象

对象并不真正位于数组“内部”。 它们可能看起来在代码中“内部”,但数组中的每个对象都是一个单独的值,数组“指向”。 这就是为什么在更改像 list[0] 这样的嵌套字段时需要小心的原因。 另一个人的作品列表可能指向数组的同一个元素!

更新嵌套状态时,您需要从要更新的点开始创建副本,一直到顶层。 让我们看看这是如何工作的。

在此示例中,两个单独的艺术品列表具有相同的初始状态。 它们应该是隔离的,但由于突变,它们的状态被意外共享,选中一个列表中的框会影响另一个列表:

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [myList, setMyList] = useState(initialList);
  const [yourList, setYourList] = useState(
    initialList
  );

  function handleToggleMyList(artworkId, nextSeen) {
    const myNextList = [...myList];
    const artwork = myNextList.find(
      a => a.id === artworkId
    );
    artwork.seen = nextSeen;
    setMyList(myNextList);
  }

  function handleToggleYourList(artworkId, nextSeen) {
    const yourNextList = [...yourList];
    const artwork = yourNextList.find(
      a => a.id === artworkId
    );
    artwork.seen = nextSeen;
    setYourList(yourNextList);
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Your list of art to see:</h2>
      <ItemList
        artworks={yourList}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

问题出在这里的代码中:

const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // Problem: mutates an existing item
setMyList(myNextList);

虽然 myNextList 数组本身是新的,但项目本身与原始 myList 数组中的相同。 因此,更改 artwork.seen 会更改原始艺术品项目。 该艺术品项目也在您的艺术品中,这会导致该错误。 像这样的错误可能很难考虑,但值得庆幸的是,如果你避免改变状态,它们就会消失。

您可以使用 map 将旧项目替换为其更新版本而无需修改。

setMyList(myList.map(artwork => {
  if (artwork.id === artworkId) {
    // Create a *new* object with changes
    return { ...artwork, seen: nextSeen };
  } else {
    // No changes
    return artwork;
  }
});

这里,... 是用于创建对象副本的对象展开语法。
使用这种方法,现有状态项都不会发生变化,并且错误已修复:

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [myList, setMyList] = useState(initialList);
  const [yourList, setYourList] = useState(
    initialList
  );

  function handleToggleMyList(artworkId, nextSeen) {
    setMyList(myList.map(artwork => {
      if (artwork.id === artworkId) {
        // Create a *new* object with changes
        return { ...artwork, seen: nextSeen };
      } else {
        // No changes
        return artwork;
      }
    }));
  }

  function handleToggleYourList(artworkId, nextSeen) {
    setYourList(yourList.map(artwork => {
      if (artwork.id === artworkId) {
        // Create a *new* object with changes
        return { ...artwork, seen: nextSeen };
      } else {
        // No changes
        return artwork;
      }
    }));
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Your list of art to see:</h2>
      <ItemList
        artworks={yourList}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

通常,您应该只改变刚刚创建的对象。 如果您要插入一个新的艺术品,您可以改变它,但如果您正在处理已经存在的东西,您需要制作一个副本。

用 Immer 编写简洁的更新逻辑

不改变地更新嵌套数组可能会有点重复。 就像对象一样:

  • 通常,您不需要更新超过几个级别的状态。 如果您的状态对象非常深,您可能希望以不同方式重组它们,使它们变平。
  • 如果你不想改变你的状态结构,你可能更喜欢使用 Immer,它可以让你使用方便但可变的语法进行编写,并负责为你生成副本。
    这是用 Immer 重写的艺术遗愿清单示例:
import { useState } from 'react';
import { useImmer } from 'use-immer';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [myList, updateMyList] = useImmer(
    initialList
  );
  const [yourArtworks, updateYourList] = useImmer(
    initialList
  );

  function handleToggleMyList(id, nextSeen) {
    updateMyList(draft => {
      const artwork = draft.find(a =>
        a.id === id
      );
      artwork.seen = nextSeen;
    });
  }

  function handleToggleYourList(artworkId, nextSeen) {
    updateYourList(draft => {
      const artwork = draft.find(a =>
        a.id === artworkId
      );
      artwork.seen = nextSeen;
    });
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Your list of art to see:</h2>
      <ItemList
        artworks={yourArtworks}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

注意使用 Immer,像 artwork.seen = nextSeen 这样的修改现在是可以的:

updateMyTodos(draft => {
  const artwork = draft.find(a => a.id === artworkId);
  artwork.seen = nextSeen;
});

这是因为您没有改变原始状态,而是改变了 Immer 提供的特殊草稿对象。 同样,您可以将 push() 和 pop() 等变异方法应用于草稿的内容。

在幕后,Immer 总是根据您对草稿所做的更改从头开始构建下一个状态。 这使您的事件处理程序非常简洁,而不会改变状态。

回顾

  • 您可以将数组放入状态,但不能更改它们。
  • 与其改变数组,不如创建它的新版本,并更新它的状态。
  • 您可以使用 [...arr, newItem] 数组展开语法来创建包含新项的数组。
  • 您可以使用 filter() 和 map() 创建包含过滤或转换项目的新数组。
  • 您可以使用 Immer 来保持代码简洁。

相关文章

网友评论

      本文标题:第二六章 添加交互-在状态中更新数组

      本文链接:https://www.haomeiwen.com/subject/xfgxkdtx.html