React Patterns 1 - Props Pass
As props Pattern
// 📌 Before
// 아래 버튼 컴포넌트를 a태그처럼 쓰고싶은데 확장이 어렵다.
<Button href="/" size="lg" >
Link
</Button>
// 📌 After
const Button = ({As = "button", size="lg", ...props})=>{
// (size 와 같은 스타일 로직 있음)
return (
<As {...props} className={`${styles.button} ${styles[size]}`} />
)
}
// 아래처럼 As Props를 통해서 Button의 스타일을 그대로 사용하면서 프리미티브 태그를 사용할 수 있다.
<Button As="a" size="lg" href="/">
Link
</Button>
asChild Pattern
const Button = ({ asChild, children, className, ...props }) => {
if (asChild) {
// 자식 요소를 클론하고 props를 병합
return React.cloneElement(children, {
...props,
className: `${styles.button} ${className || ''}`,
});
}
// React.createElement(type, props, ...children)
return React.createElement(
'button',
{
className: `${styles.button} ${className || ''}`,
...props,
},
children,
);
};
asChild Pattern with Slot
import { isValidElement, Children, cloneElement, ReactNode } from 'react';
interface SlotProps extends React.HTMLAttributes<HTMLElement> {
children?: ReactNode;
}
export function Slot({ children, className, ...props }: SlotProps) {
const child = Children.only(children);
if (!isValidElement(child)) {
return null;
}
const childProps = child.props as Record<string, any>;
const combinedClassName = [childProps.className, className].filter(Boolean).join(' ');
return cloneElement(child, {
...props,
...childProps,
className: combinedClassName || undefined,
} as any);
}
// Usage
// export function Button({ children, asChild, ...props }: { children: ReactNode; asChild?: boolean }) {
// const Comp = asChild ? Slot : 'button';
// return <Comp {...props} className="button" />;
// }
Type of Pass
📌 복습, Component vs Element
1.Component : JSX를 리턴하는 함수형, 클래스형 컴포넌트
- 호출이 가능하다.
<Element />형태로 호출 (반환값은 Element)
2.Element : 리액트 요소의 형태, 컴포넌트 함수를 호출 한 결과
const sub = <div>o</div>형태- React.createElement, React.cloneElement의 결과값
📌 3가지 유형으로 전달 가능
1.ComponentType 전달
2.Element 전달
3.Render Props 전달
type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;
-> 클래스 컴포넌트, 함수형 컴포넌트 참조 자체를 래퍼런스로 넘겨주는 목적
장점 : 추가적인 컴포넌트의 조작 없이, 참조를 그대로 넘긴다.
type FunctionComponent<P = {}> = (props: P) => ReactElement | null;
-> 인라인 방식으로 컴포넌트를 정의하여 넘겨줄 수 있다.
장점 : 컴포넌트 합성이 가능
- 루트 컴포넌트는 props를 전달해주고. 받은 props로 컴포넌트 정의 가능.
- 루트 컴포넌트는 전달받은 depth1 컴포넌트로 자유롭게 위치 배정, 컴포넌트 복제가 가능.
type Slot = React.ReactNode | null
-> (인라인) 엘리먼트를 넘겨주는 목적
장점 : props로 엘리먼트를 전달받아, 적절한 위치에 배치 가능.
📌 React 컴포넌트 합성에 자주 사용되는 함수들
- React.Children.map
- React.Children.forEach
- React.Children.count
- React.Children.only
- React.Children.toArray
- React.createElement - JSX대신 직접 사용
- React.cloneElement - props를 병합할때 사용
- React.isValidElement - element 검증
- React.Fragment
- React.memo
import React from 'react';
function Wrapper({ children }) {
// 1. React.Children.map: 각 자식에 props 추가 (key 유지)
const mappedChildren = React.Children.map(children, (child, index) => {
// 유효한 React 엘리먼트일 경우에만 props를 복제하고 추가
if (React.isValidElement(child)) {
return React.cloneElement(child, {
index: index,
style: { color: index % 2 === 0 ? 'blue' : 'red' }
});
}
return child;
});
// 2. React.Children.count: 자식의 개수 세기
const childCount = React.Children.count(children);
// 3. React.Children.toArray: 자식들을 배열로 변환 후 순서 뒤집기
const reversedChildren = React.Children.toArray(children).reverse();
// 4. React.Children.forEach: 각 자식에 대한 로그 출력 (부수 효과)
console.log('--- React.Children.forEach 출력 ---');
React.Children.forEach(children, (child) => {
if (React.isValidElement(child)) {
console.log(`Child type: ${child.type}`);
} else {
console.log(`Child content: ${child}`);
}
});
console.log('---------------------------------');
// 5. React.Children.only: (예외 발생 가능성이 있어 주석 처리)
// const onlyOne = React.Children.only(children); // 자식이 하나가 아니면 에러 발생
return (
<div style={{ border: '1px solid gray', padding: '10px' }}>
<h3>총 자식 수: {childCount}</h3>
<h4>1. map 결과 (스타일 적용):</h4>
<div>{mappedChildren}</div>
<h4>2. toArray 결과 (순서 뒤집기):</h4>
<div>{reversedChildren}</div>
</div>
);
}
// 사용 예시
function App() {
return (
<Wrapper>
<div>첫 번째 아이템</div>
<span>두 번째 아이템</span>
{'텍스트 노드'}
{null} {/* null/undefined는 무시되지만, map/forEach는 'null'로 순회할 수 있음 */}
</Wrapper>
);
}
import React from 'react';
// 1. React.createElement: JSX 없이 엘리먼트 생성
// <h1 className="main-title">Hello, React!</h1> 와 동일
const headerElement = React.createElement(
'h1',
{ className: 'main-title' },
'Hello, ',
React.createElement('span', { style: { color: 'green' } }, 'React!')
);
function Button({ children, onClick }) {
return <button onClick={onClick}>{children}</button>;
}
// 사용 예시
function ComponentManipulator() {
const customButton = <Button onClick={() => alert('Original Click')}>Original Text</Button>;
// 2. React.cloneElement: props 병합
const clonedButton = React.cloneElement(
customButton,
{
onClick: () => alert('Cloned Click!'), // onClick props 덮어쓰기
style: { backgroundColor: 'yellow' } // 새로운 props 추가
},
'Cloned Text' // children 덮어쓰기
);
// 3. React.isValidElement: 엘리먼트 검증
const isHeaderValid = React.isValidElement(headerElement); // true
const isStringValid = React.isValidElement('Hello'); // false
console.log(`Is headerElement a valid React Element? ${isHeaderValid}`);
console.log(`Is 'Hello' a valid React Element? ${isStringValid}`);
return (
<div>
{headerElement}
<p>Original Button:</p>
{customButton}
<p>Cloned Button (props와 children이 변경됨):</p>
{clonedButton}
</div>
);
}
Render Props 패턴
📌 Render props to chilren
- children에게 props를 전달하여 컴포넌트를 합성시키는 방식
import type React from 'react';
import { useState } from 'react';
// Render Props children
interface RenderPropsChildProps {
// children의 props를 타이핑
children: (props: {
count: number;
countUp: () => void;
countDown: () => void;
reset: () => void;
}) => React.ReactElement;
}
function RenderPropsChild({ children }: RenderPropsChildProps) {
const [count, setCount] = useState(0);
const countUp = () => {
setCount(count + 1);
};
const countDown = () => {
setCount(count - 1);
};
const reset = () => {
setCount(0);
};
return <div>{children({ count, countUp, countDown, reset })}</div>;
}
export default RenderPropsChild;
---
const LIST_ITEM_COUNT = 50;
function Widget() {
return (
<div>
{/* 1. render props child */}
<RenderPropsChild>
{({ count, countUp, countDown, reset }) => {
return (
<div>
<div>{count}</div>
<button type="button" onClick={countUp}>
+
</button>
<button type="button" onClick={countDown}>
-
</button>
<button type="button" onClick={reset}>
reset
</button>
</div>
);
}}
</RenderPropsChild>
</div>
);
}
📌 Render props to multiple props
- chlidren props뿐 아니라 다른 요소로 렌더러 함수를 받아도 된다.
import { useState } from 'react';
import React from 'react';
function RenderPropsMultiple({
children,
titleRender,
listItemRender,
listItemCount,
}: {
children: React.ReactElement;
titleRender: (props: { onToogle: () => void }) => React.ReactElement;
listItemRender: (props: {
onToogle: () => void;
index: number;
}) => React.ReactElement;
listItemCount: number;
}) {
const [isHidden, setIsHidden] = useState(false);
const onToogle = () => {
setIsHidden((prev) => !prev);
};
return (
<div>
{/* 타이틀 렌더러 위치 지정, 토글 기능 제공 */}
<h2>{titleRender({ onToogle })}</h2>
{/* 스크롤 컨테이너 추가, 리스트 아이템 복사 횟수 조절 */}
<div style={{ maxHeight: '200px', overflow: 'auto' }}>
{!isHidden &&
Array.from({ length: listItemCount }).map((_, index) => {
if (!React.isValidElement(listItemRender({ onToogle, index }))) {
return null;
}
return React.cloneElement(listItemRender({ onToogle, index }), {
key: index,
});
})}
</div>
<hr />
{children}
</div>
);
}
---
function Widget() {
return (
<div>
{/* 2. render props multiple */}
<RenderPropsMultiple
listItemCount={LIST_ITEM_COUNT}
titleRender={({ onToogle }) => (
<h2>
title{' '}
<button type="button" onClick={onToogle}>
toogle
</button>{' '}
</h2>
)}
listItemRender={({ onToogle, index }) => (
<div>
Item {index}
{index === LIST_ITEM_COUNT - 1 && (
<button type="button" onClick={onToogle}>
x
</button>
)}
</div>
)}
>
<div>did it </div>
</RenderPropsMultiple>
</div>
);
}