React更新State的Array类型数据

311 阅读8分钟

在JavaScript中Array是可变数据,但是当将它们存储在state中时,应该将它们视为不可变数据。和对象一样,当你更新存储在state的array时,需要新建一个新的数组(或是拷贝一个现有数组),然后给state设置为新的array。

不能直接修改array

在Javascript中,array属于object。就像object一样,把state中数组看作仅仅可读的,不能直接修改数组本身。不能给数组的元素重新赋值(如arr[0] = 'bird'),不能使用一些方法操作数组,如push()和pop。

如果你想修改数组,给设置state函数传一个新的数组。可以通过调用不改变数组本身的方法如filter()和map()创建一个新的数组。

下面是一个常见数组操作的参考表。当在React state中处理数组时,避免使用左列中的方法,而使用右列中的方法:

不使用(改变数组))使用(返回一个新数组)
增加pushunshiftconcat[...arr]展开语法
删除popshiftsplicefilterslice
替换splicearr[i] = ...重新赋值map
排序reversesort先复制数组,然后对复制的新数组排序

向数组添加项

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={() => {
        artists.push({
          id: nextId++,
          name: name,
        });
      }}>Add</button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

4.png

上面代码通过push()方法添加项,界面没有显示添加的内容。

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

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={() => {
        setArtists([
          ...artists,
          { id: nextId++, name: name }
        ]);
      }}>Add</button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

5.png

删除数组的项

从数组中删除项的最简单方法是将其过滤掉。换句话说,将生成一个不包含该项的新数组。要做到这一点,使用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>
    </>
  );
}

6.png 点击删除按钮,则把对应的项删除了。下面看看事件处理函数:

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

artists.filter(a => a.id !== artist.id)创建一个与artist.id不同的项组成的数组。filter()方法不修改原数组。点击删除按钮过滤artist数组,得到一个新数组,新数组通过设置函数赋值,然后触发重新渲染。

修改数组项

如果想修改数组的一些项,使用map()方法创建一个新数组。传递给map()方法的函数根据data 或index处理每一项。

在这个例子中,一个数组保存了两个圆和一个正方形的坐标。当你按下按钮时,圆圈向下移动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,
        }} />
      ))}
    </>
  );
}

1.png

2.png

替换数组的项

替换数组中的一个或多个项是非常常见的。arr[0] = 'bird'这样的赋值会改变原始数组,需要使用map。 要替换一个项,使用map创建一个新数组。在map调用中,接收index为第二个参数。用index决定是返回原始值(第一个参数值)还是返回一个新值。

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>
  );
}

3.png

向数组插入元素

有时想要在既不在开头也不在末尾的特定位置插入一项。可以使用...数组展开语法和slice()方法。slice()方法从数组中切出一个“切片”。想要插入一个新项,首先创建一个数组在插入点之前展开切片,然后插入新项,然后是原始数组的其余部分。

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>
    </>
  );
}

4.png

对数组做其他的更改

有些时候不能单独使用扩展语法和不修改原数组的方法(如map()和filter())来完成。比如,你想对一个数组排序。JavaScript的reverse()和sort()方法会改变原始数组,不能直接使用它们。

可以先复制数组,然后再对其进行更改。

例如:

import { useState } from 'react';

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>
    </>
  );
}

5.png

首先用 [...list]展开语法创建原始数组的副本。现在有了一个副本,可以使用如nextList.reverse()或nextList.sort()之类的操作数组的方法,或者甚至使用nextList[0] = "something"重新赋值。

即使复制一个数组,也不能直接改变其中的现有项。这是因为复制是浅层的——新数组包含与原数组相同的项。如果在复制的数组中修改对象,就是在改变现有state。例如,像这样的代码就是一个问题。

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

虽然nextList和list是两个不同的数组,但nextList[0]和list[0]指向同一个对象。如果改变nextList[0]同时也了改变list[0],应该避免!可以用与更新嵌套JavaScript对象类似的方法来解决这个问题——复制想要更改的单个项,而不是改变它们。

更新数组object类型的项

对象并不真正位于数组的“内部”。在代码中,它们可能看起来在“内部”,实际上数组内的每个项是指向对象的指针。在更改嵌套字段(如list[0])时需要小心,另一个列表可能同时指向数组的相同元素!

在更新嵌套的state时,需要创建一个副本。看下面的例子:

在本例中,两个独立的艺术品列表具有相同的初始状态。它们应该是隔离的,但是由于它们共享了数据对象,选中一个列表会影响另一个列表:

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>
  );
}

6.png 代码的问题出现在这里:

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同时更改了原始数组的项,产生了一个bug。像这样的漏洞可能很难考虑,但是如果不改变state,就不会有这样的问题。

使用map将新项替换旧项,不改变原始state。

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

...是用于创建对象副本的对象展开语法。

使用这种方法,现有的state项都不会被改变,修复了bug:

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能直接改变state并且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 [yourList, 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={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,直接修改state对象artwork.seen = nextSeen

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

实际上没有修改原始的state,修改的是Immer提供的另外的一个特殊的draft对象。类似的,也可以使用push()和pop()方法修改draft。在幕后,Immer根据你对draft的改变创建新的state。这样使你的代码写起来简洁并且不改变原始的state。

参考文献: Updating Arrays in State