Coder Social home page Coder Social logo

redux-side-effects's Introduction

redux-side-effects

NPM version Dependencies Build status Downloads

What if your reducers were generators? You could yield side effects and return application state.

Believe it or not, but side effects are still tied with your application's domain. Ideally, you would be able to keep them in reducers. But wait! Everybody is saying that reducers must be pure! So yeah, just keep the reducer pure and reduce side effects as well.

Why?

Some people (I am one of them) believe that Elm has found the proper way how to handle side effects. Yes, we have a solution for async code in redux and it's redux-thunk but the solution has two major drawbacks:

  1. Application logic is not in one place, which leads to the state where business domain may be encapsulated by service domain.

  2. Unit testing of some use cases which heavy relies on side effect is nearly impossible. Yes, you can always test those things in isolation but then you will lose the context. It's breaking the logic apart, which is making it basically impossible to test as single unit.

Therefore ideal solution is to keep the domain logic where it belongs (reducers) and abstract away execution of side effects. Which means that your reducers will still be pure (Yes! Also hot-reloadable and easily testable). There are basically two options, either we can abuse reducer's reduction (which is basically a form of I/O Monad) or we can simply put a bit more syntactic sugar on it.

Because ES6 generators is an excellent way how to perform lazy evaluation, it's also a perfect tool for the syntax sugar to simplify working with side effects.

Just imagine, you can yield a side effect and framework runtime is responsible for executing it after reducer reduces new application state. This ensures that Reducer remains pure.

import { sideEffect } from 'redux-side-effects';

const loggingEffect = (dispatch, message) => console.log(message);

function* reducer(appState = 1, action) {
  yield sideEffect(loggingEffect, 'This is side effect');

  return appState + 1;
}

The function is technically pure because it does not execute any side effects and given the same arguments the result is still the same.

Usage

API of this library is fairly simple, the only possible functions are createEffectCapableStore and sideEffect. createEffectCapableStore is a store enhancer which enables us to use Reducers in form of Generators. sideEffect returns declarative Side effect and allows us easy testing. In order to use it in your application you need to import it, keep in mind that it's named import therefore following construct is correct:

import { createEffectCapableStore } from 'redux-side-effects'

The function is responsible for creating Redux store factory. It takes just one argument which is original Redux createStore function. Of course you can provide your own enhanced implementation of createStore factory.

To create the store it's possible to do the following:

import { createStore } from 'redux';
import { createEffectCapableStore } from 'redux-side-effects';

const reducer = appState => appState;

const storeFactory = createEffectCapableStore(createStore);
const store = storeFactory(reducer);

Basically something like this should be fully functional:

import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux';
import { createEffectCapableStore, sideEffect } from 'redux-side-effects';

import * as API from './API';

const storeFactory = createEffectCapableStore(createStore);

const addTodoEffect = (dispatch, todo) => API.addTodo(todo).then(() => dispatch({type: 'TODO_ADDED'}));

const store = storeFactory(function*(appState = {todos: [], loading: false}, action) {
  if (action.type === 'ADD_TODO') {
    yield sideEffect(addTodoEffect, action.payload);

    return {...appState, todos: [...appState.todos, action.payload], loading: true};
  } else if (action.type === 'TODO_ADDED') {
    return {...appState, loading: false};
  } else {
    return appState;
  }
});

render(<Application store={store} />, document.getElementById('app-container'));

Declarative Side Effects

The sideEffect function is just a very simple declarative way how to express any Side Effect. Basically you can only yield result of the function and the function must be called with at least one argument which is Side Effect execution implementation function, all the additional arguments will be passed as arguments to your Side Effect execution implementation function.

const effectImplementation = (dispatch, arg1, arg2, arg3) => {
  // Your Side Effect implementation
};


yield sideEffect(effectImplementation, 'arg1', 'arg2', 'arg3'....);

Be aware that first argument provided to Side Effect implementation function is always dispatch so that you can dispatch new actions within Side Effect.

Unit testing

Unit Testing with redux-side-effects is a breeze. You just need to assert iterable which is result of Reducer.

function* reducer(appState) {
  if (appState === 42) {
    yield sideEffect(fooEffect, 'arg1');

    return 1;
  } else {
    return 0;
  }
}

// Now we can effectively assert whether app state is correctly mutated and side effect is yielded.
it('should yield fooEffect with arg1 when condition is met', () => {
  const iterable = reducer(42);

  assert.deepEqual(iterable.next(), {
    done: false,
    value: sideEffect(fooEffect, 'arg1')
  });
  assert.equal(iterable.next(), {
    done: true,
    value: 1
  });
})

Example

There's very simple fully working example including unit tests inside example folder.

You can check it out by:

cd example
npm install
npm start
open http://localhost:3000

Or you can run tests by

cd example
npm install
npm test

Contribution

In case you are interested in contribution, feel free to send a PR. Keep in mind that any created issue is much appreciated. For local development:

  npm install
  npm run test:watch

You can also npm link the repo to your local Redux application so that it's possible to test the expected behaviour in real Redux application.

Please for any PR, don't forget to write unit test.

Need Help?

You can reach me on reactiflux - username tomkis1, or DM me on twitter.

FAQ

Does redux-side-effects work with working Redux application?

Yes! I set this as the major condition when started thinking about this library. Therefore the API is completely backwards compatible with any Redux application.

My middlewares are not working anymore, what has just happened?

If you are using middlewares you have to use createEffectCapableStore for middleware enhanced store factory, not vice versa. Meaning:

    const createStoreWithMiddleware = applyMiddleware(test)(createStore);
    const storeFactory = createEffectCapableStore(createStoreWithMiddleware);
    const store = storeFactory(reducer);

