#Recompose Workshop
##Stateless Function Components
Review stateless function components
- best way to code markup
- they take an object props, as their first arg, and context as second
- they return jsx
- they have no state...
- best practice
they look like this:
const Square = ({ color, width }) => (
<div style={{ width, height: width, backgroundColor: color }}></div>
);
const Square = ({ pred, color, width }) => {
if (!pred()) return <span></span>; // better as a ternary or branch from recompose
return <div style={{ width, height: width, backgroundColor: color }}></div>
};
###Why src
Forget ES6 classes vs. createClass().
An idiomatic React application consists mostly of function components.
const Greeting = props => (
<p>
Hello, {props.name}!
</p>
);
Function components have several key advantages:
They help prevent abuse of the setState() API, favoring props instead. They encourage the "smart" vs. "dumb" component pattern. They encourage code that is more reusable and modular. They discourage giant, complicated components that do too many things. In the future, they will allow React to make performance optimizations by avoiding unnecessary checks and memory allocations. (Note that although Recompose encourages the use of function components whenever possible, it works with normal React components as well.)
##Higher Order Components
Higher order components are function that receive a react component, and return a new react component "augmented" with new behaviour. I put augmented in quotes here because generally you are not mutating the react component that you take into the HOC, but rather wrapping it with functions that manipulate it's props and contain other behaviours in themselves.
###References
HigherOrderComponent : Component => EnhancedComponent
HigherOrderComponent : (args, Component) => EnhancedComponent
HigherOrderComponent : args => Component => EnhancedComponent
#####React Redux
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
#####Theming Component
This component is completely reuseable, and can be applied to any react component.
// takes in a function for later use
// takes in the component to enhance
export const connectTheme = mapThemeToCss => Composed => {
// returns a new stateless component
const wrapped = (props, context) => {
// return the original component if there is no theme available on context
if (!context || !context.theme) return <Composed {...props} />;
// return a wrapped component directly (useSheet is a third party HOC)
return React.createElement(useSheet(PluckClasses(Composed), mapThemeToCss(context.theme)), props);
};
// apply the proper context types to the top level returned component
wrapped.contextTypes = { theme: PropTypes.object };
// set the displayName to something readable
wrapped.displayName = `Theme(${Composed.displayName || 'component'})`;
return wrapped;
};
##Composition The eventual return function of these HOC's (after we supply all initial args) is:
HigherOrderComponent : Component => EnhancedComponent
Like any unary (accepting one argument) function of Type => Type we can compose this function with any other function of Type => Type
ie:
const enhance = compose(
connect( // returns a function of Component => Component
mapStateToProps,
mapDispatchToProps
),
connectTheme(mapThemeToCss) // ditto
);
Now we can use our enhanced HOC on any react component
export default enhance(() => <div>derp</div>);
or more commonly
const enhance = /*...*/;
const Markup = () => (
<div>Some Larger Tree</div>
);
export default enhance(Markup);
##Recompose Recompose is a React utility belt for function components and higher-order components. Think of it like lodash for React. Uses:
// counter is the stateProp, setCounter is a setState function for that specific prop, 0 is the initialstate
const enhance = withState('counter', 'setCounter', 0)
// purely functional component decorated with some state!
const Counter = enhance(({ counter, setCounter }) =>
<div>
Count: {counter}
<button onClick={() => setCounter(n => n + 1)}>Increment</button>
<button onClick={() => setCounter(n => n - 1)}>Decrement</button>
</div>
)
same thing but more redux-like
// pure reducer
const counterReducer = (count, action) => {
switch (action.type) {
case INCREMENT:
return count + 1
case DECREMENT:
return count - 1
default:
return count
}
}
const enhance = withReducer('counter', 'dispatch', counterReducer, 0)
const Counter = enhance(({ counter, dispatch }) =>
<div>
Count: {counter}
<button onClick={() => dispatch({ type: INCREMENT })}>Increment</button>
<button onClick={() => dispatch({ type: DECREMENT })}>Decrement</button>
</div>
)
Create Context (like react-redux 'provide')
export const ThemeProvider = withContext(
{ theme: PropTypes.object },
props => ({ theme: props.theme }) // pure function
);
Get Context
const enhance = getcontext({ theme: proptypes.object });
Branch (apply one or another HOC based on predicate)
const enhance = branch(
props => props.theme, // predicate
omitProp('sheet'),
c => c // identity function (an HOC that does nothing)
);
We can bring all those together with composition to create easy to understand libraries of reusable functionality like Gild: our themeing library.
Gilds connectTheme (similar to connect from react-redux) before recompose
// takes in a function for later use (mapThemeToCss)
// return a function that takes a component and returns a component (standard HOC signature)
export const connectTheme = mapThemeToCss => Composed => {
// create the eventual stateless component
const wrapped = (props, context) => {
// return the original component if there is no theme available on context
if (!context || !context.theme) return <Composed {...props} />;
// return a wrapped component directly (useSheet is a third party HOC)
return React.createElement(useSheet(PluckClasses(Composed), mapThemeToCss(context.theme)), props);
};
// apply the proper context types to the top level returned component
wrapped.contextTypes = { theme: PropTypes.object };
// set the displayName to something readable
wrapped.displayName = `Theme(${Composed.displayName || 'component'})`;
// finally return the new component
return wrapped;
};
After recompose
export const connectTheme = mapThemeToCss =>
compose(
comp => setDisplayName(wrapDisplayName(comp, 'gild'))(comp),
getContext({ theme: PropTypes.object }),
branch(
props => props.theme,
compose(
withSheet(mapThemeToCss),
mapProps(props => ({ ...props, theme: props.sheet.classes })),
omitProp('sheet')
),
c => c
)
);
explained:
export const connectTheme = mapThemeToCss =>
compose(
// an HOC that wraps the displayName with GILD
comp => setDisplayName(wrapDisplayName(comp, 'gild'))(comp),
// hoc that gets context, getContext is self documenting
getcontext({ theme: proptypes.object }),
// if getContext passed theme to props
branch(
props => props.theme,
// a further composition of recompose utils
compose(
// a more complicated HOC defined in another file
withSheet(mapThemeToCss),
// unnesting a prop
mapProps(props => ({ ...props, theme: props.sheet.classes })),
// omitting the prop we extracted classes from
omitProp('sheet')
),
// if there's no theme prop we just return a HOC that does nothing but return the original Comp
c => c
)
);
full code here -- https://github.com/influitive/Gild
###Pros
- Much less code to reason about
- Code that is left is all pure functions (easier to reason about)
- Consistency across projects, similar to how lodash/ramda is a common toolset
- No coupling
- Very high reuse capability
- We're already using tons of third party HOC's so compose(...) is natural