- Quick Start
- Installation
- Describing the UI
- Adding Interactivity
- Managing State
- Escape Hatches
GET STARTED
LEARN REACT
Збереження та скидання стану
Стан ізольовано між компонентами. React відстежує, який стан належить якому компоненту, на основі їх місця в дереві інтерфейсу користувача. Ви можете контролювати, коли зберігати стан, а коли скидати його між рендерингами.
- Коли React вирішує зберегти або скинути стан
- Як змусити React скинути стан компонента
- Як ключі та типи впливають на збереження стану
Стан прив'язано до позиції у дереві рендерингу
React збирає дерева візуалізації для структури компонентів у вашому UI.
Коли ви надаєте компоненту стан, ви можете подумати, що стан "живе" всередині компонента. Але насправді стан зберігається всередині React. React пов'язує кожен фрагмент стану, який він утримує, з відповідним компонентом, згідно того, де цей компонент знаходиться у дереві рендерингу.
Тут є лише один JSX-тег <Counter />
, але він відображається у двох різних позиціях:
import { useState } from 'react';
export default function App() {
const counter = <Counter />;
return (
<div>
{counter}
{counter}
</div>
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
label {
display: block;
clear: both;
}
.counter {
width: 100px;
text-align: center;
border: 1px solid gray;
border-radius: 4px;
padding: 20px;
margin: 0 20px 20px 0;
float: left;
}
.hover {
background: #ffffd8;
}
Ось як це виглядає у вигляді дерева:
Це два окремі лічильники, оскільки кожен з них рендериться у своїй позиції в дереві.Зазвичай вам не потрібно думати про ці позиції для використання React, але це може бути корисно для розуміння того, як це працює.
У React кожен компонент на екрані має повністю ізольований стан. Наприклад, якщо ви рендерите два компоненти Counter
поруч, кожен з них отримає свої власні, незалежні стани score
та hover
.
Спробуйте клацнути обидва лічильники і помітити, що вони не впливають один на одного:
import { useState } from 'react';
export default function App() {
return (
<div>
<Counter />
<Counter />
</div>
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
.counter {
width: 100px;
text-align: center;
border: 1px solid gray;
border-radius: 4px;
padding: 20px;
margin: 0 20px 20px 0;
float: left;
}
.hover {
background: #ffffd8;
}
Як бачите, при оновленні одного лічильника оновлюється лише стан для цього компонента:
React зберігатиме стан доти, доки ви рендерите той самий компонент у тій самій позиції у дереві. Щоб побачити це, збільште обидва лічильники, потім видаліть другий компонент, знявши галочку "Відображати другий лічильник", а потім додайте його назад, встановивши її знову:
import { useState } from 'react';
export default function App() {
const [showB, setShowB] = useState(true);
return (
<div>
<Counter />
{showB && <Counter />}
<label>
<input
type="checkbox"
checked={showB}
onChange={e => {
setShowB(e.target.checked)
}}
/>
Render the second counter
</label>
</div>
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
label {
display: block;
clear: both;
}
.counter {
width: 100px;
text-align: center;
border: 1px solid gray;
border-radius: 4px;
padding: 20px;
margin: 0 20px 20px 0;
float: left;
}
.hover {
background: #ffffd8;
}
Помітьте, як тільки ви припиняєте рендеринг другого лічильника, його стан повністю зникає. Це тому, що коли React видаляє компонент, він знищує його стан.
Якщо поставити галочку "Зобразити другий лічильник", другий Counter
та його стан ініціалізується з нуля (score = 0
) і додається до DOM.
React зберігає стан компонента доти, доки він рендериться у своїй позиції у дереві інтерфейсу користувача. Якщо його видалено або на тому ж місці рендериться інший компонент, React відкидає його стан.
Той самий компонент у тій самій позиції зберігає стан
У цьому прикладі є два різні теги <Counter />
:
import { useState } from 'react';
export default function App() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy ? (
<Counter isFancy={true} />
) : (
<Counter isFancy={false} />
)}
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
Use fancy styling
</label>
</div>
);
}
function Counter({ isFancy }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
if (isFancy) {
className += ' fancy';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
label {
display: block;
clear: both;
}
.counter {
width: 100px;
text-align: center;
border: 1px solid gray;
border-radius: 4px;
padding: 20px;
margin: 0 20px 20px 0;
float: left;
}
.fancy {
border: 5px solid gold;
color: #ff6767;
}
.hover {
background: #ffffd8;
}
Коли ви встановлюєте або знімаєте прапорець, стан лічильника не скидається. Незалежно від того, чи isFancy
є true
, чи false
, у вас завжди є <Counter />
як перший нащадок div
, повернутий з кореневого App
компонента:
Це той самий компонент у тій самій позиції, тому з точки зору React це той самий лічильник.
Пам'ятайте, що для React важлива позиція в дереві інтерфейсу користувача, а не в розмітці JSX! Цей компонент має два повернутих речення
з різними <Counter />
JSX-тегами всередині та зовні if
:
import { useState } from 'react';
export default function App() {
const [isFancy, setIsFancy] = useState(false);
if (isFancy) {
return (
<div>
<Counter isFancy={true} />
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
Use fancy styling
</label>
</div>
);
}
return (
<div>
<Counter isFancy={false} />
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
Use fancy styling
</label>
</div>
);
}
function Counter({ isFancy }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
if (isFancy) {
className += ' fancy';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
label {
display: block;
clear: both;
}
.counter {
width: 100px;
text-align: center;
border: 1px solid gray;
border-radius: 4px;
padding: 20px;
margin: 0 20px 20px 0;
float: left;
}
.fancy {
border: 5px solid gold;
color: #ff6767;
}
.hover {
background: #ffffd8;
}
Ви можете очікувати, що стан буде скинуто після встановлення прапорця, але цього не станеться! Це тому, що обидва ці теги <Counter />
відображаються в одній позиції. React не знає, де ви розміщуєте умови у вашій функції. Все, що він "бачить" - це дерево, яке ви повертаєте.
В обох випадках компонент App
повертає <div>
з <Counter />
як першим нащадком. Для React ці два лічильники мають однакову "адресу": перший нащадок першого нащадка кореня. Саме так React зіставляє їх між попереднім і наступним рендерингом, незалежно від того, як ви структуруєте свою логіку.
Різні компоненти у тому самому стані скидання позиції
У цьому прикладі встановлення прапорця замінить <Counter>
на <p>
:
import { useState } from 'react';
export default function App() {
const [isPaused, setIsPaused] = useState(false);
return (
<div>
{isPaused ? (
<p>See you later!</p>
) : (
<Counter />
)}
<label>
<input
type="checkbox"
checked={isPaused}
onChange={e => {
setIsPaused(e.target.checked)
}}
/>
Take a break
</label>
</div>
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
label {
display: block;
clear: both;
}
.counter {
width: 100px;
text-align: center;
border: 1px solid gray;
border-radius: 4px;
padding: 20px;
margin: 0 20px 20px 0;
float: left;
}
.hover {
background: #ffffd8;
}
Тут ви перемикаєтеся між різними типами компонентів у тій самій позиції. Спочатку перший нащадок <div>
містив Counter
. Але коли ви поміняли місцями p
, React видалив Counter
з дерева інтерфейсу користувача і знищив його стан.
Крім того, коли ви рендерите інший компонент у тій самій позиції, він скидає стан усього свого піддерева. Щоб побачити, як це працює, збільште лічильник, а потім поставте галочку:
import { useState } from 'react';
export default function App() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy ? (
<div>
<Counter isFancy={true} />
</div>
) : (
<section>
<Counter isFancy={false} />
</section>
)}
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
Use fancy styling
</label>
</div>
);
}
function Counter({ isFancy }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
if (isFancy) {
className += ' fancy';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
label {
display: block;
clear: both;
}
.counter {
width: 100px;
text-align: center;
border: 1px solid gray;
border-radius: 4px;
padding: 20px;
margin: 0 20px 20px 0;
float: left;
}
.fancy {
border: 5px solid gold;
color: #ff6767;
}
.hover {
background: #ffffd8;
}
Стан лічильника скидається при натисканні прапорця. Хоча ви рендерите Counter
, перший нащадок div
змінюється з div
на section
. Коли дочірнє div
було вилучено з DOM, все дерево під ним (включно з Counter
та його станом) також було знищено.
Як правило, якщо ви хочете зберегти стан між повторними рендерингами, структура вашого дерева повинна "збігатися" від одного рендерингу до іншого. Якщо структура відрізняється, стан буде знищено, оскільки React знищує стан, коли видаляє компонент з дерева.
Ось чому не слід вкладати визначення функцій компонентів.
Тут визначено функцію компонента MyTextField
всередині MyComponent
:
import { useState } from 'react';
export default function MyComponent() {
const [counter, setCounter] = useState(0);
function MyTextField() {
const [text, setText] = useState('');
return (
<input
value={text}
onChange={e => setText(e.target.value)}
/>
);
}
return (
<>
<MyTextField />
<button onClick={() => {
setCounter(counter + 1)
}}>Clicked {counter} times</button>
</>
);
}
Кожного разу, коли ви натискаєте кнопку, стан вводу зникає! Це відбувається тому, що для кожного рендерингу different MyTextField
створюється функція MyComponent
. Ви рендерите компонент different у тій самій позиції, тому React скидає всі стани нижче. Це призводить до багів та проблем з продуктивністю. Щоб уникнути цієї проблеми, завжди оголошуйте функції компонентів на верхньому рівні і не вкладайте їх визначення.
Скидання стану на ту саму позицію
За замовчуванням React зберігає стан компонента, поки він залишається в тій самій позиції. Зазвичай, це саме те, що вам потрібно, тому це має сенс як поведінка за замовчуванням. Але іноді вам може знадобитися скинути стан компонента. Розглянемо програму, яка дозволяє двом гравцям стежити за своїми очками під час кожного ходу:
import { useState } from 'react';
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA ? (
<Counter person="Taylor" />
) : (
<Counter person="Sarah" />
)}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
Next player!
</button>
</div>
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{person}'s score: {score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
h1 {
font-size: 18px;
}
.counter {
width: 100px;
text-align: center;
border: 1px solid gray;
border-radius: 4px;
padding: 20px;
margin: 0 20px 20px 0;
}
.hover {
background: #ffffd8;
}
Наразі, коли ви змінюєте програвач, рахунок зберігається. Два Counter
з'являються у тій самій позиції, тому React вважає їх тим самим Counter
, чий проп
person було змінено.
Але концептуально, у цьому застосунку вони мають бути двома окремими лічильниками. Вони можуть з'являтися в одному місці інтерфейсу, але один з них є лічильником для Тейлора, а інший - для Сари.
Існує два способи скидання стану при перемиканні між ними:
- Відображати компоненти у різних позиціях
- Надайте кожному компоненту явну ідентичність з
key
Варіант 1: Відображення компонента у різних позиціях
Якщо ви хочете, щоб ці два Counter
були незалежними, ви можете відрендерити їх у двох різних позиціях:
import { useState } from 'react';
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA &&
<Counter person="Taylor" />
}
{!isPlayerA &&
<Counter person="Sarah" />
}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
Next player!
</button>
</div>
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{person}'s score: {score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
h1 {
font-size: 18px;
}
.counter {
width: 100px;
text-align: center;
border: 1px solid gray;
border-radius: 4px;
padding: 20px;
margin: 0 20px 20px 0;
}
.hover {
background: #ffffd8;
}
- Спочатку
isPlayerA
єtrue
. Отже, перша позиція містить станCounter
, а друга порожня. - При натисканні кнопки "Наступний гравець" перша позиція очищується, але друга тепер містить
Counter
.
Стани кожного Counter
знищуються кожного разу, коли вони видаляються з DOM. Ось чому вони скидаються кожного разу, коли ви натискаєте кнопку.
Це рішення зручне, коли ви маєте лише кілька незалежних компонентів, що рендеряться в одному місці. У цьому прикладі таких компонентів лише два, тому немає потреби рендерити їх окремо у JSX.
Варіант 2: Скидання стану за допомогою ключа
Існує також інший, більш загальний спосіб скидання стану компонента.
Ви могли бачити ключі
під час відображення списків. Ключі не тільки для списків! Ви можете використовувати ключі, щоб змусити React розрізняти будь-які компоненти. За замовчуванням, React використовує порядок всередині батька ("перший лічильник", "другий лічильник") для розрізнення компонентів. Але ключі дозволяють вказати React, що це не просто first або second лічильник , а конкретний лічильник - наприклад, Taylor's. Таким чином, React знатиме лічильник Taylor's, де б він не з'явився в дереві!
У цьому прикладі два <Counter />
не мають спільного стану, хоча вони з'являються в одному місці у JSX:
import { useState } from 'react';
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
Next player!
</button>
</div>
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{person}'s score: {score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
h1 {
font-size: 18px;
}
.counter {
width: 100px;
text-align: center;
border: 1px solid gray;
border-radius: 4px;
padding: 20px;
margin: 0 20px 20px 0;
}
.hover {
background: #ffffd8;
}
Перемикання між Тейлором та Сарою не зберігає стан. Це тому, що ви дали їм різні ключі
:
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
Вказівка key
вказує React використовувати сам key
як частину позиції, а не їхній порядок всередині батька. Ось чому, навіть якщо ви відображаєте їх в одному місці в JSX, React бачить їх як два різних лічильника, і тому вони ніколи не матимуть спільного стану. Кожного разу, коли лічильник з'являється на екрані, створюється його стан. Кожного разу, коли він видаляється, його стан знищується. Перемикання між ними скидає їхній стан знову і знову.
Пам'ятайте, що ключі не є глобально унікальними. Вони лише визначають позицію всередині батьківського .
Скидання форми за допомогою ключа
Скидання стану за допомогою ключа особливо корисне при роботі з формами
У цьому застосунку чату компонент <Chat>
містить стан введення тексту:
import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';
export default function Messenger() {
const [to, setTo] = useState(contacts[0]);
return (
<div>
<ContactList
contacts={contacts}
selectedContact={to}
onSelect={contact => setTo(contact)}
/>
<Chat contact={to} />
</div>
)
}
const contacts = [
{ id: 0, name: 'Taylor', email: '[email protected]' },
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob', email: '[email protected]' }
];
export default function ContactList({
selectedContact,
contacts,
onSelect
}) {
return (
<section className="contact-list">
<ul>
{contacts.map(contact =>
<li key={contact.id}>
<button onClick={() => {
onSelect(contact);
}}>
{contact.name}
</button>
</li>
)}
</ul>
</section>
);
}
import { useState } from 'react';
export default function Chat({ contact }) {
const [text, setText] = useState('');
return (
<section className="chat">
<textarea
value={text}
placeholder={'Chat to ' + contact.name}
onChange={e => setText(e.target.value)}
/>
<br />
<button>Send to {contact.email}</button>
</section>
);
}
.chat, .contact-list {
float: left;
margin-bottom: 20px;
}
ul, li {
list-style: none;
margin: 0;
padding: 0;
}
li button {
width: 100px;
padding: 10px;
margin-right: 10px;
}
textarea {
height: 150px;
}
Спробуйте ввести що-небудь на вході, а потім натисніть "Аліса" або "Боб", щоб обрати іншого одержувача. Ви помітите, що стан введення збережено, оскільки <Chat>
буде показано у тій самій позиції у дереві.
У багатьох програмах це може бути бажаною поведінкою, але не у програмі чату! Ви не хочете, щоб користувач надсилав повідомлення, яке він уже набрав, не тій особі через випадкове клацання. Щоб виправити це, додайте ключ :
:
<Chat key={to.id} contact={to} />
Це гарантує, що при виборі іншого одержувача компонент Chat
буде відтворено з нуля, включаючи будь-який стан у дереві під ним. React також заново створить DOM-елементи замість того, щоб використовувати їх повторно.
Тепер перемикання отримувача завжди очищає текстове поле:
import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';
export default function Messenger() {
const [to, setTo] = useState(contacts[0]);
return (
<div>
<ContactList
contacts={contacts}
selectedContact={to}
onSelect={contact => setTo(contact)}
/>
<Chat key={to.id} contact={to} />
</div>
)
}
const contacts = [
{ id: 0, name: 'Taylor', email: '[email protected]' },
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob', email: '[email protected]' }
];
export default function ContactList({
selectedContact,
contacts,
onSelect
}) {
return (
<section className="contact-list">
<ul>
{contacts.map(contact =>
<li key={contact.id}>
<button onClick={() => {
onSelect(contact);
}}>
{contact.name}
</button>
</li>
)}
</ul>
</section>
);
}
import { useState } from 'react';
export default function Chat({ contact }) {
const [text, setText] = useState('');
return (
<section className="chat">
<textarea
value={text}
placeholder={'Chat to ' + contact.name}
onChange={e => setText(e.target.value)}
/>
<br />
<button>Send to {contact.email}</button>
</section>
);
}
.chat, .contact-list {
float: left;
margin-bottom: 20px;
}
ul, li {
list-style: none;
margin: 0;
padding: 0;
}
li button {
width: 100px;
padding: 10px;
margin-right: 10px;
}
textarea {
height: 150px;
}
Збереження стану для вилучених компонентів
У реальній програмі чату ви, ймовірно, захочете відновити стан введення, коли користувач знову вибере попереднього отримувача. Існує декілька способів зберегти стан "живим" для компонента, який більше не відображається:
- Ви могли б відобразити усі чати, а не лише поточний, але приховати всі інші за допомогою CSS. Чати не будуть видалені з дерева, тому їх локальний стан буде збережено. Це рішення чудово працює для простих інтерфейсів. Але воно може бути дуже повільним, якщо приховані дерева великі і містять багато DOM-вузлів.
- Ви можете підняти стан догори і утримувати очікуване повідомлення для кожного отримувача у батьківському компоненті. Таким чином, коли дочірні компоненти буде видалено, це не матиме значення, оскільки саме батьківський компонент зберігає важливу інформацію. Це найпоширеніше рішення.
- Ви також можете використовувати інше джерело для застосунку до React-стану. Наприклад, ви, ймовірно, хочете, щоб чернетка повідомлення зберігалася, навіть якщо користувач випадково закрили сторінку. Щоб реалізувати це, ви можете використовувати компонент
Chat
, який ініціалізує свій стан читанням зlocalStorage
, і зберігати чернетки там же.
Незалежно від обраної стратегії, чат з Алісою концептуально відрізняється від чату з Бобом, тому має сенс надавати ключ
до дерева <Chat>
на основі поточного адресата.
- React зберігає стан доти, доки той самий компонент рендериться у тій самій позиції.
- Стан не зберігається у тегах JSX. Він пов'язаний з позицією в дереві, в яку ви помістили цей JSX.
- Ви можете змусити піддерево скинути свій стан, надавши йому інший ключ.
- Не вкладайте визначення компонентів, інакше ви випадково скинете стан.
Виправлено зникнення вхідного тексту
У цьому прикладі показано повідомлення при натисканні кнопки. Однак, натискання кнопки також випадково скидає вхідні дані. Чому це відбувається? Виправте це так, щоб натискання кнопки не призводило до скидання введеного тексту.
import { useState } from 'react';
export default function App() {
const [showHint, setShowHint] = useState(false);
if (showHint) {
return (
<div>
<p><i>Hint: Your favorite city?</i></p>
<Form />
<button onClick={() => {
setShowHint(false);
}}>Hide hint</button>
</div>
);
}
return (
<div>
<Form />
<button onClick={() => {
setShowHint(true);
}}>Show hint</button>
</div>
);
}
function Form() {
const [text, setText] = useState('');
return (
<textarea
value={text}
onChange={e => setText(e.target.value)}
/>
);
}
textarea { display: block; margin: 10px 0; }
Проблема у тому, що Form
відображається у різних позиціях. У гілці if
він є другим нащадком <div>
, а у гілці else
- першим. Тому тип компонента у кожній позиції змінюється. Перша позиція змінюється між утриманням p
і Form
, тоді як друга позиція змінюється між утриманням Form
і button
. React скидає стан при кожній зміні типу компонента.
Найпростіше рішення - уніфікувати гілки так, щоб Form
завжди рендерився в одній позиції:
import { useState } from 'react';
export default function App() {
const [showHint, setShowHint] = useState(false);
return (
<div>
{showHint &&
<p><i>Hint: Your favorite city?</i></p>
}
<Form />
{showHint ? (
<button onClick={() => {
setShowHint(false);
}}>Hide hint</button>
) : (
<button onClick={() => {
setShowHint(true);
}}>Show hint</button>
)}
</div>
);
}
function Form() {
const [text, setText] = useState('');
return (
<textarea
value={text}
onChange={e => setText(e.target.value)}
/>
);
}
textarea { display: block; margin: 10px 0; }
Технічно, ви також можете додати null
перед <Form />
у гілці else
для відповідності структурі гілки if
:
import { useState } from 'react';
export default function App() {
const [showHint, setShowHint] = useState(false);
if (showHint) {
return (
<div>
<p><i>Hint: Your favorite city?</i></p>
<Form />
<button onClick={() => {
setShowHint(false);
}}>Hide hint</button>
</div>
);
}
return (
<div>
{null}
<Form />
<button onClick={() => {
setShowHint(true);
}}>Show hint</button>
</div>
);
}
function Form() {
const [text, setText] = useState('');
return (
<textarea
value={text}
onChange={e => setText(e.target.value)}
/>
);
}
textarea { display: block; margin: 10px 0; }
Таким чином, Form
завжди є другим дочірнім елементом, тому він залишається у тій же позиції і зберігає свій стан. Але такий підхід є менш очевидним і створює ризик того, що хтось інший видалить цей null
.
Поміняти місцями два поля форми
Ця форма дозволяє вам ввести ім'я та прізвище. У ній також є прапорець, який визначає, яке поле буде першим. Коли ви встановите прапорець, поле "Прізвище" з'явиться перед полем "Ім'я".
Це майже працює, але є помилка. Якщо ви заповните поле "Ім'я" і поставите галочку, текст залишиться у першому полі (яке зараз є "Прізвище"). Виправте це так, щоб текст введення такожпереміщався, коли ви змінюєте порядок.
Здається, що для цих полів недостатньо їх позиції в межах батьківського. Чи можна якось сказати React, як узгодити стан між повторними рендерингами?
import { useState } from 'react';
export default function App() {
const [reverse, setReverse] = useState(false);
let checkbox = (
<label>
<input
type="checkbox"
checked={reverse}
onChange={e => setReverse(e.target.checked)}
/>
Reverse order
</label>
);
if (reverse) {
return (
<>
<Field label="Last name" />
<Field label="First name" />
{checkbox}
</>
);
} else {
return (
<>
<Field label="First name" />
<Field label="Last name" />
{checkbox}
</>
);
}
}
function Field({ label }) {
const [text, setText] = useState('');
return (
<label>
{label}:{' '}
<input
type="text"
value={text}
placeholder={label}
onChange={e => setText(e.target.value)}
/>
</label>
);
}
label { display: block; margin: 10px 0; }
Дайте ключ
обом компонентам <Field>
в обох гілках if
та else
. Це вказує React, як "підібрати" правильний стан для будь-якої з гілок <Field>
, навіть якщо їхній порядок у батьківській гілці змінюється:
import { useState } from 'react';
export default function App() {
const [reverse, setReverse] = useState(false);
let checkbox = (
<label>
<input
type="checkbox"
checked={reverse}
onChange={e => setReverse(e.target.checked)}
/>
Reverse order
</label>
);
if (reverse) {
return (
<>
<Field key="lastName" label="Last name" />
<Field key="firstName" label="First name" />
{checkbox}
</>
);
} else {
return (
<>
<Field key="firstName" label="First name" />
<Field key="lastName" label="Last name" />
{checkbox}
</>
);
}
}
function Field({ label }) {
const [text, setText] = useState('');
return (
<label>
{label}:{' '}
<input
type="text"
value={text}
placeholder={label}
onChange={e => setText(e.target.value)}
/>
</label>
);
}
label { display: block; margin: 10px 0; }
Скинути форму деталізації
Це список контактів, який можна редагувати. Ви можете відредагувати дані вибраного контакту, а потім або натиснути "Зберегти", щоб оновити їх, або "Скинути", щоб скасувати зміни.
При виборі іншого контакту (наприклад, Аліса) стан оновлюється, але форма продовжує показувати дані попереднього контакту. Виправте це так, щоб форма перезавантажувалася при зміні вибраного контакту.
import { useState } from 'react';
import ContactList from './ContactList.js';
import EditContact from './EditContact.js';
export default function ContactManager() {
const [
contacts,
setContacts
] = useState(initialContacts);
const [
selectedId,
setSelectedId
] = useState(0);
const selectedContact = contacts.find(c =>
c.id === selectedId
);
function handleSave(updatedData) {
const nextContacts = contacts.map(c => {
if (c.id === updatedData.id) {
return updatedData;
} else {
return c;
}
});
setContacts(nextContacts);
}
return (
<div>
<ContactList
contacts={contacts}
selectedId={selectedId}
onSelect={id => setSelectedId(id)}
/>
<hr />
<EditContact
initialData={selectedContact}
onSave={handleSave}
/>
</div>
)
}
const initialContacts = [
{ id: 0, name: 'Taylor', email: '[email protected]' },
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob', email: '[email protected]' }
];
export default function ContactList({
contacts,
selectedId,
onSelect
}) {
return (
<section>
<ul>
{contacts.map(contact =>
<li key={contact.id}>
<button onClick={() => {
onSelect(contact.id);
}}>
{contact.id === selectedId ?
<b>{contact.name}</b> :
contact.name
}
</button>
</li>
)}
</ul>
</section>
);
}
import { useState } from 'react';
export default function EditContact({ initialData, onSave }) {
const [name, setName] = useState(initialData.name);
const [email, setEmail] = useState(initialData.email);
return (
<section>
<label>
Name:{' '}
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
</label>
<label>
Email:{' '}
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</label>
<button onClick={() => {
const updatedData = {
id: initialData.id,
name: name,
email: email
};
onSave(updatedData);
}}>
Save
</button>
<button onClick={() => {
setName(initialData.name);
setEmail(initialData.email);
}}>
Reset
</button>
</section>
);
}
ul, li {
list-style: none;
margin: 0;
padding: 0;
}
li { display: inline-block; }
li button {
padding: 10px;
}
label {
display: block;
margin: 10px 0;
}
button {
margin-right: 10px;
margin-bottom: 10px;
}
Передайте key={selectedId}
компоненту EditContact
. Таким чином, перемикання між різними контактами призведе до скидання форми:
import { useState } from 'react';
import ContactList from './ContactList.js';
import EditContact from './EditContact.js';
export default function ContactManager() {
const [
contacts,
setContacts
] = useState(initialContacts);
const [
selectedId,
setSelectedId
] = useState(0);
const selectedContact = contacts.find(c =>
c.id === selectedId
);
function handleSave(updatedData) {
const nextContacts = contacts.map(c => {
if (c.id === updatedData.id) {
return updatedData;
} else {
return c;
}
});
setContacts(nextContacts);
}
return (
<div>
<ContactList
contacts={contacts}
selectedId={selectedId}
onSelect={id => setSelectedId(id)}
/>
<hr />
<EditContact
key={selectedId}
initialData={selectedContact}
onSave={handleSave}
/>
</div>
)
}
const initialContacts = [
{ id: 0, name: 'Taylor', email: '[email protected]' },
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob', email: '[email protected]' }
];
export default function ContactList({
contacts,
selectedId,
onSelect
}) {
return (
<section>
<ul>
{contacts.map(contact =>
<li key={contact.id}>
<button onClick={() => {
onSelect(contact.id);
}}>
{contact.id === selectedId ?
<b>{contact.name}</b> :
contact.name
}
</button>
</li>
)}
</ul>
</section>
);
}
import { useState } from 'react';
export default function EditContact({ initialData, onSave }) {
const [name, setName] = useState(initialData.name);
const [email, setEmail] = useState(initialData.email);
return (
<section>
<label>
Name:{' '}
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
</label>
<label>
Email:{' '}
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</label>
<button onClick={() => {
const updatedData = {
id: initialData.id,
name: name,
email: email
};
onSave(updatedData);
}}>
Save
</button>
<button onClick={() => {
setName(initialData.name);
setEmail(initialData.email);
}}>
Reset
</button>
</section>
);
}
ul, li {
list-style: none;
margin: 0;
padding: 0;
}
li { display: inline-block; }
li button {
padding: 10px;
}
label {
display: block;
margin: 10px 0;
}
button {
margin-right: 10px;
margin-bottom: 10px;
}
Очистити зображення під час його завантаження
При натисканні кнопки "Далі" браузер почне завантажувати наступне зображення. Однак, оскільки воно відображається у тому ж тегу <img>
, за замовчуванням ви бачитимете попереднє зображення, доки не завантажиться наступне. Це може бути небажано, якщо вам важливо, щоб текст завжди відповідав зображенню. Змініть це так, щоб при натисканні кнопки "Далі" попереднє зображення одразу зникало.
Чи можна змусити React перестворювати DOM замість того, щоб використовувати його повторно?
import { useState } from 'react';
export default function Gallery() {
const [index, setIndex] = useState(0);
const hasNext = index < images.length - 1;
function handleClick() {
if (hasNext) {
setIndex(index + 1);
} else {
setIndex(0);
}
}
let image = images[index];
return (
<>
<button onClick={handleClick}>
Next
</button>
<h3>
Image {index + 1} of {images.length}
</h3>
<img src={image.src} />
<p>
{image.place}
</p>
</>
);
}
let images = [{
place: 'Penang, Malaysia',
src: 'https://i.imgur.com/FJeJR8M.jpg'
}, {
place: 'Lisbon, Portugal',
src: 'https://i.imgur.com/dB2LRbj.jpg'
}, {
place: 'Bilbao, Spain',
src: 'https://i.imgur.com/z08o2TS.jpg'
}, {
place: 'Valparaíso, Chile',
src: 'https://i.imgur.com/Y3utgTi.jpg'
}, {
place: 'Schwyz, Switzerland',
src: 'https://i.imgur.com/JBbMpWY.jpg'
}, {
place: 'Prague, Czechia',
src: 'https://i.imgur.com/QwUKKmF.jpg'
}, {
place: 'Ljubljana, Slovenia',
src: 'https://i.imgur.com/3aIiwfm.jpg'
}];
img { width: 150px; height: 150px; }
Ви можете вказати ключ
до тегу <img>
. Коли цей key
змінюється, React повторно створює DOM-вузол <img>
з нуля. Це спричиняє короткий спалах при завантаженні кожного зображення, тому це не те, що ви хотіли б робити для кожного зображення у вашому застосунку. Але це має сенс, якщо ви хочете, щоб зображення завжди відповідало тексту.
import { useState } from 'react';
export default function Gallery() {
const [index, setIndex] = useState(0);
const hasNext = index < images.length - 1;
function handleClick() {
if (hasNext) {
setIndex(index + 1);
} else {
setIndex(0);
}
}
let image = images[index];
return (
<>
<button onClick={handleClick}>
Next
</button>
<h3>
Image {index + 1} of {images.length}
</h3>
<img key={image.src} src={image.src} />
<p>
{image.place}
</p>
</>
);
}
let images = [{
place: 'Penang, Malaysia',
src: 'https://i.imgur.com/FJeJR8M.jpg'
}, {
place: 'Lisbon, Portugal',
src: 'https://i.imgur.com/dB2LRbj.jpg'
}, {
place: 'Bilbao, Spain',
src: 'https://i.imgur.com/z08o2TS.jpg'
}, {
place: 'Valparaíso, Chile',
src: 'https://i.imgur.com/Y3utgTi.jpg'
}, {
place: 'Schwyz, Switzerland',
src: 'https://i.imgur.com/JBbMpWY.jpg'
}, {
place: 'Prague, Czechia',
src: 'https://i.imgur.com/QwUKKmF.jpg'
}, {
place: 'Ljubljana, Slovenia',
src: 'https://i.imgur.com/3aIiwfm.jpg'
}];
img { width: 150px; height: 150px; }
Виправлено неправильний стан у списку
У цьому списку кожен Contact
має стан, який визначає, чи було для нього натиснуто кнопку "Показати email". Натисніть "Показати email" для Аліси, а потім встановіть прапорець "Показувати у зворотному порядку". Ви помітите, що саме лист Тейлор розгорнуто, а лист Аліси, який перемістився вниз, виглядає згорнутим.
Виправте це так, щоб розгорнутий стан був пов'язаний з кожним контактом, незалежно від обраного впорядкування.
import { useState } from 'react';
import Contact from './Contact.js';
export default function ContactList() {
const [reverse, setReverse] = useState(false);
const displayedContacts = [...contacts];
if (reverse) {
displayedContacts.reverse();
}
return (
<>
<label>
<input
type="checkbox"
value={reverse}
onChange={e => {
setReverse(e.target.checked)
}}
/>{' '}
Show in reverse order
</label>
<ul>
{displayedContacts.map((contact, i) =>
<li key={i}>
<Contact contact={contact} />
</li>
)}
</ul>
</>
);
}
const contacts = [
{ id: 0, name: 'Alice', email: '[email protected]' },
{ id: 1, name: 'Bob', email: '[email protected]' },
{ id: 2, name: 'Taylor', email: '[email protected]' }
];
import { useState } from 'react';
export default function Contact({ contact }) {
const [expanded, setExpanded] = useState(false);
return (
<>
<p><b>{contact.name}</b></p>
{expanded &&
<p><i>{contact.email}</i></p>
}
<button onClick={() => {
setExpanded(!expanded);
}}>
{expanded ? 'Hide' : 'Show'} email
</button>
</>
);
}
ul, li {
list-style: none;
margin: 0;
padding: 0;
}
li {
margin-bottom: 20px;
}
label {
display: block;
margin: 10px 0;
}
button {
margin-right: 10px;
margin-bottom: 10px;
}
Проблема у тому, що у цьому прикладі було використано index як ключ
:
{displayedContacts.map((contact, i) =>
<li key={i}>
Однак, ви хочете, щоб стан був пов'язаний з кожним конкретним контактом.
Використання ідентифікатора контакту як ключа
натомість виправляє проблему:
import { useState } from 'react';
import Contact from './Contact.js';
export default function ContactList() {
const [reverse, setReverse] = useState(false);
const displayedContacts = [...contacts];
if (reverse) {
displayedContacts.reverse();
}
return (
<>
<label>
<input
type="checkbox"
value={reverse}
onChange={e => {
setReverse(e.target.checked)
}}
/>{' '}
Show in reverse order
</label>
<ul>
{displayedContacts.map(contact =>
<li key={contact.id}>
<Contact contact={contact} />
</li>
)}
</ul>
</>
);
}
const contacts = [
{ id: 0, name: 'Alice', email: '[email protected]' },
{ id: 1, name: 'Bob', email: '[email protected]' },
{ id: 2, name: 'Taylor', email: '[email protected]' }
];
import { useState } from 'react';
export default function Contact({ contact }) {
const [expanded, setExpanded] = useState(false);
return (
<>
<p><b>{contact.name}</b></p>
{expanded &&
<p><i>{contact.email}</i></p>
}
<button onClick={() => {
setExpanded(!expanded);
}}>
{expanded ? 'Hide' : 'Show'} email
</button>
</>
);
}
ul, li {
list-style: none;
margin: 0;
padding: 0;
}
li {
margin-bottom: 20px;
}
label {
display: block;
margin: 10px 0;
}
button {
margin-right: 10px;
margin-bottom: 10px;
}
Стан пов'язано з позицією у дереві. Ключ
дозволяє вказати іменовану позицію замість того, щоб покладатися на порядок.