A small utility for easily creating specialised versions of existing React components, based on the functional programming concept of currying.
Frequently in React we want to take an out-of-the-box component, or a generic one we've created ourselves, and use it in a specialised way. Let's say, for example, that our app has Call To Action buttons that use the Material UI Button
component, but with the variant="raised"
prop to give it more visual emphasis:
function MyAppForm({ formData, onSubmit }) {
return (
<Form>
// form fields go here
<Button variant="raised" onClick={ onSubmit }>Submit Form</Button>
</Form>
);
}
If we use these everywhere, it's a good idea to create a React component to represent this sort of use case, so that if we want to change the apperance or behaviour of all the buttons, we can just update our component. So we do
function CallToAction(props) {
return <Button variant="raised" { ...props } />;
}
This is fine, but a little more boilerplatey than we need. We only need to supply three things to make this declaration:
- The component we want to specialise (
Button
in this case) - The props we want to use to specialise it (
{ variant: "raised" }
here) - The displayname of the component (
CallToAction
) - this is optional and could be autogenerated.
This suggests a possible simpler syntax for this sort of operation:
const CallToAction = curry(Button, { variant: "raised"}, "CallToAction");
…but we can be even more concise once we notice that React Component Type + props = React Element
. So we can instead write simply
const CallToAction = curry(<Button variant="raised" />, "CallToAction");
This is much more readable and allows lots of cool tricks, like dynamically creating your curry based on the output of other components.
Install with npm install react-curry-component
, then use like this:
import React from 'react';
import { Button } from '@material-ui/core';
import curry from 'react-curry-component';
const CallToAction = curry(<Button variant="raised" />, "CallToAction");
The second argument is optional and defaults to a generated displayname of the form Curried(ComponentType)
:
// this will have the name `Curried(Button)`
const CallToAction = curry(<Button variant="raised" />);
Unlike currying in Functional Programming, there's a possibility that your curried component could get props that contradicts the ones you provide in your curry. In most cases we probably want this more "recent" value to take precedence - we call this a soft curry, and it's a lot like providing default props. However, in some cases, we may want our curried props to take precendence, in which case we call it a hard curry.
import React from 'react';
import { Button } from '@material-ui/core';
import { curryHard, currySoft } from 'react-curry-component';
const DefaultRaisedButton = currySoft(<Button variant="raised" />);
const buttonElement2 = <DefaultRaisedButton variant="outlined" />; // variant will be "outlined"
const AlwaysRaisedButton = curryHard(<Button variant="raised" />);
const buttonElement1 = <AlwaysRaisedButton variant="outlined" />; // variant will be "raised"
The default export for this package is currySoft
.
For some React props, it makes more sense to do some sort of clever merge rather than just use either prop. For example, with className
or style
, it makes more sense to use both the curried value and the supplied value. You can use currySmart
to do this:
import React from 'react';
import { currySmart } from 'react-curry-component';
const MyButton = currySmart(<button className="btn" />);
const buttonElement = <MyButton className="submit-btn" />; // className will be "btn submit-btn"
const MyTitle = currySmart(<h2 style={ { fontFamily: "Comic Sans" } } />);
const titleElement = <MyTitle style={ { padding: "8px" } }>Hurray for Curry!</MyTitle>; //will have the font family and the padding applied
In the case of event handlers, currySmart
will ensure that both handlers are triggered:
import React from 'react';
import { currySmart } from 'react-curry-component';
const MyButton = currySmart(<button onClick={ sendButtonAnalytics } />);
const buttonElement = <MyButton onClick={ handleSubmit } />; // clicking on this will trigger analytics and submit behaviour
All other props will be handled using the normal currySoft
behaviour. You can switch to curryHard
default behaviour like this:
import React from 'react';
import { currySmart } from 'react-curry-component';
const MyButton = currySmart(<button onClick={ sendButtonAnalytics } />, true); // will prefer curried props
This also gives preference to curried style instructions if they conflict with the "normal" props.
In some specialised circumstances you may want to customised the currying behaviour for certain props, so you can supply a propsReducer
that determines exactly how the curried props are combined with the element props.
import React from 'react';
import { Button } from '@material-ui/core';
import { curry } from 'react-curry-component';
function linkReducer(curriedProps, props) {
const { href: curryHref, ...otherCurriedProps } = curriedProps;
const { href, ...otherProps } = props;
//only allow overwrite by HTTPS links
const combinedHref = href.startsWith("https")) ? href : curryHref;
//do a default soft curry on other props
return { ...otherCurriedProps, ...otherProps, className: combinedClassName };
}
const Btn = curry(<Button className="btn" />, "Btn", classNameReducer);
// className will be "big btn"
const buttonElement = <Btn className="big" />;
Bear in mind that your propsReducer
function will be called on every render of every instance of your curried function, so avoid making it too intensive if you're planning to do lots of frequent updates on a large number of curried elements.