Published on

React Component Design - Pattern Summary

Authors
  • avatar
    Name
    Alex Yu
    Twitter

讓我們來綜合整理一下介紹過的 pattern

Compound Component&Provider/Context

Compound ComponentProvider/Context都將state保留在自身的component內,並且提供定義好的介面跟元件給外部使用,讓使用方式跟元件符合一定程度的上下文語意,而且內部元件的render順序可以讓外部決定。Compound Component為了解決prop drilling的問題通常會搭配Provider/Context使用,並且可以增加一些機制避免元件在錯誤的地方被使用,或者透過檢查children的方式讓不在定義介面內的children component也可以正常render

function Usage({ onToggle = (...args) => console.log('onToggle', ...args) }) {
  return (
    <Toggle onToggle={onToggle}>
      <Toggle.On>The button is on</Toggle.On>
      <Toggle.Off>The button is off</Toggle.Off>
      <div>
        <Toggle.Button />
      </div>
    </Toggle>
  );
}

Render Props

將想要傳給childrenprops傳給childrenrenderstate跟一部分的function還是保留在自身的component內,換言之,只保留最小限度的邏輯在自身component內,給外部最大程度的自由度去render想要的東西。

function Usage({ onToggle = (...args) => console.log('onToggle', ...args) }) {
  return (
    <Toggle onToggle={onToggle}>
      {({ on, toggle }) => (
        <div>
          {on ? 'The button is on' : 'The button is off'}
          <Switch on={on} onClick={toggle} />
          <hr />
          <button aria-label="custom-button" onClick={toggle}>
            {on ? 'on' : 'off'}
          </button>
        </div>
      )}
    </Toggle>
  );
}

Prop Collections and Getters

Prop Collections 用意在集結一些共用的propsProp Getters用意在改寫Prop Collections提供出來的propsProp CollectionsProp Getters通常搭配一起使用,保留給外部使用者有可以客製化的方式。

function Usage({
  onToggle = (...args) => console.log('onToggle', ...args),
  onButtonClick = () => console.log('onButtonClick'),
}) {
  return (
    <Toggle onToggle={onToggle}>
      {({ on, getTogglerProps }) => (
        <div>
          <Switch {...getTogglerProps({ on })} />
          <hr />
          <button
            {...getTogglerProps({
              'aria-label': 'custom-button',
              onClick: onButtonClick,
              id: 'custom-button-id',
            })}
          >
            {on ? 'on' : 'off'}
          </button>
        </div>
      )}
    </Toggle>
  );
}

State Initializers and Reducer

State Initializers讓外部使用者可以重新 reset state到一開始的狀態。State Reducer則是讓外部使用者決定action觸發時要執行的邏輯,在自身的component內還是會有自身的state,但如果外部使用者有傳入state reducer時則會讓外部使用者決定最終的state,並且傳回給內部component做最終的state更新。

function Usage({
  initialOn = false,
  onToggle = (...args) => console.log('onToggle', ...args),
  onReset = (...args) => console.log('onReset', ...args),
}) {
  return (
    <Toggle initialOn={initialOn} onToggle={onToggle} onReset={onReset}>
      {({ getTogglerProps, on, reset }) => (
        <div>
          <Switch {...getTogglerProps({ on })} />
          <hr />
          <button onClick={() => reset()}>Reset</button>
        </div>
      )}
    </Toggle>
  );
}
class Usage extends React.Component {
  static defaultProps = {
    onToggle: (...args) => console.log('onToggle', ...args),
    onReset: (...args) => console.log('onReset', ...args),
  };

  initialState = { timesClicked: 0 };

  state = this.initialState;

  handleToggle = (...args) => {
    this.setState(({ timesClicked }) => ({
      timesClicked: timesClicked + 1,
    }));
    this.props.onToggle(...args);
  };

  handleReset = (...args) => {
    this.setState(this.initialState);
    this.props.onReset(...args);
  };

