• Stars
    star
    189
  • Rank 204,649 (Top 5 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created over 7 years ago
  • Updated almost 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Fantasy Land type for React Components

react-dream-logo

React Dream

Build Status npm version

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:

import ReactDream from 'react-dream'
import { View } from 'react-native'

const DreamView = ReactDream(View)

…or for a web div:

import React from 'react'
import ReactDream from 'react-dream'

const DreamView = ReactDream(props => <div {...props} />)

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.

import React from 'react'
import { render } from 'react-dom'
import { withHandlers, withState } from 'recompose'
import { of } from 'react-dream'
import { Html } from 'react-dream-web-builtins'

const withChildren = North => South => Wrapper => ({ north, south, wrapper, ...props }) =>
  <Wrapper { ...props } { ...wrapper }}>
    <North { ...props } { ...north }} />
    <South { ...props } { ...south }} />
  </Wrapper>

const Title = Html.H1
  .style(() => ({
    fontFamily: 'sans-serif',
    fontSize: 18,
  }))
  .name('Title')

const Tagline = Html.P
  .style(() => ({
    fontFamily: 'sans-serif',
    fontSize: 13,
  }))
  .name('Tagline')

const HeaderWrapper = Html.Header
  .removeProps('clicked', 'updateClicked')
  .style(({ clicked }) => ({
    backgroundColor: clicked ? 'red' : 'green',
    cursor: 'pointer',
    padding: 15,
  }))
  .name('HeaderWrapper')
  .map(
    withHandlers({
      onClick: ({ clicked, updateClicked }) => () => updateClicked(!clicked),
    })
  )
  .map(withState('clicked', 'updateClicked', false))

const Header = of(withChildren)
  .ap(Title)
  .ap(Tagline)
  .ap(HeaderWrapper)
  .contramap(({ title, tagline }) => ({
    north: { children: title },
    south: { children: tagline },
  }))
  .name('Header')

Header.fork(Component =>
  render(
    <Component
      title="Hello World"
      tagline="Of Fantasy Land Types for React"
    />,
    document.getElementById('root')
  )
)

Render part could also be written:

render(
  <Header.Component
    title="Hello World"
    tagline="Of Fantasy Land Types for React"
  />,
  document.getElementById('root')
)

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:

import React from 'react'
import { render } from 'react-dom'
import { compose, withHandlers, withState } from 'recompose'
import { ap, removeProps, contramap, map, name, of, style } from 'react-dream'
import { Html } from 'react-dream-web-builtins'

const withChildren = North => South => Wrapper => ({ north, south, wrapper, ...props }) =>
  <Wrapper { ...props } { ...wrapper }}>
    <North { ...props } { ...north }} />
    <South { ...props } { ...south }} />
  </Wrapper>

const Title = compose(
  name('Title'),
  style(() => ({
    fontFamily: 'sans-serif',
    fontSize: 18,
  }))
)(Html.H1)

const Tagline = compose(
  name('Tagline'),
  style(() => ({
    fontFamily: 'sans-serif',
    fontSize: 13,
  }))
)(Html.P)

const HeaderWrapper = compose(
  map(withState('clicked', 'updateClicked', false)),
  map(
    withHandlers({
      onClick: ({ clicked, updateClicked }) => () => updateClicked(!clicked),
    })
  ),
  name('HeaderWrapper'),
  style(({ clicked }) => ({
    backgroundColor: clicked ? 'red' : 'green',
    cursor: 'pointer',
    padding: 15,
  })),
  removeProps('clicked', 'updateClicked')
)(Html.Header)

const Header = compose(
  name('Header'),
  contramap(({ title, tagline }) => ({
    north: { children: title },
    south: { children: tagline },
  })),
  ap(HeaderWrapper),
  ap(Tagline),
  ap(Title)
)(of(withChildren))

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.

import React from 'react'
import ReactDream from 'react-dream'
import { withHandlers, withState } from 'recompose'

