An opinionated way to build frontend applications. Works nicely with Warped Reducers.
Warped Components requires React 16.8.3 or later
$ npm install --save warped-components react xstream
- The view is handled by React.
- The state is handled by Redux.
- The effects are handled by Cycle with XStream.
- The wiring is handled by Warped Components.
Warped Components does two things to facilitate your application:
- 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
andmapDispatchToProps
functions, it takes aselectors
object whose values are functions that map the state to individual properties, and anactions
object whose values are functions that given a payload, return a Redux Standard Action. - 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:
- 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.
- 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.
- 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).
Does zero to two distinct things to a component, depending on the options:
- Connects it to the state using the given selectors and actions.
- Associates a Redux reducer and a Cycle application with the component.
The options (all optional, though at least one should be provided) are:
selectors
: An object whose keys correspond to properties, and values are functions that receive the state and return the value for the property.actions
: A hash-map of action creators. The underlying component will receive them as props, and when called, the resulting action is dispatched to the store.reducer
: A Redux reducer - a function which takes a state and an action, and returns a new state. Warped Components makes sure that whenever the connected component is mounted, this reducer will act as part of the reducers of your store.effects
: A Cycle application - a function which takes a mapping of stream / stream-producers, and returns a mapping of streams.
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);
This component does the wiring for your application:
- Setting up your Redux store, and swapping out its
reducer
as needed. - Setting up your Cycle application, and swapping out its
main
as needed. - Linking the Redux Store and Cycle apps by adding two drivers:
state
: A read-only driver exposing a memory stream of the latest Redux state.action
: A read-write driver exposing a stream of dispatched actions and allowing other actions to be dispatched.
It takes the following optional props:
initialState
: Some initial state for your store.enhancer
: Store enhancer, allows middleware, debug tooling, etcetera.drivers
: Cycle drivers determine what kind of effects can occur.
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')
);
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.
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.
Given a mapping of action creators, as returned from
createReducer, returns a mapDispatchToProps
function,
as accepted by connect
from React Redux.
Given an array of reducers, returns a single reducer which transforms the state by calling all reducers in sequence.
Given an array of main
functions that take sources and return sinks,
returns a single main
function which combines the effects of each.