github.com/radix-ui/react-useControllableState ์์ค์ฝ๋
primitives/packages/react/use-controllable-state/src/useControllableState.tsx at main · radix-ui/primitives
Radix Primitives is an open-source UI component library for building high-quality, accessible design systems and web apps. Maintained by @workos. - radix-ui/primitives
github.com
๐น๏ธ useControllableState() ๋?
Radix ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ์ ๊ณตํ๋ React ํ ์ผ๋ก, ์ ์ด๋(controllable) ์ํ์ ๋ด๋ถ ์ํ๋ฅผ ๋์์ ๊ด๋ฆฌํ๋ Hook์ด๋ค.
์ฆ, ๋ถ๋ชจ ์ปดํฌ๋ํธ์์ ์ํ๋ฅผ ์ง์ ๊ด๋ฆฌํ ์๋ ์๊ณ , ์ํ๊ฐ ์ ๊ณต๋์ง ์์์ ๊ฒฝ์ฐ ๋ด๋ถ์ ์ผ๋ก ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ๋ฐฉ์์ด๋ค.
"Controlled + Uncontrolled" ์ํ ๊ด๋ฆฌ ํจํด์ ๊ตฌํํ ๋ ์ ์ฉํ๋ค.
๐ค ์ useControllableState()๊ฐ ํ์ํด?
React ์ปดํฌ๋ํธ์์ ์ํ๋ฅผ ๊ด๋ฆฌํ ๋ ์ ์ด๋ ์ํ์ ๋น์ ์ด ์ํ ๋ ๊ฐ์ง ๋ฐฉ์์ด ์๋ค.
๐ข ์ ์ด๋ ์ํ(Controllable)
์ํ๋ฅผ ๋ถ๋ชจ์์ ์ง์ ๊ด๋ฆฌํ๊ณ , value์ onChange๋ฅผ props๋ก ์ ๋ฌํ์ฌ ๋์
function Parent() {
const [value, setValue] = React.useState("");
return <Input value={value} onChange={setValue} />;
}
์ฌ๊ธฐ์ Input ์ปดํฌ๋ํธ๋ ์์ ํ ์ ์ด๋ ์ํ๋ฅผ ๋ฐ๋ฅด๊ฒ ๋๋ค.
๐ด ๋น์ ์ด๋ ์ํ (Uncontrolled)
useState()๋ฅผ ์ด์ฉํด ์ปดํฌ๋ํธ ๋ด๋ถ์์ ์์ฒด์ ์ผ๋ก ์ํ๋ฅผ ๊ด๋ฆฌํ๋ค.
function Input() {
const [value, setValue] = React.useState("");
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
์ฌ๊ธฐ์ Input ๋ด๋ถ์์ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ฏ๋ก ์ธ๋ถ์์ ์ ์ดํ ์ ์๋ค.
ํ์ง๋ง,
์ด ๋๊ฐ์ง ๋ฐฉ์์ ๋ชจ๋ ์ง์ํ๋ ํ์ด๋ธ๋ฆฌ๋ ๋ฐฉ์์ด ํ์ํ ๋๊ฐ ์๋ค.
๐ useControllableState()๋ฅผ ์ฌ์ฉํ๋ฉด ๋ถ๋ชจ์์ ์ํ๋ฅผ ์ ๊ณตํ๋ฉด Controlled, ์ ๊ณตํ์ง ์์ผ๋ฉด Uncontrolled ์ํ๋ก ๋์ํ๊ฒ ๋ง๋ค ์ ์๋ค.
์ฆ, ๋ถ๋ชจ๊ฐ ์์์ ์ํ๋ฅผ ๊ด๋ฆฌํ๋๋ก ์ํ๋ฅผ ๋ด๋ ค์ค ์๋ ์๊ณ , ์์๋ด์์ ๋จ๋ ์ผ๋ก ์ํ๋ฅผ ๊ด๋ฆฌํ ์ ์๋ค.
ํด๋น Hook์ ์ฌ์ฉํ๋ ค๋ฉด ์ฐ์ Radix์์ ๋ค์ด ๋ฐ์์ผํ๋ค.
npm install @radix-ui/react-use-controllable-state
๐งฉ useControllableState() ๋ฏ์ด๋ณด๊ธฐ
github.com/radix-ui/primitives/use-controllableState
primitives/packages/react/use-controllable-state/src/useControllableState.tsx at main · radix-ui/primitives
Radix Primitives is an open-source UI component library for building high-quality, accessible design systems and web apps. Maintained by @workos. - radix-ui/primitives
github.com
์ฝ๋๋ ๋ค์๊ณผ ๊ฐ๋ค
(useContollableState()์ ์ฌ์ฉ๋ useCallbackRef()์ ์ค๋ช ์ ์ํ๋ฉด ํผ์ณ์ ๋ณด๊ธฐ)
์ฐธ๊ณ
github.com/radix-ui/primitives/useCallbackRef
primitives/packages/react/use-callback-ref/src/useCallbackRef.tsx at main · radix-ui/primitives
Radix Primitives is an open-source UI component library for building high-quality, accessible design systems and web apps. Maintained by @workos. - radix-ui/primitives
github.com
import * as React from 'react';
/**
* A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a
* prop or avoid re-executing effects when passed as a dependency
*/
function useCallbackRef<T extends (...args: any[]) => any>(callback: T | undefined): T {
// ์ฝ๋ฐฑ ํจ์๊ฐ๊ฐ ๋ค์ด์ค๋ฉด ํจ์๋ฅผ Ref ์ ์ฅ.
// ์ฝ๋ฐฑ ํจ์๊ฐ ๋ณ๊ฒฝ๋์ด๋ ๋ฆฌ๋ ๋๋ง์ ๋ฐฉ์ง.
const callbackRef = React.useRef(callback);
React.useEffect(() => {
// useEffect()๋ฅผ ์ฌ์ฉํ์ฌ callback์ด ๋ณ๊ฒฝ๋ ๋๋ง๋ค callbackRef.current๋ฅผ ์
๋ฐ์ดํธ
// ์์กด์ฑ ๋ฐฐ์ด์ด ์๊ธฐ ๋๋ฌธ์ ๋ชจ๋ ๋ ๋๋ง ์ดํ ์คํ.
callbackRef.current = callback;
});
// https://github.com/facebook/react/issues/19240
// useMemo()๋ฅผ ์ฌ์ฉํ์ฌ ์๋ก์ด ํจ์๋ฅผ ๋ฐํ
// ์์กด์ฑ์ด ๋น์ด ์์ผ๋ฏ๋ก, ์ด ํจ์๋ ํ ๋ฒ๋ง ์์ฑ
// ๊ฒฐ๊ณผ์ ์ผ๋ก useCallbackRef๊ฐ ๋ฐํํ๋ ํจ์๋ ์ฒ์ ์์ฑ๋ ์ดํ ๋ณ๊ฒฝ๋์ง ์์.
// ํ์ง๋ง callbackRef.current๊ฐ ์ฝ๋ฐฑ ํจ์๋ฅผ ์ฐธ์กฐํ๊ธฐ ๋๋ฌธ์ ์ต์ ์ฝ๋ฐฑ์ ์ ์ง.
return React.useMemo(() => (
(...args) => callbackRef.current?.(...args)) as T
,[]);
}
export { useCallbackRef };
๐ค useCallback๊ณผ์ ์ฐจ์ด
const memoizedCallback = useCallback(() => {
console.log("callback ์คํ!");
}, [dependency]); // dependency๊ฐ ๋ฐ๋ ๋๋ง๋ค ํจ์๊ฐ ์๋ก ์์ฑ๋จ
dependency๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค ์๋ก์ด ํจ์๊ฐ ์์ฑ ๐ useEffect์ ์์กด์ฑ ๋ฐฐ์ด์ ์์ผ๋ฉด ๋ถํ์ํ ์ฌ์คํ ๋ฐ์
const stableCallback = useCallbackRef(() => {
console.log("callback ์คํ!");
});
useCallbackRef๋ฅผ ์ฌ์ฉํ๋ฉด ์ต์ด์ ์์ฑ๋ ํจ์๊ฐ ์ ์ง๋จ.
ํ์ง๋ง ๋ด๋ถ์ ์ผ๋ก๋ ์ต์ ์ฝ๋ฐฑ์ ์ฐธ์กฐํ๊ณ ์์ด ์ต์ ๋ก์ง์ ์คํ ๊ฐ๋ฅ
๐ค ์ธ์ ์ฌ์ฉํ๋ฉด ์ข์๊น?
1. ์ฝ๋ฐฑ ํจ์๋ฅผ ref๋ก ์ ์ฅํ๊ณ ์ต์ ์ํ๋ฅผ ์ ์งํ๋ฉด์๋ ๋ถํ์ํ ์ฌ์์ฑ์ ๋ฐฉ์งํ๊ณ ์ถ์ ๋
2. ์ฝ๋ฐฑ ํจ์๋ฅผ prop์ผ๋ก ๋๊ธธ ๋, ๋ถํ์ํ ๋ฆฌ๋ ๋๋ง์ ๋ฐฉ์งํ๊ณ ์ถ์ ๋
3. useEffect ์์กด์ฑ ๋ฐฐ์ด์ ์ฝ๋ฐฑ ํจ์๋ฅผ ๋ฃ๊ณ ์ถ์ง๋ง, ์ฝ๋ฐฑ ๋ณ๊ฒฝ์ผ๋ก ์ธํด ๋ถํ์ํ ์ฌ์คํ์ ๋ฐฉ์งํ๊ณ ์ถ์ ๋
โ ๊ฒฐ๋ก
1. ์ฝ๋ฐฑ์ด ๋ณ๊ฒฝ๋๋๋ผ๋ ํจ์ ์ฐธ์กฐ๊ฐ ์ ์ง
2. ๋ถํ์ํ ๋ฆฌ๋ ๋๋ง ๋ฐฉ์ง (prop์ผ๋ก ์ ๋ฌ ์ ์ ์ฉ)
3. useEffect ๋ฑ์ ์์กด์ฑ ๋ฐฐ์ด์์ ์์ ํ๊ฒ ์ฌ์ฉ ๊ฐ๋ฅ
4. ์ต์ ์ํ์ ์ฝ๋ฐฑ์ ์ ์งํ๋ฉด์๋ ์์ ์ ์ธ ์ฐธ์กฐ ์ ๊ณต
React์์ ์ต์ ์ฝ๋ฐฑ์ ์ ์งํ๋ฉด์๋ ๋ถํ์ํ ๋ฆฌ๋ ๋๋ง์ ๋ฐฉ์งํ๋ ์ต์ ํ๋ ์ฝ๋๋ฅผ ์์ฑํ ์ ์๋ค.
import * as React from 'react';
import { useCallbackRef } from '@radix-ui/react-use-callback-ref';
// params๋
// prop (์ ์ดํ ๊ฐ)
// defaultProp (์ ์ดํ ๊ฐ์ ๊ธฐ๋ณธ ๊ฐ)
// onChange (์ ์ดํ ๊ฐ์ ์ ์ดํ ํจ์)
type UseControllableStateParams<T> = {
prop?: T | undefined;
defaultProp?: T | undefined;
onChange?: (state: T) => void;
};
// setState ํ์
์ ์
type SetStateFn<T> = (prevState?: T) => T;
function useControllableState<T>({
prop,
defaultProp,
onChange = () => {},
}: UseControllableStateParams<T>) {
// ๋น์ ์ด์ผ ๊ฒฝ์ฐ, useUncontrollableState() ์ ์ฉ
const [uncontrolledProp, setUncontrolledProp] = useUncontrolledState({ defaultProp, onChange });
// prop๊ฐ์ด ๋์ด์ค๋ฉด ์ ์ด ์ํ, undefined๋ผ๋ฉด ๋น์ ์ด๋ก ๊ฐ ์ ์ธ
const isControlled = prop !== undefined;
// ์ ์ด ์ํ๋ฉด prop์ ์ฌ์ฉ, ๊ทธ๋ ์ง ์๋ค๋ฉด ๋น์ ์ด์ ๊ฐ์ ์ ์ฉ
const value = isControlled ? prop : uncontrolledProp;
// onChange ์ต์ ํ
const handleChange = useCallbackRef(onChange);
const setValue: React.Dispatch<React.SetStateAction<T | undefined>> = React.useCallback(
(nextValue) => {
if (isControlled) {
const setter = nextValue as SetStateFn<T>;
const value = typeof nextValue === 'function' ? setter(prop) : nextValue;
if (value !== prop) handleChange(value as T);
} else {
setUncontrolledProp(nextValue);
}
},
[isControlled, prop, setUncontrolledProp, handleChange]
);
return [value, setValue] as const;
}
function useUncontrolledState<T>({
defaultProp,
onChange,
}: Omit<UseControllableStateParams<T>, 'prop'>) {
const uncontrolledState = React.useState<T | undefined>(defaultProp);
const [value] = uncontrolledState;
const prevValueRef = React.useRef(value);
const handleChange = useCallbackRef(onChange);
React.useEffect(() => {
if (prevValueRef.current !== value) {
handleChange(value as T);
prevValueRef.current = value;
}
}, [value, prevValueRef, handleChange]);
return uncontrolledState;
}
export { useControllableState };
๐ useState์ ๋น๊ต
Feature | useState() | useControllableState() |
์ ์ด๋ ์ํ ์ง์ | โ (์ธ๋ถ์์ ๊ด๋ฆฌ ๋ถ๊ฐ) | โ (prop์ผ๋ก ๊ด๋ฆฌ ๊ฐ๋ฅ) |
๋น์ ์ด ์ํ ์ง์ |
โ (๋ด๋ถ์์ ๊ด๋ฆฌ ๊ฐ๋ฅ) | โ (defaultProp ์ฌ์ฉ ๊ฐ๋ฅ) |
ํผํฉ ์ฌ์ฉ ๊ฐ๋ฅ |
โ (์๋์ผ๋ก ์ฒ๋ฆฌํด์ผ ํจ) | โ (์๋์ผ๋ก ์ ํ๋จ) |
๋ถ๋ชจ ์ํ ๋ณํ ๋ฐ์ | โ (์๋์ผ๋ก ์ ๋ฐ์ดํธ ํ์) | โ (์๋ ๋ฐ์) |
๐ ์ฌ์ฉ๋ฒ
๊ธฐ๋ณธ ๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ๋ค
์ปจํธ๋กค ๋ ์์ ์ปดํฌ๋ํธ์ ๋ค์๊ณผ ๊ฐ์ด ํ ๋นํ๋ค.
const [state, setState] = useControllableState({
prop: controlledValue, // ์ ์ด๋ ์ํ ๊ฐ
onChange: onChange, // ๋ณ๊ฒฝ ์ ์คํ๋ ํจ์
defaultProp: defaultValue, // ๋ด๋ถ์ ์ผ๋ก ์ฌ์ฉํ ๊ธฐ๋ณธ ์ํ
});
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
};
โ prop: props๋ก ๋๊ฒจ๋ฐ๋ prop๊ฐ (Controlled).
โ defaultProp: prop์ ๊ธฐ๋ณธ ์ํ๊ฐ (Uncontrolled).
โ onChange: ์คํํ ์ฝ๋ฐฑ ํจ์.
๊ทธ๋ฆฌ๊ณ useControllableState()๋ก ๋ฐ๋ setState()๋ฅผ ์คํํด์ฃผ๋ฉด ๋๋ค.
๋ค์ ํ ๋ฒ ๋งํ๋ฉด, ๋ถ๋ชจ๋ก๋ถํฐ, prop๊ณผ onChange๋ฅผ ๋ฐ์ผ๋ฉด ์ ์ด, ๋ฐ์ง ์์๋ค๋ฉด ๋น์ ์ด ๋ฐฉ์์ผ๋ก ๋์ํ๋ค.
* ์์ฑํ๋ฉด์ ํท๊ฐ๋ ธ๋๋ฐ ์ฌ๊ธฐ์ ์ ์ด/๋น์ ์ด๋ ๋ถ๋ชจ๊ฐ ์์์ ์ปจํธ๋กคํ ์ง ๋ง์ง์ ์ฌ๋ถ์ด๋ค.
(์๋ํ๋ฉด.. ๋ฉ๋ชจ๋ฆฌ์ ๊ดํ์ฌ ์ ์ด/๋น์ ์ด ๊ณ์ ํท๊ฐ๋ ค์ ์ด๊ฒ๋ ๋ฆฌ๋ ๋๋ง ๋๋๋ฐ ์...? ์ด๋ฌ๊ณ ์์๋ค..... ํ์.....)
๐ป ์์์ฝ๋
"use client";
import { useControllableState } from "@radix-ui/react-use-controllable-state";
import React, { useState } from "react";
type InputProps = {
value?: string;
onChange?: (value: string) => void;
defaultValue?: string;
};
function Input({ value, onChange, defaultValue }: InputProps) {
const [inputValue, setInputValue] = useControllableState({
prop: value,
onChange: onChange,
defaultProp: defaultValue,
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log("๋น์ ์ด");
setInputValue(e.target.value);
};
return (
<div className="flex items-center relative">
<input
value={inputValue}
onChange={handleChange}
className="pl-8 border rounded-md p-2"
/>
</div>
);
}
export default function Parent() {
const [value, setValue] = useState("");
const handleChange = (value: string) => {
console.log("์ ์ด");
setValue(value);
};
return (
<div className="flex items-center">
<Input value={value} onChange={handleChange} defaultValue={value} />
</div>
);
}
์ฝ๋๋ฅผ ๋ณต์ฌ ๋ถ์ฌ๋ฃ๊ธฐ ํ ํ, <Parent/>์ปดํฌ๋ํธ์์ <Input/>์ ํธ์ถํ ๋, props๋ฅผ
โ ์ ์ด: value, onChange() ๋ ๊ฐ
โ ๋น์ ์ด: defaultValue ํ ๊ฐ
โ ํ์ด๋ธ๋ฆฌ๋: value, onChnage(), defalutValue ์ธ ๊ฐ
์ ๋ณํ๋ฅผ ์ฃผ์ด๊ฐ๋ฉด์ ๊ฐ๋ฐ์ ๋๊ตฌ - Console์ ํ์ธํด๋ณด๋ฉด ์ดํดํ๊ธฐ ์ฝ๋ค.
๐ ๊ฒฐ๋ก
๋ถ๋ชจ๋ก๋ถํฐ ๋๊ฒจ์ฃผ๋ ๊ฐ์ ๋ฐ๋ผ์ ํธ์ถํ๋ ์ปดํฌ๋ํธ๋ฅผ ์ ์ดํ ์ง ๋น์ ์ดํ ์ง ๋์์ฃผ๋ Hook์ด๋ค.
'Library > React' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[ React ] ๋ฆฌ์กํธ ์ปดํฌ๋ํธ ๋ถ๋ฆฌ ๊ธฐ์ค์ ์์๋ณด์. (0) | 2025.04.04 |
---|---|
[ React / UI ] ํฉ์ฑ ์ปดํฌ๋ํธ(Compound-Component)์ ๋ํด ์์๋ณด์. (1) | 2025.01.31 |
[ React / UI ] ์ปดํฌ๋ํธ ์ถ์ํ (2) | 2025.01.31 |
[ React / UI ] Headless UI ์ ๋ํด ์์๋ณด์. (6) | 2025.01.31 |
[ React / UI ] Atomic ๋์์ธ ํจํด์ ๋ํด ์์๋ณด์. (4) | 2024.09.21 |