const Counter = ReactDream(({counter, onClick}) =>
  <div>
    <button onClick={onClick}>Add 1</button>
    <p>{counter}</p>
  </div>
)
  .map(
    withHandlers({
      onClick: ({ counter, updateCount }) => () => updateCount(counter + 1),
    })
  )
  .map(withState('counter', 'updateCount', 0))

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
  .contramap(({label}) => ({
    children: label
  }))
  .name('Title')

render(
  <Title.Component
    label='This will be the content now'
  />,
  domElement
)

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 = Html.Div
  .promap(
    ({title}) => ({children: title}),
    setDisplayName('Header')
  )

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 => Right => ({left, ...props}) =>
  left
    ? <Left {...props} />
    : <Right {...props} />

const TitleOrSubtitle = of(eitherLeftOrRight)
  .ap(Html.H1)
  .ap(Html.H2)
  .addProps({isTitle} => ({
    left: isTitle
  }))

render(
  <TitleOrSubtitle.Component isTitle={true}>
    This will be an H1 title
  </TitleOrSubtitle.Component>
  , domElement
)

Parent-children pattern example

const withChildren = North => South => Wrapper => ({north, south, wrapper, ...props}) =>
  <Wrapper { ...props } { ...wrapper }}>
    <North { ...props } { ...north }} />
    <South { ...props } { ...south }} />
  </Wrapper>

const PageHeader = of(withChildren)
  .ap(Html.H1)
  .ap(Html.P)
  .ap(Html.Header)
  .addProps({title, subtitle} => ({
    north: { children: title },
    south: { children: subtitle },
  }))

render(
  <PageHeader.Component
    title='Hello World'
    subtitle='Lorem ipsum dolor sit amet et consectetur'
  />
  , domElement
)

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:

import { Html } from 'react-dream'

const Header = Html.H1
  .concat(Html.P)

Since props are passed to both elements in the composition, invoking the above defined Header like this:

<Header.Component>Hello</Header.Component>

…will result in:

<h1>Hello</h1>
<p>Hello</p>

So to make concatenation more useful, it is necessary for the elements to be configured to capture the props that are useful for them:

import { Html } from 'react-dream'

const Header = Html.H1
  .contramap(({title}) => ({children: title}))
  .concat(
    Html.P
      .contramap(({description}) => ({children: description}))
  )

This way the composition can be used like this:

<Header.Component
  title='Hello'
  description='World!'
/>

…and will result in:

<h1>Hello</h1>
<p>World!</p>

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

import ReactDream from 'react-dream'
import { Svg } from 'react-dream-web-builtins'

const wrapWithGLayer = Component => ReactDream(props =>
  <g>
    <Component {...props} />
  </g>
)

