A small utility for easily creating specialised versions of existing React components, based on the functional programming concept of currying.
Motivation
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:
{ return <Form> // form fields go here <Button ="raised" =>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
{ return <Button ="raised" />;}
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 = ;
…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 = ;
This is much more readable and allows lots of cool tricks, like dynamically creating your curry based on the output of other components.
# Usage
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 = ;
React Displayname
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 = ;
Advanced techniques
Soft and Hard currying
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 = ;const buttonElement2 = <DefaultRaisedButton ="outlined" />; // variant will be "outlined" const AlwaysRaisedButton = ;const buttonElement1 = <AlwaysRaisedButton ="outlined" />; // variant will be "raised"
The default export for this package is currySoft
.
Smart currying
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 = ;const buttonElement = <MyButton ="submit-btn" />; // className will be "btn submit-btn" const MyTitle = ;const titleElement = <MyTitle =>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 = ;const buttonElement = <MyButton = />; // 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 = ; // will prefer curried props
This also gives preference to curried style instructions if they conflict with the "normal" props.
Custom prop behaviour
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'; { const href: curryHref ...otherCurriedProps = curriedProps; const href ...otherProps = props; //only allow overwrite by HTTPS links const combinedHref = href) ? href : curryHref; //do a default soft curry on other props return ...otherCurriedProps ...otherProps className: combinedClassName ;} const Btn = ; // className will be "big btn"const buttonElement = <Btn ="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.