React patterns
Introduction
React Components
We will speak about properties of React Component as Props
. But in JS terms properties are just an plain object.
For more compact syntax instead of React.Component
, React.Element
we will write Component
, Element
.
In general, we will work with functional stateless components - pure functions that takes Props
as an argument and returns JSX/Element
:
React.Component :: Props -> Element
High-Order Component (HOC)
A Higher-Order Component is a function that accepts a Component
as an argument and returns another Component
:
Component -> Component
Example of identity HOC:
const identity = (NextComponent) => (props) => <NextComponent {...props} />
const MyContainer = () => "I am MyContainer";
const NewContainer = identity(MyContainer);
In example above identity
HOC creates NewContainer
which renders exactly the same output as the MyContainer
.
Note: The term originates from high-order function.
Note: Sometimes HOCs are called component decorators. See what decorators really are and why they are occasionally mismatched with term high-Order component.
Examples of HOCs
Collections of HOCs
Providing a context of libraries
In many cases is HOC used for adding functionality to your component from certain library via React context API.
See:
- reactjs/redux -
connect
- yahoo/react-intl -
injectIntl
HOC - erikras/redux-form -
reduxForm
Patterns
1. Static Component
// Before Ramda:
const Loading = () => "Loading...";
ReactDOM.render(<Loading />, rootEl) // renders "Loading..."
// After Ramda:
const Loading = R.always("Loading...");
ReactDOM.render(<Loading />, rootEl) // renders "Loading..."
2. Composition of High-Order components
Use function composition instead of nesting calls.
// Before Ramda
connect()(
reduxForm()(
injectIntl(Container)
)
)
// After Ramda
R.compose(
connect(),
reduxForm(),
injectIntl
)(Container)
// or
R.pipe(
injectIntl,
reduxForm(),
connect()
)(Container)
If you are composing from exactly two HOCs, you can use R.o
.
It is highly recommended to use just one of R.o
, R.compose
, R.pipe
in the scope of your Application for composing HOCs.
3. Branching with R.ifElse
Use R.ifElse
for conditional render.
Lets define following components:
const Loading = R.always("Loading...");
const Section = ({ content }) => <section>{content}</section>;
Than define HOC of conditional render:
// Before Ramda:
const Content = (props) => props.loading ?
<Loading /> :
<Section {...props} />;
// After Ramda:
const withLoading = R.ifElse(R.prop('loading'), Loading)
const Content = withLoading(Section)
In this example withLoading
HOC can be simply reused for all your components with loading
property.
4. Branching with R.cond
Use R.cond
for conditional render.
Lets define following components:
const Loading = R.always("Loading...");
const Missing = R.always("No results.");
const Section = ({ content }) => <section>{content}</section>;
Than define HOC of conditional render:
// Before Ramda:
const Content = (props) => {
if (props.loading) {
return <Loading />;
}
if (!props.items.length) {
return <Missing />;
}
return <Section {...props} />
}
// After Ramda:
const Content = R.cond([
[R.prop('loading'), Loading],
[R.isEmpty('items'), Missing],
[R.T, Section],
]);
5. Mapping properties with mapProps
Lets define following functions:
// Props -> Component -> Element
const createElement = R.curryN(2, React.createElement);
// mapProps :: (a -> a) -> Component -> Component
const mapProps = R.flip(R.useWith(R.o, [createElement, R.identity]));
See Appendix for process of refactor to pointfreee version of mapProps
.
With mapProps
you can transform properties of final component with your custom mapping functions:
const Section = ({ heading, children, ...rest }) => (
<section {...rest}>
<h1>{heading}</h1>
{children}
</section>
);
const SectionWithUpperHeading = mapProps(
(props) => ({ ...props, heading: R.toUpper(props.heading) })
)(Section)
Follows few examples with mapProps
.
5.1 Transforming properites with R.evolve
Assuming component Section
from section 5:
const EnhancedSection = mapProps(
R.evolve({ heading: R.toUpper })
)(Section)
// ...
<EnhancedSection heading="Truth about Ramda">
Ramda is awesome!
</EnhancedSection>
// renders to:
<section>
<h1>TRUTH ABOUT RAMDA</h1>
Ramda is awesome!
</section>
5.2 Adding props with computeProps
Lets introduce function computeProps
(see Appendix section):
const computeProps = R.converge(R.merge, [R.nthArg(1), R.call]);
Assuming following contrieved component SimpleSection
:
const SimpleSection = ({
heading,
childrenLength,
children,
...rest
}) => (
<section {...rest}>
<h1>{heading}</h1>
<div>{children}</div>
<div>{childrenLength}</div>
</section>
);
const EnhancedSection = mapProps(
({ children }) => ({ childrenLength: R.length(children) })
)(SimpleSection)
// ...
<EnhancedSection heading="Truth about Ramda">
Ramda is awesome!
</EnhancedSection>
// renders to:
<section>
<h1>Truth about Ramda</h1>
<div>Ramda is awesome!</div>
<div>17</div>
</section>
5.3 Picking properties with R.pick
Assuming component Section
from section 5:
const EnhancedSection = mapProps(
R.pick(["className", "children", "heading"])
)(Section)
// ...
<EnhancedSection
className="EnhancedSection"
heading="Truth about Ramda"
invalidAttibute="invalid"
>
Ramda is awesome!
</EnhancedSection>
// renders to:
<section class="EnhancedSection">
<h1>Truth about Ramda</h1>
Ramda is awesome!
</section>
6. Defining displayName
with setDisplayName
Lets define following HOC setDisplayName
:
const setDisplayName = (displayName) => R.tap(
(C) => C.displayName = displayName
);
Appendix
Pointfree mapProps
const mapProps = R.curry(
(mapping, C) => (props) => <C {...mapping(props)} />
);
// Replacing JSX
const mapProps = R.curry(
(mapping, C) => (props) => React.createElement(C, mapping(props))
);
// Introducting curried version of React.createElement
const createElement = R.curryN(2, React.createElement);
const mapProps = R.curry(
(mapping, C) => (props) => renderComponent(C)(mapping(props))
);
// R.o for functional composition
const mapProps = R.curry(
(mapping, C) => (props) => R.o(renderComponent(C), mapping)(props)
);
// removing explicit argument `props`
const mapProps = R.curry(
(mapping, C) => R.o(renderComponent(C), mapping)
);
// final pointfree version with R.flip
const mapProps = R.flip(R.useWith(R.o, [renderComponent, R.identity]));
Pointfree computeProps
const computeProps = R.curry(
(props, mapping) => R.merge(props, mapping(props))
);
// Adding converge is pretty straigtforward
const computeProps = R.converge(R.merge, [R.nthArg(1), R.call]);