-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Recipes
If you have a recipe to share, please Add It. Just follow the styles and remember to add a table of contents entry.
- Write a Higher Order Component from Scratch
- Mix HOC from Various Libraries
- Reusable withToggle HOC
- Perform a Transformation on a Prop Before the Component Receives it
- Create Specific Components Based on Generic Components
- Show a Spinner While a Component is Loading
- Show Error Messages based on Non-Optimal States
- Track state history for undo and redo functionality
- Call backend API with fetcher
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
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
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
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
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
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
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.
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;
}
When calling backend API, it's boring to handling loading status and errors. So this HOC can be useful to you.
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