React Dream
Fantasy Land type for React Components
Caution: Experimental (not extremely anymore though)
Installation
npm add react-dream
You will also need a couple of peer dependencies:
npm add react recompose
Table of contents
Usage
Lifting React components into ReactDream
For example, for a ReactNative View:
const DreamView =
…or for a web div
:
const DreamView =
Complete example
Here is an extensive example that can be found in examples:
If you are not familiar with Fantasy Land types, I can highly recommend the video tutorials by Brian Lonsdorf
Note that this and the following examples use already-built wrappers that you can pull from react-dream-web-builtins. This are convenient but might not be easy to tree shake when bundling, so use with caution.
const withChildren = <Wrapper ...props ...wrapper }> <North ...props ...north } /> <South ...props ...south } /> </Wrapper> const Title = HtmlH1 style fontFamily: 'sans-serif' fontSize: 18 name'Title' const Tagline = HtmlP style fontFamily: 'sans-serif' fontSize: 13 name'Tagline' const HeaderWrapper = HtmlHeader style backgroundColor: clicked ? 'red' : 'green' cursor: 'pointer' padding: 15 name'HeaderWrapper' const Header = ofwithChildren name'Header' Header
Render part could also be written:
Pointfree style
All methods of ReactDream
are available as functions that can be partially applied and then take the ReactDream component as the last argument. This makes it possible to write compositions that can then be applied to a ReactDream object. The elements of the example above could be rewritten as:
const withChildren = <Wrapper ...props ...wrapper }> <North ...props ...north } /> <South ...props ...south } /> </Wrapper> const Title = HtmlH1 const Tagline = HtmlP const HeaderWrapper = HtmlHeader const Header = ofwithChildren
API
The following are the methods of objects of the ReactDream type. There are two types of methods:
- Algebras: they come from Fantasy Land, and they are defined following that specification.
- Helpers: they are derivations (use cases) of the methods that come from the algebras. Added for convenience.
ReactDream implements these Fantasy Land algebras:
- Profunctor (map, contramap, promap)
- Applicative (of, ap)
- Semigroup (concat)
- Monad (chain)
Check Fantasy Land for more details.
map(Component => EnhancedComponent)
map
allows to wrap the function with regular higher-order components, such as the ones provided by recompose.
const Counter =
This is because map
expects a function from a -> b
in the general case but from Component -> a
in this particular case since holding components is the intended usage of ReactDream. Higher-order components are functions from Component -> Component
, so they perfectly fit the bill.
contramap(props => modifiedProps)
contramap
allows to preprocess props before they reach the component.
const Title = H1 name'Title'
This is a common pattern for higher-order Components, and the key advantage of using contramap
instead of map
for this purpose is that if the wrapped component is a stateless, function component, you avoid an unnecessary call to React. Another advantage is that functions passed to contramap
as an argument are simply pure functions, without mentioning React at all, with the signature Props -> Props
.
promap(props => modifiedProps, Component => EnhancedComponent)
promap
can be thought of as a shorthand for doing contramap
and map
at the same time. The first argument to it is the function that is going to be used to contramap
and the second is the one to be used to map
:
const Header = HtmlDiv
ap + of
ap
allows you to apply a higher-order components to regular components, and of
allows you to lift any value to ReactDream
, which is useful for lifting higher-order components.
Applying second-order components (Component -> Component
) can also be done with map
: where ap
shines is in allowing you to apply a higher-order component that takes two or more components (third or higher order, such as Component -> Component -> Component -> Component
), that is otherwise not possible with map
. This makes it possible to abstract control flow or composition patterns in higher-order components:
Control flow example
const eitherLeftOrRight = left ? <Left ...props /> : <Right ...props /> const TitleOrSubtitle = ofeitherLeftOrRight
Parent-children pattern example
const withChildren = <Wrapper ...props ...wrapper }> <North ...props ...north } /> <South ...props ...south } /> </Wrapper> const PageHeader = ofwithChildren
concat
Requires React 16+
concat
constructs a new component that wraps the current component and another one being passed as siblings, passing the props to both of them. For example:
const Header = HtmlH1
Since props are passed to both elements in the composition, invoking the above defined Header
like this:
<HeaderComponent>Hello</HeaderComponent>
…will result in:
HelloHello
So to make concatenation more useful, it is necessary for the elements to be configured to capture the props that are useful for them:
const Header = HtmlH1
This way the composition can be used like this:
<HeaderComponent title='Hello' description='World!'/>
…and will result in:
HelloWorld!
Note: while concat
is for all practical purposes associative (as far as the resulting elements in the DOM are concerned), the React Components themselves are not joined together in an associative way, and this can be seen in the React DevTools. This violation of associativity is what makes it impossible for ReactDream to implement Monoid.
chain
chain
is useful as a escape hatch if you want to escape from ReactDream and do something very React-y
const wrapWithGLayer = const LayerWithCircle = SvgCircle
Aside from Fantasy Land algebras, ReactDream provides the methods:
fork(Component => {})
Calls the argument function with the actual component in the inside. This function is intended to be used to get the component for rendering, which is a side effect:
H1
addProps(props => propsToAdd : Object)
addProps
allows you to pass a function whose result will be merged with the regular props. This is useful to add derived props to a component:
const Picture = SvgSvg
The new props will be merged below the regular ones, so that the consumer can always override your props:
import { Svg } from 'react-dream-web-builtins' const Picture = Svg.Svg .addProps(props => ({+ // This will be now ignored viewBox: `0 0 ${props.width} ${props.height}` })) render( <Picture.Component+ viewBox='0 0 100 100' width={50} height={50} />, domElement)
addProps
is a use case of contramap
…is equivalent to:
removeProps(...propNamesToRemove : [String])
removeProps
filters out props. Very useful to avoid the React warnings of unrecognized props.
const ButtonWithStates = HtmlButton style color: pressed ? 'red' : hovered ? 'orange' : 'black'
removeProps
is an use case of contramap
…is equivalent to:
defaultProps(props : Object)
defaultProps
allows you to set the, well, defaultProps
of the wrapped React component.
const SubmitButton = HtmlButton
defaultProps
is an use case of map
const SubmitButton = HtmlButton
Under the hood is using recompose
’s defaultProps
function:
const SubmitButton = HtmlButton
propTypes(propTypes : Object)
propTypes
sets the propTypes
of the React component.
const Title = HtmlH1 style backgroundColor: highlighted ? 'yellow' : 'transparent'
propTypes
is an use case of map
The example above is equivalent to:
const Title = HtmlH1 style backgroundColor: highlighted ? 'yellow' : 'transparent'
style(props => stylesToAdd : Object)
The style
helper gives a simple way of adding properties to the style
prop of the target component. It takes a function from props to a style object. The function will be invoked each time with the props. The result will be set as the style
prop of the wrapper component. If there are styles coming from outside, they will be merged together with the result of this function. For example:
const Title = HtmlH1 style color: highlighted ? 'red' : 'black'
The resulting style will be: { color: 'red', backgroundColor: 'green' }
.
style
is an use case of contramap
style color: hovered ? 'red' : 'black'
…is equivalent to:
name(newDisplayName : String)
Sets the displayName
of the component:
const Tagline = H2name'Tagline'
name
is an use case of map
name'Tagline'
…is equivalent to:
rotate(props => rotation : number)
rotate
sets up a style transform
property with the specified rotation, in degrees. If there is a transform already, rotate
will append to it:
const Title = HtmlH1
…will result in transform: 'translateX(20px) rotate(45deg)'
Just a reminder: rotations start from the top left edge as the axis, which is rarely what one wants. If you want the rotation to happen from the center, you can set
transform-origin: 'center'
, that with ReactDream would be.style(props => ({transformOrigin: 'center'}))
.
rotate
is an use case of contramap
…is equivalent to:
scale(props => scaleFactor : number)
scale
sets up a style transform
property with the specified scaling factor. If there is a transform already, scale
will append to it:
const Title = HtmlH1
…will result in transform: 'translateX(20px) scale(1.5)'
scale
is an use case of contramap
…is equivalent to:
translate(props => [x : number, y : number, z : number])
translate
allows you to easily set up the transform
style property with the specified displacement. If there is a transform already, translate
will append to it:
const Title = HtmlH1
…will result in transform: 'translateZ(30px) translateY(30px) translateX(30px)'
translate
is an use case of contramap
…is equivalent to:
Debugging
The downside of chaining method calls is that debugging is not super intuitive. Since there are no statements, it’s not possible to place a console.log()
or debugger
call in the middle of the chain without some overhead. To simplify that, two methods for debugging are bundled:
log(props => value : any)
Whenever the Component is called with new props, it will print:
- The component displayName
- The value by the argument function. The value can be anything, it will be passed as-is to the
console.log
function.
Pretty useful to debug what exactly is happening in the chain:
const Title = HtmlH1 name'Title'
log
will become a no-op when the NODE_ENV
is production
.
For more details check out @hocs/with-log documentation which React Dream is using under the hood.
log
is an use case of map
…is equivalent to:
debug()
Careful: This method allows you to inject a debugger
statement at that point in the chain. The result will allow you to inspect the Component and its props, from the JavaScript scope of the @hocs/with-debugger higher-order component.
const App = HtmlDiv
It will be called on each render of the component.
debug
will become a no-op when the NODE_ENV
is production
.
For more details check out @hocs/with-debugger documentation which React Dream is using under the hood.
debug
is an use case of map
…is equivalent to:
Built-in Primitives
A separate package, react-dream-web-builtins ships with a complete set of HTML and SVG primitives lifted into the type. You can access them like:
const MyDiv = HtmlDiv const MyLayer = SvgG
Read more in the package README