const LayerWithCircle = Svg.Circle
  .contramap(() => ({
    r: 5,
    x: 10,
    y: 10
  })
  .chain(wrapWithGLayer)

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.fork(Component => render(<Component>Hello</Component>, domElement))

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:

import { Svg } from 'react-dream-web-builtins'

const Picture = Svg.Svg
  .addProps(props => ({
    viewBox: `0 0 ${props.width} ${props.height}`
  }))

render(
  <Picture.Component
    width={50}
    height={50}
  />,
  domElement
)

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

.addProps(({width, height}) => ({
  viewBox: `0 0 ${props.width} ${props.height}`
}))

…is equivalent to:

.contramap(props => ({
  ...props,
  viewBox: `0 0 ${props.width} ${props.height}`
}))

removeProps(...propNamesToRemove : [String])

removeProps filters out props. Very useful to avoid the React warnings of unrecognized props.

const ButtonWithStates = Html.Button
  .removeProps('hovered', 'pressed')
  .style(({hovered, pressed}) => ({
    color: pressed ? 'red' : (hovered ? 'orange' : 'black')
  }))

removeProps is an use case of contramap

.removeProps('title', 'hovered')

…is equivalent to:

.contramap(({title, hovered, ...otherProps}) => otherProps)

defaultProps(props : Object)

defaultProps allows you to set the, well, defaultProps of the wrapped React component.

const SubmitButton = Html.Button
  .defaultProps({ type: 'submit' })

defaultProps is an use case of map

const SubmitButton = Html.Button
  .defaultProps({ type: 'submit' })

Under the hood is using recompose’s defaultProps function:

import { defaultProps } from 'recompose'

const SubmitButton = Html.Button
  .map(defaultProps({ type: 'submit' }))

propTypes(propTypes : Object)

propTypes sets the propTypes of the React component.

import PropTypes from 'prop-types'

const Title = Html.H1
  .style(({ highlighted }) => ({
    backgroundColor: highlighted ? 'yellow' : 'transparent'
  }))
  .propTypes({
    children: PropTypes.node,
    highlighted: PropTypes.bool
  })

propTypes is an use case of map

The example above is equivalent to:

import PropTypes from 'prop-types'
import { setPropTypes } from 'recompose'

const Title = Html.H1
  .style(({ highlighted }) => ({
    backgroundColor: highlighted ? 'yellow' : 'transparent'
  }))
  .map(setPropTypes({
    children: PropTypes.node,
    highlighted: PropTypes.bool
  }))

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 = Html.H1
  .style(props => ({color: highlighted ? 'red' : 'black'}))

render(
  <Title
    highlighted
    style={{backgroundColor: 'green'}}
  />,
  domElement
)

The resulting style will be: { color: 'red', backgroundColor: 'green' }.

style is an use case of contramap

.style(({hovered}) => ({
  color: hovered ? 'red' : 'black'
}))

…is equivalent to:

.contramap(props => ({
  style: {
    color: props.hovered ? 'red' : 'black',
    ...props.style
  },
  ...props
}))

name(newDisplayName : String)

Sets the displayName of the component:

const Tagline = H2.name('Tagline')

name is an use case of map

.name('Tagline')

…is equivalent to:

import { setDisplayName } from 'recompose'

.map(setDisplayName('Title'))

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 = Html.H1
  .rotate(props => 45)

render(
  <Title.Component style={{ transform: 'rotate(45deg)' }} />,
  document.getElementById('root')
)

…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

.rotate(props => 45)

…is equivalent to:

.contramap(props => ({
  style: {
    transform: props.transform
      ? `${props.transform} rotate(45deg)`
      : 'rotate(45deg)'
    ...props.style
  },
  ...props
}))

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 = Html.H1
  .scale(props => 1.5)

render(
  <Title.Component style={{ transform: 'scale(1.5)' }} />,
  document.getElementById('root')
)

…will result in transform: 'translateX(20px) scale(1.5)'

scale is an use case of contramap
.scale(props => 2)

…is equivalent to:

.contramap(props => ({
  style: {
    transform: props.transform
      ? `${props.transform} scale(2)`
      : 'scale(2)'
    ...props.style
  },
  ...props
}))

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 = Html.H1
  .translate(props => [30])
  .translate(props => [null, 30])
  .translate(props => [null, null, 30])

…will result in transform: 'translateZ(30px) translateY(30px) translateX(30px)'

translate is an use case of contramap

.translate(({x, y}) => [x, y])

…is equivalent to:

.contramap(props => ({
  style: {
    transform: props.transform
      ? `${props.transform} translate(${x}px, ${y}px)`
      :  `translate(${x}px, ${y}px)`
    ...props.style
  },
  ...props
}))

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 = Html.H1
  .log(props => 'what props gets to the H1?')
  .log(props => props)
  .contramap(({hovered, label}) => ({
    children: hovered ? 'Hovered!' : label
  }))
  .log(({label}) => 'is there a label before the contramap? ' + label)
  .name('Title')
  .log(({label}) => 'does it also get a label from outside? ' + label)

render(
  <Title.Component hovered label='Label from outside' />,
  domElement
)

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

.log(({a}) => `a is: ${a}`)

…is equivalent to:

import withLog from '@hocs/with-log'

.map(withLog(({a}) => `a is: ${a}`))

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.

import React from 'react'
import { render } from 'react-dom'
import { Html } from 'react-dream-web-builtins'

const App = Html.Div
  .debug()
  .removeProps('a', 'c', 'randomProp')
  .addProps(() => ({
    a: '1',
    c: '4'
  }))

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

.debug()

…is equivalent to:

import withDebugger from '@hocs/with-debugger'

.map(withDebugger)

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:

import { Svg, Html } from 'react-dream-web-builtins'

const MyDiv = Html.Div

const MyLayer = Svg.G

Read more in the package README

License

MIT

More Repositories

1

sketch2json

Get a JSON output out of a buffer of Sketch v43+ data (works both in Node and in the browser)
JavaScript
187
star
2

sketch-loader

Webpack loader for Sketch (+43) files
JavaScript
68
star
3

piano

Out-of-the-box sinatra server for haml + sass + coffee-script development
Ruby
18
star
4

tessellation

Thesis on how to build functional, reactive and well structured front end applications. A JavaScript way of doing the Elm architecture
JavaScript
17
star
5

washington

A pure, functional† unit testing tool with a dependency-free test suite API
JavaScript
13
star
6

which-ramda-function-should-i-use

Find out which Ramda function should you use, without thinking
JavaScript
11
star
7

playing-react-fantasy-land-types

Playing around with a Fantasy Land type for React component
JavaScript
10
star
8

redux-haiku

Redux side-effects using state diffs in subcribers
JavaScript
9
star
9

docker-pokemonsay

Dockerized version of https://github.com/possatti/pokemonsay
Makefile
8
star
10

redux-walk

A control-flow extension for Redux that keeps you functional
JavaScript
7
star
11

react-context-props

[deprecated] Decorate React Components so they can get context props as regular props
JavaScript
7
star
12

object-pattern

Object Pattern structures for JavaScript
JavaScript
5
star
13

html2jsx

Convert HTML/SVG text to JSX functions
JavaScript
4
star
14

day-dream

A playground collection of fantasy-land types that I find useful
JavaScript
4
star
15

ReactDreamWalkthroughNativeExperiment

JavaScript
3
star
16

kiel

Eager port / service scanner for Node.js
JavaScript
2
star
17

redux-subscriptions

Higher-level API for the Redux store.subscribe.
JavaScript
2
star
18

react-simple-grid-tool

React component for showing a Grid on top of other components - useful for debugging alignment
JavaScript
2
star
19

react-dream-benchmark

Benchmark React Dream compositions against naive Recompose
JavaScript
2
star
20

react-dream-experiments

Experiments for testing out features of https://github.com/xaviervia/react-dream
JavaScript
2
star
21

object-difference

An experimental tool for getting the difference between two object structures with as little metadata as possible.
JavaScript
2
star
22

fantasy-color

Fantasy Land type for CSS color
JavaScript
2
star
23

redux-subscribers-example

This is a simple and useless command line app meant to demonstrate how listening to state diff in Redux is useful for more than just UI update.
JavaScript
2
star
24

skiss

JavaScript
1
star
25

fast

DSL for file system interaction
Ruby
1
star
26

react-dream-examples

Examples of usage of React Dream, a Fantasy Land type for React components
JavaScript
1
star
27

learnfast

LearnFast helps you to comprehend text written in a language you want to learn.
JavaScript
1
star
28

essays

Collection of essays on various topics that I find myself interested in. Content quality or correctness is not guaranteed. Beware.
1
star
29

versionifier

A silly gem to update Ruby project version from command line
Ruby
1
star
30

mediador

Add event support as a mixin
JavaScript
1
star
31

patch-react-proptypes-add-introspection

🤕 Patch React PropTypes to add introspection capabilities and get runtime insights into the propTypes declaration of third party React components
JavaScript
1
star
32

hoc-with-store

Higher-order React component for binding a Redux Store
JavaScript
1
star
33

dotfiles

My dotfiles
Shell
1
star
34

react-dream-web-builtins

Builtin React Dream components for web primitives
JavaScript
1
star
35

ast-loader

Webpack loader for babylon’s abstract syntax tree documents, converted to JS with babel-generator
JavaScript
1
star
36

shell-server

WIP
JavaScript
1
star
37

html2react-json

Convert HTML/SVG text to React JSON as defined by https://github.com/gorangajic/react-render-to-json
JavaScript
1
star