Jun 개발노트

Higher-Order Components

2020-09-27

HOC란?

  • 컴포넌트의 공통 기능을 관리하기 위해 사용되는 함수
  • 컴포넌트를 입력으로 받아서 컴포넌트를 출력해주는 함수이다.
  • HOC를 사용하여 코드의 중복을 줄일 수 있다.

HOC를 사용하여 코드의 중복을 줄일 수 있다.

  1. componentDidMount
  2. componentWillUnmount
  3. handleChange
class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      comments: DataSource.getComments()
    });
  }

  render() {
    return (
      <div>
        {this.state.comments.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}

class BlogPost extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      blogPost: DataSource.getBlogPost(props.id)
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      blogPost: DataSource.getBlogPost(this.props.id)
    });
  }

  render() {
    return <TextBlock text={this.state.blogPost} />;
  }
}

HOC 활용

function withSubscription(WrappedComponent, selectData) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

주의사항

  1. Don’t Mutate the Original Component

    • logProps의 문제점
      • 다른 HOC의 input으로 입력되면 componentDidUpdate가 override된다
      • 함수형 컴포넌트는 componentDidUpdate가 없어 작동하지 않는다.
    function logProps(InputComponent) {
      InputComponent.prototype.componentDidUpdate = function(prevProps) {
        console.log('Current props: ', this.props);
        console.log('Previous props: ', prevProps);
      };
      // The fact that we're returning the original input is a hint that it has
      // been mutated.
      return InputComponent;
    }
    
    // EnhancedComponent will log whenever props are received
    const EnhancedComponent = logProps(InputComponent);
    
    • 해결책
    function logProps(WrappedComponent) {
      return class extends React.Component {
        componentDidUpdate(prevProps) {
          console.log('Current props: ', this.props);
          console.log('Previous props: ', prevProps);
        }
        render() {
          // Wraps the input component in a container, without mutating it. Good!
          return <WrappedComponent {...this.props} />;
        }
      }
    }
    
  2. HOC내에서 render 생명주기 사용하지 않기

    • 매번 새로운 값이라고 판단(React's diffing alogirithm 구분하지 못함)
    render() {
      // A new version of EnhancedComponent is created on every render
      // EnhancedComponent1 !== EnhancedComponent2
      const EnhancedComponent = enhance(MyComponent);
      // That causes the entire subtree to unmount/remount each time!
      return <EnhancedComponent />;
    }
    
  3. Static Method 전달

    function enhance(WrappedComponent) {
      class Enhance extends React.Component {/*...*/}
      // Must know exactly which method(s) to copy :(
      Enhance.staticMethod = WrappedComponent.staticMethod;
      return Enhance;
    }
    
    // hoist-not-react-static 이용
    import hoistNonReactStatic from 'hoist-non-react-statics';
    function enhance(WrappedComponent) {
      class Enhance extends React.Component {/*...*/}
      hoistNonReactStatic(Enhance, WrappedComponent);
      return Enhance;
    }
    
    // 정적메소드만 따로 export하여 사용하기
    // Instead of...
    MyComponent.someFunction = someFunction;
    export default MyComponent;
    
    // ...export the method separately...
    export { someFunction };
    
    // ...and in the consuming module, import both
    import MyComponent, { someFunction } from './MyComponent.js';
    
  4. Ref는 전달

    function logProps(Component) {
      class LogProps extends React.Component {
        render() {
          const {forwardedRef, ...rest} = this.props;
          return <Component ref={forwardedRef} {...rest} />;
        }
      }
      return React.forwardRef((props, ref) => {
        return <LogProps {...props} forwardedRef={ref} />;
      });
    }