컴파운드 컴포넌트 + uncontrolled component + co-location 삼신기로,
API 컨슈머의 상태 관리 부담을 덜어줄 수 있습니다.
또한 전역 상태도 피할 수 있습니다.
원문 번역입니다 : https://jjenzz.com/avoid-global-state-colocate
코로케이션이 뭔가요?(What is colocation anyway?)
리액트와 코로케이션(Colocation in React)
첫 번째 시도입니다.
export const Dialog = ({ onClose, ...props }) => {
const ref = React.useRef();
React.useEffect(() => {
const handleClick = event => {
const dialog = ref.current;
if (!dialog?.contains(event.target)) {
onClose();
}
};
document.addEventListener('click', handleClick, { capture: true });
return () => {
document.removeEventListener('click', handleClick, { capture: true });
};
});
return ReactDOM.createPortal(<div {...props} ref={ref} />, document.body);
};
Dialog 컴포넌트에 몇 개의 이벤트 리스너를 추가했습니다.
전역 상태를 방지하는 방법을 생각하여 한 번에 하나만 열 수 있도록 허용하는 컴포넌트 요구 사항을 함께 배치할 수 있었습니다.
좀 더 복잡한 패턴(More complex patterns)
제품 : Modulz
- 전역 상태가 화면에서 선택했다고 알려주는 내용에 따라 표시할 속성을 결정하는 많은 스위치 절을 도입합니다.
- 화면에서 사물의 모양을 변경하기 위해 전역 상태 또는 props 드릴을 통해 선택된 속성을 전달합니다.
코드 측면에서도 트리 셰이킹하기 어려울 것입니다.
누군가 Rectangle(이미지 메인 화면 내부)을 렌더링하지 않는다 해도,
그 로직은 관계없이 직사각형을 처리하기 위해 앱 전체에 퍼질 것입니다.
해결책
이렇게 복잡한 것에 코로케이션을 어떻게 적용할 수 있을까요?
Dialog와 같은 아이디어를 적용해 보겠습니다.
왜 단일 PropertiesPanel 인스턴스를 재사용하려고 할까요?
- Rectangle과 관련된 내용만 재사용하고 싶습니다
- 실제로 재사용 하고 싶은것은 위치입니다.
첫 번째 요구 사항은 사각형이 재사용할 수 있는 각 속성 패널 부분에 대한 컴포넌트를 생성하여 해결되지만,
문제는 해당 코드를 Rectangle 컴포넌트와 함께 배치하면 사이드바에 표시되지 않는다는 것입니다.
잠깐만, Dialog는 컴포넌트 렌더링 위치와 상관없이 화면 중간에 나타났었죠?
컴포넌트 렌더링 위치와 돔이 삽입되는 위치와의 분리 > Portal입니다!
프로퍼티 패널이 포털로 들어갈 수 있는 사이드바용 빈 공간을 만들면,
코로케이션의 힘을 사용할 수 있습니다.
import {
Styler,
StylerSize,
StylerBackgroundColor,
StylerBorder,
// StylerTextShadow - not necessary in Rectangle so will be tree-shaken
} from './Styler';
const RectangleContext = React.createContext({});
export const Rectangle = ({ children }) => {
const ref = React.useRef(null);
const [style, setStyle] = React.useState({});
const [selected, setSelected] = React.useState(false);
React.useEffect(() => {
// click outside logic to set `selected` to `false`
});
return (
<RectangleContext.Provider
value={React.useMemo(
() => ({ selected, style, onStyleChange: setStyle }),
[style, selected],
)}
>
<div style={style} ref={ref} onClick={() => setSelected(true)}>
{children}
</div>
</RectangleContext.Provider>
);
};
// 해당 컴포넌트는 패널에 그려집니다!!!
export const RectangleStyler = () => {
const context = React.useContext(RectangleContext);
return context.selected ? (
<Styler style={context.style} onStyleChange={context.onStyleChange}>
<StylerSize />
<StylerBackgroundColor />
<StylerBorder />
</Styler>
) : null;
};
Rectangle 내부의 Styler 컴포넌트는 내부에서 렌더링한 프로퍼티를 기반으로 스타일 객체를 반환합니다.
이러한 속성이 변경되면 Rectangle의 style 속성을 업데이트합니다.
주 : 내부 Styler의 데이터를 이용해 외부 Rectangle의 스타일 업데이트
우리의 Sketch 열화버전에서 사용하기 전에 RectangleStyler를 래핑하면서,
사이드바에서 렌더링할 SidebarPortal 컴포넌트가 필요합니다.
주 : RectangleStyler는 SidebarPortal 내부에서 그려집니다.
const SidebarContext = React.createContext([]);
export const SidebarProvider = props => {
const sidebarState = React.useState(null);
return <SidebarContext.Provider value={sidebarState} {...props} />;
};
export const Sidebar = () => {
const [, setSidebar] = React.useContext(SidebarContext);
return <div ref={setSidebar} />;
};
export const SidebarPortal = ({ children }) => {
const [sidebar] = React.useContext(SidebarContext);
return sidebar ? ReactDOM.createPortal(children, sidebar) : null;
};
// Main.js
export const Main = () => (
<main>
<Rectangle>
<SidebarPortal>
<RectangleStyler />
</SidebarPortal>
</Rectangle>
</main>
);
// App.js
export const App = () => (
<div>
<SidebarProvider>
<Main />
<Sidebar />
</SidebarProvider>
</div>
);
그리고 짜잔! 직사각형을 클릭하면 사이드바에서 해당 속성이 열립니다.
하지만 아직 문제가 있습니다. 사이드바에서 스타일러와 상호 작용하면 닫힙니다.
아래에서 테스트해 보세요.
이는 외부 클릭 로직 때문입니다.
rectangle.contains를 이용해 내부를 선택하는지를 파악하고 있습니다.
스타일러는 사이드바에 있기 때문에 패널이 닫히게 됩니다.
주 : DOM 위치와 관계없이 React Tree 내 관계에 따라 이벤트 버블링
export const Rectangle = ({ children }) => {
const ref = React.useRef();
const [style, setStyle] = React.useState({});
const [selected, setSelected] = React.useState(false);
const isClickInsideRef = React.useRef(false);
React.useEffect(() => {
const handleClick = () => {
if (!isClickInsideRef.current) setSelected(false);
isClickInsideRef.current = false;
};
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('click', handleClick);
};
});
return (
<RectangleContext.Provider
value={React.useMemo(
() => ({ selected, style, onStyleChange: setStyle }),
[style, selected],
)}
>
<div
style={style}
ref={ref}
onClick={() => setSelected(true)}
onClickCapture={() => (isClickInsideRef.current = true)}
>
{children}
</div>
</RectangleContext.Provider>
);
};
React 클릭 이벤트를 통해 isClickInside의 불리언 값을 설정합니다.
클릭이 RectangleStyler에서 발생하면,
해당 컴포넌트는 리액트 트리의 자식이며 Rectangle로 전파되기 때문에 true가 됩니다.
더 나아가기
Uncontrolled Compound Components
다음은 Feedback 컴포넌트로 Dialog를 사용하여 코로케이션 원칙을 적용하는 방법을 보여줍니다.
import * as Dialog from '@radix-ui/react-dialog';
export const Feedback = ({ children, ...props }) => (
<Dialog.Root>
<Dialog.Trigger asChild>{children}</Dialog.Trigger>
<Dialog.Content {...props}>
<h2>Thoughts?</h2>
<form>
<textarea />
<button>Submit feedback</button>
</form>
<Dialog.Close>Close</Dialog.Close>
</Dialog.Content>
</Dialog.Root>
);
// render (
// <Feedback>
// <PrimaryButton>Feedback</PrimaryButton>
// </Feedback>
//
// ...
//
// <Feedback>
// <SecondaryButton>Feedback</SecondaryButton>
// </Feedback>
// )
결론
코로케이션은 복잡성을 크게 줄일 수 있습니다.
이것이 인스턴스 재사용보다 컴포넌트 재사용을 선호하는
애플리케이션에 원칙을 적용하는 데 영감을 주었으면 합니다.
'FrontEnd' 카테고리의 다른 글
리액트와 가상돔(virtual dom) (0) | 2022.10.18 |
---|---|
리액트 디자인 패턴 : React Portal[리액트 포털/리액트 포탈]과 이벤트 버블링 (0) | 2022.10.18 |
컴파운드 컴포넌트 잘만들기 2편 : Smarter, Dumb Breadcrumb (0) | 2022.10.17 |
컴파운드 컴포넌트 잘만들기 1편 : 컴파운드 컴포넌트 알아보기 (0) | 2022.10.17 |
보다 탄력적인 웹을 위한 점진적 향상[Progressively enhance for a more resilient web] (0) | 2022.10.14 |