工程師與貓
ESC
Content
    ↑↓ navigate open esc close
    Published on

    React Component Design - Pattern Summary

    Authors
    • avatar
      Name
      Alex Yu

    讓我們來綜合整理一下介紹過的 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