Frontend Scrapbook

Notes that make a difference

Advanced React Patterns

By admin

on Fri Sep 11 2020

Patterns to enhance the flexibility and simplicity of both your components and implementations

Compound Components

<select> // select hold on to the state and each option as value
 <option>
 <option>
</select>

In React, we can build compound components like below. We get flexibility in the order of how components are rendered. Toggle.Button is responsible for render the button. Toggle.On for the text when the state of the button is ‘on’ etc.

<Toggle onToggle={onToggle}>
      <Toggle.On>The button is on</Toggle.On>
      <Toggle.Off>The button is off</Toggle.Off>
      <Toggle.Button />
</Toggle>

from within the Toggle Component we could define children elements are static properties and use React.Children.map an React.cloneElement
....
static On = (props) => (props.on ? props.children : null)
static Off = (props) => (props.on ? null : props.children)
static Button = ({on, toggle}) => (
    <Switch on={on} onClick={toggle} />
)
....
....

render(){
  return React.Children.map(this.props.children, (childElement) 
    => {
      return React.cloneElement(childElement, {
        on: this.state.on,
        toggle: this.toggle,
      })
  })
}

Flexible Compound Compounds

<Toggle onToggle={onToggle}>
   <Toggle.On>The button is on</Toggle.On>
   <Toggle.Off>The button is off</Toggle.Off>
   <div>
    <Toggle.Button />
   </div>
</Toggle>

In the above example, the React.cloneElement method will not help to receive props as the React.cloneElement is performed on the direct children of Toggle Component. To have this flexibility into our Compound Components, we can use Context API

const ToggleContext = React.createContext({
  on: false,
  toggle: () => {},
}) // initialize with some default value.

....
....
In the Toggle's render method,
render() {
  return (
   <ToggleContext.Provider
     value={{on: this.state.on, toggle: this.toggle}}>
        {this.props.children}
   </ToggleContext.Provider>
  )
}

We could potentially improve the performance by moving the object to state.
Because every time render is called, value object is re-created, which will cause re-render all consumers. We can avoid this behavior by delcaring,

state = {on: false, toggle: this.toggle}

So we have,

render() {
  return (
   <ToggleContext.Provider
     value={state}>
        {this.props.children}
   </ToggleContext.Provider>
  )
}

In the Toggle's static functions (children), we could consume the context value like below.

static Button = (props) => {
  return (
      <ToggleContext.Consumer>
        {(context) => (
          <Switch
            on={context.on}
            onClick={context.toggle}
            {...props}
          />
        )}
      </ToggleContext.Consumer>
  )
}

Render Prop Component

We get more flexibility with this pattern.

<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>

In Toggle Component's class,
state = {on: false};
....
....
In the Toggle component's render, we could then
return () {
 return this.props.children({on: false, toggle: this.toggle});
}


We could do Component Injection as well, but render props are more powerful. React.createElement will add more layer of nesting in React Tree. We could get all benefits by using render props.

functon ToggleChild = ({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 onToggle={onToggle}>
      {ToggleChild}
</Toggle>

Inside Toggle Component's render
render() {
 return React.createElement(this.props.children, {on: 
this.state.on, toggle: this.toggle });
}


We could also implement Render Props pattern by passing as property to Component ( through 'render' ). But as children, it gives consistency as Context API also uses similar pattern as for stylistic ( easier to see properties passed to components ) reasons.

Prop Collections

Instead of individually passing the props to components, we could pass as Props Collection. Useful in cases like Render Prop. You could pass a collection object to this.props.children function, especially when we have many props to pass into.

//ToggleComponent
getStateAndHelpers() {
    return {
      on: this.state.on,
      toggle: this.toggle,
      togglerProps: {
        onClick: this.toggle,
        'aria-expanded': this.state.on,
      },
    }
  }

render() {
   return this.props.children(this.getStateAndHelpers())
}

The problem with the above approach is that if the caller wants to have have its own click handler, then we can either have to override the props returned by getStateHelpers. One approach to solve this problem is using Prop Getters

Prop Getters

getStateAndHelpers() {
    return {
      on: this.state.on,
      toggle: this.toggle,
      togglerProps: {
        'aria-pressed': this.state.on,
        onClick: this.toggle,
      },
      getTogglerProps: ({onClick, ...props}) => {
        return {
          onClick: (...args) => {
            onClick && onClick(...args)
            this.toggle()
          },
          'aria-expanded': this.state.on,
          ...props,
        }
      },
    }
  }

<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>

getTogglerProps can be called with a custom object which then can combine the props. 

State Reducer Component Pattern

This is a very useful pattern when used with If we need to have a logic in Render Props that can control the internal state of the Toggle Component, say, to disable the Toggle component after a number of clicks, for example, we could use a State Reducer Component pattern. In this pattern, we pass a state reducer to the Toggle Component

state = {timesClicked: 0};
toggleStateReducer = (state, changes) => {
    if (this.state.timesClicked >= 4) {
      return {...changes, on: false} // if it is clicked more than 3 times. Toggle will always be in 'off' state.
    }
    return changes;
}
render() {
    const {timesClicked} = this.state
    return (
      <Toggle
        stateReducer={this.toggleStateReducer}
        onToggle={this.handleToggle}
        onReset={this.handleReset}
      >
        {(toggle) => (
          <div>
            <Switch
              {...toggle.getTogglerProps({
                on: toggle.on,
              })}
            />
            {timesClicked > 4 ? (
              <div data-testid="notice">
                Whoa, you clicked too much!
                <br />
              </div>
            ) : timesClicked > 0 ? (
              <div data-testid="click-count">
                Click count: {timesClicked}
              </div>
            ) : null}
            <button onClick={toggle.reset}>Reset</button>
          </div>
        )}
      </Toggle>
    )
}

In the Toggle Component, we then use somethng like

....
....
internalSetState = (changes, callback) => {
    this.setState((currState) => {
      const changesObj =
        typeof changes === 'function' ? changes(currState) : changes
      const reducedChanges = this.props.stateReducer(
        currState,
        changesObj,
      )
      return reducedChanges
    }, callback)
}

Control Props Pattern

In situations when want to control the Component from the parent component, we could use this pattern.

//Parent Component
state = {bothOn: false}
  handleToggle = (on) => {
    this.setState({bothOn: on})
  }
  render() {
    const {bothOn} = this.state
    return (
      <div>
        <Toggle
          on={bothOn}
          onToggle={this.handleToggle}
        />
        <Toggle
          on={bothOn}
          onToggle={this.handleToggle}
        />
      </div>
    )
  }
}


class Toggle extends React.Component {
  state = {on: false}
  isOnControlled() {
    return this.props.on !== undefined
  }

  getState() {
    return {
      on: this.isOnControlled() ? this.props.on : this.state.on,
    }
  }

  toggle = () => {
    if (this.isOnControlled()) {
      this.props.onToggle(!this.getState().on)
      return;
    }
    this.setState(
      ({on}) => ({on: !on}),
      () => {
        this.props.onToggle(this.getState().on)
      },
    )
  }
  render() {
    const {on} = this.getState()
    return <Switch on={on} onClick={this.toggle} />
  }
}

HOC ( Higher Order Components )

withToggle(({toggle} => {toggle.on ? 'Disabled' : 'Enablded' };

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)
}

hoistNonReactStatics module can be use incase if want to expose static methods of the passed component to the return Wrapper.