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

    React Component Design - Provider

    Authors
    • avatar
      Name
      Alex Yu

    Provider主要是為了解決Props drilling的問題,Props drilling指的是props要傳好幾層往下到想要存取這個props的 component,例如

    class Toggle extends React.Component {
      state = { on: false };
      toggle = () => {
        /*...*/
      };
      render() {
        return this.props.children({ ...this.state, toggle: this.toggle });
      }
    }
     
    const Layer1 = ({ toggle, ...props }) => <Layer2 toggle={toggle} />;
    const Layer2 = ({ toggle, ...props }) => <Layer3 toggle={toggle} />;
    const Layer3 = ({ toggle, ...props }) => <button onClick={toggle} />;
     
    class App extends React.Component {
      handleToggle = () => {};
     
      render() {
        return (
          <Toggle onToggle={this.handleToggle}>
            <Layer1 />
          </Toggle>
        );
      }
    }

    ReactProvider/Context API 能解決這個問題,讓props中間不用傳遞這麼多層,只要使用Consumer就可以存取Provider裡面的value,來看看實際例子

    import React, { Fragment } from 'react';
    import { Switch } from '../switch';
     
    const ToggleContext = React.createContext();
     
    class Toggle extends React.Component {
      static Consumer = ToggleContext.Consumer;
     
      state = { on: false };
     
      toggle = () =>
        this.setState(
          ({ on }) => ({ on: !on }),
          () => this.props.onToggle(this.state.on)
        );
     
      render() {
        return (
          <ToggleContext.Provider
            value={{ on: this.state.on, toggle: this.toggle }}
            {...this.props}
          />
        );
      }
    }
     
    const Layer1 = () => <Layer2 />;
    const Layer2 = () => (
      <Toggle.Consumer>
        {({ on }) => (
          <Fragment>
            {on ? 'The button is on' : 'The button is off'}
            <Layer3 />
          </Fragment>
        )}
      </Toggle.Consumer>
    );
    const Layer3 = () => <Layer4 />;
    const Layer4 = () => (
      <Toggle.Consumer>
        {({ on, toggle }) => <Switch on={on} onClick={toggle} />}
      </Toggle.Consumer>
    );
     
    function Usage({ onToggle = (...args) => console.log('onToggle', ...args) }) {
      return (
        <Toggle onToggle={onToggle}>
          <Layer1 />
        </Toggle>
      );
    }

    驗證 Consumer

    上面的例子是最簡易版的Provider,但這邊的Consumer要在Provider底下使用才會正常,如果我們希望Consumer不是在Provider底下使用時就噴error,可以改寫成下面的方式

    import React, { Fragment } from 'react';
    import { Switch } from '../switch';
     
    const ToggleContext = React.createContext();
     
    function ToggleConsumer(props) {
      return (
        <ToggleContext.Consumer {...props}>
          {(context) => {
            if (!context) {
              throw new Error(
                `Toggle.Consumer cannot be rendered outside the Toggle component`
              );
            }
            return props.children(context);
          }}
        </ToggleContext.Consumer>
      );
    }
     
    class Toggle extends React.Component {
      static Consumer = ToggleConsumer;
     
      // same as before
    }

    避免不必要的 re-render

    Providervalue中除了會因state 改變而 re-render 以外,toggle也會在每次 re-render 時產生一個新的實例,如果我們要避免不必要的 re-render,可以用下面的技巧

    class Toggle extends React.Component {
      static Consumer = ToggleConsumer;
     
      toggle = () =>
        this.setState(
          ({ on }) => ({ on: !on }),
          () => this.props.onToggle(this.state.on)
        );
     
      state = { on: false, toggle: this.toggle };
     
      render() {
        return <ToggleContext.Provider value={this.state} {...this.props} />;
      }
    }

    支援 Render Props

    在某些情況我們可能會希望Provider支援Render Props的作法,讓上層決定要做客製化的 ui 顯示

    class Toggle extends React.Component {
      // same as before
     
      render() {
        const { children, ...rest } = this.props
        const ui =
          typeof children === 'function' ? children(this.state) : children
     
        return (
          <ToggleContext.Provider value={this.state} {...rest}>
            {ui}
          </ToggleContext.Provider>
        )
      }
    }
     
    function Usage({
      onToggle = (...args) => console.log('onToggle', ...args),
    }) {
      return (
        <Toggle onToggle={onToggle}>
    		{({ on }) => (
    			on ? <Layer1 /> : <OtherLayer>
    		)}
        </Toggle>
      )
    }

    支援 Compound Component

    如果我們希望Toggle保留一定的介面供外部使用,並且用Compound Component的形式將一些特定的 component export 出去給外部使用,可以參考下面例子

    class Toggle extends React.Component {
      static Consumer = ToggleConsumer;
     
      static On = ({ children }) => (
        <ToggleConsumer>{({ on }) => (on ? children : null)}</ToggleConsumer>
      );
     
      static Off = ({ children }) => (
        <ToggleConsumer>{({ on }) => (on ? null : children)}</ToggleConsumer>
      );
     
      static Button = (props) => (
        <ToggleConsumer>
          {({ on, toggle }) => <Switch on={on} onClick={toggle} {...props} />}
        </ToggleConsumer>
      );
     
      // same as before
    }
     
    const Layer1 = () => <Layer2 />;
    const Layer2 = () => (
      <Fragment>
        <Toggle.On>The button is on</Toggle.On>
        <Toggle.Off>The button is off</Toggle.Off>
        <Layer3 />
      </Fragment>
    );
    const Layer3 = () => <Layer4 />;
    const Layer4 = () => <Toggle.Button />;
     
    function Usage({ onToggle = (...args) => console.log('onToggle', ...args) }) {
      return (
        <Toggle onToggle={onToggle}>
          <Layer1 />
        </Toggle>
      );
    }