headless ui를 만들어보려 하는데 라이브러리를 까보면 대부분 context api를 사용해서 구현되어 있는 것 같았다. 그래서 headless ui 만드는 김에 context api에 대해 기록하고자 한다.
리액트는 상태값을 공유할 때 props로 전달하는데, 간혹 props drilling이 발생할 수 있다.
function App() {
return <RootComponent value="hi" />
}
function RootComponent({ value }) {
return (
<>
<AComponent value={value} />
<BComponent value={value} />
</>
)
}
function AComponent({ value }) {
return <p>AComponent: {value}</p>
}
function BComponent({ value }) {
return <p>BComponent: {value}</p>
}
지금은 단순히 값을 하나만 넘겨줘서 단순해 보이지만 넘겨주는 값이 많다면 이는 가독성도 안좋아지고 컴포넌트 유지보수하기에 무리가 있어보인다.
그 때 사용하는게 Context API 이다.
Context는 리액트 컴포넌트 간 상태값을 공유할 수 있도록 도와주는 기능이다. 크게 2가지 목적으로 사용되는 듯 하다.
우선 context를 생성해야한다.
const TextContext = createContext({
text: "" // initialize state
});
function TextProvider({ children, text }) {
const value = useMemo(() => {
return { text }
}, [text])
return <TextContext.Provider value={value}>{children}</TextContext.Provider>
}
이 때 생성된 context도 하나의 컴포넌트이므로 Provider에 감싸서 제공해야한다.
context를 통해 공유할 초기 상태값을 지정한 후 Provider에 심어주면 된다.
context가 전달하는 value는 객체이다.
객체가 변경되면 Provider 내부에 있는 모든 자식 컴포넌트들이 리렌더링된다. 이를 방지하기 위해 useMemo로 캐싱처리를 한다.
위에서 예시로 들었던 AComponent와 BComponent에 적용해보면 다음과 같다.
export default function App() {
return (
<TextProvider text="hi">
<AComponent />
<BComponent />
</TextProvider>
)
}
function AComponent() {
const { text } = useContext(TextContext);
return <p>AComponent: {text}</p>
}
function BComponent() {
const { text } = useContext(TextContext);
return <p>BComponent: {text}</p>
}