Skip to content
This repository has been archived by the owner on Sep 10, 2022. It is now read-only.

Recipes

Xu Deng edited this page Jul 30, 2018 · 17 revisions

Contribute to this Page

If you have a recipe to share, please Add It. Just follow the styles and remember to add a table of contents entry.

Table of Contents

Write a Higher Order Component from Scratch

It's good to understand how to write your own HOC without recompose so you understand how Recompose can help reduce boilerplate. This example does not use Recompose to create two HOCs. See it in Plunkr

const { Component } = React;

const overrideProps = (overrideProps) => (BaseComponent) => (props) =>
  <BaseComponent {...props} {...overrideProps} />;

const alwaysBob = overrideProps({ name: 'Bob' });

const neverRender = (BaseComponent) =>
  class extends Component {
    shouldComponentUpdate() {
      return false;
    }
    render() {
      return <BaseComponent {...this.props} />;
    }
  };

const User = ({ name }) =>
  <div className="User">{ name }</div>;

const User2 = alwaysBob(User);
const User3 = neverRender(User);

const App = () =>
  <div>
    <User name="Tim" />
    <User2 name="Joe" />
    <User3 name="Steve" />
  </div>;

by @kindberg

Mix HOC from Various Libraries

Use compose to mix and match HOC from recompose and other libraries such as redux' connect HOC. See it in Plunkr

const { Component } = React;
const { compose, setDisplayName, setPropTypes } = Recompose;
const { connect } = Redux();

const enhance = compose(
  setDisplayName('User'),
  setPropTypes({
    name: React.PropTypes.string.isRequired,
    status: React.PropTypes.string
  }),
  connect()
);

const User = enhance(({ name, status, dispatch }) =>
  <div className="User" onClick={
      () => dispatch({ type: "USER_SELECTED" })
    }>
    { name }: { status }
  </div>
);

by @kindberg

Reusable withToggle HOC

Can help with interactions that require a simple show/hide such as tooltips, dropdowns, expandable menus, etc. See it in Plunkr

const { Component } = React;
const { compose, withState, withHandlers } = Recompose;

const withToggle = compose(
  withState('toggledOn', 'toggle', false),
  withHandlers({
    show: ({ toggle }) => (e) => toggle(true),
    hide: ({ toggle }) => (e) => toggle(false),
    toggle: ({ toggle }) => (e) => toggle((current) => !current)
  })
)

const StatusList = () =>
  <div className="StatusList">
    <div>pending</div>
    <div>inactive</div>
    <div>active</div>
  </div>;

const Status = withToggle(({ status, toggledOn, toggle }) =>
  <span onClick={ toggle }>
    { status }
    { toggledOn && <StatusList /> }
  </span>
);

const Tooltip = withToggle(({ text, children, toggledOn, show, hide }) =>
  <span>
    { toggledOn && <div className="Tooltip">{ text }</div> }
    <span onMouseEnter={ show } onMouseLeave={ hide }>{ children }</span>
  </span>
);

const User = ({ name, status }) =>
  <div className="User">
    <Tooltip text="Cool Dude!">{ name }</Tooltip><Status status={ status } />
  </div>;

const App = () =>
  <div>
    <User name="Tim" status="active" />
  </div>;

by @kindberg

And again but using withReducer helper See it in Plunkr

const { Component } = React;
const { compose, withReducer, withHandlers } = Recompose;

const withToggle = compose(
  withReducer('toggledOn', 'dispatch', (state, action) => {
    switch(action.type) {
      case 'SHOW':
        return true;
      case 'HIDE':
        return false;
      case 'TOGGLE':
        return !state;
      default:
        return state;
    }
  }, false),
  withHandlers({
    show: ({ dispatch }) => (e) => dispatch({ type: 'SHOW' }),
    hide: ({ dispatch }) => (e) => dispatch({ type: 'HIDE' }),
    toggle: ({ dispatch }) => (e) => dispatch({ type: 'TOGGLE' })
  })
);

// Everything else is the same...

by @kindberg

Perform a Transformation on a Prop Before the Component Receives it

In this example we take a flexible and generic UserList component and we populate it with specific users three ways to form three specific components that will filter out only the users that the component is meant to show. See it in Plunkr

const { Component } = React;
const { mapProps } = Recompose;

const User = ({ name, status }) =>
  <div className="User">{ name }{ status }</div>;

const UserList = ({ users, status }) =>
  <div className="UserList">
    <h3>{ status } users</h3>
    { users && users.map((user) => <User {...user} />) }
  </div>;

