- Quick Start
- Installation
- Describing the UI
- Adding Interactivity
- Managing State
- Escape Hatches
GET STARTED
LEARN REACT
Оновлення масивів у стані
Масиви є змінюваними в JavaScript, але вам слід розглядати їх як незмінні, коли ви зберігаєте їх у стані. Як і у випадку з об'єктами, якщо ви хочете оновити масив, що зберігається у стані, вам потрібно створити новий (або зробити копію існуючого), а потім встановити стан для використання нового масиву.
- Як додавати, видаляти або змінювати елементи масиву в стані React .
- Як оновити об'єкт всередині масиву
- Як зробити копіювання масиву менш повторюваним за допомогою Immer
Оновлення масивів без мутації
У JavaScript масиви - це ще один тип об'єктів. Як і з об'єктами , ви повинні розглядати масиви у стані React як доступні лише для читання. Це означає, що ви не повинні перепризначати елементи всередині масиву, як arr[0] = 'bird'
, а також не повинні використовувати методи, які мутують масив, такі як push()
і pop()
.
Натомість, кожного разу, коли ви хочете оновити масив, вам потрібно буде передавати новий масив до вашої функції встановлення стану. Для цього ви можете створити новий масив з оригінального масиву у вашому стані, викликавши його методи, що не мутують, такі як filter()
та map()
. Після цього ви можете встановити свій стан до нового отриманого масиву.
Довідкова таблиця типових операцій над масивами. Коли ви працюєте з масивами всередині стану React, вам потрібно уникати методів у лівій колонці, а натомість надавати перевагу методам у правій колонці:
уникнути (мутує масив) | prefer (повертає новий масив) | |
---|---|---|
додавання | push , unshift |
concat , [...arr] поширений синтаксис (приклад) |
видалення | pop , shift , splice |
filter , slice (example) |
заміна | splice , arr[i] = ... assignment |
map (example) |
сортування | 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={() => {
artists.push({
id: nextId++,
name: name,
});
}}>Add</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
button { margin-left: 5px; }
Замість цього створіть масив new, який міститиме наявні елементи і новий елемент у кінці. Це можна зробити кількома способами, але найпростіший - використати синтаксис ...
розширення масиву :
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>
</>
);
}
button { margin-left: 5px; }
Синтаксис розширення масиву також дозволяє додавати елемент, розміщуючи його перед початковим ...artists
:
setArtists([
{ id: nextId++, name: name },
...artists // Put old items at the end
]);
Таким чином, spread може виконувати роботу як 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,
}} />
))}
</>
);
}
body { height: 300px; }
Заміна елементів у масиві
Особливо часто потрібно замінити один або декілька елементів у масиві. Присвоєння на кшталт 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>
);
}
button { margin: 5px; }
Вставка в масив
Іноді вам може знадобитися вставити елемент у певну позицію, яка не є ані початковою, ані кінцевою. Для цього ви можете скористатися синтаксисом поширення масиву ...
разом із методом 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>
</>
);
}
button { margin-left: 5px; }
Внесення інших змін до масиву
Є речі, які ви не можете зробити лише за допомогою поширеного синтаксису та методів, що не мутують, таких як 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>
</>
);
}
Тут ви використовуєте синтаксис поширення [...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
змінює оригінальний елемент ілюстрації. Цей елемент ілюстрації також знаходиться у yourList
, що і спричиняє проблему. Такі помилки важко помітити, але, на щастя, вони зникають, якщо ви уникаєте стану мутації.
За допомогою 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, який дозволяє писати за допомогою зручного, але мутуючого синтаксису і подбає про створення копій за вас.
Ось приклад Art Bucket List, переписаний за допомогою 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>
);
}
{
"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"
}
}
Зверніть увагу, що з Іммером мутація на кшталт artwork.seen = nextSeen
тепер працює:
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
Це відбувається тому, що ви мутуєте не стан original, а спеціальний draft
об'єкт, наданий Immer. Аналогічно, ви можете застосувати методи мутації, такі як push()
та pop()
, до вмісту draft
.
За лаштунками Immer завжди будує наступний стан з нуля відповідно до змін, які ви зробили у draft
. Це робить ваші обробники подій дуже лаконічними і не призводить до мутації стану.
- Ви можете переводити масиви у стан, але не можете їх змінювати.
- Замість мутації масиву створити нову його версію і оновити стан до неї.
- Для створення масивів з новими елементами можна використовувати синтаксис поширення масиву
[...arr, newItem]
. - Ви можете використовувати
filter()
таmap()
для створення нових масивів з відфільтрованими або перетвореними елементами. - Ви можете використовувати Immer для стислості коду.
Оновити товар у кошику
Заповніть логіку handleIncreaseClick
так, щоб натискання "+" збільшувало відповідне число:
import { useState } from 'react';
const initialProducts = [{
id: 0,
name: 'Baklava',
count: 1,
}, {
id: 1,
name: 'Cheese',
count: 5,
}, {
id: 2,
name: 'Spaghetti',
count: 2,
}];
export default function ShoppingCart() {
const [
products,
setProducts
] = useState(initialProducts)
function handleIncreaseClick(productId) {
}
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name}
{' '}
(<b>{product.count}</b>)
<button onClick={() => {
handleIncreaseClick(product.id);
}}>
+
</button>
</li>
))}
</ul>
);
}
button { margin: 5px; }
Ви можете скористатися функцією map
для створення нового масиву, а потім за допомогою синтаксису поширення об'єктів ...
створити копію зміненого об'єкта для нового масиву:
import { useState } from 'react';
const initialProducts = [{
id: 0,
name: 'Baklava',
count: 1,
}, {
id: 1,
name: 'Cheese',
count: 5,
}, {
id: 2,
name: 'Spaghetti',
count: 2,
}];
export default function ShoppingCart() {
const [
products,
setProducts
] = useState(initialProducts)
function handleIncreaseClick(productId) {
setProducts(products.map(product => {
if (product.id === productId) {
return {
...product,
count: product.count + 1
};
} else {
return product;
}
}))
}
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name}
{' '}
(<b>{product.count}</b>)
<button onClick={() => {
handleIncreaseClick(product.id);
}}>
+
</button>
</li>
))}
</ul>
);
}
button { margin: 5px; }
Видалення товару з кошика
.У цьому кошику працює кнопка "+", але кнопка "-" нічого не робить. Вам потрібно додати до неї обробник події, щоб її натискання зменшувало count
відповідного товару. Якщо ви натискаєте "-", коли кількість дорівнює 1, товар повинен автоматично видалятися з кошика. Переконайтеся, що він ніколи не показує 0.
import { useState } from 'react';
const initialProducts = [{
id: 0,
name: 'Baklava',
count: 1,
}, {
id: 1,
name: 'Cheese',
count: 5,
}, {
id: 2,
name: 'Spaghetti',
count: 2,
}];
export default function ShoppingCart() {
const [
products,
setProducts
] = useState(initialProducts)
function handleIncreaseClick(productId) {
setProducts(products.map(product => {
if (product.id === productId) {
return {
...product,
count: product.count + 1
};
} else {
return product;
}
}))
}
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name}
{' '}
(<b>{product.count}</b>)
<button onClick={() => {
handleIncreaseClick(product.id);
}}>
+
</button>
<button>
–
</button>
</li>
))}
</ul>
);
}
button { margin: 5px; }
Ви можете спочатку використати map
для створення нового масиву, а потім filter
для видалення продуктів з count
встановленим значенням 0
:
import { useState } from 'react';
const initialProducts = [{
id: 0,
name: 'Baklava',
count: 1,
}, {
id: 1,
name: 'Cheese',
count: 5,
}, {
id: 2,
name: 'Spaghetti',
count: 2,
}];
export default function ShoppingCart() {
const [
products,
setProducts
] = useState(initialProducts)
function handleIncreaseClick(productId) {
setProducts(products.map(product => {
if (product.id === productId) {
return {
...product,
count: product.count + 1
};
} else {
return product;
}
}))
}
function handleDecreaseClick(productId) {
let nextProducts = products.map(product => {
if (product.id === productId) {
return {
...product,
count: product.count - 1
};
} else {
return product;
}
});
nextProducts = nextProducts.filter(p =>
p.count > 0
);
setProducts(nextProducts)
}
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name}
{' '}
(<b>{product.count}</b>)
<button onClick={() => {
handleIncreaseClick(product.id);
}}>
+
</button>
<button onClick={() => {
handleDecreaseClick(product.id);
}}>
–
</button>
</li>
))}
</ul>
);
}
button { margin: 5px; }
Виправте мутації за допомогою немутаційних методів
У цьому прикладі всі обробники подій у App.js
використовують мутацію. Як наслідок, редагування та видалення справ не працює. Перепишіть handleAddTodo
, handleChangeTodo
і handleDeleteTodo
, щоб використовувати немутативні методи:
import { useState } from 'react';
import AddTodo from './AddTodo.js';
import TaskList from './TaskList.js';
let nextId = 3;
const initialTodos = [
{ id: 0, title: 'Buy milk', done: true },
{ id: 1, title: 'Eat tacos', done: false },
{ id: 2, title: 'Brew tea', done: false },
];
export default function TaskApp() {
const [todos, setTodos] = useState(
initialTodos
);
function handleAddTodo(title) {
todos.push({
id: nextId++,
title: title,
done: false
});
}
function handleChangeTodo(nextTodo) {
const todo = todos.find(t =>
t.id === nextTodo.id
);
todo.title = nextTodo.title;
todo.done = nextTodo.done;
}
function handleDeleteTodo(todoId) {
const index = todos.findIndex(t =>
t.id === todoId
);
todos.splice(index, 1);
}
return (
<>
<AddTodo
onAddTodo={handleAddTodo}
/>
<TaskList
todos={todos}
onChangeTodo={handleChangeTodo}
onDeleteTodo={handleDeleteTodo}
/>
</>
);
}
import { useState } from 'react';
export default function AddTodo({ onAddTodo }) {
const [title, setTitle] = useState('');
return (
<>
<input
placeholder="Add todo"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<button onClick={() => {
setTitle('');
onAddTodo(title);
}}>Add</button>
</>
)
}
import { useState } from 'react';
export default function TaskList({
todos,
onChangeTodo,
onDeleteTodo
}) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<Task
todo={todo}
onChange={onChangeTodo}
onDelete={onDeleteTodo}
/>
</li>
))}
</ul>
);
}
function Task({ todo, onChange, onDelete }) {
const [isEditing, setIsEditing] = useState(false);
let todoContent;
if (isEditing) {
todoContent = (
<>
<input
value={todo.title}
onChange={e => {
onChange({
...todo,
title: e.target.value
});
}} />
<button onClick={() => setIsEditing(false)}>
Save
</button>
</>
);
} else {
todoContent = (
<>
{todo.title}
<button onClick={() => setIsEditing(true)}>
Edit
</button>
</>
);
}
return (
<label>
<input
type="checkbox"
checked={todo.done}
onChange={e => {
onChange({
...todo,
done: e.target.checked
});
}}
/>
{todoContent}
<button onClick={() => onDelete(todo.id)}>
Delete
</button>
</label>
);
}
button { margin: 5px; }
li { list-style-type: none; }
ul, li { margin: 0; padding: 0; }
У handleAddTodo
можна використовувати синтаксис розкриття масиву. У handleChangeTodo
ви можете створити новий масив за допомогою map
. У handleDeleteTodo
ви можете створити новий масив за допомогою filter
. Тепер список працює коректно:
import { useState } from 'react';
import AddTodo from './AddTodo.js';
import TaskList from './TaskList.js';
let nextId = 3;
const initialTodos = [
{ id: 0, title: 'Buy milk', done: true },
{ id: 1, title: 'Eat tacos', done: false },
{ id: 2, title: 'Brew tea', done: false },
];
export default function TaskApp() {
const [todos, setTodos] = useState(
initialTodos
);
function handleAddTodo(title) {
setTodos([
...todos,
{
id: nextId++,
title: title,
done: false
}
]);
}
function handleChangeTodo(nextTodo) {
setTodos(todos.map(t => {
if (t.id === nextTodo.id) {
return nextTodo;
} else {
return t;
}
}));
}
function handleDeleteTodo(todoId) {
setTodos(
todos.filter(t => t.id !== todoId)
);
}
return (
<>
<AddTodo
onAddTodo={handleAddTodo}
/>
<TaskList
todos={todos}
onChangeTodo={handleChangeTodo}
onDeleteTodo={handleDeleteTodo}
/>
</>
);
}
import { useState } from 'react';
export default function AddTodo({ onAddTodo }) {
const [title, setTitle] = useState('');
return (
<>
<input
placeholder="Add todo"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<button onClick={() => {
setTitle('');
onAddTodo(title);
}}>Add</button>
</>
)
}
import { useState } from 'react';
export default function TaskList({
todos,
onChangeTodo,
onDeleteTodo
}) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<Task
todo={todo}
onChange={onChangeTodo}
onDelete={onDeleteTodo}
/>
</li>
))}
</ul>
);
}
function Task({ todo, onChange, onDelete }) {
const [isEditing, setIsEditing] = useState(false);
let todoContent;
if (isEditing) {
todoContent = (
<>
<input
value={todo.title}
onChange={e => {
onChange({
...todo,
title: e.target.value
});
}} />
<button onClick={() => setIsEditing(false)}>
Save
</button>
</>
);
} else {
todoContent = (
<>
{todo.title}
<button onClick={() => setIsEditing(true)}>
Edit
</button>
</>
);
}
return (
<label>
<input
type="checkbox"
checked={todo.done}
onChange={e => {
onChange({
...todo,
done: e.target.checked
});
}}
/>
{todoContent}
<button onClick={() => onDelete(todo.id)}>
Delete
</button>
</label>
);
}
button { margin: 5px; }
li { list-style-type: none; }
ul, li { margin: 0; padding: 0; }
Виправити мутації за допомогою Immer
Це той самий приклад, що й у попередньому завданні. Цього разу виправте мутації за допомогою Immer. Для вашої зручності useImmer
вже імпортовано, тому вам потрібно змінити змінну стану todos
, щоб використовувати її.
import { useState } from 'react';
import { useImmer } from 'use-immer';
import AddTodo from './AddTodo.js';
import TaskList from './TaskList.js';
let nextId = 3;
const initialTodos = [
{ id: 0, title: 'Buy milk', done: true },
{ id: 1, title: 'Eat tacos', done: false },
{ id: 2, title: 'Brew tea', done: false },
];
export default function TaskApp() {
const [todos, setTodos] = useState(
initialTodos
);
function handleAddTodo(title) {
todos.push({
id: nextId++,
title: title,
done: false
});
}
function handleChangeTodo(nextTodo) {
const todo = todos.find(t =>
t.id === nextTodo.id
);
todo.title = nextTodo.title;
todo.done = nextTodo.done;
}
function handleDeleteTodo(todoId) {
const index = todos.findIndex(t =>
t.id === todoId
);
todos.splice(index, 1);
}
return (
<>
<AddTodo
onAddTodo={handleAddTodo}
/>
<TaskList
todos={todos}
onChangeTodo={handleChangeTodo}
onDeleteTodo={handleDeleteTodo}
/>
</>
);
}
import { useState } from 'react';
export default function AddTodo({ onAddTodo }) {
const [title, setTitle] = useState('');
return (
<>
<input
placeholder="Add todo"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<button onClick={() => {
setTitle('');
onAddTodo(title);
}}>Add</button>
</>
)
}
import { useState } from 'react';
export default function TaskList({
todos,
onChangeTodo,
onDeleteTodo
}) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<Task
todo={todo}
onChange={onChangeTodo}
onDelete={onDeleteTodo}
/>
</li>
))}
</ul>
);
}
function Task({ todo, onChange, onDelete }) {
const [isEditing, setIsEditing] = useState(false);
let todoContent;
if (isEditing) {
todoContent = (
<>
<input
value={todo.title}
onChange={e => {
onChange({
...todo,
title: e.target.value
});
}} />
<button onClick={() => setIsEditing(false)}>
Save
</button>
</>
);
} else {
todoContent = (
<>
{todo.title}
<button onClick={() => setIsEditing(true)}>
Edit
</button>
</>
);
}
return (
<label>
<input
type="checkbox"
checked={todo.done}
onChange={e => {
onChange({
...todo,
done: e.target.checked
});
}}
/>
{todoContent}
<button onClick={() => onDelete(todo.id)}>
Delete
</button>
</label>
);
}
button { margin: 5px; }
li { list-style-type: none; }
ul, li { margin: 0; padding: 0; }
{
"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"
}
}
За допомогою Immer ви можете писати код у мутаційному режимі, якщо ви мутуєте лише частини чернетки
, які надає вам Immer. У цьому прикладі всі мутації виконуються на draft
, тому код працює:
import { useState } from 'react';
import { useImmer } from 'use-immer';
import AddTodo from './AddTodo.js';
import TaskList from './TaskList.js';
let nextId = 3;
const initialTodos = [
{ id: 0, title: 'Buy milk', done: true },
{ id: 1, title: 'Eat tacos', done: false },
{ id: 2, title: 'Brew tea', done: false },
];
export default function TaskApp() {
const [todos, updateTodos] = useImmer(
initialTodos
);
function handleAddTodo(title) {
updateTodos(draft => {
draft.push({
id: nextId++,
title: title,
done: false
});
});
}
function handleChangeTodo(nextTodo) {
updateTodos(draft => {
const todo = draft.find(t =>
t.id === nextTodo.id
);
todo.title = nextTodo.title;
todo.done = nextTodo.done;
});
}
function handleDeleteTodo(todoId) {
updateTodos(draft => {
const index = draft.findIndex(t =>
t.id === todoId
);
draft.splice(index, 1);
});
}
return (
<>
<AddTodo
onAddTodo={handleAddTodo}
/>
<TaskList
todos={todos}
onChangeTodo={handleChangeTodo}
onDeleteTodo={handleDeleteTodo}
/>
</>
);
}
import { useState } from 'react';
export default function AddTodo({ onAddTodo }) {
const [title, setTitle] = useState('');
return (
<>
<input
placeholder="Add todo"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<button onClick={() => {
setTitle('');
onAddTodo(title);
}}>Add</button>
</>
)
}
import { useState } from 'react';
export default function TaskList({
todos,
onChangeTodo,
onDeleteTodo
}) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<Task
todo={todo}
onChange={onChangeTodo}
onDelete={onDeleteTodo}
/>
</li>
))}
</ul>
);
}
function Task({ todo, onChange, onDelete }) {
const [isEditing, setIsEditing] = useState(false);
let todoContent;
if (isEditing) {
todoContent = (
<>
<input
value={todo.title}
onChange={e => {
onChange({
...todo,
title: e.target.value
});
}} />
<button onClick={() => setIsEditing(false)}>
Save
</button>
</>
);
} else {
todoContent = (
<>
{todo.title}
<button onClick={() => setIsEditing(true)}>
Edit
</button>
</>
);
}
return (
<label>
<input
type="checkbox"
checked={todo.done}
onChange={e => {
onChange({
...todo,
done: e.target.checked
});
}}
/>
{todoContent}
<button onClick={() => onDelete(todo.id)}>
Delete
</button>
</label>
);
}
button { margin: 5px; }
li { list-style-type: none; }
ul, li { margin: 0; padding: 0; }
{
"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"
}
}
Ви також можете змішувати і поєднувати мутативні та немутативні підходи за допомогою Immer.
Наприклад, у цій версії handleAddTodo
реалізовано шляхом мутації Immer draft
, тоді як handleChangeTodo
та handleDeleteTodo
використовують немутативні методи map
та filter
:
import { useState } from 'react';
import { useImmer } from 'use-immer';
import AddTodo from './AddTodo.js';
import TaskList from './TaskList.js';
let nextId = 3;
const initialTodos = [
{ id: 0, title: 'Buy milk', done: true },
{ id: 1, title: 'Eat tacos', done: false },
{ id: 2, title: 'Brew tea', done: false },
];
export default function TaskApp() {
const [todos, updateTodos] = useImmer(
initialTodos
);
function handleAddTodo(title) {
updateTodos(draft => {
draft.push({
id: nextId++,
title: title,
done: false
});
});
}
function handleChangeTodo(nextTodo) {
updateTodos(todos.map(todo => {
if (todo.id === nextTodo.id) {
return nextTodo;
} else {
return todo;
}
}));
}
function handleDeleteTodo(todoId) {
updateTodos(
todos.filter(t => t.id !== todoId)
);
}
return (
<>
<AddTodo
onAddTodo={handleAddTodo}
/>
<TaskList
todos={todos}
onChangeTodo={handleChangeTodo}
onDeleteTodo={handleDeleteTodo}
/>
</>
);
}
import { useState } from 'react';
export default function AddTodo({ onAddTodo }) {
const [title, setTitle] = useState('');
return (
<>
<input
placeholder="Add todo"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<button onClick={() => {
setTitle('');
onAddTodo(title);
}}>Add</button>
</>
)
}
import { useState } from 'react';
export default function TaskList({
todos,
onChangeTodo,
onDeleteTodo
}) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<Task
todo={todo}
onChange={onChangeTodo}
onDelete={onDeleteTodo}
/>
</li>
))}
</ul>
);
}
function Task({ todo, onChange, onDelete }) {
const [isEditing, setIsEditing] = useState(false);
let todoContent;
if (isEditing) {
todoContent = (
<>
<input
value={todo.title}
onChange={e => {
onChange({
...todo,
title: e.target.value
});
}} />
<button onClick={() => setIsEditing(false)}>
Save
</button>
</>
);
} else {
todoContent = (
<>
{todo.title}
<button onClick={() => setIsEditing(true)}>
Edit
</button>
</>
);
}
return (
<label>
<input
type="checkbox"
checked={todo.done}
onChange={e => {
onChange({
...todo,
done: e.target.checked
});
}}
/>
{todoContent}
<button onClick={() => onDelete(todo.id)}>
Delete
</button>
</label>
);
}
button { margin: 5px; }
li { list-style-type: none; }
ul, li { margin: 0; padding: 0; }
{
"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"
}
}
За допомогою Immer ви можете вибрати стиль, який буде найбільш природним для кожного окремого випадку.