TL;DR
uncontrolled component는 컴포넌트의 관심사를 달성하기 위해 스스로 내부 상태를 관리하는 컴포넌트입니다.
컴포넌트에 상태 관리 책임을 넘기고 편해지는 방법을 알아봅시다.
원문 번역입니다 : https://jjenzz.com/component-control-freak
Are You a Component Control Freak?
It's tempting to always control the components we implement but we can sometimes simplify things if we use the uncontrolled pattern.
jjenzz.com
우리는 종종 컴포넌트의 상태를 직접 제어하려 합니다.
리액트 튜토리얼(React’s own intro tutorial)을 포함하여, 상태 관리 튜토리얼이 도처에 널려있기 때문에,
상태 관리를 반드시 해야 한다는 강박에 빠지기 쉽습니다.
- 예를 들어, input에는 value와 onChange prop이 있습니다.
Uncontrolled components
Uncontrolled components는 상태 관리를 스스로 수행하는 컴포넌트 입니다.
꼭 필요할 때애만 사용자가 초기 상태를 지정하는 것을 허용합니다.
Uncontrolled Component 만들기
다음은 dumb(stateless) 컴포넌트가 자주 빌드되는 방법입니다.
몇몇 prop과 callback을 제공하면 dumb Counter 컴포넌트를 얻을 수 있습니다.
const Counter = ({ count = 0, onChange }) => {
// Effect with `count` dependency so effect runs every time `count` changes
React.useEffect(() => {
setTimeout(() => onChange(count + 1), 1000);
}, [count]);
return <span>{count}</span>;
};
const App = () => {
const [countOneValue, setCountOneValue] = React.useState(0);
const [countTwoValue, setCountTwoValue] = React.useState(10);
return (
<div>
<p>
<Counter count={countOneValue} onChange={setCountOneValue} />
</p>
<p>
<Counter count={countTwoValue} onChange={setCountTwoValue} />
</p>
</div>
);
};
예상대로 동작하지만 두 개의 제어되는 카운터 상태가 필요합니다.
Counter 컴포넌트를 사용하려는 사람은 모두 유사한 상태를 구현해야 합니다.
동일한 리프팅 상태를 반복하는 이러한 상황에 처했을 때 uncontrolled 컴포넌트로 변경하는 것이 도움이 됩니다.
즉, 부모에서 상태를 Counter로 이동하여 컴포넌트 스스로 상태를 관리하도록 합니다.
const App = () => (
<div>
<p>
<Counter />
</p>
<p>
<Counter defaultCount={10} />
</p>
</div>
);
const Counter = ({ defaultCount = 0 }) => {
const [count, setCount] = React.useState(defaultCount);
React.useEffect(() => {
setTimeout(() => setCount(prevCount => prevCount + 1), 1000);
}, [count]);
return <span>{countValue}</span>;
};
자체적으로 상태를 관리하지만, 예상대로 동작하는 매우 간단한 uncontrolled component를 얻었습니다.
API 컨슈머는 기대되는 동작을 위해 상태를 더 이상 제어할 필요가 없으며
결과적으로 상위 컴포넌트의 상용구를 제거할 수 있었습니다.
uncontrolled component control하기
- 버튼을 클릭하면 10씩 증가하길 원할 수 있습니다.
- 문제 없습니다. step prop을 추가할 수 있습니다.
- 갑자기 2의 배수로 증가하기를 원합니다.
const Counter = ({
count: countProp,
defaultCount = 0,
onChange = () => {},
}) => {
// local state for uncontrolled version
const [countState, setCountState] = React.useState(defaultCount);
// whether consumer is trying to control it or not. we use a ref because
// components should not switch between controlled/uncontrolled at runtime
const isControlled = React.useRef(countProp !== undefined).current;
// the count value we render depending on whether it is controlled
const count = isControlled ? countProp : countState;
// maintaining change callback in a ref (more on that later)
const handleChangeRef = React.useRef(onChange);
React.useLayoutEffect(() => {
handleChangeRef.current = onChange;
});
React.useEffect(() => {
const handleChange = handleChangeRef.current;
setTimeout(() => {
if (isControlled) {
handleChange(count + 1);
} else {
setCountState((prevCount = 0) => {
const nextCount = prevCount + 1;
handleChange(nextCount);
return nextCount;
});
}
}, 1000);
}, [isControlled, count]);
return <span>{count}</span>;
};
이 접근 방식은 효과 종속성으로 추가하지 않고 핸들러에 대한 업데이트된 참조를 제공하므로,
카운트가 변경될 때만 효과가 실행되도록 합니다.
const Counter = ({
count: countProp,
defaultCount = 0,
onChange = () => {},
}) => {
const [countState, setCountState] = React.useState(defaultCount);
const isControlled = React.useRef(countProp !== undefined).current;
const count = isControlled ? countProp : countState;
// ...
if (isControlled) {
onChange(count + 1);
} else {
// if component is uncontrolled, we set the internal state
setCountState((prevCount = 0) => {
const nextCount = prevCount + 1;
onChange(nextCount);
return nextCount;
});
}
// ...
return <span>{count}</span>;
};
isControlled prop의 정의 여부를 확인하는 isControlled 불리언을 유지합니다.
그런 다음 isControlled 불리언을 사용하여 컴포넌트를 내부 상태 관리(제어되지 않음)에서 props(제어됨) 사용으로 전환합니다.
즉 우리는, React form의 필드와 유사한 것을 만들었습니다.
제어되거나 제어되지 않을 수 있는 컴포넌트입니다.
Uncontrolled forms
const Contact = () => {
const [name, setName] = React.useState(‘’);
const [email, setEmail] = React.useState(‘’);
const [message, setMessage] = React.useState(‘’);
const [isSubscribed, setIsSubscribed] = React.useState(false);
function handleSubmit(event) {
fetch(‘/contact’, {
mode: ‘POST’,
body: JSON.stringify({ name, email, message, isSubscribed }),
});
event.preventDefault();
}
return (
<form onSubmit={handleSubmit}>
<label>
Name
<input type=“text” value={name} onChange={event => setName(event.target.value)} />
</label>
<label>
Email
<input type=“email” value={email} onChange={event => setEmail(event.target.value)} />
</label>
<label>
Message
<textarea value={message} onChange={event => setMessage(event.target.value)} />
</label>
<label>
<input type=“checkbox” checked={isSubscribed} onChange={event => setIsSubscribed(event.checked)} />
Please check this box if you would like to subscribe to our newsletter
</label>
</form>
);
}
const Contact = () => {
function handleSubmit(event) {
const formData = new FormData(event.currentTarget);
const body = Object.fromEntries(formData.entries());
fetch(‘/contact’, { mode: ‘POST’, body: JSON.stringify(body) });
event.preventDefault();
}
return (
<form onSubmit={handleSubmit}>
<label>
Name
<input type=“text” name="name" />
</label>
<label>
Email
<input type=“email” name="email" />
</label>
<label>
Message
<textarea name="message" />
</label>
<label>
<input type=“checkbox” name="isSubscribed" />
Please check this box if you would like to subscribe to our newsletter
</label>
</form>
);
}
- 불필요한 상태 관리를 제거하여 컴포넌트를 단순화했습니다.
- 모든 키 입력이 전체 form을 렌더링하는 것을 방지하였습니다.
위는 매우 일반적입니다.
onSubmit 및 onChange 이벤트가 실행될 때, 필드의 객체 리터럴 표현을 제공하는 유틸 함수와 함께 Form 컴포넌트를 만듭니다
Uncontrolled 컴포넌트와 key prop
const DEFAULT_COMMENT = 'Sounds decent';
const App = () => {
const [selectedPostId, setSelectedPostId] = React.useState();
const [comment, setComment] = React.useState(DEFAULT_COMMENT);
React.useEffect(() => {
// reset state back to original
setComment(DEFAULT_COMMENT);
}, [selectedPostId]);
function handleSubmitComment(event) {
submitComment(comment);
event.preventDefault();
}
return (
<div>
<ul>
{['1', '2', '3'].map(postId => (
<li key={postId}>
<input
type="radio"
value={postId}
onChange={event => setSelectedPostId(event.target.value)}
checked={selectedPostId === postId}
/>{' '}
Post {postId}
</li>
))}
</ul>
{selectedPostId && (
<form onSubmit={handleSubmitComment}>
<h2>Comment on post {selectedPostId}</h2>
<textarea
value={comment}
onChange={event => setComment(event.target.value)}
/>
<br />
<button>comment</button>
</form>
)}
</div>
);
};
키 prop이 변경되면 컴포넌트를 다시 인스턴스화합니다. 즉 상태를 초기화 합니다.
이를 활용하여 comment form을 원래 상태로 되돌릴 수 있습니다.
const App = () => {
const [selectedPostId, setSelectedPostId] = React.useState();
return (
<div>
<ul>
{['1', '2', '3'].map(postId => (
<li key={postId}>
<input
type="radio"
value={postId}
onChange={event => setSelectedPostId(event.target.value)}
checked={selectedPostId === postId}
/>{' '}
Post {postId}
</li>
))}
</ul>
{selectedPostId && <Comment key={selectedPostId} id={selectedPostId} />}
</div>
);
};
const Comment = ({ id }) => {
function handleSubmitComment(event) {
const formData = new FormData(event.currentTarget);
submitComment(formData.get('comment'));
event.preventDefault();
}
return (
<form onSubmit={handleSubmitComment}>
<h2>Comment on post {id}</h2>
<textarea name="comment" defaultValue="Sounds decent" />
<br />
<button>comment</button>
</form>
);
};
- input은 uncontrolled 값을 사용해 보여주고
- 내부적으로 이전값 역할을 할 상태와 에러 플래그를 이용해 리렌더링 할 때,
- 요청 실패 시 key만 바꿔주도록 하면
- 리렌더링 시 이전값 역할을 하는 내부 상태를 defaultValue로 리렌더링
결론
- 이 상태가 정말 필요한가요?
- 제어되지 않는(uncontrolledl) API를 사용하면 복잡성을 줄일 수 있나요?
- 컴포넌트를 초기화 하려는 경우 uncontrolled/key prop 페어링이 더 가벼운 컴포넌트에 도움이 될 수 있습니다.
- 초기 상태를 복사해서 유지하지 마세요!
저는 이런 식으로 생각함으로써 응용 프로그램의 복잡성을 줄이는 데 성공했습니다.
여러분도 유용하게 사용하시길 바랍니다 🙂
'FrontEnd' 카테고리의 다른 글
[번역] 서버에서 Redux 사용하기 (0) | 2023.03.15 |
---|---|
[Redux] 리덕스의 state, action, reducer (0) | 2023.03.14 |
[번역] Headless UI Component란 무엇인가? (2) | 2023.03.13 |
[React, framer-motion] 언마운트 대상 DOM 요소에 애니메이션 적용하기 with AnimatePresence (0) | 2023.03.08 |
[CSS] 실전 Grid Layout 활용 (0) | 2023.03.04 |