const users = [
  { name: "Tim", status: 'active' },
  { name: "Bob", status: 'active' },
  { name: "Joe", status: 'active' },
  { name: "Jim", status: 'inactive' },
];

const filterByStatus = (status) => mapProps(
  ({ users }) => ({
    status,
    users: users.filter(u => u.status === status)
  })
);

const ActiveUsers = filterByStatus('active')(UserList);
const InactiveUsers = filterByStatus('inactive')(UserList);
const PendingUsers = filterByStatus('pending')(UserList);

const App = () =>
  <div className="App">
    <ActiveUsers users={ users } />
    <InactiveUsers users={ users } />
    <PendingUsers users={ users } />
  </div>;

by @kindberg

Create Specific Components Based on Generic Components

It can be helpful to save a certain configuration of props into a pre-configured component. See it in Plunkr

const { Component } = React;
const { withProps } = Recompose;

const HomeLink = withProps(({ query }) => ({ href: '#/?query=' + query }))('a');
const ProductsLink = withProps({ href: '#/products' })('a');
const CheckoutLink = withProps({ href: '#/checkout' })('a');

const App = () =>
  <div className="App">
    <header>
      <HomeLink query="logo">Logo</HomeLink>
    </header>
    <nav>
      <HomeLink>Home</HomeLink>
      <ProductsLink>Products</ProductsLink>
      <CheckoutLink>Checkout</CheckoutLink>
    </nav>
  </div>;

by @kindberg

Show a Spinner While a Component is Loading

Pretty self explanatory :) See it in Plunkr

const { Component } = React;
const { compose, lifecycle, branch, renderComponent } = Recompose;

const withUserData = lifecycle({
  state: { loading: true },
  componentDidMount() {
    fetchData().then((data) =>
      this.setState({ loading: false, ...data }));
  }
});

const Spinner = () =>
  <div className="Spinner">
    <div className="loader">Loading...</div>
  </div>;

const isLoading = ({ loading }) => loading;

const withSpinnerWhileLoading = branch(
  isLoading,
  renderComponent(Spinner)
);

const enhance = compose(
  withUserData,
  withSpinnerWhileLoading
);

const User = enhance(({ name, status }) =>
  <div className="User">{ name }{ status }</div>
);

const App = () =>
  <div>
    <User />
  </div>;

by @kindberg

Show Error Messages based on Non-Optimal States

The idea here is that your components could contain only the "happy path" rendering logic. All other unhappy states—loading, insufficient data, no results, errors—could be handled via a nonOptimalStates HOC. See it in Plunkr

const { Component } = React;
const { compose, lifecycle, branch, renderComponent } = Recompose;

const User = ({ name, status }) =>
  <div className="User">{ name }{ status }</div>;

const withUserData = lifecycle({
  componentDidMount() {
    fetchData().then(
      (users) => this.setState({ users }),
      (error) => this.setState({ error })
    );
  }
});

const UNAUTHENTICATED = 401;
const UNAUTHORIZED = 403;
const errorMsgs = {
  [UNAUTHENTICATED]: 'Not Authenticated!',
  [UNAUTHORIZED]: 'Not Authorized!',
};

const AuthError = ({ error }) =>
  error.statusCode &&
    <div className="Error">{ errorMsgs[error.statusCode] }</div>;

const NoUsersMessage = () =>
  <div>There are no users to display</div>;

const hasErrorCode = ({ error }) => error && error.statusCode;
const hasNoUsers = ({ users }) => users && users.length === 0;

const nonOptimalStates = (states) =>
  compose(...states.map(state =>
    branch(state.when, renderComponent(state.render))));

const enhance = compose(
  withUserData,
  nonOptimalStates([
    { when: hasErrorCode, render: AuthError },
    { when: hasNoUsers, render: NoUsersMessage }
  ])
);

const UserList = enhance(({ users, error }) =>
  <div className="UserList">
    { users && users.map((user) => <User {...user} />) }
  </div>
);

const App = () =>
  <div className="App">
    <UserList />
  </div>;

by @kindberg

For more great Recompose learning watch @timkindberg's egghead.io course.

Track state history for undo and redo functionality

Here's an example of how to create a generic history thread hoc (for undo/redo) using recompose. See it in WebpackBin

// HistoryDemo.jsx
import React from 'react'
// withHistory (made with recompose, see below)
import withHistory from './withHistory'

