Poznajemy dalej hook useState – tym razem stan jako tablica obiektów. Do dzieła.
Ok, początek naszego przykładu:
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);
Wyjaśnienie:
- Initial todos są powyżej poziomu RFC bo inaczej tworzone by były od nowa przy każdym renderze-rerenderze (no chyba, że użylibyśmy useMemo)
- nextId jest powyżej poziomu RFC, bo potrzebujemy zmiennej globalnej, a wrzucenie tego w RFC sprawiłoby tak samo, że nextId przy każdym renderze przypisywaną by miało wartość 3 a nie o to chodzi
Ok, lecimy dalej:
function handleAddTodo(title) {
setTodos([
...todos,
{
id: nextId++,
title: title,
done: false
}
]);
}
Stan jest niemutowalny i to jest jego niemutowalność w praktyce, tworzenie nowego obiektu z kopią zawartości starego plus nowym itemem, porównajmy to sobie do arr.push, to już wiemy czym jest mutowalność.
Teraz patent na niemutowalną edycję:
function handleChangeTodo(nextTodo) {
setTodos(todos.map(t => {
if (t.id === nextTodo.id) {
return nextTodo;
} else {
return t;
}
}));
}
Przyjmujemy nowy item, ustawiamy nowy obiekt (map nie jest metodą in-place, tworzy nowy) i tam iterujemy w map po todosach. Jeżeli id się zgadza z tym, który przekazaliśmy to jego zwracamy, jeżeli nie, to jest to inny todo, nieedytowany przez nas – return t.
Ok, teraz niemutowalne usuwanie:
function handleDeleteTodo(todoId) {
setTodos(
todos.filter(t => t.id !== todoId)
);
}
Niemutowalne usuwanie polega na stworzeniu kopii (filter nie jest in place) i umieszczeniu tam wszystkich innych todosów, poza tym, który nas nie interesuje, który ma być „usunięty”.
Teraz markup:
return (
<>
<AddTodo
onAddTodo={handleAddTodo}
/>
<TaskList
todos={todos}
onChangeTodo={handleChangeTodo}
onDeleteTodo={handleDeleteTodo}
/>
</>
);
}
Warto to sobie uświadomić – funkcje z RFC będą tworzone od nowa przy każdym renderze, nawet jak się nie zmienią i jako propsy przekazywane do komponentów dzieci będą wymuszać re-render, nawet jak to nie jest potrzebne – ale techniki optymalizacji poznamy już niedługo.
Ok, teraz wykorzystanie tych funkcji:
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>
</>
)
}
Proste, teraz reszta komponentów:
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>
);
}
Jak przeanalizujemy sobie to wszystko to wrzucam z githuba WebDevSimplified hook useArray, który obrazuje świetnie niemutowalność i używanie tablic jako stanu:
import { useState } from "react"
export default function useArray(defaultValue) {
const [array, setArray] = useState(defaultValue)
function push(element) {
setArray(a => [...a, element])
}
function filter(callback) {
setArray(a => a.filter(callback))
}
function update(index, newElement) {
setArray(a => [
...a.slice(0, index),
newElement,
...a.slice(index + 1, a.length),
])
}
function remove(index) {
setArray(a => [...a.slice(0, index), ...a.slice(index + 1, a.length)])
}
function clear() {
setArray([])
}
return { array, set: setArray, push, filter, update, remove, clear }
}
Mamy co prawda nieco inne podejście do slice (autor zakłada, że to może być jakakolwiek tablica, niekoniecznie tablica obiektów, swoją drogą slice nie jest in-place, nie mylić ze splice), w remove mógł użyć filter, generalnie jednak są tu wszystkie koncepcje niemutowalności stanu jako obiektu złożonego…