Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save mfrancois3k/efa2ada40454eb870a87617a5f5684c4 to your computer and use it in GitHub Desktop.
Save mfrancois3k/efa2ada40454eb870a87617a5f5684c4 to your computer and use it in GitHub Desktop.
React advanced patterns

1. Compound Components

Compound Components pattern is grouping related components together in a way so that it becomes easier to read and more extensible.

Use case:

  • When we want to leave state management to a parent/container/enclosing component and leave the markup to the the user of the components

How?

  • Extract the component that manages the state.
  • Extract inner/user components and make them consume two things: 1- the parent/enclosing component's state by passing props from the parent via Context API (Example 1) or via render props method (Example 2). 2- A mechanism (usually a callback) to request for updating the state.

Advantages

  • The user of the component owns the markup. The implementation of the parent component doesn’t need a fixed markup structure. If using Context, you can do whatever you like, nest child component 10 levels deep and it will still work.
  • The developer can rearrange the components in any order.
  • The components don’t have to be jammed together explicitly, they can be written independently but they are still able to communicate via their parent component.

Explanation

Compound components is a pattern in which components are used together such that they share an implicit state that lets them communicate with each other in the background.

Think of compound components like the <select> and <option> elements in HTML. Apart they don’t do too much, but together they allow you to create the complete experience. — Kent C. Dodds

Side note: Context vs passing props

It's cumbersome to pass props to all the nested levels of children, if not using Context i.e with cloning elements with props. With Context it's easily doable.

This example uses Context to store and share state.

Usage:

import React from 'react';
import TabSwitcher, { Tab, TabPanel } from './TabSwitcher';