const HistoryDemo = ({
  counter,
  pushHistory,
  undo,
  redo,
  index,
  thread
}) => (
  <div>
    <h1>HistoryDemo</h1>
    <p><span>Count:</span><num>{counter}</num></p>
    <button onClick={() => pushHistory({counter: counter + 1})}>increment</button>
    <button onClick={() => pushHistory({counter: counter - 1})}>decrement</button>
    <hr/>
    <button disabled={index <= 0} onClick={undo}>Undo</button>
    <button disabled={index >= thread.length-1} onClick={redo}>Redo</button>
  </div>
)

const initialState = {
  counter: 1
}

export default withHistory(initialState)(HistoryDemo);

// withHistory.js
// creating the hoc itself with recompose:
import {
  compose,
  withStateHandlers
} from 'recompose'

export default (initialState) => withStateHandlers({ thread: [initialState], index: 0, ...initialState }, {
  pushHistory: (state, props) => newState => _updateThread(state, newState),
  go: (state, props) => i => _moveIndex(state, i),
  forward: (state, props) => () => _moveIndex(state, 1),
  backward: (state, props) => () => _moveIndex(state, -1),
  undo: (state, props) => () => _moveIndex(state, -1),
  redo: (state, props) => () => _moveIndex(state, 1)
})

function _updateThread (state, newState) {
  const updateThread = [
    ...state.thread.slice(0, state.index + 1),
    newState
  ];
  const updateIndex = updateThread.length - 1
  return {
    index: updateIndex,
    thread: updateThread,
    ...updateThread[updateIndex]
  }
}

function _moveIndex (state, moveBy) {
  const targetIndex = clamp(state.index + moveBy, 0, state.thread.length - 1)
  return {
    index: targetIndex,
    thread: state.thread,
    ...state.thread[targetIndex]
  }
}

function clamp(num, min, max) {
  return num <= min ? min : num >= max ? max : num;
}

by @mattmcfarland

Call backend API with fetcher

When calling backend API, it's boring to handling loading status and errors. So this HOC can be useful to you.

Edit 72wjxylnjx

import {
  compose,
  withStateHandlers,
  withHandlers,
  mapProps,
  lifecycle
} from "recompose";
import { upperFirst, omit, identity } from "lodash-es";

// Handle Rest API
function withFetcher(
  name: string,
  fetch: (props: any) => Promise<any>,
  { fetchOnMount = false } = {}
) {
  return compose(
    withStateHandlers<
      { [key: string]: { data: any; loading: boolean; error: any } },
      any,
      any
    >(
      {
        [`${name}Fetcher`]: {
          data: null,
          loading: false,
          error: null
        }
      },
      {
        [`receive${upperFirst(name)}Data`]: () => (data: any) => ({
          [`${name}Fetcher`]: {
            data,
            loading: false,
            error: null
          }
        }),
        [`receive${upperFirst(name)}Error`]: ({
          [`${name}Fetcher`]: { data }
        }) => (error: any) => ({
          [`${name}Fetcher`]: {
            data,
            loading: false,
            error: error || true
          }
        }),
        [`start${upperFirst(name)}Fetch`]: ({
          [`${name}Fetcher`]: prevState
        }) => () => ({
          [`${name}Fetcher`]: {
            ...prevState,
            loading: true
          }
        })
      }
    ),
    withHandlers({
      ["fetch" + upperFirst(name)]: (props: any) => () => {
        props[`start${upperFirst(name)}Fetch`]();
        fetch(props).then(
          props[`receive${upperFirst(name)}Data`],
          props[`receive${upperFirst(name)}Error`]
        );
      }
    }),
    mapProps(props =>
      omit(props, [
        `receive${upperFirst(name)}Data`,
        `receive${upperFirst(name)}Error`,
        `start${upperFirst(name)}Fetch`
      ])
    ),
    fetchOnMount
      ? lifecycle({
          componentDidMount() {
            (this as any).props["fetch" + upperFirst(name)]();
          }
        })
      : identity
  );
}

export default withFetcher;

Example usage

const GithubApi = withFetcher(
  "githubApis",
  async () => {
    try {
      const response = await fetch("https://api.github.com");
      return await response.json();
    } catch (error) {
      throw error.message;
    }
  },
  { fetchOnMount: true }
)((props: any) => (
  <div className="container">
    <p>error: {JSON.stringify(props.githubApisFetcher.error)}</p>
    <p>loading: {JSON.stringify(props.githubApisFetcher.loading)}</p>
    <dl>
      {entries(props.githubApisFetcher.data).map(([key, value]) => (
        <React.Fragment key={key}>
          <dt>{key}</dt>
          <dd>{value}</dd>
        </React.Fragment>
      ))}
    </dl>
  </div>
));

return <GithubApi />;

by @d8660091