• Stars
    star
    745
  • Rank 60,864 (Top 2 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created almost 8 years ago
  • Updated almost 7 years ago

Reviews

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

Repository Details

Bring functional reactive programming to Redux using Cycle.js

Redux-cycles

Redux + Cycle.js = Love

Handle redux async actions using Cycle.js.

Build Status

Table of Contents

Install

npm install --save redux-cycles

Then use createCycleMiddleware() which returns the redux middleware function with two driver factories attached: makeActionDriver() and makeStateDriver(). Use them when you call the Cycle run function (can be installed via npm install --save @cycle/run).

import { run } from '@cycle/run';
import { createCycleMiddleware } from 'redux-cycles';

function main(sources) {
  const pong$ = sources.ACTION
    .filter(action => action.type === 'PING')
    .mapTo({ type: 'PONG' });

  return {
    ACTION: pong$
  }
}

const cycleMiddleware = createCycleMiddleware();
const { makeActionDriver } = cycleMiddleware;

const store = createStore(
  rootReducer,
  applyMiddleware(cycleMiddleware)
);

run(main, {
  ACTION: makeActionDriver()
})

By default @cycle/run uses xstream. If you want to use another streaming library simply import it and use its run method instead.

For RxJS:

import { run } from '@cycle/rxjs-run';

For Most.js:

import { run } from '@cycle/most-run';

Example

Try out this JS Bin.

See a real world example: cycle autocomplete.

Why?

There already are several side-effects solutions in the Redux ecosystem:

Why create yet another one?

The intention with redux-cycles was not to worsen the "JavaScript fatigue". Rather it provides a solution that solves several problems attributable to the currently available libraries.

  • Respond to actions as they happen, from the side.

    Redux-thunk forces you to put your logic directly into the action creator. This means that all the logic caused by a particular action is located in one place... which doesn't do the readability a favor. It also means cross-cutting concerns like analytics get spread out across many files and functions.

    Redux-cycles, instead, joins redux-saga and redux-observable in allowing you to respond to any action without embedding all your logic inside an action creator.

  • Declarative side-effects.

    For several reasons: code clarity and testability.

    With redux-thunk and redux-observable you just smash everything together.

    Redux-saga does make testing easier to an extent, but side-effects are still ad-hoc.

    Redux-cycles, powered by Cycle.js, introduces an abstraction for reaching into the real world in an explicit manner.

  • Statically typable.

    Because static typing helps you catch several types of mistakes early on. It also allows you to model data and relationships in your program upfront.

    Redux-saga falls short in the typing department... but it's not its fault entirely. The JS generator syntax is tricky to type, and even when you try to, you'll find that typing anything inside the catch/finally blocks will lead to unexpected behavior.

    Observables, on the other hand, are easier to type.

I already know Redux-thunk

If you already know Redux-thunk, but find it limiting or clunky, Redux-cycles can help you to:

  • Move business logic out of action creators, leaving them pure and simple.

You don't necessarily need Redux-cycles if your goal is only that. You might find Redux-saga to be easier to switch to.

I already know Redux-saga

Redux-cycles can help you to:

  • Handle your side-effects declaratively.

    Side-effect handling in Redux-saga makes testing easier compared to thunks, but you're still ultimately doing glorified function calls. The Cycle.js architecture pushes side-effect handling further to the edges of your application, leaving your "cycles" operate on pure streams.

  • Type your business logic.

    Most of your business logic lives in sagas... and they are hard/impossible to statically type. Have you had silly bugs in your sagas that Flow could have caught? I sure had.

I already know Redux-observable

Redux-cycles appears to be similar to Redux-observable... which it is, due to embracing observables. So why might you want to try Redux-cycles?

In a word: easier side-effect handling. With Redux-observable your side-effectful code is scattered through all your epics, directly.

It's hard to test. The code is less legible.

Do I have to buy all-in?

Should you go ahead and rewrite the entirety of your application in Redux-cycles to take advantage of it?

Not at all.

It's not the best strategy really. What you might want to do instead is to identify a small distinct "category" of side-effectful logic in your current side-effect model, and try transitioning only this part to use Redux-cycles, and see how you feel.

A great example of a small category like that could be:

  • local storage calls
  • payments API

The domain API layer often is not the easiest one to switch, so if you're thinking that... think of something smaller :)

Redux-saga can still be valuable, even if using Redux-cycles. Certain sagas read crystal clear; sagas that orchestrate user flow.

Like onboarding maybe: after the user signs up, and adds two todos, show a "keep going!" popup.

This kind of logic fits the imperative sagas model perfectly, and it will likely look more cryptic if you try to redo it reactively.

Life's not all-or-nothing, you can definitely use Redux-cycles and Redux-saga side-by-side.

What's this Cycle thing anyway?

Cycle.js is an interesting and unusual way of representing real-world programs.

The program is represented as a pure function, which takes in some sources about events in the real world (think a stream of Redux actions), does something with it, and returns sinks, aka streams with commands to be performed.

stream
is like an asynchronous, always-changing array of values
source
is a stream of real-world events as they happen
sink
is a stream of commands to be performed
a cycle (not to be confused with Cycle.js the library)
is a building block of Cycle.js, a function which takes sources (at least ACTION and STATE), and returns sinks

Redux-cycles provides an ACTION source, which is a stream of Redux actions, and listens to the ACTION sink.

function main(sources) {
  const pong$ = sources.ACTION
    .filter(action => action.type === 'PING')
    .mapTo({ type: 'PONG' });

  return {
    ACTION: pong$
  }
}

Custom side-effects are handled similarly — by providing a different source and listening to a different sink. An example with HTTP requests will be shown later in this readme.

Aside: while the Cycle.js website aims to sell you on Cycle.js for everything—including the view layer—you do not have to use Cycle like that. With Redux-cycles, you are effectively using Cycle only for side-effect management, leaving the view to React, and the state to Redux.

What does this look like?

Here's how Async is done using redux-observable. The problem is that we still have side-effects in our epics (ajax.getJSON). This means that we're still writing imperative code:

const fetchUserEpic = action$ =>
  action$.ofType(FETCH_USER)
    .mergeMap(action =>
      ajax.getJSON(`https://api.github.com/users/${action.payload}`)
        .map(fetchUserFulfilled)
    );

With Cycle.js we can push them even further outside our app using drivers, allowing us to write entirely declarative code:

function main(sources) {
  const request$ = sources.ACTION
    .filter(action => action.type === FETCH_USER)
    .map(action => ({
      url: `https://api.github.com/users/${action.payload}`,
      category: 'users',
    }));

  const action$ = sources.HTTP
    .select('users')
    .flatten()
    .map(fetchUserFulfilled);

  return {
    ACTION: action$,
    HTTP: request$
  };
}

This middleware intercepts Redux actions and allows us to handle them using Cycle.js in a pure data-flow manner, without side effects. It was heavily inspired by redux-observable, but instead of epics there's an ACTION driver observable with the same actions-in, actions-out concept. The main difference is that you can handle them inside the Cycle.js loop and therefore take advantage of the power of Cycle.js functional reactive programming paradigms.

Drivers

Redux-cycles ships with two drivers:

  • makeActionDriver(), which is a read-write driver, allowing to react to actions that have just happened, as well as to dispatch new actions.
  • makeStateDriver(), which is a read-only driver that streams the current redux state. It's a reactive counterpart of the yield select(state => state) effect in Redux-saga.
import sampleCombine from 'xstream/extra/sampleCombine'

function main(sources) {
  const state$ = sources.STATE;
  const isOdd$ = state$.map(state => state.counter % 2 === 0);
  const increment$ = sources.ACTION
    .filter(action => action.type === INCREMENT_IF_ODD)
    .compose(sampleCombine(isOdd$))
    .map(([ action, isOdd ]) => isOdd ? increment() : null)
    .filter(action => action);

  return {
    ACTION: increment$
  };
}

Here's an example on how the STATE driver works.

Utils

combineCycles

Redux-cycles ships with a combineCycles util. As the name suggests, it allows you to take multiple cycle apps (main functions) and combine them into a single one.

Example:

import { combineCycles } from 'redux-cycles';

// import all your cycle apps (main functions) you intend to use with the middleware:
import fetchReposByUser from './fetchReposByUser';
import searchUsers from './searchUsers';
import clearSearchResults from './clearSearchResults';

export default combineCycles(
  fetchReposByUser,
  searchUsers,
  clearSearchResults
);

You can see it used in the provided example.

Testing

Since your main Cycle functions are pure dataflow, you can test them quite easily by giving streams as input and expecting specific streams as outputs. Checkout these example tests. Also checkout the cyclejs/time project, which should work perfectly with redux-cycles.

Why not just use Cycle.js?

Mainly because Cycle.js does not say anything about how to handle state, so Redux, which has specific rules for state management, is something that can be used along with Cycle.js. This middleware allows you to continue using your Redux/React stack, while allowing you to get your hands wet with FRP and Cycle.js.

What's the difference between "adding Redux to Cycle.js" and "adding Cycle.js to Redux"?

This middleware doesn't mix Cycle.js with Redux/React at all (like other cycle-redux middlewares do). It behaves completely separately and it's meant to (i) intercept actions, (ii) react upon them functionally and purely, and (iii) dispatch new actions. So you can build your whole app without this middleware, then once you're ready to do async stuff, you can plug it in to handle your async stuff with Cycle.

You should think of this middleware as a different option to handle side-effects in React/Redux apps. Currently there's redux-observable and redux-saga (which uses generators). However, they're both imperative and non-reactive ways of doing async. This middleware is a way of handling your side effects in a pure and reactive way using Cycle.js.

More Repositories

1

awesome-cyclejs

A curated list of awesome Cycle.js resources
826
star
2

create-cycle-app

Create Cycle.js apps with no build configuration.
JavaScript
238
star
3

cyclic-router

Router Driver built for Cycle.js
JavaScript
109
star
4

cycle-android

A Cycle.js port for Android
Java
88
star
5

one-fits-all

The one-fits-all flavor for create-cycle-app
JavaScript
34
star
6

cycle-canvas

A canvas driver for Cycle.js
JavaScript
33
star
7

typescript-starter-cycle

An opinionated starter for Cycle.js projects powered by TypeScript.
TypeScript
33
star
8

xstream-boilerplate

DEPRECATED! use create-cycle-app instead
JavaScript
27
star
9

README

Cycle.js Community README, code of conduct and introductions.
JavaScript
24
star
10

create-cycle-app-flavors

JavaScript
17
star
11

cyclejs-sortable

Makes all children of a selected component sortable
TypeScript
17
star
12

cyclejs-utils

A few helper functions for dealing with sinks
TypeScript
15
star
13

component-combinators

Component model for cyclejs
CSS
13
star
14

cycle-idb

A cycle driver for IndexedDB.
JavaScript
13
star
15

cycle-remote-data

A Cycle.js driver for elegantly loading data
TypeScript
12
star
16

boids

Boids in Cycle.js (bird flocking simulator)
JavaScript
12
star
17

cyclejs-modal

An easy way to open custom modals in a cyclejs app
TypeScript
12
star
18

cycle-keyboard

A keyboard driver for cycle.js
TypeScript
9
star
19

learn-to-cycle

A book on @cyclejs
9
star
20

cycle-mouse-driver

Handles mouse events on the document in Cycle.js applications
JavaScript
9
star
21

built-with-cycle

A website to showcase the cool projects built with Cycle.js
JavaScript
9
star
22

cycle-webworker

A simple webworker driver for Cycle.js
TypeScript
8
star
23

cycle-regl

A Cycle.js driver for regl, a declarative and functional interface to WebGL
TypeScript
6
star
24

cycle-delayed-driver

Create a driver in the future as a response to a specific event
JavaScript
5
star
25

cycle-svg-pan-and-zoom

A Google Maps style SVG pan and zoom component for Cycle.js
JavaScript
5
star
26

xstream-marbles

Interactive marble diagrams of xstream Streams
TypeScript
5
star
27

cycle-serverless

A driver for using cyclejs and xstream in serverless FaaS environments (Azure Functions)
JavaScript
2
star
28

utilities

A collection of small helpful scripts related to Cycle.js
PHP
1
star
29

cycle-classic-dom

A tiny helper library for working with classic JS DOM libraries in Cycle.js
TypeScript
1
star