function App() {
  return (
    <div className="App">
      <h1>TabSwitcher with Compound Components</h1>

      /* TabSwitcher keeps a shared state */
      <TabSwitcher>

      /* TabPanel and Tab consumes the shared state */

        /* TabPanels adjust their render with the state */
        <TabPanel whenActive="a">
          <div>a panel</div>
        </TabPanel>
        <TabPanel whenActive="b">
          <div>b panel</div>
        </TabPanel>

        /* Tabs update the state via a callback stored in the state object */
        <Tab id="a">
          <button>a</button>
        </Tab>
        <Tab id="b">
          <button>b</button>
        </Tab>
      </TabSwitcher>
    </div>
  );
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

Declarations:

import React, { Component, createContext } from 'react';

const context = createContext({});

const { Provider, Consumer } = context;

const Tab = ({ id, children }) => (
  <Consumer>
    {({ changeTab }) => <div onClick={() => changeTab(id)}>{children}</div>}
  </Consumer>
);

const TabPanel = ({ whenActive, children }) => (
  <Consumer>
    {({ activeTabId }) => (activeTabId === whenActive ? children : null)}
  </Consumer>
);

class TabSwitcher extends Component {
  state = {
    activeTabId: 'a'
  };

  changeTab = newTabId => {
    console.log(newTabId);
    this.setState({
      activeTabId: newTabId
    });
  };

  render() {
    return (
      <Provider
        value={{
          activeTabId: this.state.activeTabId,
          changeTab: this.changeTab
        }}
      >
        {this.props.children}
      </Provider>
    );
  }
}

export default TabSwitcher;
export { Tab, TabPanel };

This example sets props on child components. It also demonstrate how to leverage static properties.

Usage:

function App() {
  return (
    <div>
    {/* Wizard is state manager */}
      <Wizard>
        {/* Note how easily you can rearrange the components */}
        <InputStep seq={0} field="name" label="Enter your name" />
        <InputStep seq={1} field="number" label="Enter your mobile number" />
        <InputStep seq={2} field="email" label="Enter your email" />

        <Wizard.Buttons.Previous />
        <Wizard.Buttons.Next />
        <Wizard.Buttons.Submit seq={3} label="Submit" />
      </Wizard>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Declarations:

import React from "react";
import ReactDOM from "react-dom";

const InputStep = props => {
  if (props.seq !== props.step) return null;

  return (
    <div>
      <label>
        {props.label}
        <input
          onChange={e => {
            props.onChange({ [props.field]: e.target.value });
          }}
        />
      </label>
    </div>
  );
};

class Wizard extends React.Component {
  // Static properties which are out components
  static Buttons = {
    Previous: props =>
      props.canGoPrevious ? (
        <button onClick={props.goPrevious}>Previous</button>
      ) : null,

    Next: props =>
      props.canGoNext ? <button onClick={props.goNext}>Next</button> : null,

    Submit: props =>
      props.canSubmit ? <input onClick={props.onSubmit} type="submit" /> : null
  };

  constructor(props) {
    super(props);
    this.state = { step: 0, userInfo: {} };
  }

  render() {
    const lastStep = 2;
    // Note how we are limited to the first level of children:
    return React.Children.map(this.props.children, child => {
      return React.cloneElement(child, {
        step: this.state.step,
        canGoNext: this.state.step < lastStep,
        canGoPrevious: this.state.step > 0,
        canSubmit: this.state.step === lastStep,
        onChange: userInfoPart => {
          this.setState(({ step, userInfo }) => ({
            step,
            userInfo: Object.assign(userInfo, userInfoPart)
          }));
        },
        goPrevious: () => {
          this.setState(({ step }) => ({ step: step - 1 }));
        },
        goNext: () => {
          this.setState(({ step }) => ({ step: step + 1 }));
        },
        onSubmit: () => {
          alert(JSON.stringify(this.state.userInfo));
        }
      });
    });
  }
}

2. Render Props

Render Props is a pattern to separate state management from rendering. Render props is the lowest level with regard to sharing logic.

Use case:

  • When we want to leave state management to a parent/container/enclosing component and leave the markup to the the user of the components and have a line of communication between.

Render props VS Compound components:

Render props adds slightly more complexity comparing to Compound Component i.e extra work for user (you can compare the examples). So it fits a lot better where the rendering needs every fine detail and control over how things are rendered.

How?

  • Extract the component that manages the state.
  • The child of the parent component is a function with the needed arguments to render themselves. Also, the mechanism to update the state is passed on by the parent.
  • The parent component calls the child function (like this.props.children(args) with the arguments derived from the state.
  • No shared state anymore. The parent communicates with its child/children via callback.

Advantages

  • The user of the component has absolute control over the markup.
  • The user of the parent component doesn't need to have access to any kind of Context.
  • Nesting wouldn't be an issue
  • It's clear what props are going to be passed to the child components i.e it's not hidden in parent component implementation
import React from "react";
import ReactDOM from "react-dom";

const InputStep = props => {
  return (
    <div>
      <label>
        {props.label}
        <input
          onChange={e => {
            props.onChange({ [props.field]: e.target.value });
          }}
        />
      </label>
    </div>
  );
};

class Wizard extends React.Component {
  // Static properties which are out components
  static Buttons = {
    Previous: props => <button onClick={props.goPrevious}>Previous</button>,

    Next: props => <button onClick={props.goNext}>Next</button>,

    Submit: props => <input onClick={props.onSubmit} type="submit" />
  };

  constructor(props) {
    super(props);
    this.state = { step: 0, userInfo: {} };
  }

  render() {
    const lastStep = 2;
    return this.props.children({
      step: this.state.step,
      canGoNext: this.state.step < lastStep,
      canGoPrevious: this.state.step > 0,
      canSubmit: this.state.step === lastStep,
      onChange: userInfoPart => {
        this.setState(({ step, userInfo }) => ({
          step,
          userInfo: Object.assign(userInfo, userInfoPart)
        }));
      },
      goPrevious: () => {
        this.setState(({ step }) => ({ step: step - 1 }));
      },
      goNext: () => {
        this.setState(({ step }) => ({ step: step + 1 }));
      },
      onSubmit: () => {
        alert(JSON.stringify(this.state.userInfo));
      }
    });
  }
}

Usage:


function App() {
  return (
    <div>
      {/* Wizard is state manager and consumes the following function */}
      <Wizard>
        {/* These are passed props. Note how we render the markup based on them*/}
        {({
          step,
          canGoNext,
          goNext,
          canGoPrevious,
          goPrevious,
          canSubmit,
          onChange,
          onSubmit
        }) => {
          return (
            <>
              {step === 0 && (
                <InputStep
                  onChange={onChange}
                  field="name"
                  label="Enter your name"
                />
              )}

              {step === 1 && (
                <InputStep
                  onChange={onChange}
                  field="number"
                  label="Enter your mobile number"
                />
              )}

              {step === 2 && (
                <InputStep
                  onChange={onChange}
                  field="email"
                  label="Enter your email"
                />
              )}

              {canGoPrevious && (
                <Wizard.Buttons.Previous goPrevious={goPrevious} />
              )}
              {canGoNext && <Wizard.Buttons.Next goNext={goNext} />}
              {canSubmit && (
                <Wizard.Buttons.Submit onSubmit={onSubmit} label="Submit" />
              )}
            </>
          );
        }}
      </Wizard>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);


3. Prop Collections

Using Render Props if we have a collection of props that is common in the child component, Prop Collection is the patter to use.

Use case:

  • The pattern is applied on top of Render Props. One good use case example is applying accessibility props.
  • Another use case is when container component needs you (user) to apply certain props for them so that they can wire up or trace things (example: autocomplete)

How?

  • The container component passes collection of props object to the child function. The responsibility of child components is to apply the props on themselves.

Advantages

  • Already mentioned in the intro.
import React from "react";
import ReactDOM from "react-dom";

const InputStep = ({ onChange, label, field, ...rest }) => {
  return (
    <div>
      <label {...rest}>
        {label}
        <input
          {...rest}
          onChange={e => {
            onChange({ [field]: e.target.value });
          }}
        />
      </label>
    </div>
  );
};

class Wizard extends React.Component {
  // Static properties which are out components
  static Buttons = {
    Previous: props => <button onClick={props.goPrevious}>Previous</button>,

    Next: props => <button onClick={props.goNext}>Next</button>,

    Submit: props => <input onClick={props.onSubmit} type="submit" />
  };

  constructor(props) {
    super(props);
    this.state = { step: 0, userInfo: {} };
  }

  render() {
    const lastStep = 2;
    return this.props.children({
      step: this.state.step,
      canGoNext: this.state.step < lastStep,
      canGoPrevious: this.state.step > 0,
      canSubmit: this.state.step === lastStep,
      accessibilityProps: {
        "aria-expanded": true,
        style: {
          fontSize: "35px"
        }
      },
      onChange: userInfoPart => {
        this.setState(({ step, userInfo }) => ({
          step,
          userInfo: Object.assign(userInfo, userInfoPart)
        }));
      },
      goPrevious: () => {
        this.setState(({ step }) => ({ step: step - 1 }));
      },
      goNext: () => {
        this.setState(({ step }) => ({ step: step + 1 }));
      },
      onSubmit: () => {
        alert(JSON.stringify(this.state.userInfo));
      }
    });
  }
}

Usage:

function App() {
  return (
    <div>
      {/* Wizard is state manager and consumes the following function */}
      <Wizard>
        {({
          step,
          canGoNext,
          goNext,
          canGoPrevious,
          goPrevious,
          // This is the common props you need to apply on all of your components
          accessibilityProps,
          canSubmit,
          onChange,
          onSubmit
        }) => {
          return (
            <>
              {step === 0 && (
                <InputStep
                  {...accessibilityProps}
                  onChange={onChange}
                  field="name"
                  label="Enter your name"
                />
              )}

              {step === 1 && (
                <InputStep
                  {...accessibilityProps}
                  onChange={onChange}
                  field="number"
                  label="Enter your mobile number"
                />
              )}

              {step === 2 && (
                <InputStep
                  {...accessibilityProps}
                  onChange={onChange}
                  field="email"
                  label="Enter your email"
                />
              )}

              {canGoPrevious && (
                <Wizard.Buttons.Previous goPrevious={goPrevious} />
              )}
              {canGoNext && <Wizard.Buttons.Next goNext={goNext} />}
              {canSubmit && (
                <Wizard.Buttons.Submit onSubmit={onSubmit} label="Submit" />
              )}
            </>
          );
        }}
      </Wizard>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

4. Prop Collections

Using Prop Collections we usually don't care what props we're getting back from the parent; we just apply them on the component. There's a chance that we accidentally override some of those props with props we set on the component. This pattern is to address this problem.

Use case:

  • Already mentioned in the intro.

How?

  • Similar to Prop Collections but instead of spreading an object, we actually call a function that accepts all the props that we want to apply to the components and the function's job is to compose those props with the common props.

Advantages

  • The consumer remains separated from the container and it never cares what props are going to be provided by the parent.
import React from "react";
import ReactDOM from "react-dom";

const InputStep = ({ seq, step, onChange, label, field, ...rest }) => {
 return (
   seq === step && (
     <div>
       <label {...rest}>
         {label}
         <input
           {...rest}
           onChange={e => {
             onChange({ [field]: e.target.value });
           }}
         />
       </label>
     </div>
   )
 );
};

class Wizard extends React.Component {
 // Static properties which are out components
 static Buttons = {
   Previous: props =>
     props.canGoPrevious && (
       <button {...props} onClick={props.goPrevious}>
         Previous
       </button>
     ),

   Next: props =>
     props.canGoNext && (
       <button {...props} onClick={props.goNext}>
         Next
       </button>
     ),

   Submit: props =>
     props.canSubmit && (
       <input {...props} onClick={props.onSubmit} type="submit" />
     )
 };

 constructor(props) {
   super(props);
   this.state = { step: 0, userInfo: {} };
 }

 render() {
   const lastStep = 2;
   const baseProps = {
     step: this.state.step,
     canGoNext: this.state.step < lastStep,
     canGoPrevious: this.state.step > 0,
     canSubmit: this.state.step === lastStep,
     onChange: userInfoPart => {
       this.setState(({ step, userInfo }) => ({
         step,
         userInfo: Object.assign(userInfo, userInfoPart)
       }));
     },
     goPrevious: () => {
       this.setState(({ step }) => ({ step: step - 1 }));
     },
     goNext: () => {
       this.setState(({ step }) => ({ step: step + 1 }));
     },
     onSubmit: () => {
       alert(JSON.stringify(this.state.userInfo));
     },
     style: {
       fontSize: "25px"
     }
   };

   const getProps = componentProps => {
     if (!componentProps) return baseProps;
     var result = { ...componentProps, ...baseProps };
     if (componentProps.style) {
       result.style = { ...componentProps.style, ...baseProps.style };
       //Object.assign(result.style, componentProps.style);
     }
     return result;
   };

   return this.props.children({ getProps });
 }
}

Usage:

function App() {
 return (
   <div>
     {/* Wizard is state manager and consumes the following function */}
     <Wizard>
       {/* getProps is the function Wizard provides and each component call with its own props to get a bigger props object to spread */}
       {({ getProps }) => {
         return (
           <>
             <InputStep
               {...getProps({
                 seq: 0,
                 field: "name",
                 label: "Enter your name",
                 style: {
                   color: "blue"
                 }
               })}
             />
             <InputStep
               {...getProps({
                 seq: 1,
                 field: "number",
                 label: "Enter your mobile number",
                 style: {
                   color: "orange"
                 }
               })}
             />
             <InputStep
               {...getProps({
                 seq: 2,
                 field: "email",
                 label: "Enter your email",
                 style: {
                   color: "aqua"
                 }
               })}
             />
             <Wizard.Buttons.Previous
               {...getProps({
                 style: {
                   color: "red"
                 }
               })}
             />
             <Wizard.Buttons.Next
               {...getProps({
                 style: {
                   color: "limegreen"
                 }
               })}
             />
             <Wizard.Buttons.Submit
               {...getProps({
                 seq: 2,
                 label: "Submit",
                 style: {
                    cursor: "pointer",
                    color: "green"
                  }
               })}
             />
           </>
         );
       }}
     </Wizard>
   </div>
 );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

5. State Initializer

Using State Initializer, parent component provides a mechanism for the user to initialize its state. It works well with Render Props pattern.

Use case:

  • Already mentioned in the intro.

How?

  • The parent exposes the mechanism to reset/initialize its state via a function
import React from "react";
import ReactDOM from "react-dom";

const InputStep = ({
  userInfo,
  seq,
  step,
  onChange,
  label,
  field,
  ...rest
}) => {
  return (
    seq === step && (
      <div>
        <label {...rest}>
          {label}
          <input
            {...rest}
            value={userInfo[field]}
            onChange={e => {
              onChange({ [field]: e.target.value });
            }}
          />
        </label>
      </div>
    )
  );
};

class Wizard extends React.Component {
  static Buttons = {
    Previous: props =>
      props.canGoPrevious && (
        <button {...props} onClick={props.goPrevious}>
          Previous
        </button>
      ),

    Next: props =>
      props.canGoNext && (
        <button {...props} onClick={props.goNext}>
          Next
        </button>
      ),

    Submit: props =>
      props.canSubmit && (
        <input {...props} onClick={props.onSubmit} type="submit" />
      ),

    Reset: props => (
      <button {...props} onClick={props.onClick}>
        Reset
      </button>
    )
  };

  constructor(props) {
    super(props);
    this.state = { step: 0, userInfo: {} };
  }

  render() {
    const lastStep = 2;

    const getProps = componentProps => {
      const baseProps = {
        userInfo: this.state.userInfo,
        step: this.state.step,
        canGoNext: this.state.step < lastStep,
        canGoPrevious: this.state.step > 0,
        canSubmit: this.state.step === lastStep,
        onChange: userInfoPart => {
          this.setState(({ step, userInfo }) => ({
            step,
            userInfo: Object.assign(userInfo, userInfoPart)
          }));
        },
        goPrevious: () => {
          this.setState(({ step }) => ({ step: step - 1 }));
        },
        goNext: () => {
          this.setState(({ step }) => ({ step: step + 1 }));
        },
        onSubmit: () => {
          alert(JSON.stringify(this.state.userInfo));
        },
        style: {
          fontSize: "25px"
        }
      };

      if (!componentProps) return baseProps;
      var result = { ...componentProps, ...baseProps };
      if (componentProps.style) {
        result.style = { ...componentProps.style, ...baseProps.style };
      }
      return result;
    };

    // this is how the component initializes itself
    const initialize = newUserInfo => {
      this.setState({ userInfo: newUserInfo });
    };

    return this.props.children({ getProps, initialize });
  }
}

Usage:

function App() {
  return (
    <div>
      {/* Wizard is state manager and consumes the following function */}
      <Wizard>
        {/* getProps is the function Wizard provides and each component call with its own props to get a bigger props object to spread */}
        {({
          getProps,
          // this is the function to initialize the state
          initialize
        }) => {
          return (
            <>
              <InputStep
                {...getProps({
                  seq: 0,
                  field: "name",
                  label: "Enter your name",
                  style: {
                    color: "blue"
                  }
                })}
              />
              <InputStep
                {...getProps({
                  seq: 1,
                  field: "number",
                  label: "Enter your mobile number",
                  style: {
                    color: "orange"
                  }
                })}
              />
              <InputStep
                {...getProps({
                  seq: 2,
                  field: "email",
                  label: "Enter your email",
                  style: {
                    color: "aqua"
                  }
                })}
              />
              <Wizard.Buttons.Reset
                {...getProps({
                  label: "Reset",
                  style: {
                    color: "red"
                  }
                })}
                onClick={() => {
                  // and we call it here
                  initialize({ name: "", age: "", email: "" });
                }}
              />
              <Wizard.Buttons.Previous
                {...getProps({
                  style: {
                    color: "red"
                  }
                })}
              />
              <Wizard.Buttons.Next
                {...getProps({
                  style: {
                    color: "limegreen"
                  }
                })}
              />
              <Wizard.Buttons.Submit
                {...getProps({
                  seq: 2,
                  label: "Submit",
                  style: {
                    cursor: "pointer",
                    color: "green"
                  }
                })}
              />
            </>
          );
        }}
      </Wizard>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

6. State Reducer

With State Reducer enables users to control how the logic works i.e how the state is changed. This is similar to Render Props where users control how things are rendered.

Use case:

  • When the user wants to take control of the another component's state or at least consult with a foreign component about it.

How?

  • The parent exposes the mechanism to broadcast its intention: changing state. In addition, it provides all the information that caller of the reducer function needs via an object called Change Types so that it can decide about the next state.
import React from "react";
import ReactDOM from "react-dom";

const InputStep = ({
  userInfo,
  seq,
  step,
  onChange,
  label,
  field,
  digitOnly,
  stateReducer,
  ...rest
}) => {
  return (
    seq === step && (
      <div>
        <div>{digitOnly && "Note: Enter only numbers"}</div>
        <label {...rest}>
          {label}
          <input
            {...rest}
            value={userInfo[field]}
            onChange={e => onChange({ [field]: e.target.value }, digitOnly)}
          />
        </label>
      </div>
    )
  );
};

class Wizard extends React.Component {
  static Buttons = {
    Previous: props =>
      props.canGoPrevious && (
        <button {...props} onClick={props.goPrevious}>
          Previous
        </button>
      ),

    Next: props =>
      props.canGoNext && (
        <button {...props} onClick={props.goNext}>
          Next
        </button>
      ),

    Submit: props =>
      props.canSubmit && (
        <input {...props} onClick={props.onSubmit} type="submit" />
      ),

    Reset: props => (
      <button {...props} onClick={props.onClick}>
        Reset
      </button>
    )
  };

  static defaultUserInfo = {
    name: "",
    number: "+98",
    email: ""
  };

  constructor(props) {
    super(props);
    this.state = { step: 0, userInfo: { ...Wizard.defaultUserInfo } };
    Object.freeze();
  }

  // `internalSetState` replaces this.setState in the whole component.
  // Here we consult with stateReducer. In addition to the nextState, we
  // provide an object containing required info.
  internalSetState(nextState, changeInfo = {}) {
    this.setState(currentState =>
      this.props.stateReducer(currentState, nextState, changeInfo)
    );
  }

  render() {
    const lastStep = 2;

    const getProps = componentProps => {
      const baseProps = {
        userInfo: this.state.userInfo,
        step: this.state.step,
        canGoNext: this.state.step < lastStep,
        canGoPrevious: this.state.step > 0,
        canSubmit: this.state.step === lastStep,
        onChange: (userInfoPart, digitOnly) => {
          const newUserInfo = Object.assign(
            { ...this.state.userInfo },
            userInfoPart
          );
          this.internalSetState(
            { userInfo: newUserInfo },
            { digitOnly, reason: "userInput" }
          );
        },
        goPrevious: () => {
          this.setState(({ step }) => ({ step: step - 1 }));
        },
        goNext: () => {
          this.setState(({ step }) => ({ step: step + 1 }));
        },
        onSubmit: () => {
          alert(JSON.stringify(this.state.userInfo));
        },
        style: {
          fontSize: "25px"
        }
      };

      if (!componentProps) return baseProps;
      var result = { ...componentProps, ...baseProps };
      if (componentProps.style) {
        result.style = { ...componentProps.style, ...baseProps.style };
      }
      return result;
    };

    // this is how the component initializes itself
    const initialize = newUserInfo => {
      this.internalSetState({ userInfo: newUserInfo }, { reason: "force" });
    };

    return this.props.children({ getProps, initialize });
  }
}

Usage

function App() {
  return (
    <div>
      {/* Wizard is state manager and consumes the following function */}
      <Wizard
        // here we define state reducer and the function checks the change info passed in addition
        // to the states.
        // So for example, if it's a force change, then reducer is not needed to consult with
        stateReducer={(currentState, nextState, { digitOnly, reason }) => {
          if (
            !reason ||
            reason === "force" ||
            !digitOnly ||
            /^\+\d*$/.test(nextState.userInfo.number)
          )
            return nextState;

          return currentState;
        }}
      >
        {/* getProps is the function Wizard provides and each component call with its own props to get a bigger props object to spread */}
        {({
          getProps,
          // this is the function to initialize the state
          initialize
        }) => {
          return (
            <>
              <InputStep
                {...getProps({
                  seq: 0,
                  field: "name",
                  label: "Enter your name",
                  style: {
                    color: "blue"
                  }
                })}
              />
              <InputStep
                {...getProps({
                  seq: 1,
                  digitOnly: true,
                  field: "number",
                  label: "Enter your mobile number",
                  style: {
                    color: "orange"
                  }
                })}
              />
              <InputStep
                {...getProps({
                  seq: 2,
                  field: "email",
                  label: "Enter your email",
                  style: {
                    color: "aqua"
                  }
                })}
              />
              <Wizard.Buttons.Reset
                {...getProps({
                  label: "Reset",
                  style: {
                    color: "red"
                  }
                })}
                onClick={() => {
                  // and we call it here
                  initialize(Wizard.defaultUserInfo);
                }}
              />
              <Wizard.Buttons.Previous
                {...getProps({
                  style: {
                    color: "red"
                  }
                })}
              />
              <Wizard.Buttons.Next
                {...getProps({
                  style: {
                    color: "limegreen"
                  }
                })}
              />
              <Wizard.Buttons.Submit
                {...getProps({
                  seq: 2,
                  label: "Submit",
                  style: {
                    cursor: "pointer",
                    color: "green"
                  }
                })}
              />
            </>
          );
        }}
      </Wizard>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);


7. Control props

React binding is one-way. A component takes its value via props and broadcast a change to its value via onChange callback. This pattern is exactly what's implemented in React controlled input or form elements. The pattern is about where the source of truth is: inside component or outside. For example, when you create an input and set value on it like this:

<input value={this.state.someField} />

So we'll have two sources: the input's internal state and the state.someField. In the mentioned case, as we have taken the control of the value of the input, the responsibility of updating the value is also on us. If we don't, the input behaves like a read-only element.

In contrast, if we did let the input element maintain its value, then we wouldn't need to do anything for value updating and hence we could only learn when the value changes.

Note:

In a controlled component, form data is handled by a React component. The alternative is uncontrolled components, where form data is handled by the DOM itself. To write an uncontrolled component, instead of writing an event handler for every state update, you can use a ref to get form values from the DOM.

Note:

A component can be controlled with respect to one value and uncontrolled with respect to another value.

Use case:

  • Hmmmm....

How?

  • The component checks the props for presence of value to realize if it's controlled or not. If it is, then on any change it doesn't update its internal state rather it suggests the change by calling onChange callback and let the the function rejects or accepts the suggested value i.e updates props.value which eventually update the value of the component.
import React, { useState } from "react";
import ReactDOM from "react-dom";

const SizeHolder = ({ size, onChange, initialValue }) => {
  const [value, setValue] = useState(size);
  const getValue = () => {
    if (isControlled) {
      return size;
    } else {
      return value || initialValue;
    }
  };

  const isControlled = size !== undefined; // what if size equals 0 :)

  return (
    <input
      value={getValue()}
      onChange={e => {
        const newSize = Number(e.target.value);
        if (isControlled) {
          onChange(newSize);
        } else {
          setValue(newSize);
        }
      }}
    />
  );
};

Usage:

const App = props => {
  const ratio = 1.33333;
  const [state, setState] = useState({
    maintainRatio: true,
    height: 600,
    width: 800
  });
  return (
    <div style={{ fontSize: "25px", lineHeight: "3em" }}>
      <label>
        Height:{" "}
        <SizeHolder
          size={state.height}
          onChange={value => {
            setState({
              height: value,
              width: Math.round(ratio * value),
              maintainRatio: state.maintainRatio
            });
          }}
        />
      </label>
      <br />
      <label>
        <input
          type="checkbox"
          checked={state.maintainRatio}
          onChange={value => {
            setState({
              height: state.height,
              width: state.width,
              maintainRatio: value.target.checked
            });
          }}
        />
        Maintain aspact ration
      </label>
      <br />
      Width
      {!state.maintainRatio && (
        // Uncontrolled component:
        // - we don't care about onChange, and
        // - the value of 'size' can be changed arbitrarily
        <SizeHolder initialValue={state.width} />
      )}
      {state.maintainRatio && (
        // Controlled component:
        // - we handle onChange event to update the state, and
        // - the value of 'size' cannot be changed arbitrarily
        <SizeHolder
          size={state.width}
          onChange={width => {
            setState({
              width,
              height: Math.round(width / ratio),
              maintainRatio: state.maintainRatio
            });
          }}
        />
      )}
      <br />
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);


8. Provider pattern

Provider pattern is to address Props drilling problem. It uses React Context API. The pattern is based on Render props as the user needs to pass a function with the argument be Context object.

Use case:

  • When you want to pass down props to all levels yet you like to avoid Props drilling problem. Also, when you want to give your users absolute freedom.

How?

The component holding state or information creates a Context and keeps its provider for itself but exposes its consumer.

Advantage:

  • Flexible
  • Props drilling doesn't occur

Disadvantage:

  • Implicit data flow

Example:

import React from "react";
import ReactDOM from "react-dom";

const WizardContext = React.createContext();

class Wizard extends React.Component {
  static Consumer = WizardContext.Consumer;

  static Buttons = {
    Previous: props => (
      <Wizard.Consumer>
        {context =>
          context.canGoPrevious && (
            <button onClick={context.goPrevious} {...props} {...context}>
              Previous
            </button>
          )
        }
      </Wizard.Consumer>
    ),

    Next: props => (
      <Wizard.Consumer>
        {context =>
          context.canGoNext && (
            <button onClick={context.goNext} {...props} {...context}>
              Next
            </button>
          )
        }
      </Wizard.Consumer>
    ),

    Submit: props => (
      <Wizard.Consumer>
        {context =>
          context.canSubmit && (
            <button
              onClick={context.onSubmit}
              {...props}
              {...context}
              type="submit"
            >
              Submit
            </button>
          )
        }
      </Wizard.Consumer>
    )
  };

  static defaultUserInfo = {
    name: "",
    number: "+98",
    email: ""
  };

  constructor(props) {
    super(props);
    this.state = { step: 0, userInfo: { ...Wizard.defaultUserInfo } };
    Object.freeze();
  }

  render() {
    const lastStep = 2;

    const baseProps = {
      userInfo: this.state.userInfo,
      step: this.state.step,
      canGoNext: this.state.step < lastStep,
      canGoPrevious: this.state.step > 0,
      canSubmit: this.state.step === lastStep,
      onChange: userInfoPart => {
        this.setState(({ step, userInfo }) => ({
          step,
          userInfo: Object.assign(userInfo, userInfoPart)
        }));
      },
      goPrevious: () => {
        this.setState(({ step }) => ({ step: step - 1 }));
      },
      goNext: () => {
        this.setState(({ step }) => ({ step: step + 1 }));
      },
      onSubmit: () => {
        alert(JSON.stringify(this.state.userInfo));
      },
      style: {
        fontSize: "25px",
        color: "green"
      }
    };
    const contextObject = { ...baseProps, userInfo: this.state.userInfo };
    if (typeof this.props.children === "function") {
      return this.props.children(contextObject);
    }
    return (
      <WizardContext.Provider value={contextObject}>
        {this.props.children}
      </WizardContext.Provider>
    );
  }
}
const InputStep = ({ seq, label, field }) => (
  <Wizard.Consumer>
    {({ userInfo, step, onChange, ...rest }) => {
      return (
        seq === step && (
          <div>
            <label {...rest}>
              {label}
              <input
                {...rest}
                value={userInfo[field]}
                onChange={e => onChange({ [field]: e.target.value })}
              />
            </label>
          </div>
        )
      );
    }}
  </Wizard.Consumer>
);

Usage:

function App() {
  return (
    <div>
      {/* Wizard is state manager */}
      <Wizard>
        {/* Note how easily you can rearrange the components */}
        <InputStep seq={0} field="name" label="Enter your name" />
        <InputStep seq={1} field="number" label="Enter your mobile number" />
        <InputStep seq={2} field="email" label="Enter your email" />

        <Wizard.Buttons.Previous />
        <Wizard.Buttons.Next />
        <Wizard.Buttons.Submit seq={3} label="Submit" />
      </Wizard>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);



9. Higher Order Component (HOC)

HOC is neither a component nor is higher order. It's a function that gets a component (either class or function) and returns a new one. It can return either a function or a class, but returning a class is not that usual. This is great for eliminating code duplication. It happens statically when your app boots up it creates all these components. HOC can always be replaced by render props, but sometimes it provides a nicer API. Sometimes you can provide both Render props and HOC.

Use case:

  • When you want to reuse a shared code across some components.

How?

The wrapper component gets another component, does a various jobs like adding props, etc and return a new component. All of this happens one time. In contrast with render props that happens at render time where you have absolute control over whether or not applying certain props or not.

Note: The objective is that the consumer of HOC cannot tell that it is in fact higher order component. It should behave as close to possible as the component that it's wrapping.

Note: To forward from wrapper to wrapped component use ref use React.forwardRef(wrappedComponentFunction)

Example 1:

import React from "react";
import ReactDOM from "react-dom";

// WithConsumer is a HOC. It takes a component and based off of the
// second argument applies certain props and decide whether to return
// anything or not. It uses Context to apply more properties.

const WithConsumer = (Component, { check, onClick } = {}) => {
  console.log(Component.name);
  const Wrapper = (props, ref) => {
    return (
      <Wizard.Consumer>
        {context =>
          (check === undefined || context[check]) && (
            <Component
              ref={ref}
              {...context}
              {...props}
              onClick={context[onClick]}
            />
          )
        }
      </Wizard.Consumer>
    );
  };

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

  // Note: 2 things:
  // - If we have static propertied on wrapped component:
  //  return hoistNonReactStatics(React.forwardRef(Wrapper), Component);

  // - The following line works perfectly except the Wrapper and Wrapped
  // component will have different refs!
  //return Wrapper;

  return React.forwardRef(Wrapper);
};

const WizardContext = React.createContext();

class Wizard extends React.Component {
  static Consumer = WizardContext.Consumer;

  static Buttons = {
    Previous: WithConsumer(props => <button {...props}>Previous</button>, {
      check: "canGoPrevious",
      onClick: "goPrevious"
    }),
    Next: WithConsumer(props => <button {...props}>Next</button>, {
      check: "canGoNext",
      onClick: "goNext"
    }),
    Submit: WithConsumer(
      props => (
        <button {...props} type="submit">
          Submit
        </button>
      ),
      {
        check: "canSubmit",
        onClick: "onSubmit"
      }
    )
  };

  static defaultUserInfo = {
    name: "",
    number: "+98",
    email: ""
  };

  constructor(props) {
    super(props);
    this.state = { step: 0, userInfo: { ...Wizard.defaultUserInfo } };
    Object.freeze();
  }

  render() {
    const lastStep = 2;

    const baseProps = {
      userInfo: this.state.userInfo,
      step: this.state.step,
      canGoNext: this.state.step < lastStep,
      canGoPrevious: this.state.step > 0,
      canSubmit: this.state.step === lastStep,
      onChange: userInfoPart => {
        this.setState(({ step, userInfo }) => ({
          step,
          userInfo: Object.assign(userInfo, userInfoPart)
        }));
      },
      goPrevious: () => {
        this.setState(({ step }) => ({ step: step - 1 }));
      },
      goNext: () => {
        this.setState(({ step }) => ({ step: step + 1 }));
      },
      onSubmit: () => {
        alert(JSON.stringify(this.state.userInfo));
      },
      style: {
        fontSize: "25px",
        color: "green"
      }
    };

    const contextObject = { ...baseProps, userInfo: this.state.userInfo };
    if (typeof this.props.children === "function") {
      return this.props.children(contextObject);
    }
    return (
      <WizardContext.Provider value={contextObject}>
        {this.props.children}
      </WizardContext.Provider>
    );
  }
}

const InputStep = ({ seq, label, field }) => (
  <Wizard.Consumer>
    {({ userInfo, step, onChange, ...rest }) => {
      return (
        seq === step && (
          <div>
            <label {...rest}>
              {label}
              <input
                {...rest}
                value={userInfo[field]}
                onChange={e => onChange({ [field]: e.target.value })}
              />
            </label>
          </div>
        )
      );
    }}
  </Wizard.Consumer>
);

Usage:

function App() {
  return (
    <div>
      <Wizard>
        <InputStep seq={0} field="name" label="Enter your name" />
        <InputStep seq={1} field="number" label="Enter your mobile number" />
        <InputStep seq={2} field="email" label="Enter your email" />

        <Wizard.Buttons.Previous />
        <Wizard.Buttons.Next />
        <Wizard.Buttons.Submit seq={3} label="Submit" />
      </Wizard>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Example 2:

import React from "react";
import ReactDOM from "react-dom";

// This HOC returns a Class Component
const WithSuffocatingStyle = Component => SuffocatingComponent;
class SuffocatingComponent extends React.Component {
  render() {
    return ">>> Nothing Here. Suffocated <<< ";
  }
}

// This HOC returns a Function Component
const WithStyle = Component => () => (
  <>
    WithStyle applied to:
    <Component />
  </>
);

function ComponentOne() {
  return (
    <div className="App">
      <h1>C1</h1>
    </div>
  );
}

function ComponentTwo() {
  return (
    <div className="App">
      <h1>C2 </h1>
    </div>
  );
}

const WithStyleComponentOne = WithStyle(ComponentOne);
const WithSuffocatingStyleComponentTwo = WithSuffocatingStyle(ComponentTwo);

Usage:


const Compose = () => (
  <>
    <WithStyleComponentOne />
    <br />
    <WithSuffocatingStyleComponentTwo />
  </>
);

const rootElement = document.getElementById("root");
ReactDOM.render(<Compose />, rootElement);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment