상태 구조 선택
잘 구조화된 상태는 수정 및 디버깅이 쉬운 구성 요소와 지속적으로 버그가 있는 구성 요소 간의 차이를 의미할 수 있습니다. 다음은 상태를 구조화할 때 염두에 두어야 할 몇 가지 팁입니다.
학습 콘텐츠
- 단일 및 다중 상태 변수는 언제 사용해야 합니까?
- 상태를 만들 때 피해야 할 사항
- 상태 구조의 일반적인 문제를 해결하는 방법
국가 규제 원칙
특정 상태를 유지하는 구성 요소를 작성할 때 사용할 상태 변수 수와 원하는 데이터 유형을 선택해야 합니다. 최적이 아닌 상태 구조로 좋은 프로그램을 작성할 수 있지만 더 나은 결정을 내리는 데 도움이 되는 몇 가지 원칙이 있습니다.
- 그룹 관련 상태. 항상 두 개 이상의 상태 변수를 동시에 업데이트하는 경우 하나의 상태 변수로 병합하는 것이 좋습니다.
- 상태 불일치를 피하십시오. 여러 상태가 서로 모순되거나 “일치하지 않는” 방식으로 상태가 구성되면 오류의 여지가 있습니다. 이것을 피하십시오.
- 복사(대체 또는 불필요) 조건을 피하십시오. 렌더링 중에 구성 요소의 props 또는 기존 상태 변수에서 일부 정보를 계산할 수 있는 경우 해당 정보를 해당 구성 요소의 상태에 포함해서는 안 됩니다.
- 상태(복사본)를 복제하지 마십시오. 동일한 데이터가 여러 상태 변수 간에 또는 중첩된 개체 내에 중복되면 동기화를 유지하기가 어려워집니다. 가능한 경우 중복을 줄입니다.
- 깊게 중첩된 상태를 피하십시오. 깊이 계층화된 상태는 업데이트하기가 쉽지 않습니다. 가능할 때마다 상태를 평평하게 만드는 것이 좋습니다.
이러한 원칙의 목표는 상태를 쉽고 오류 없이 업데이트하는 것입니다. 상태에서 중복 및 중복 데이터를 제거하면 모든 데이터를 동기화 상태로 유지하는 데 도움이 됩니다. 이는 데이터베이스 엔지니어가 오류 발생 가능성을 줄이기 위해 데이터베이스 구조를 “정규화”하는 방식과 유사합니다. 알베르트 아인슈타인의 말: “국가를 가능한 한 단순하게 만드되 그보다 더 단순하게 만들지 말라.”
이제 이러한 원칙이 실제로 어떻게 적용되는지 살펴보겠습니다.
그룹 상태
단일 상태 변수를 사용할지 아니면 여러 상태 변수를 사용할지 결정하기 어려운 경우가 있습니다.
그렇게 해야 하나요?
const (x, setX) = useState(0);
const (y, setY) = useState(0);
그 쯤?
const (position, setPosition) = useState({ x: 0, y: 0 });
기술적으로는 이 두 가지 접근 방식 중 하나를 사용할 수 있습니다. 그러나 두 개의 상태 변수가 지속적으로 함께 변경되는 경우 하나의 상태 변수로 통합하는 것이 좋습니다. 그런 다음 커서를 움직이면 빨간 점의 모든 좌표가 업데이트되는 이 예에서와 같이 항상 동기화 상태를 유지해야 한다는 것을 기억할 수 있습니다.
import { useState } from 'react';
export default function MovingDot() {
const (position, setPosition) = useState({
x: 0,
y: 0
});
return (
<div
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}
style={{
position: 'relative',
width: '100vw',
height: '100vh',
}}>
<div style={{
position: 'absolute',
backgroundColor: 'red',
borderRadius: '50%',
transform: `translate(${position.x}px, ${position.y}px)`,
left: -10,
top: -10,
width: 20,
height: 20,
}} />
</div>
)
}
데이터를 개체 또는 배열로 그룹화하는 또 다른 경우는 필요한 다양한 상태의 수를 모를 때입니다. 예를 들어 사용자가 사용자 지정 필드를 추가할 수 있는 양식이 있는 경우에 유용합니다.
잡다
상태 변수가 개체인 경우 다른 필드를 명시적으로 복사하지 않고는 한 필드만 업데이트할 수 없습니다. 예를 들어 위의 예에서는 y 속성이 전혀 없기 때문에 setPosition({ x: 100 })을 수행할 수 없습니다! 대신 x를 설정하려는 경우 두 개의 상태 변수로 분할하여 setPosition({ …position, x: 100 }) 또는 setX(100)를 실행해야 합니다.
충돌 상태 방지
다음은 isSending 및 isSent 상태 변수가 포함된 호텔 피드백 양식입니다.
import { useState } from 'react';
export default function FeedbackForm() {
const (text, setText) = useState('');
const (isSending, setIsSending) = useState(false);
const (isSent, setIsSent) = useState(false);
async function handleSubmit(e) {
e.preventDefault();
setIsSending(true);
await sendMessage(text);
setIsSending(false);
setIsSent(true);
}
if (isSent) {
return <h1>Thanks for feedback!</h1>
}
return (
<form onSubmit={handleSubmit}>
<p>How was your stay at The Prancing Pony?</p>
<textarea
disabled={isSending}
value={text}
onChange={e => setText(e.target.value)}
/>
<br />
<button
disabled={isSending}
type="submit"
>
Send
</button>
{isSending && <p>Sending...</p>}
</form>
);
}
// Pretend to send a message.
function sendMessage(text) {
return new Promise(resolve => {
setTimeout(resolve, 2000);
});
}
이 코드는 작동하지만 문을 “불가능한” 상태로 열어 둡니다. 예를 들어 setIsSent와 setIsSending을 함께 호출하는 것을 잊은 경우 isSending과 isSent가 동시에 true일 수 있습니다. 구성 요소가 복잡할수록 무슨 일이 일어났는지 파악하기가 더 어렵습니다.
isSending과 isSent가 동시에 참일 수는 없으므로 ‘들어가는 중'(초기), ‘보내는 중’, ‘보내는 중’의 세 가지 유효한 상태 중 하나를 가질 수 있는 상태 변수로 대체하는 것이 좋습니다.
import { useState } from 'react';
export default function FeedbackForm() {
const (text, setText) = useState('');
const (status, setStatus) = useState('typing');
async function handleSubmit(e) {
e.preventDefault();
setStatus('sending');
await sendMessage(text);
setStatus('sent');
}
const isSending = status === 'sending';
const isSent = status === 'sent';
if (isSent) {
return <h1>Thanks for feedback!</h1>
}
return (
<form onSubmit={handleSubmit}>
<p>How was your stay at The Prancing Pony?</p>
<textarea
disabled={isSending}
value={text}
onChange={e => setText(e.target.value)}
/>
<br />
<button
disabled={isSending}
type="submit"
>
Send
</button>
{isSending && <p>Sending...</p>}
</form>
);
}
// Pretend to send a message.
function sendMessage(text) {
return new Promise(resolve => {
setTimeout(resolve, 2000);
});
}
가독성을 위해 일부 상수를 선언할 수 있습니다.
const isSending = status === 'sending';
const isSent = status === 'sent';
그러나 이들은 상태 변수가 아니므로 서로 동기화되지 않을까 걱정할 필요가 없습니다.
중복 상태 방지
렌더링 중에 구성 요소의 props 또는 기존 상태 변수에서 일부 정보를 계산할 수 있는 경우 해당 정보를 해당 구성 요소의 상태에 배치해서는 안 됩니다.
예를 들어, 이 양식을 고려하십시오. 작동하지만 중복 상태를 찾을 수 있습니까?
import { useState } from 'react';
export default function Form() {
const (firstName, setFirstName) = useState('');
const (lastName, setLastName) = useState('');
const (fullName, setFullName) = useState('');
function handleFirstNameChange(e) {
setFirstName(e.target.value);
setFullName(e.target.value + ' ' + lastName);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
setFullName(firstName + ' ' + e.target.value);
}
return (
<>
<h2>Let’s check you in</h2>
<label>
First name:{' '}
<input
value={firstName}
onChange={handleFirstNameChange}
/>
</label>
<label>
Last name:{' '}
<input
value={lastName}
onChange={handleLastNameChange}
/>
</label>
<p>
Your ticket will be issued to: <b>{fullName}</b>
</p>
</>
);
}
이 양식에는 이름, 성 및 전체 이름의 세 가지 상태 변수가 있습니다. 그러나 fullName은 불필요합니다. 렌더링 중에 언제든지 firstName 및 lastName에서 fullName을 계산할 수 있으므로 상태에서 제거하십시오.
다음과 같이 할 수 있습니다.
import { useState } from 'react';
export default function Form() {
const (firstName, setFirstName) = useState('');
const (lastName, setLastName) = useState('');
const fullName = firstName + ' ' + lastName;
function handleFirstNameChange(e) {
setFirstName(e.target.value);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
}
return (
<>
<h2>Let’s check you in</h2>
<label>
First name:{' '}
<input
value={firstName}
onChange={handleFirstNameChange}
/>
</label>
<label>
Last name:{' '}
<input
value={lastName}
onChange={handleLastNameChange}
/>
</label>
<p>
Your ticket will be issued to: <b>{fullName}</b>
</p>
</>
);
}
전체 이름은 여기에서 상태 변수가 아닙니다. 대신 렌더링 중에 계산됩니다.
const fullName = firstName + ' ' + lastName;
따라서 변경 핸들러는 이를 업데이트하기 위해 특별한 작업을 수행할 필요가 없습니다. setFirstName 또는 setLastName을 호출하면 다시 렌더링이 트리거되고 새 데이터에서 다음 전체 이름이 계산됩니다.
심층 분석 – 소품이 그대로 미러링되지 않음
중복 상태의 일반적인 예는 다음과 같은 코드입니다.
function Message({ messageColor }) {
const (color, setColor) = useState(messageColor);
여기서 색상 상태 변수는 MessageColor 소품으로 초기화됩니다. 문제는 부모 구성 요소가 나중에 다른 값(예: “파란색” 대신 “빨간색”)을 전달하면 색상 상태 변수가 업데이트되지 않는다는 것입니다! 상태는 첫 번째 렌더링 중에만 초기화됩니다.
이 때문에 상태 변수의 일부 소품을 “미러링”하면 혼란이 발생할 수 있습니다. 대신 코드에서 직접 messagecolor 속성을 사용하십시오. 상수를 사용하여 더 짧은 이름 지정:
function Message({ messageColor }) {
const color = messageColor;
이렇게 하면 부모 구성 요소에서 전달된 소품과 동기화되지 않습니다.
“‘미러링’ 소품을 상태로 전환하는 것은 특정 소품에 대한 모든 업데이트를 무시하려는 경우에만 적절합니다. 기본적으로 새 값이 무시된다는 것을 분명히 하기 위해 첫 글자 또는 기본값으로 소품 이름을 시작합니다.
function Message({ initialColor }) {
// The `color` state variable holds the *first* value of `initialColor`.
// Further changes to the `initialColor` prop are ignored.
const (color, setColor) = useState(initialColor);
상태 복제 방지
이 메뉴 목록 구성 요소를 사용하면 여러 여행 간식 중에서 간식을 선택할 수 있습니다.
import { useState } from 'react';
const initialItems = (
{ title: 'pretzels', id: 0 },
{ title: 'crispy seaweed', id: 1 },
{ title: 'granola bar', id: 2 },
);
export default function Menu() {
const (items, setItems) = useState(initialItems);
const (selectedItem, setSelectedItem) = useState(
items(0)
);
return (
<>
<h2>What's your travel snack?</h2>
<ul>
{items.map(item => (
<li key={item.id}>
{item.title}
{' '}
<button onClick={() => {
setSelectedItem(item);
}}>Choose</button>
</li>
))}
</ul>
<p>You picked {selectedItem.title}.</p>
</>
);
}
현재 선택된 항목은 selectedItem 상태 변수에 객체로 저장됩니다. 그러나 이것은 좋지 않습니다. selectedItem의 내용은 항목 목록의 항목 중 하나와 동일한 개체입니다. 즉, 요소 자체에 대한 정보가 두 위치에 복제됩니다.
이것이 왜 문제가 되어야 합니까? 각 요소를 편집 가능하게 만들어 보겠습니다.
import { useState } from 'react';
const initialItems = (
{ title: 'pretzels', id: 0 },
{ title: 'crispy seaweed', id: 1 },
{ title: 'granola bar', id: 2 },
);
export default function Menu() {
const (items, setItems) = useState(initialItems);
const (selectedItem, setSelectedItem) = useState(
items(0)
);
function handleItemChange(id, e) {
setItems(items.map(item => {
if (item.id === id) {
return {
...item,
title: e.target.value,
};
} else {
return item;
}
}));
}
return (
<>
<h2>What's your travel snack?</h2>
<ul>
{items.map((item, index) => (
<li key={item.id}>
<input
value={item.title}
onChange={e => {
handleItemChange(item.id, e)
}}
/>
{' '}
<button onClick={() => {
setSelectedItem(item);
}}>Choose</button>
</li>
))}
</ul>
<p>You picked {selectedItem.title}.</p>
</>
);
}
처음 항목 선택을 클릭한 다음 편집하면 입력 업데이트가 표시되지만 변경 사항이 아래 캡션에 반영되지 않습니다. 상태가 중복되어 선택한 항목을 업데이트하는 것을 잊었기 때문입니다.
선택한 항목을 업데이트할 수도 있지만 중복 항목을 제거하는 것이 더 쉬운 솔루션입니다. 이 예제에서는 selectedItem 객체(항목 내부의 객체로 복제본 생성) 대신 selectedId를 보존한 다음 항목 배열에서 해당 ID를 가진 항목을 검색하여 selectedItem을 가져옵니다.
import { useState } from 'react';
const initialItems = (
{ title: 'pretzels', id: 0 },
{ title: 'crispy seaweed', id: 1 },
{ title: 'granola bar', id: 2 },
);
export default function Menu() {
const (items, setItems) = useState(initialItems);
const (selectedId, setSelectedId) = useState(0);
const selectedItem = items.find(item =>
item.id === selectedId
);
function handleItemChange(id, e) {
setItems(items.map(item => {
if (item.id === id) {
return {
...item,
title: e.target.value,
};
} else {
return item;
}
}));
}
return (
<>
<h2>What's your travel snack?</h2>
<ul>
{items.map((item, index) => (
<li key={item.id}>
<input
value={item.title}
onChange={e => {
handleItemChange(item.id, e)
}}
/>
{' '}
<button onClick={() => {
setSelectedId(item.id);
}}>Choose</button>
</li>
))}
</ul>
<p>You picked {selectedItem.title}.</p>
</>
);
}
(또는 선택한 인덱스를 상태로 유지할 수 있습니다.)
상태는 다음과 같이 복제되었습니다.
- 항목 = ({ id: 0, 제목: ‘브레첼’}, …)
- selectedItem = {id: 0, 제목: ‘프레첼’}
그러나 변경 후의 모습은 다음과 같습니다.
- 항목 = ({ id: 0, 제목: ‘브레첼’}, …)
- 선택된 ID = 0
중복은 사라지고 필요한 상태만 남습니다!
이제 선택한 항목을 편집하면 다음 메시지가 즉시 업데이트됩니다. 이는 setItems가 다시 렌더링을 트리거하고 items.find(…)가 업데이트된 제목이 있는 항목을 찾기 때문입니다. 선택한 ID만 중요하므로 선택한 항목은 상태를 유지할 필요가 없습니다. 나머지는 렌더링 중에 계산할 수 있습니다.
깊게 중첩된 상태 피하기
행성, 대륙 및 국가로 구성된 여행 계획을 상상해 보십시오. 다음 예제와 같이 중첩 개체 및 배열을 사용하여 상태를 구조화할 수 있습니다.
export const initialTravelPlan = {
id: 0,
title: '(Root)',
childPlaces: ({
id: 1,
title: 'Earth',
childPlaces: ({
id: 2,
title: 'Africa',
childPlaces: ({
id: 3,
title: 'Botswana',
childPlaces: ()
}, {
id: 4,
title: 'Egypt',
childPlaces: ()
}, {
id: 5,
title: 'Kenya',
childPlaces: ()
}, {
id: 6,
title: 'Madagascar',
childPlaces: ()
}, {
id: 7,
title: 'Morocco',
childPlaces: ()
}, {
id: 8,
title: 'Nigeria',
childPlaces: ()
}, {
id: 9,
title: 'South Africa',
childPlaces: ()
})
}, {
id: 10,
title: 'Americas',
childPlaces: ({
id: 11,
title: 'Argentina',
childPlaces: ()
}, {
id: 12,
title: 'Brazil',
childPlaces: ()
}, {
id: 13,
title: 'Barbados',
childPlaces: ()
}, {
id: 14,
title: 'Canada',
childPlaces: ()
}, {
id: 15,
title: 'Jamaica',
childPlaces: ()
}, {
id: 16,
title: 'Mexico',
childPlaces: ()
}, {
id: 17,
title: 'Trinidad and Tobago',
childPlaces: ()
}, {
id: 18,
title: 'Venezuela',
childPlaces: ()
})
}, {
id: 19,
title: 'Asia',
childPlaces: ({
id: 20,
title: 'China',
childPlaces: ()
}, {
id: 21,
title: 'Hong Kong',
childPlaces: ()
}, {
id: 22,
title: 'India',
childPlaces: ()
}, {
id: 23,
title: 'Singapore',
childPlaces: ()
}, {
id: 24,
title: 'South Korea',
childPlaces: ()
}, {
id: 25,
title: 'Thailand',
childPlaces: ()
}, {
id: 26,
title: 'Vietnam',
childPlaces: ()
})
}, {
id: 27,
title: 'Europe',
childPlaces: ({
id: 28,
title: 'Croatia',
childPlaces: (),
}, {
id: 29,
title: 'France',
childPlaces: (),
}, {
id: 30,
title: 'Germany',
childPlaces: (),
}, {
id: 31,
title: 'Italy',
childPlaces: (),
}, {
id: 32,
title: 'Portugal',
childPlaces: (),
}, {
id: 33,
title: 'Spain',
childPlaces: (),
}, {
id: 34,
title: 'Turkey',
childPlaces: (),
})
}, {
id: 35,
title: 'Oceania',
childPlaces: ({
id: 36,
title: 'Australia',
childPlaces: (),
}, {
id: 37,
title: 'Bora Bora (French Polynesia)',
childPlaces: (),
}, {
id: 38,
title: 'Easter Island (Chile)',
childPlaces: (),
}, {
id: 39,
title: 'Fiji',
childPlaces: (),
}, {
id: 40,
title: 'Hawaii (the USA)',
childPlaces: (),
}, {
id: 41,
title: 'New Zealand',
childPlaces: (),
}, {
id: 42,
title: 'Vanuatu',
childPlaces: (),
})
})
}, {
id: 43,
title: 'Moon',
childPlaces: ({
id: 44,
title: 'Rheita',
childPlaces: ()
}, {
id: 45,
title: 'Piccolomini',
childPlaces: ()
}, {
id: 46,
title: 'Tycho',
childPlaces: ()
})
}, {
id: 47,
title: 'Mars',
childPlaces: ({
id: 48,
title: 'Corn Town',
childPlaces: ()
}, {
id: 49,
title: 'Green Hill',
childPlaces: ()
})
})
};
이미 방문한 장소를 삭제하는 버튼을 추가하고 싶다고 가정해 보겠습니다. 어떻게 해야 하나요? 중첩된 상태를 업데이트하려면 변경된 부분부터 시작 부분까지 개체의 복사본을 만들어야 합니다. 깊게 중첩된 장소를 삭제하려면 장소 상위 장소의 전체 체인을 복사해야 합니다. 이러한 코드는 매우 장황할 수 있습니다.
상태가 너무 중첩되어 쉽게 업데이트할 수 없는 경우 “플랫”으로 만드는 것이 좋습니다. 다음은 해당 데이터를 재구성할 수 있는 한 가지 방법입니다. 각 장소에 하위 장소의 배열이 있는 트리형 구조 대신 각 장소에 하위 장소 ID의 배열이 포함될 수 있습니다. 그런 다음 각 장소 ID에서 해당 장소로의 매핑을 저장할 수 있습니다.
이 데이터를 재구성하는 것은 데이터베이스 테이블을 보는 것과 유사할 수 있습니다.
export const initialTravelPlan = {
0: {
id: 0,
title: '(Root)',
childIds: (1, 43, 47),
},
1: {
id: 1,
title: 'Earth',
childIds: (2, 10, 19, 27, 35)
},
2: {
id: 2,
title: 'Africa',
childIds: (3, 4, 5, 6 , 7, 8, 9)
},
3: {
id: 3,
title: 'Botswana',
childIds: ()
},
4: {
id: 4,
title: 'Egypt',
childIds: ()
},
5: {
id: 5,
title: 'Kenya',
childIds: ()
},
6: {
id: 6,
title: 'Madagascar',
childIds: ()
},
7: {
id: 7,
title: 'Morocco',
childIds: ()
},
8: {
id: 8,
title: 'Nigeria',
childIds: ()
},
9: {
id: 9,
title: 'South Africa',
childIds: ()
},
10: {
id: 10,
title: 'Americas',
childIds: (11, 12, 13, 14, 15, 16, 17, 18),
},
11: {
id: 11,
title: 'Argentina',
childIds: ()
},
12: {
id: 12,
title: 'Brazil',
childIds: ()
},
13: {
id: 13,
title: 'Barbados',
childIds: ()
},
14: {
id: 14,
title: 'Canada',
childIds: ()
},
15: {
id: 15,
title: 'Jamaica',
childIds: ()
},
16: {
id: 16,
title: 'Mexico',
childIds: ()
},
17: {
id: 17,
title: 'Trinidad and Tobago',
childIds: ()
},
18: {
id: 18,
title: 'Venezuela',
childIds: ()
},
19: {
id: 19,
title: 'Asia',
childIds: (20, 21, 22, 23, 24, 25, 26),
},
20: {
id: 20,
title: 'China',
childIds: ()
},
21: {
id: 21,
title: 'Hong Kong',
childIds: ()
},
22: {
id: 22,
title: 'India',
childIds: ()
},
23: {
id: 23,
title: 'Singapore',
childIds: ()
},
24: {
id: 24,
title: 'South Korea',
childIds: ()
},
25: {
id: 25,
title: 'Thailand',
childIds: ()
},
26: {
id: 26,
title: 'Vietnam',
childIds: ()
},
27: {
id: 27,
title: 'Europe',
childIds: (28, 29, 30, 31, 32, 33, 34),
},
28: {
id: 28,
title: 'Croatia',
childIds: ()
},
29: {
id: 29,
title: 'France',
childIds: ()
},
30: {
id: 30,
title: 'Germany',
childIds: ()
},
31: {
id: 31,
title: 'Italy',
childIds: ()
},
32: {
id: 32,
title: 'Portugal',
childIds: ()
},
33: {
id: 33,
title: 'Spain',
childIds: ()
},
34: {
id: 34,
title: 'Turkey',
childIds: ()
},
35: {
id: 35,
title: 'Oceania',
childIds: (36, 37, 38, 39, 40, 41, 42),
},
36: {
id: 36,
title: 'Australia',
childIds: ()
},
37: {
id: 37,
title: 'Bora Bora (French Polynesia)',
childIds: ()
},
38: {
id: 38,
title: 'Easter Island (Chile)',
childIds: ()
},
39: {
id: 39,
title: 'Fiji',
childIds: ()
},
40: {
id: 40,
title: 'Hawaii (the USA)',
childIds: ()
},
41: {
id: 41,
title: 'New Zealand',
childIds: ()
},
42: {
id: 42,
title: 'Vanuatu',
childIds: ()
},
43: {
id: 43,
title: 'Moon',
childIds: (44, 45, 46)
},
44: {
id: 44,
title: 'Rheita',
childIds: ()
},
45: {
id: 45,
title: 'Piccolomini',
childIds: ()
},
46: {
id: 46,
title: 'Tycho',
childIds: ()
},
47: {
id: 47,
title: 'Mars',
childIds: (48, 49)
},
48: {
id: 48,
title: 'Corn Town',
childIds: ()
},
49: {
id: 49,
title: 'Green Hill',
childIds: ()
}
};
이제 상태가 평평하므로(일명 정규화됨) 중첩된 요소를 업데이트하는 것이 더 쉽습니다.
이제 장소를 제거하려면 두 단계로 상태를 업데이트하기만 하면 됩니다.
상위 장소의 업데이트된 버전은 childIds 배열에서 제거된 ID를 제외해야 합니다.
“테이블” 루트 개체의 업데이트된 버전에는 상위 위치의 업데이트된 버전이 포함되어야 합니다.
다음은 예입니다.
import { useState } from 'react';
import { initialTravelPlan } from './places.js';
export default function TravelPlan() {
const (plan, setPlan) = useState(initialTravelPlan);
function handleComplete(parentId, childId) {
const parent = plan(parentId);
// Create a new version of the parent place
// that doesn't include this child ID.
const nextParent = {
...parent,
childIds: parent.childIds
.filter(id => id !== childId)
};
// Update the root state object...
setPlan({
...plan,
// ...so that it has the updated parent.
(parentId): nextParent
});
}
const root = plan(0);
const planetIds = root.childIds;
return (
<>
<h2>Places to visit</h2>
<ol>
{planetIds.map(id => (
<PlaceTree
key={id}
id={id}
parentId={0}
placesById={plan}
onComplete={handleComplete}
/>
))}
</ol>
</>
);
}
function PlaceTree({ id, parentId, placesById, onComplete }) {
const place = placesById(id);
const childIds = place.childIds;
return (
<li>
{place.title}
<button onClick={() => {
onComplete(parentId, id);
}}>
Complete
</button>
{childIds.length > 0 &&
<ol>
{childIds.map(childId => (
<PlaceTree
key={childId}
id={childId}
parentId={id}
placesById={placesById}
onComplete={onComplete}
/>
))}
</ol>
}
</li>
);
}
상태를 원하는 만큼 중첩할 수 있지만 상태를 “평평하게” 만들면 많은 문제를 해결할 수 있습니다. 상태를 더 쉽게 업데이트하고 중첩된 개체의 다른 부분에서 중복을 방지합니다.
심층 분석 – 향상된 메모리 사용량
이상적으로는 메모리 사용을 개선하기 위해 Table 개체에서 삭제된 항목(및 해당 항목의 자식!)도 제거해야 합니다. 이 버전이 바로 그것입니다.. 업데이트 논리를 더 간결하게 만들기 위해 Always도 사용했습니다.
import { useImmer } from 'use-immer';
import { initialTravelPlan } from './places.js';
export default function TravelPlan() {
const (plan, updatePlan) = useImmer(initialTravelPlan);
function handleComplete(parentId, childId) {
updatePlan(draft => {
// Remove from the parent place's child IDs.
const parent = draft(parentId);
parent.childIds = parent.childIds
.filter(id => id !== childId);
// Forget this place and all its subtree.
deleteAllChildren(childId);
function deleteAllChildren(id) {
const place = draft(id);
place.childIds.forEach(deleteAllChildren);
delete draft(id);
}
});
}
const root = plan(0);
const planetIds = root.childIds;
return (
<>
<h2>Places to visit</h2>
<ol>
{planetIds.map(id => (
<PlaceTree
key={id}
id={id}
parentId={0}
placesById={plan}
onComplete={handleComplete}
/>
))}
</ol>
</>
);
}
function PlaceTree({ id, parentId, placesById, onComplete }) {
const place = placesById(id);
const childIds = place.childIds;
return (
<li>
{place.title}
<button onClick={() => {
onComplete(parentId, id);
}}>
Complete
</button>
{childIds.length > 0 &&
<ol>
{childIds.map(childId => (
<PlaceTree
key={childId}
id={childId}
parentId={id}
placesById={placesById}
onComplete={onComplete}
/>
))}
</ol>
}
</li>
);
}
경우에 따라 중첩된 상태의 일부를 하위 구성 요소로 이동하여 상태 중첩을 줄일 수 있습니다. 이 방법은 저장할 필요가 없는 임시 UI 상태에 적합합니다. B. 요소에 마우스 포인터가 있는지 여부.
요약
- 두 개의 상태 변수가 항상 함께 업데이트되는 경우 하나로 병합하는 것이 좋습니다.
- “불가능한” 상태가 생성되지 않도록 상태 변수를 신중하게 선택하십시오.
- 업데이트 오류를 줄이기 위해 상태를 구성하십시오.
- 상태를 동기화할 필요가 없도록 중복 및 중복 상태를 피하십시오.
- 특별히 업데이트를 방지하려는 경우가 아니면 status props를 사용하지 마십시오.
- 선택과 같은 UI 패턴의 경우 개체 자체 대신 ID 또는 인덱스를 상태로 유지합니다.
- 깊게 중첩된 상태를 업데이트하는 것이 복잡한 경우 평면화합니다.
작업 1/4: 업데이트되지 않은 구성 요소 수정
이 시계 구성 요소에는 색상과 시간이라는 두 가지 소품이 필요합니다. 콤보 상자에서 다른 색상을 선택하면 시계 구성 요소는 부모와 다른 색상의 소품을 가져옵니다. 그러나 어떤 이유로 표시된 색상이 업데이트되지 않습니다. 왜? 문제를 해결하기 위해.
https://codesandbox.io/s/fix-a-component-thats-not-updating-dpzgqj?file=/Clock.js
작업 2/4: 작동하지 않는 포장 목록 수정
이 포장 목록에는 포장된 항목 수와 총 항목 수를 표시하는 바닥글이 있습니다. 처음에는 잘 작동하는 것 같지만 오류가 있습니다. 예를 들어 항목을 포장됨으로 표시한 다음 삭제하면 카운터가 올바르게 업데이트되지 않습니다. 항상 올바르게 업데이트되도록 카운터를 수정하십시오.
https://codesandbox.io/s/fix-a-broken-packing-list-3gq6v5?file=/App.js
작업 3/4: 사라지는 선택 항목 수정
상태의 문자 목록이 있습니다. 특정 캐릭터 위로 마우스를 가져가거나 초점을 맞추면 해당 캐릭터가 강조 표시됩니다. 현재 강조 표시된 문자는 상태 변수 “highlightedLetter”에 저장됩니다. 개별 문자를 “표시” 또는 “표시 해제”하여 상태의 문자 배열을 업데이트할 수 있습니다.
이 코드는 작동하지만 UI에 작은 버그가 있습니다. “별표” 또는 “별표 취소”를 누르면 하이라이트가 잠시 사라집니다. 하지만 마우스 포인터를 움직이거나 키보드로 다른 문자로 전환하면 바로 다시 나타납니다. 왜 이런 일이 발생합니까? 버튼을 클릭한 후 사라지지 않도록 강조 표시를 수정합니다.
https://codesandbox.io/s/fix-the-disappearing-selection-45m34p?file=/App.js
작업 4/4: 다중 선택 구현
이 예제에서 각 문자에는 isSelected 속성과 onToggle 핸들러가 있어 선택된 것으로 표시합니다. 이 방법은 작동하지만 상태는 selectedId(null 또는 ID)로 저장되므로 한 번에 하나의 문자만 선택할 수 있습니다.
다중 선택을 지원하도록 상태 구조를 수정합니다. (코딩하기 전에 구성 방법에 대해 생각하십시오.) 각 확인란은 다른 확인란과 독립적이어야 합니다. 선택한 문자를 클릭하면 선택이 해제됩니다. 마지막으로 바닥글에 선택한 항목의 정확한 수가 표시되어야 합니다.
https://codesandbox.io/s/implement-multiple-selection-2st5o6?file=/App.js
