const EnhancedComponent = higherOrderComponent(WrappedComponent);
변형(mutation)된 고차 컴포넌트는 누수된 추상화(leaky abstraction)임
고차 컴포넌트(HOC)는 변경(mutation) 대신 인수로 받은 컴포넌트를 컨테이너 컴포넌트로 감싸서 조합(composition)해야함
// class component
function logProps(WrappedComponent) {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log('Current props: ', this.props);
console.log('Previous props: ', prevProps);
}
render() {
return <WrappedComponent {...this.props} />;
}
}
}
고차 컴포넌트(HOC)는 인수로 받은 컴포넌트에 기능을 추가함
고차 컴포넌트는 특정 관심사와 관련 없는 props를 전달해야함
render() {
// 이 HOC에만 해당되는 extraProps는 걸러내서 컴포넌트에 전달하지 않음
const { extraProp, ...passThroughProps } = this.props;
// state나 instance method를 컴포넌트에 props로 전달함
const injectedProp = someStateOrInstanceMethod;
return (
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
// React Redux의 'connect'
const ConnectedComment = connect(commentSelector, commentActions);(CommentList);
// connect는 고차 컴포넌트를 반환하는 고차 함수
const enhance = connect(commentListSelector, commentListActions);
// ehnhance는 Redux store에 연결된 컴포넌트를 반환하는 고차 컴포넌트
const ConnectedComment = enhance(CommentList);
// before
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))
// after
const enhance = compose(
// withRouter와 connect 둘 다 단일 인수 고차 컴포넌트임
withRouter,
connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
function withSubscription(WrappedComponent) {
class WithSubscription extends React.Component {/* ... */}
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
return WithSubscription;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
render
에서 반환된 컴포넌트가 이전 렌더링된 컴포넌트와 동일하다면(===
), 새로운 서브트리와 비교하여 서브트리를 재귀적으로 업데이트함render
에서 반환된 컴포넌트가 이전 렌더링된 컴포넌트와 동일하지 않다면(!==
), 기존 서브트리는 완전히 마운트 해제됨render() {
// render가 호출될 때마다 새로운 버전의 EnhancedComponent가 생성됨
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// 때문에 매번 서브트리가 마운트 해제 후 다시 마운트됨
return <EnhancedComponent />;
}
// 정적 함수를 정의
WrappedComponent.staticMethod = function() {/*...*/}
// 컨포넌트에 고차 컴포넌트를 적용함
const EnhancedComponent = enhance(WrappedComponent);
// 컨테이너 컴포넌트에는 래핑된 컴포넌트의 정적 메서드가 없음
typeof EnhancedComponent.staticMethod === 'undefined' // true
메서드를 반환하기 전에 컴페이너 컴포넌트에 메서드를 복사함
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
hoist-non-react-statics
라이브러리의 hoistNonReactStatic을 사용하여 모든 non-React 정적 메서드를 자동으로 복사할 수 있음import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
// MyComponent.js
export default MyComponent;
export { someFunction };
// withMyComponent.js
import MyComponent, { someFunction } from './MyComponent.js';
ref
가 key
처럼 특별하게 취급되지 때문임React.forwardRef
를 사용해서 고차 컴포넌트의 컨테이너에서 전달받은 ref를 래핑된 컴포넌트에 전달할 수 있음// Compnent => EnhancedComponent
const withHigherOrderComponent = (Component) => (props) =>
<Component {...props} />;
// before
const fetchData = () => {
return { data: null, isLoading: true };
};
const App = () => {
const { data, isLoading } = fetchData();
if (isLoading) return <div>Loading data.</div>;
if (!data) return <div>No data loaded yet.</div>;
if (!data.length) return <div>Data is empty.</div>;
return <TodoList data={data} />;
};
// after
const withConditionalFeedback = (Component) => (props) => {
if (props.isLoading) return <div>Loading data.</div>;
if (!props.data) return <div>No data loaded yet.</div>;
if (!props.data.length) return <div>Data is empty.</div>;
return <Component {...props} />;
};
const App = () => {
const { data, isLoading } = fetchData();
return <TodoList data={data} isLoading={isLoading} />;
};
const BaseTodoList = ({ data }) => {
return (
<ul>
{data.map((item) => (
<TodoItem key={item.id} item={item} />
))}
</ul>
);
};
const TodoList = withConditionalFeedback(BaseTodoList);
const withHigherOrderComponent = (Component, configuration) =>
(props) => <Component {...props} />;
const withHigherOrderComponent = (configuration) => (Component) =>
(props) => <Component {...props} />;
const withConditionalFeedback = (dataEmptyFeedback) => (Component)
=> (props) => {
if (props.isLoading) return <div>Loading data.</div>;
if (!props.data) return <div>No data loaded yet.</div>;
if (!props.data.length)
return <div>{dataEmptyFeedback || 'Data is empty.'}</div>;
return <Component {...props} />;
};
...
const TodoList = withConditionalFeedback('Todos are empty.')(
BaseTodoList
);
위 코드에서 dataEmptyFeedback라는 피드백이 제공되지 않더라도 일반적인 fallback을 사용할 수 있음
다른 조건부 렌더링에 대해서도 피드백을 제공하는 예시
const withConditionalFeedback =
({ loadingFeedback, noDataFeedback, dataEmptyFeedback }) =>
(Component) =>
(props) => {
if (props.isLoading)
return <div>{loadingFeedback || 'Loading data.'}</div>;
if (!props.data)
return <div>{noDataFeedback || 'No data loaded yet.'}</div>;
if (!props.data.length)
return <div>{dataEmptyFeedback || 'Data is empty.'}</div>;
return <Component {...props} />;
};
...
const TodoList = withConditionalFeedback({
loadingFeedback: 'Loading Todos.',
noDataFeedback: 'No Todos loaded yet.',
dataEmptyFeedback: 'Todos are empty.',
})(BaseTodoList);
위 코드에서 여러 인수를 전달하는 대신 하나의 configuration obect를 인수로 전달함
null
을 인수로 전달하지 않아도 됨바깥에서 고차 컴포넌트에 특정 형태(configuration)를 넣고 싶을 때, 고차 컴포넌트를 또 다른 함수로 감싸고 configuration 객체를 인수로 전달함
const withLoadingFeedback = (Component) => (props) => {
if (props.isLoading) return <div>Loading data.</div>;
return <Component {...props} />;
};
const withNoDataFeedback = (Component) => (props) => {
if (!props.data) return <div>No data loaded yet.</div>;
return <Component {...props} />;
};
const withDataEmptyFeedback = (Component) => (props) => {
if (!props.data.length) return <div>Data is empty.</div>;
return <Component {...props} />;
};
const TodoList = withLoadingFeedback(
withNoDataFeedback(
withDataEmptyFeedback(BaseTodoList)
)
);
한 컴포넌트에 여러 고차 컴포넌트를 적용할 때 2가지 주의 사항이 있음
!data null
체크의 반환값에 의존함!props.data.length
에 대한 null pointer 예외가 발생할 수 있음compose()
함수를 사용하여 고차 컴포넌트 함수들을 조합해서 사용할 수 있음
compose()
함수는 함수 배열을 인수로 받아서, 오른쪽에서 왼쪽순으로 함수의 반환값을 다음 함수의 인수로 전달함const compose = (...fns) =>
fns.reduceRight((prevFn, nextFn) =>
(...args) => nextFn(prevFn(...args)),
value => value
);
const TodoList = compose(
withLoadingFeedback,
withNoDataFeedback,
withDataEmptyFeedback
)(BaseTodoList);
const withLoadingFeedback = (feedback) => (Component) => (props) => {
if (props.isLoading) return <div>{feedback}</div>;
return <Component {...props} />;
};
const withNoDataFeedback = (feedback) => (Component) => (props) => {
if (!props.data) return <div>{feedback}</div>;
return <Component {...props} />;
};
const withDataEmptyFeedback = (feedback) => (Component) => (props) => {
if (!props.data.length) return <div>{feedback}</div>;
return <Component {...props} />;
};
const TodoList = compose(
withLoadingFeedback('Loading Todos.'),
withNoDataFeedback('No Todos loaded yet.'),
withDataEmptyFeedback('Todos are empty.')
)(BaseTodoList);