wearereasonablepeople / warped-components

:cyclone: An opinionated way to build frontend applications
MIT License
18 stars 0 forks source link
cyclejs react redux

Warped Components

Build Status Greenkeeper Enabled

An opinionated way to build frontend applications. Works nicely with Warped Reducers.

Installation

Warped Components requires React 16.8.3 or later

$ npm install --save warped-components react xstream

In a nutshell

Warped Components does two things to facilitate your application:

  1. It manages the creation of your Redux store, and uses React Redux to connect your components to the global state. But instead of taking mapStateToProps and mapDispatchToProps functions, it takes a selectors object whose values are functions that map the state to individual properties, and an actions object whose values are functions that given a payload, return a Redux Standard Action.
  2. It lets you associate your Cycle applications and Redux reducers with specific React components. Then, whenever React renders those components, Warped Components will update the global Redux reducer to contain only the reducers found in the tree, and it will shut down and start up the appropriate Cycle applications based on which are present in the tree. All of this is achieved by using React Collect.

This approach has the following benefits:

  1. An architectural benefit, where all the logic related to a component, including asynchronous side-effects and state transformations, are all kept in a single place. This means that most changes to the application will be centred around a single directory in your source code.
  2. If you use code-splitting and multiple entry points, this approach facilitates a smaller main bundle, because the reducers and effects for components outside of the initial render tree don't have to be included.
  3. Your Redux reducers and Cycle applications are "hot by design". This means that if you use module.hot, your reducer logic and side-effect logic is also automatically hot-reloaded (given that you don't remount the WarpedApp, but only its children).

API

Automatic wiring

warped :: WarpedOptions -⁠> ReactComponent -⁠> ReactComponent

Does zero to two distinct things to a component, depending on the options:

The options (all optional, though at least one should be provided) are:

If you would just like the reducer or effects to be mounted without a "view", for example when a reducer is in change of handling some state shared between multiple components, then pass null as the ReactComponent. The returned ReactComponent can still be rendered anywhere in the tree to have its reducer and/or effects be mounted.

For the definition of actions and reducer, we recommend using Warped Reducers, and for the definition of the selectors, we highly recommend using an Optics library like Ramda's lens related functions or partial.lenses.

import {warped} from 'warped-components';
import {createReducer, noopAction} from 'warped-reducers';
import {lensProp, compose, set, view} from 'ramda';
import React from 'react';

// We use a lens to describe our slice of the global state.
// How you do it is up to you, though.
export const dataState = compose (
  lensProp ('app'),
  lensProp ('data')
);

// We use warped-reducers to create our reducer and actions.
export const {types, actions, reducer} = createReducer ('App') ({
  loadData: noopAction,
  setData: set (dataState)
});

// A small Cycle app describes the side-effects of our component.
export const effects = ({action, http}) => ({
  http: action.filter (({type}) => type === types.loadData).mapTo ({
    url: 'https://api.github.com/users/Avaq',
    category: types.loadData
  }),
  action: http.select (types.loadData).flatten ().map (({body: {name}}) =>
    actions.setData (name)
  )
});

// The selectors are used to map the global state to component props.
export const selectors = {
  data: view (dataState)
};

// This is our view.
export const App = ({data, loadData}) => (
  <div>
    <h1>{data || 'name unknown'}</h1>
    <button onClick={loadData}>Load!</button>
  </div>
);

// Warped Components wires the view to all of the above.
export default warped ({reducer, effects, selectors, actions}) (App);

WarpedApp :: ReactComponent

This component does the wiring for your application:

It takes the following optional props:

import {WarpedApp} from 'warped-components';
import {devToolsEnhancer} from 'redux-devtools-extension';
import {makeHTTPDriver} from '@cycle/http';
import {render} from 'react-dom';
import React from 'react';
import App from './my-app';

const drivers = {http: makeHTTPDriver ()};

render (
  <WarpedApp enhancer={devToolsEnhancer ()} drivers={drivers}>
    <App />
  </WarpedApp>,
  document.getElementById ('app')
);

Redux Utilities

If you prefer using React Redux and Redux directly, rather than using the WarpedApp, you can use these utilities to ease the interaction with Warped Reducers.

compileSelectors :: StrMap ((a, b) -⁠> c) -⁠> (a, b) -⁠> StrMap c

Given a mapping of selectors, returns a mapStateToProps function, as accepted by connect from React Redux.

The selectors are given the state (and previous props), and are expected to return a slice of the state. We recommend using Optics, such as the lens-related functions from Ramda, to create the selectors.

compileDispatchers :: StrMap (a -⁠> b) -⁠> (b -⁠> c) -⁠> StrMap (a -⁠> c)

Given a mapping of action creators, as returned from createReducer, returns a mapDispatchToProps function, as accepted by connect from React Redux.

combineReducers :: Array ((a, b) -⁠> a) -⁠> (a, b) -⁠> a

Given an array of reducers, returns a single reducer which transforms the state by calling all reducers in sequence.

Cycle utilities

combineCycles :: Array (StrMap Any -⁠> StrMap Stream) -⁠> StrMap Any -⁠> StrMap Stream

Given an array of main functions that take sources and return sinks, returns a single main function which combines the effects of each.