  toggleStateReducer = (state, changes) => {
    if (changes.type === 'forced') {
      return changes;
    }
    if (this.state.timesClicked >= 4) {
      return { ...changes, on: false };
    }
    return changes;
  };

  render() {
    const { timesClicked } = this.state;

    return (
      <Toggle
        stateReducer={this.toggleStateReducer}
        onToggle={this.handleToggle}
        onReset={this.handleReset}
        ref={this.props.toggleRef}
      >
        {({ on, toggle, reset, getTogglerProps }) => (
          <div>
            <Switch
              {...getTogglerProps({
                on: on,
              })}
            />
            {timesClicked > 4 ? (
              <div data-testid="notice">
                Whoa, you clicked too much!
                <br />
                <button onClick={() => toggle({ type: 'forced' })}>
                  Force Toggle
                </button>
                <br />
              </div>
            ) : timesClicked > 0 ? (
              <div data-testid="click-count">Click count: {timesClicked}</div>
            ) : null}
            <button onClick={reset}>Reset</button>
          </div>
        )}
      </Toggle>
    );
  }
}

Control Props

Control Props在自身的component保留自身的state,若外部有傳入value時,則 state 的控制權便交給外部使用者。通常在自身componentstate改變時也會需要通知外部使用者,讓外部使用者有可以決定最終value變化的權利。

class Usage extends React.Component {
  state = { bothOn: false };

  lastWasButton = false;

  handleStateChange = (changes) => {
    const isButtonChange =
      changes.type === Toggle.stateChangeTypes.toggleOn ||
      changes.type === Toggle.stateChangeTypes.toggleOff;

    if (
      changes.type === Toggle.stateChangeTypes.toggle ||
      (this.lastWasButton && isButtonChange)
    ) {
      this.setState({ bothOn: changes.on });
      this.lastWasButton = false;
    } else {
      this.lastWasButton = isButtonChange;
    }
  };

  render() {
    const { bothOn } = this.state;
    const { toggle1Ref, toggle2Ref } = this.props;

    return (
      <div>
        <Toggle
          on={bothOn}
          onStateChange={this.handleStateChange}
          ref={toggle1Ref}
        />
        <Toggle
          on={bothOn}
          onStateChange={this.handleStateChange}
          ref={toggle2Ref}
        />
      </div>
    );
  }
}

Higher-Order Component

Higher-Order Component簡稱HOC,通常是將一些共用的邏輯放在此處,介面是傳入一個component,回傳一個新的component。但這種方式有一些缺點,而且在現今 React 版本已過時,可以選擇用Render Propshook方式取代。

function withToggle(Component) {
  function Wrapper(props, ref) {
    return (
      <Toggle.Consumer>
        {(toggleContext) => (
          <Component toggle={toggleContext} {...props} ref={ref} />
        )}
      </Toggle.Consumer>
    );
  }

  Wrapper.displayName = `withToggle(${
    Component.displayName || Component.name
  })`;

  return hoistNonReactStatics(React.forwardRef(Wrapper), Component);
}

const Layer1 = () => <Layer2 />;
const Layer2 = withToggle(({ toggle: { on } }) => (
  <Fragment>
    {on ? 'The button is on' : 'The button is off'}
    <Layer3 />
  </Fragment>
));
const Layer3 = () => <Layer4 />;
const Layer4 = withToggle(({ toggle: { on, toggle } }) => (
  <Switch on={on} onClick={toggle} />
));

function Usage({ onToggle = (...args) => console.log('onToggle', ...args) }) {
  return (
    <Toggle onToggle={onToggle}>
      <Layer1 />
    </Toggle>
  );
}

延伸問題

  • HOC vs Render Props vs Hooks

最後想介紹一個網站:https://www.patterns.dev/ 裡面介紹了更多種的 design pattern,包含如何用 js 實現一般常見的 pattern,以及 React 跟 Vue 相關的 patterns,有興趣的人可在參考,但請謹記 pattern 是用在適合的場景下才採取的一種策略,並不是一定要將 code 改成 pattern 的寫法。

Referenece