is correct.

Can I compose reducers?

Yes! yield* can help you with the composition. The concept is explained in this gist

I keep getting warning "createEffectCapableStore enhancer from redux-side-effects package is used yet the provided root reducer is not a generator function", what does that mean?

Keep in mind that your root reducer needs to be generator function therefore this will throw the warning:

const storeFactory = createEffectCapableStore(createStore);
const store = storeFactory(function(appState) { return appState; });

but this will work:

const storeFactory = createEffectCapableStore(createStore);
const store = storeFactory(function* (appState) { return appState; });

Can I use ()* => {} instead of function*()?

Unfortunately no. Only function* is valid ES6 syntax.

I am using combineReducers, how does this work with redux-side-effects?

If you are using standard Redux combineReducer in your application, please use the imported version from this package, original implementation does not work with generators. However, keep in mind that this method is opinionated and therefore you should probably provide your own implementation.

Usage is simple:

import { combineReducers } from 'redux-side-effects'

redux-side-effects's People

Contributors

0xr avatar minedeljkovic avatar tomkis avatar vojtatranta avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

redux-side-effects's Issues

Complex ajax workflows?

So if I'm understanding this correctly, this is like redux-thunk, except that the side effects are done right in the reducer. But if I want a complex ajax workflow, for example, coordinate two ajax lookups coming out of order, it's the same monkey business as redux-thunk (where in this case, one solution is to add an extra field to the store, fetchingId or something similar)?

Relation to redux-saga?

The docs for redux-elm states:

the Effect executor must be a Saga from redux-saga project

I am wondering why redux-side-effects exists, if the same authors recommend redux-saga. Shouldn't there at least be some mention of redux-saga in the README for this project?

hot reloading does not work

HR does not work because proper implementation of replaceReducer is not provided. The next reducer should be enhanced with enhanceReducer.

1.0.0 broke time travelling and hot reloading

In version 1.0.0 effects are deferred from enhanced reducers even on dispatch initiated by redux dev tools, during time travelling or reducer hot reloading.
If I remember well, one of main goals of redux-side-effects was not to brake any of dev tools functionality.
Is there a workaround for this, or am I missing something about new implementation?

Why do you use generators?

Why do you use generators if they compile into while loops? Is it not too early to use them?
Have you encountered any troubles with that?

Add compose function which supports generators

The function is especially very useful with immutable data structures. May be a fork of redux compose function. The support of generators should be transparent yet the function should always return generator.

/**
 * @param {...Function} functions to be composed, may be either function* or function
 * @returns  {Function} Functions which takes initial argument and returns iterable
 */
function* compose(...fns) { }

Composition is right to left so that it's more easier for people to use:
compose(f, g)(x) is actually (g ∘ f)(x)

Using with react-router-redux

Having troubles integrating with react-router-redux:

import { syncHistoryWithStore, routerReducer, routerMiddleware } from 'react-router-redux'

let reducers = combineReducers({
  app: app.reducer,
  routing: routerReducer,
});

const storeFactory = createEffectCapableStore(createStore);

const store = compose(
    applyMiddleware(routerMiddleware(browserHistory), thunk),
    typeof window === 'object' && typeof window.devToolsExtension !== 'undefined' ? window.devToolsExtension() : f => f
)(storeFactory)(reducers);

const history = syncHistoryWithStore(browserHistory, store);

Fails on syncHistoryWithStore with error:
Expected the routing state to be available either as state.routing or as the custom expression you can specify as selectLocationState in the syncHistoryWithStore() options. Ensure you have added the routerReducer to your store's reducers via combineReducers or whatever method you use to isolate your reducers.

[docs] Readme has broken links to Redux combineReducer

Hello

as per issue title, at the bottom of readme there are a couple of links to Redux combineReducer function, and they're both broken.

  • http://rackt.org/redux/docs/api/combineReducers.html -> https://redux.js.org/api/combinereducers
  • http://rackt.org/redux/docs/api/combineReducers.html#notes -> https://redux.js.org/api/combinereducers#notes

A few ideas

I really like the direction this module takes to side effects in redux, and I've been exploring its usages in an isomorphic progressively enhanced web app. However, there are a couple of things which I have questions on/would like to see changed. I am willing to write a PR for all these things if you would like.

  1. Rename createEffectCapableStore to applySideEffects or something like that (this is just a preference thing so it is more inline with the Redux applyMiddleware).
  2. Whatever is yielded is dispatched. So if a user yields an action object, that will be dispatched. If the user yields a function, that will be dispatched. This would require the use of redux-thunk middleware to accomplish the current functionality, but I think that's desired for most redux developers.
  3. Defer side effect execution to after the reducer using setImmediate instead of executing the effects on the next dispatch. I'm not sure why effects are executed in the next dispatch currently. This change would eliminate the need for AppStateWithEffects and for wrapping the dispatch function. This change would also have the benefit of making the module's surface area less.
  4. Have a hook/promise which executes/resolves when all side effects have completed. I'm not exactly sure how this one would work, but because I mainly use this in a server rendering data prefetching context I need to know when all side effects have been resolved. Maybe this could be accomplished (with the implementation of suggestion 2) with an array which collects all the return values of dispatched side effects which I could use Promise.all(…) on later.

I really love this module, I love the approach it's taking, I live the literature surrounding it, and I am really excited for what it could mean in the progressive enhancement community. These are just a couple suggestions/challenges/questions (call it what you want ☺️) I had while exploring the source code.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.