Git Product home page Git Product logo

redux-loop's Introduction

redux-loop

A port of the Elm Architecture to Redux that allows you to sequence your effects naturally and purely by returning them from your reducers.

Isn't it incorrect to cause side-effects in a reducer?

Yes! Absolutely.

Doesn't redux-loop put side-effects in the reducer?

It doesn't. The values returned from the reducer when scheduling an effect with redux-loop only describe the effect. Calling the reducer will not cause the effect to run. The value returned by the reducer is just an object that the store knows how to interpret when it is enhanced by redux-loop. You can safely call a reducer in your tests without worrying about waiting for effects to finish and what they will do to your environment.

What are the environment requirements for redux-loop?

redux-loop requires polyfills for ES6 Promise and Symbol to be included if the browsers you target don't natively support them.

Why use this?

Having used and followed the progression of Redux and the Elm Architecture, and after trying other effect patterns for Redux, we came to the following conclusion:

Synchronous state transitions caused by returning a new state from the reducer in response to an action are just one of all possible effects an action can have on application state.

Many other methods for handling effects in Redux, especially those implemented with action-creators, incorrectly teach the user that asynchronous effects are fundamentally different from synchronous state transitions. This separation encourages divergent and increasingly specific means of processing particular types effects. Instead, we should focus on making our reducers powerful enough to handle asynchronous effects as well as synchronous state transitions. With redux-loop, the reducer doesn't just decide what happens now due to a particular action, it decides what happens next. All of the behavior of your application can be traced through one place, and that behavior can be easily broken apart and composed back together. This is one of the most powerful features of the Elm architecture, and with redux-loop it is a feature of Redux as well.

Installation

npm install --save redux-loop

Support

Potential bugs, general discussion, and proposals or RFCs should be submitted as issues to this repo, we'll do our best to address them quickly. We use this library as well and want it to be the best it can! For questions about using the library, submit questions on StackOverflow with the redux-loop tag.

Don't see a feature you want?

If you're interested in adding something to redux-loop but don't want to wait for us to incorporate the idea you can follow these steps to get your own installable version of redux-loop with your feature included:

  1. Fork the main repo here
  2. Add your feature or change
  3. Change the package "name" in package.json to be "@<your-npm-username>/redux-loop
  4. Commit to master and npm publish
  5. npm install @<your-npm-username>/redux-loop

We are always interested in new ideas, but sometimes we get a little busy and fall behind on responding and reviewing PRs. Hopefully this process will allow you to continue making progress on your projects and also provide us with more context if and when you do decide to make a PR for your new feature or change. The best way to verify new features for a library is to use them in real-world scenarios!

Contributing

Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. Multiple language translations are available at contributor-covenant.org

redux-loop's People

Contributors

0xr avatar bdwain avatar cha0s avatar dependabot[bot] avatar dustyburwell avatar g12i avatar gniquil avatar gyzerok avatar jamesjtong avatar jarvisaoieong avatar jgautsch avatar kumar303 avatar kuzminadya avatar lambdahands avatar laszlopandy avatar leonaves avatar mdibyo avatar notwoods avatar nweber-gh avatar on3iro avatar rikukissa avatar sbdchd avatar sdaves avatar sorrycc avatar stereobooster avatar tarjei avatar thewillhuang avatar timarney avatar tschaub avatar willisplummer 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  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

redux-loop's Issues

Roadmap

Meta-issue to track future plans

  • Add lift effect
  • Return an array from loop instead of an object for easy destructuring
  • Port redux examples
  • Better test coverage
  • Write full-length tutorial

Effect.promise executed twice.

Hi, I got two reducers (out of 7) that return effects.

This is with redux-loop 2.0.0.

Now, the simplest of the two is doing something very odd. When I return a promise, the promiseFactory function is executed twice(!):

import { loop, Effects } from 'redux-loop'

import { createAction, handleActions } from 'redux-actions'

export const GROWL_MESSAGE = 'growl'
export const GROWL_CLEAR = 'growl-clear'

export const growlAction = createAction(GROWL_MESSAGE, function(message, type = 'info', timeout = 6000) {
  return { message, type, timeout }
})

export const clearGrowlAction = createAction(GROWL_CLEAR)

export function executeTimeout(growl) {
  console.log("executeTimeout")
  return new Promise(function(resolve, reject) {
    setTimeout(
      function() {
        resolve(clearGrowlAction(growl))
      },
      growl.timeout
    )
  }).catch(function(e) {
    console.error(e)
  })
}

export const growlReducer = function(state = { growls: []}, { type, payload }) {

  if (type === GROWL_MESSAGE) {
    const growl = {
      message: payload.message,
      type: payload.type,
      timeout: payload.timeout,
    }
    state.growls = [growl, ...state.growls]
    return loop(state, Effects.promise(executeTimeout, growl))
  } else if (type === GROWL_CLEAR) {
    state.growls = state.growls.filter((growl) => {
      return growl !== payload
    })
  }
  return state
}
export default growlReducer

Now, when growlAction() is dispatched, the reducer is executed once, but then executeTimeout is called twice with the same arguments.

I'm a bit stumped at this one. Any ideas?

When I add a console.log to install( -> dispatch() ) in install.js like this:

function dispatch(action) {
        var dispatchedAction = store.dispatch(action);
        console.log("DISPATCH " + action.type, store.getState())
        var effect = (0, _loop.getEffect)(store.getState());
        return runEffect(action, effect).then(function () {});
      }

I get that the action type is different (once it is GROWL_CLEAR, then it is an action in another reducer) in the two occasions, but that the effect for the executeTimeout.

Any ideas what is happening here?

I'm a bit stumped

Why doesn't loop() work in "success" actions?

I noticed the following behavior in loop(..). Given the following:

export function fetchSummary(payload = {}) {
  return fetch(`/api/dev_summary`, {
    method: 'POST',
    body: payload
  })
  .then((res) => res.json())
  .then((res) => ax.fetchSuccess(res))
  .catch(() => ax.fetchFailed())
}

The loop in this snippet works:

[ax.fetchSummary]: (state, payload) => {
    return loop(
      state,
      Effects.promise(api.fetchSummary, payload)
    )
}

This snippet doesn't.

[ax.fetchSuccess]: (state, payload) => {
    return loop(
      state,
      Effects.constant(ax.loadingComplete())
    )
}

I wonder if anyone knows why this might be happening.

Edit: When I say "works," I mean that the side effect causing action doesn't get called. In the second case, ax.loadingComplete never gets called. I also tried using Effects.promise and Effects.batch and had the same result.

Question: What's appropriate approach to do server-side-rendering with redux-loop?

After involving redux-loop in our code base, we found it is hard to enable server side rendering. The reason is that there is no way to let redux know that some side effect (e.g. API call) has taken place.

Basically, an init action is dispatched to store on HTTP request. It has to wait for that action to be fully taken care of before rendering and sending back HTML tags to browser. If side effect is triggered in actions like thunk, it is feasible. However, side effect is triggered by reducer in redux-loop. There is no way to let Redux wait for side effect to be done.

Any suggestion for this?

redux-loop 2.1.0 breaks redux-form

Hi, I just upgraded to 2.1.0 to get access to Effects.call, when I experienced this.

I'm using http://redux-form.com/4.2.0 and this works quite nicely with redux-loop 2.0.0.

When I upgrade to 2.1 things stop working.

That is, if I change a field, the action is dispatched (as seen in DevTools), but the the new state is not propagated to React - i.e. the properties in the form are not updated.

I have tried to understand what is breaking here for a few hours now, but I'm a bit stumped to be honest and would love to get some tips to what this could be / where I should start looking for clues to what is failing.

Kind regards,
Tarjei

Add an Effect for dispatching an unknown amount of actions

Hi, I'm working on implementing file upload progress using redux-loop.

My problem is observing progress events from the XHR object. How many of these there will be I will not know on beforehand.

What I'm looking for is a way to dispatch a number of effects (i.e. actions) from within my single Effect.promise()

What I would like is not to have to pass the dispatch function into the file upload actions, but instead that Effect.promise provided a way to emit new effects. Would that be possible?

Was that understandable?

Regards,
Tarjei

Add second param in Effects.batch

Use case:

const initReducer = (state = {isloading: true}, action) => {
  if (action.type === INIT) {
    return loop({
      ...state,
      isloading: true,
    },
      Effects.batch([
        Effects.promise(someAsyncAction),
        Effects.promise(anotherAsyncAction),
      ],
        Effects.constant(inited())
      )
    );
  }

  if (action.type === INITED) {
    return loop({
      ...state,
      isloading: false,
    },
      Effects.none()
    );
  }

  return loop(state, Effects.none());
}

If it make sense, i am willing to submit a PR.

Allow for passing dispatch to side-effect methods

It'd be nice if the side-effects could have access to the dispatch method to dispatch arbitrary actions that are not tied to the specific task/cmd/effect they are running at the time.

Here's a scenario where this would definitely be helpful.

I have an API client in my project that all API requests go through. Its main job is to add auth headers to the requests, but the other thing it does is force log you out when you get a 401 back (because your token expired). With redux-loop right now I typically have to use it like this to check for the 401.

function fetchItems(userid){
  return new Promise(resolve, => {
     apiClient(`/users/${userId}/items`)
       .then(rsp => {
            //process rsp
            resolve(getSuccessAction(processedRsp));
        }).catch(err => {
             resolve(err.status === 401 ? get401ErrorAction() : getItemErrorAction(err));
        });
  });
}

The problem is that I have to repeat this 401 logic for every request. And also there's only 1 resolved action, so you can't dispatch the item error if you need the 401 error.

Ideally, the api client could take a dispatch method, and then just choose to dispatch a 401 error in its internal catch block before the caller handles the promise. And then the caller can also dispatch an error fetching action and not worry about if the 401 happened or not.

That's just one scenario, but I could see it being useful pretty often.

Question about pulling different state slices together

This was mentioned offhand in #4 (comment), but I was curious how I could get state information from outside the slice the reducer is operating on, if I switched to using redux-loop.

Currently, I would use redux-thunk and the getState function that's passed to the function you return from an action creator. So, for example, I have an action creator now that looks sort of like:

function actionCreator(id) {
   return thunk(dispatch, getState) {
       SomeApi.get(id).then(function(results) {
           dispatch({
               type: 'ACTION',
               someOtherInfo: getState().path.to.info,
               results: results
           })
      }
}

I understand how I'd move the api call to an Effect, but I'm not sure how I'd replicate getState(). I suppose I could have some magic properties in the action object that were replaced with the state by middleware. I'm curious if you have any thoughts or patterns for this situation.

Kudos on this library, it looks so much cleaner and easier to test than using redux-thunk.

Effects not being executed when multiple dispatches in the same tick

I've been working with redux loop and have been loving it, although I think I've found a bug where if any dispatch occurs in the same tick of the event loop after a dispatch which creates an effect, the effect will not be executed.

Specifically I've experienced this with redux-form, which will dispatch actions on React lifecycle hooks (i.e. componentDidMount and componentWillRecieveProps).

I'm working on creating a minimal reproducible scenario, but comments are welcome.

I believe it has something to do with https://github.com/raisemarketplace/redux-loop/blob/master/modules/install.js#L31, although I'm still digging.

why the ...state is wrong

const initialState = {
  loading:false,
  lists:[]
}
export function fetchDetails(id) {
  return fetch(`/api/details/${id}`)
    .then((r) => r.json())
    .then(loadingSuccess)
    .catch(loadingFailure);
}

export default function reducer(state=initialState, action) {
  switch (action.type) {
    case 'LOADING_START':
      return loop({
         ...state,
          loading: true
        },
        Effects.promise(fetchDetails, action.payload.id)
      );

    case 'LOADING_SUCCESS':
      return {
        ...state,
        loading: false,
        details: action.payload
      };

    case 'LOADING_FAILURE':
      return {
        ...state,
        loading: false,
        error: action.payload.message
      };

    default:
      return state;
  }
}

How to implement initial data fetch

If I correctly understand the intention of this library, it is supposed to move decision making as close to reducers as possible. Classic example of React+redux flow is to fetch data needed to render a React component from componentDidMount lifecycle hook in the component itself:

class DataViewer extends React.Component {
  componentDidMount() {
    this.props.dispatch(fetchDataIfNeeded());
  }

  render() {
    return (
      <div>
        { this.props.data ? 
          <DataList data={ this.props.data }/> : 
          <Preloader /> 
        }
      </div>
    );
  }
}

If we try to move this logic inside reducer, we can add fetchDataIfNeeded to Effect returned after an action leading to displaying this component occurred, like in this example.

Additionally, we have to handle the initial state leading to display DataViewer without any action made yet. Example situation:

  1. The initial state is restored from URL.
  2. The initial state is passed to createStore.
  3. We don't want to handle data fetching manually, but instead, we assume reducer to check if we have enough data to show DataViewer and return a loop with fetchDataIfNeeded if it is the case.

The question is how to handle this init step in reducer. Using @@INIT action is considered an anti-pattern. @@ReduxLoop/INIT happens later if we already returned initial effect.

So, what is the suggested way to decide the need of initial effects?

Return an array from "loop()" instead of an object

Hi,

is there any reason why the loop function returns an object ?

As my experiment (which is turning into a real app now 😄 ) grow, I end up writing this kind of code a lot :

const update = (model = initialModel, action) => {
  switch(action.type) {
    case MODIFY_THING:
      const { model: m, effect: e } = thing.update(model.thing, action.payload.action)
      return loop(
        { ...model, thing: m},
        Effects.batch([
          Effects.lift(e, modifyThing)
        ])
      )
    default:
      return loop(
        model,
        Effects.none()
      )
}

It's a tiny detail, but being able to directly extract the model and the effect from the loop function would be a great DX boost :

const [m, e] = thing.update(/*...*/)

Also it would be closer to Elm's API, which uses a Tuple representation.

Don't executes initial effects for nested reducers

There is my code

const initialState = loop(
    Map(),
    Effects.promise(getKeys)
);

export default function search(state = initialState, action) {
    switch (action.type) {
        default:
            return state;
    }
}

const reducers = {
    search,
};

const rootReducer = combineReducers(
    reducers,
    Map(),
    (child, key) => child.get(key),
    (child, key, value) => child.set(key, value)
);

const store = createStore(rootReducer, install());

But my effect in search reducer don't call on start, only if i create full initial state for store like this

const initialState = rootReducer(Map(), {
    type: '@@APP_STATE_INIT',
});

const store = createStore(rootReducer, initialState, install());

I think it's a problem with setup initial state from nested reducers in combineReducer but don't know how to fix it

How to disable the noisy error message?

Is there any way to disable the error message that starts with loop Promise caught when returned from action of type? I have to wade my way through it to find the real errors.

loop Promises must not throw!

Did you forget to do one of the following?

- Call `.catch` on a Promise in a function passed to `Effects.promise`

  const asyncEffect = (val) => {
    return api.doStuff(val)
      .then((stuff) => Actions.success(stuff))
      .catch((error) => Actions.failure(error)); // <-- You have to do this!
  };

- Return an action from a `.catch` callback

  const asyncEffect = (val) => {
    return api.doStuff(val)
      .then((stuff) => {
        return Actions.success(stuff); // <-- Make sure to return here!
      })
      .catch((error) => {
        return Actions.failure(error): // <-- And return here!
      });
  };

Suggestion: single batch action after all effects are resolved

Currently batch(effects) is dispatching the resulting actions once all effects are resolved.

Proposal:
In addition to sending invidual actions when effects are resolved, also send one single action after all effects are resolved.

Motivation:
Imagine you have collection (list) of items you want to refresh all at once, but they don't have API to do it in one request so you need to invoke API request for all of them one by one. You would like to start spinner before actual request are initiated and stop spinner after all effects are resolved.
Currently this is very hard achievable as there is no action to be added indicating all effects were resolved.

Sample code:

ACTIONS

export function refreshAllDeliveries() {
  return {
    type: ActionTypes.DELIVERY_REFRESH_ALL
  };
}

// fetches single delivery
export function fetchDelivery(trackingNr, pinCode, color) {
  return api.findDelivery(trackingNr, pinCode)
  .then((response) => ({
    type: ActionTypes.DELIVERY_FETCH_SUCCESS,
    payload: {
      trackingNr,
      pinCode,
      ...response.data,
      lastUpdated: Date.now(),
    }
  }))
  .catch((response) => ({
    type: ActionTypes.DELIVERY_FETCH_FAILURE,
    payload: response
  }));
}

REDUCER

...
case ActionTypes.DELIVERY_REFRESH_ALL:
  // create batch from deliveries to handle situations where API only allows retrieval of items one by one
  const batch = values(state.deliveries).reduce(
    (deliveriesToRefresh, delivery) => {
      if (['UNSPECIFIED_TIME_WINDOWS', 'SCHEDULED', 'ASSIGNED'].indexOf(delivery.state) > -1) {
        const promise = Effects.promise(
          fetchDelivery,
          delivery.trackingNr,
          delivery.pinCode,
          delivery.color
        )
        deliveriesToRefresh.push(promise);
      }
      return deliveriesToRefresh;
    }, []
  );
  return loop(
    state,
    Effects.batch(batch)
  );
...

React component where we call refreshAllDeliveries and have spinner

class DeliveriesPage extends Component {
  render() {
    const isFetching = this.props.isFetching;

  render() {
    const isFetching = this.props.isFetching; //passed react-redux mapStateToProps

    return (
      <div>
        <Refresh
          isRefreshing={isFetching}
          onClick={this.props.refreshAllDeliveries}
        />
      </div>
  }
}

Possibly set context for cmd method somehow

it would be nice to be able to set the context of the method that a command runs somehow. The issue i ran into is that I am calling setTimeout in 1 command, returning the id and storing it so that i can later call clearTimeout.

I'd like to have Cmd.arbitrary(clearTimeout, state.get('timeoutId'), but that doesn't run clearTimeout in the context of window, which causes an error. So i need a useless wrapper function like this

export function clearRefreshTimeout(id){
  clearTimeout(id);
}

i'd hate to clutter up the cmd methods with more parameters, but something to set the context would be nice

Why is 'Effects' needed?

Effects just seems like a way to obfuscate the API without achieving much. I've defined loop like this in my application to remove the overhead:

import {loop as _loop, Effects} from 'redux-loop'

export const loop = (state, ...effects) =>
  _loop(state, Effects.batch(effects.map(e =>
    typeof e === 'function' ?
      Effects.promise(() => Promise.resolve(e())) :
    e ?
      Effects.constant(e) :
      Effects.none()
  )))

Which can be used like this:

loop(
  state,
  action1,
  () => action2,
  () => new Promise(resolve => resolve(action3)),
  null,
)

Original:

loop(
  state,
  Effects.batch([
    Effects.constant(action1),
    Effects.promise(() => Promise.resolve((() => action2)())),
    Effects.promise(() => new Promise(resolve => resolve(action3)))
    Effects.none()
  ])
)

Does anyone have any thoughts regarding this revised API?

Should I open a PR & implement it?

Add sanity checks

Due to lack of type-safety in JavaScript, there are a ton of places in redux-loop where folks could pass the wrong kind of argument and be met with hard-to-understand errors. Let's instead catch them at the source and through errors with helpful messages!

Finishing this will constitute a major release so I've created the sanity_checks branch for folks to submit PRs against.

Work with reducers from packages such as apollo stack/redux-form

Apollo stack has to check apollo reducer in combined reducer, in redux-loop way, reducer is been transformed into immutable way.

When apollo starts it check the apolloReducer and raise an error:

Existing store does not use apolloReducer for apollo

The same problem might happened when using redux-form

import {createStore, combineReducers} from 'redux';
import {reducer as formReducer} from 'redux-form';
const reducers = {
  // ... your other reducers here ...
  form: formReducer     // <---- Mounted at 'form'. See note below.
}
const reducer = combineReducers(reducers);
const store = createStore(reducer);

Is there any way to separate the imported reducer to use no immutable way?

why not allow effects that do not return actions

I know in the docs it explicitly states that you have to return an action. But would it break the elm architecure if you didn't? That's bascially what none() does. I just tried it and it's trivial to implement.

I can see a lot of scenarios where it makes sense to me to do this, and a subscriber would just be weird. For example, i have an object that can get updated, and when it does I want to trigger a write to localstorage. the new state depends on the previous state, so the reducer needs to be involved before the write to localstorage. but after that, there's nothing to do. i would like to return from my reducer something like

loop(newState, Effects.call(writeAuthUpdatesToStorage, newState));

instead of having to subscribe to the store and compare newUser and oldUser every state change.

Is this a change that could be considered? (I'd make the pull request if so)

is it possible to dispatch actions manually from an effect?

I had some middleware i wrote that intercepts actions and enhances them using getState and dispatch (just like mapStateToProps and mapDispatchToProps from react-redux).

I tried to use these dispatch calls in some of the effects that redux-loop triggered. However, i noticed that the dispatch method that the middleware added to the action did not call redux-loop functionality, so any effects that the reducer would try to trigger due to the dispatch would just be ignored.

that was with the store enhancers defined like this

const storeEnhancer = compose(
  loopInstall(),
  applyMiddleware(createActionEnhancerMiddleware(enhancers)),
);

If i switched the order there, then any actions triggered by a redux-loop effect would never hit the middleware that enhanced the action.

So it seems like there's no way to make this work based on how applyMiddleware and redux-loop work right now. I was just curious if this is an issue that has been noticed in the past and the typical way around it? Or if a change in how redux-loop works would allow it to work with middleware that triggers dispatches.

Monad flatMap/bind For Effects ?

I have been using redux-loop for a while, it works great! In my attempt to understand Effects, and trying to add a typed mental model to it, I have reach a conclusion that an Effect is a stacked Monad of List and Promise (Promises are impure though, a better analogy would be Task monad, eg. from Data.Task).

newtype EffectOf a = EffectOf (Promise [a])
type Action = /* Plain JS Object */
type Effect = EffectOf Action

With this understanding, one thing critical in Monads, yet not exposed as public api is flatMap/bind. Effects.lift does serve as a map/fmap, though. Here's a sketch of what flatMap may look like:

const effTypes = {
  ...,
  FLATMAP : 'FLATMAP'
}
Effects.flatMap = (eff, func) => {
  return {
    type : effTypes.FLATMAP,
    effect : eff,
    factory : func,
    [isEffectSymbol] : true
  };
}
function effectToPromise(effect) {
  switch(effect.type) {
    ...
    case effTypes.FLATMAP:
      return effectToPromise(effect.effect)
        .then(actions =>
            Effects.batch( actions.map(effect.factory) ))
        .then(effectToPromise);
  }
}

This allows the creation of Effect that "evaluates" to another Effect, and run the "flattend" Effect as normal. Here's one artificial use case for it:

function getUser(action) {
  return Effects.promise(() =>
    fetch(`${apiUrl}/users/{action.payload.id}`)
      .then(user => 
          Effects.constant({ type: LOAD_SUCCESS, payload: {user} }))
      .catch(reason => Effects.none()));
}
function getProject(action) {
  return Effects.promise(() => 
    fetch(`${apiUrl}/projects/{action.payload.user.currentProjectId}`)
      .then(project => 
          Effects.constant({ type: LOAD_SUCCESS, 
              payload: {...action.payload,project} }))
      .catch(reason => Effects.none()));
}
const reducer = createReducer({
  [LOAD_DATA_START](state, action) {
    const newState = { ...state, loading:true };
    // Effects.constant serves as unit/return from Monal definition
    const eff = Effects.constant(action);
    const apiEff = Effects.flatMap(
        Effects.flatMap(eff, getUser), getProject);
    // with haskell do notation:
    // const apiEff = do {
    //   userSuccessAction <- getUser(action)
    //   getProject(userSuccessAction)
    // }
    return loop(newState, apiEff);
  }
})

Although, this is easier to do with just promises (have a single Effects.promise) the above decomposition of getUser and getProject serves as small units of Effect factories, that can be reused individually in other context - and it will be nice if redux-loop includes machinery to compose them.

Another example below adapted from a real use case I had:

// dataReducer is a reducer that uses Effects.promise to fetch data
const decorateReducer = (dataReducer) => {
  return (state, action) => {
    const nextState = dataReducer(state, action);
    const effect = getEffect(nextState);
    if(effect) {
      const nextEffect = Effects.flatMap(effect, (act) =>
          (act.type === SUCCESS 
          ? Effect.batch([act, hideLoadingGif()].map(Effects.constant)) 
          : Effect.batch([act, showErrorModal(act.payload)].map(Effects.constant))));
      return loop(state, nextEffect);
    }
    return nextState;
  };
}


import {decorateReducer} from 'somewhere-else';

const appReducer = decorateReducer((state, action) => {
  // does just data logic here
});

Lastly, I have this implemented in my fork and plan to play around with the idea a bit more (specifically how to integrate ap for applicative functor as well). If there's any interest in this direction of exploration, I can make a PR and some more examples.

Expose `liftReducer` and/or `liftState`

Hey. I'm trying out the library and the first issue I've encountered is that some actions produce effects and others don't, which means my reducer returns inconsistent structures. I'd like to be able to normalize that for testing.

I see you've solved this problem internally with liftReducer / liftState. It'd be great to have access to those in my application/tests.

For my own sanity, I could see myself normalizing them upfront like this:

import { Effects, loop, liftReducer: liftReducerToLooper } from 'redux-loop';
// ...
export const reducer = liftReducerToLooper(createReducer({
  [FOO_ACTION]: (state, action) => {
    return {...state, foo: 'bar' };
  },
  [EFFECTING_ACTION]: (state, action) => { 
     return loop(state, Effects.promise(someFn));
  },
}, initialState));

What do you think? I know there's always a tension between minimal surface area and convenience.

New Effect API to allow for reusable side-effect methods

Copied this comment from #54

After using redux-loop a bit more in practice, I've started to have some thoughts on how the api could encourage a little more code reuse and simplify a few things.

One issue with forcing effects to return/resolve with redux actions is that the implementations of the side-effects become tied to redux/redux-loop. Suddenly a pretty standard function to make a request, such as this,

import apiClient from './apiClient.js'; //to abstract away fetch details like json parsing and error codes

function fetchItems(userid){
  return apiClient(`/users/${userId}/items`)
      .then(rsp => {
           //process rsp
           return processedRsp;
       }).catch(err => {
            console.log(':(');
            return err;
       });
}

can't be used by redux-loop without changing it in a way that makes it unusable by something else (you can no longer reject the promise, and your resolution needs to be a redux action and not just the list of items).

Additionally, even if you wanted it only for redux-loop, it would be impossible (or at least awkward) to use a side-effect to return different types of actions depending on how it is called (I haven't had a need to do this yet, but it's not hard to imagine a case where you'd want it)

What if we allowed side-effects to become completely agnostic to redux-loop/redux in general, and instead return whatever they felt was necessary to complete their purpose (so for async methods, prob would be a promise that resolves or rejects to indicate success).

So instead of defining the actions triggered (or not) by the effect in the method, it could be defined in the Effect object itself.

The new effect objects returned by a reducer might look like this

//actions.js
function itemsLoadedActionCreator(items){
    return {
       type: 'ITEMS_LOADED',
       items: items
    };
}
// ... other action creators 

//reducer.js
return loop(newState, Effect.promise(fetchItems, itemsLoadedActionCreator, failHandler, params...));

Effect.call(writeToLocalStore, syncMethodActionCreator, params...)

//Effects.constant could be implemented as syntactic sugar for 
Effects.call(null, constantAction());

//batch would be basically the same
//I imagine lift would be similar, though I haven't looked into it much yet

And now your effects are completely reusable and agnostic to your choice of state management. You could swap out redux-loop or even redux iteself without having to change them.

Encourage use of "reducer creators" in documentation

Without redux-loop making changes to the store can be quite painful - for me it meant adding constants, actionCreators, updating middleware AND the reducer. Having to make changes in a bunch of places sucks.

Now I dispatch directly to the store and only worry about the reducer. To shift the power of middleware to the reducer I've started relying on reducer creators which I use like so:

import {createReducer} from '_utils/redux-plus'
import api from '_utils/api'

export default createReducer({
  GET_DATA: api('GET', '/api/data/solar-system'),
  GET_DATA_SUCCESS: (state, {payload}) => ({planets: payload}),
}, {planets: []})

Implementation

/**
 * An easy way to create api requests in reducers (using redux-loop)
 *
 * api('POST', '/api/endpoint')
 * api('POST', '/api/endpoint', {headers: {Authentication: 'password'}})
 * api({method: 'POST', url: '/api/endpoint', headers: {Authentication: 'password'}})
 * api((state, {payload}) => xr.post('/api/endpoint', payload))
 * api('POST', '/api/endpoint', (state) => ({counter: state.counter + 1, ...state}))
 */

import xr from 'xr'
import {loop} from '_utils/redux-plus'

const generateRequest = (config) =>
  typeof config === 'function' ? config :
    (state, {payload}) =>
      xr({[config.method === 'GET' ? 'params' : 'data']: payload, ...config})

const api = (requestConfig, updateConfig = s => _.get(s, '0', s)) => (state, action) => {
  const effect = () =>
    generateRequest(requestConfig)(state, action).then(
      response => ({type: `${action.type}_SUCCESS`, payload: response.data, meta: {response}}),
      error => ({type: `${action.type}_FAILURE`, payload: error}))
  return loop(updateConfig(state, action), effect)
}

export default (...args) =>
  typeof args[0] === 'string' && typeof args[1] === 'string' ?
    typeof args[2] !== 'function' ?
      api({method: args[0], url: args[1], ...args[2]}, args[3]) :
      api({method: args[0], url: args[1]}, args[2]) :
    api(...args)

I love this pattern and think it'd be great if the documentation contained some notes about it and an example or two.

Overhead with effectTypes.NONE

Every time you dispatch an action with no effects, redux-loop creates a new "no-effect" object and starts a promise that doesn't do anything on resolve. Could this be optimized away?

In my application I'm creating ~100 pure (side-effect-free) actions per second, and these empty promises show up in the profile.

Prevent batching of child reducers?

Hi there,

I have two unrelated async tasks I want to run after a location change. One is quick (geolocation) while the other is slow (loading the Google Maps API). I've noticed that combineReducers from redux-loop batches all child effects. This means that even though geolocation is finished, its action won't dispatch until the Maps API loads. Is there a way to prevent batching at the top level or is there a more idiomatic solution to this problem?

Route action as effect (react-router-redux)

Hi, I'm struggling with making effects work with react-router-redux. I've setup react-router-redux routerMiddleware and trying to change history as effect.

Example:

import { push } from 'react-router-redux';

function reducer(state, action) {
  switch(action.type) {
  case 'FIRST_ACTION':
    return loop(
      state.set('firstRun', true),
      Effects.constant(push('/delivery'))
    );
}

In redux DevTools I see that action @@router/CALL_HISTORY_METHOD is triggered, but history (url) is not changed. Trying to figure out, what I'm doing wrong here.

Getting "loop Promises must not throw!" error when running the example

Hi there,

First of all, I would like to thank you for this really awesome library!

I have started playing around with the example. The delay function rejects the promise with a 20% chance. If the promise gets rejected, the error gets printed to the console.

I am considering to choose this framework to manage my async flow as I'm porting my third-party JS application from Flux to Redux.
I can work this around by ensuring that my promises won't throw, but I cannot use this awesome library if it prints errors to the console. That's strictly forbidden in my third-party application.

Suggestion: Add Effects.call type

Some sync tasks such as access localStorage, call history method are also side effect.

Now we only can use the Effects.promise to do some stuff. It works but a bit strange.

e.g.

// reducer
export default (state, action) => {
  if (action.type === 'SAVE_ACCESS_TOKEN') {
    return loop(
      state
    ,
      Effects.promise(saveAccessToken, action.accessToken)
      // Effects.call(saveAccessToken, action.accessToken)
    );
  }
  return loop(state, Effects.none());
}
// tasks
export saveAccessToken = (accessToken) => {
  localStorage.accessToken = accessToken;
  return Promise.resolve({type: 'SAVE_ACCESS_TOKEN_SUCCESS'});
  // return {type: 'SAVE_ACCESS_TOKEN_SUCCESS'};
}

combineReducers does not support root immutable state

The implementation of combineReducers provided with this library does not support a root immutable state such as Map. (Which is odd as the example in the repo uses a Map as its root state—but doesn't use combineReducers). The problematic areas are here in combineReducers.js:

// line 22
const previousStateForKey = state[key];

And the mapValues function in utils.js:

export function mapValues(object, mapper) {
  return Object.keys(object).reduce((current, key) => {
    current[key] = mapper(object[key], key);
    return current;
  }, {});
}

This limits the root state to being a plain object. I have written my own version of these two functions for my project, but it's a bit awkward as I have to use isLoop from the library like so:

import { isLoop } from '../../../node_modules/redux-loop/lib/loop';

Which obviously could break at any time as this isn't an exposed function. Ideally, support for this would be built in. I was thinking about opening a pull request, but I wasn't sure how you might want to go about this (if you wanted it at all). Obviously I could drop in my function and export it as "combineReducersImmutable" or something similar. Or I could adjust the original function to check for immutable root states and to act appropriately.

Both these add code for one specific library however, which I've never been a huge fan of (but perhaps I'm overthinking it—it is a fairly popular library). So the last thing I thought of was to adjust combineReducers so it can accept any root data structure, something like:

export function combineReducers(reducerMap, initialState = {}, accessor, mutator) {
    // code...
}

// and then to use:
const appReducer = combineReducers(reducers, Map(), 'get', 'set');

I may be jumping the shark here however (the accessor and mutator function names as strings makes me cringe—perhaps there's a better way? I'm just spitballing with this last suggestion really).

Anyway, would be interested to hear your thoughts, and happy to submit a pull request for an approach you prefer.

Add ability to lift effects

redux-loop notably lacks the functionality provided by Effects.map in elm-effects. There's an important difference between Elm and redux-loop here, in that functions are not comparable in Elm but we can still do reference equality in JavaScript, so adding this functionality should not break deepEquals comparisons. Also, the ability to do arbitrary mapping might lead folks to try to chain transformations together using anonymous functions, which they won't be able to test easily.

In my Elm experience the only use I've found for Effects.map is to map nested actions onto a dedicated higher-order action for the parent's update function.

With all of this in mind, I want to limit the scope of any mapping functionality in redux-loop to just lifting actions into higher-order actions, at least for now. This should take the form of a new effect type in redux-loop called lift.

As of redux-loop 1.0.4, all effects promises resolve an array of actions regardless of whether they are guaranteed to produce 1 or 0. This should make it easy to store a lower effect and a reference to an action creator to lift with, and just map over the lower actions with it once they've resolved. Ideally, we should have some sanity checks in place to throw if the result of lifting is not also an action, but I want to track adding sanity checks as a different story. So feel free to add them, or not.

Suggestion: Dont use Symbol

In mobile, the browser compatibility of Symbol is poor.
We dont intend to use polyfill in the mobile env to increase the file size.

P.S. I have used half of day to debug in IOS env because of the Symbol. It's very difficult to find a tool to inspect the iOS device.

Conflict between install() and devToolsExtension's store enhancer

Hello,
I'm trying to use the redux devtools extension with redux-loop.
I don't think they play well together because as soon as the devToolsExtension enhancer is called, the state is of the shape [state, effect] which shouldn't happen when having install() called first.

Here is my code :

  const middlewares = [
    // this should be redux-loop's getState which returns an object
    ({ getState }) => {
      return (next) => (action) => {
        console.log(getState())// [state, effect] :(
        return next(action)
      }
    }

  ]

  const store = createStore(
    (state = {}) => state,
    compose(
      applyMiddleware(...middlewares),
      global.devToolsExtension ? global.devToolsExtension() : s => s,
      install()
    )
  )

If I remove the devToolsExtension, the state is an object as expected and everything works fine.

add batchSerial effect type?

I've run into a few scenarios where i want to perform multiple effects as a result of a state change, but have then run in order instead of in parallel. Right now, the best option I see to do that is using Effect.call like this

function reducer(...){
  //state changes...
  const effectFn = () => {
    action1();
    action2();
  };

  return loop(newState, Effects.call(effectFn));
}

but you can't reference that function in a test. And while you could export effectFn somewhere so it could be referenced in a test, it could get out of hand quickly if you had to start exporting lots of difference combos of actions.

I thought it could be nice to have a batchSerial (or just serial?) action that works the same as batch, but runs everything in order and waits for the previous effect to finish

return loop(newState, Effects.batchSerial([
   Effects.call(action1],
   Effects.call(action2)
);

Observable effects completion statuses for better server rendering

I was in the middle of brainstorm about better server-rendering and side effects management in redux apps when I found this repo. I want to share some thoughts here and maybe take part in developing if you find them sensible.

tl;dr

Having a way to observe the effects completion statuses (like how many effects are not completed yet) could give us an ability to completely separate UI and application state. By achieving this we could get much simpler server-rendering techniques.

Current server-rendering approach

I assume React as a view layer below, but conclusions are framework-agnostic.

The main difference in server and client rendering is the need to wait for all the API calls and other side effects finish before first rendering to string. These days this is solved by specifying which effects a UI component should perform (ex. API calls to be performed), getting the list of all the effects according to a route, perform all of them, dispatch resulting actions through redux store and then render it all only once. A good example of this approach is redial server-rendering documentation.

Managing side effects differently during server rendering and client initialization feels wrong for me. I tend to believe there should be a solution to write effects management once and reuse it on both server and client sides.

Main idea

Making decisions on next effects as close to the store as possible (which is done thanks to this project) gives us an advantage, because we separate all possible state transitions from UI.

Let's assume we have a queue of effects. When a reducer returns an effect, we put it into a queue. When this effect is resolved, it removes itself from queue. So, looking on the queue length we can definitely say if our system is stable (came to the state there we no longer wait for anything despite user input).

Given this ability, server-rendering is simplified to these steps:

  1. Get all the initial parameters from router.
  2. Init store with these parameters.
  3. Subscribe to store changes. On each store change check if effects queue is empty.
  4. Once the queue is empty we assume system is stable and there is nothing to wait for. So, we perform server rendering. Done.

Client initialization has no difference, except we don't have to check queue emptiness.

Potential problems

I believe there could be an effect that is not ever resolved in classical meaning; getting data from a websocket or infinite timers are examples which come to my mind. For such effects, there should probably be a infinite flag to avoid taking them into account.

Combine Effects.promise and Effects.constant to Effects.call

if the Effects.promise internal resolve implementation add a safe promise wrapper, e.g.

switch (effect.type) {
    case effectTypes.PROMISE:
      return Promise.resolve(effect.factory(...effect.args)).then((action) => [action]);

then the Effects.constant call

Effects.constant(someAction(1))

can change to

Effects.promise(someAction, 1)

and I propose rename it to Effects.call or something other.

In user land, we don't need to care whether it is a task or simple action.

immutablejs combinereducers example has high overhead

When used with immutablejs as described in the readme, combineReducers looks like it might suffer from high overhead because each time you call the setState method, immutablejs will create a new copy of the store. It would be more efficient if the mutations were batched with the immutablejs method withMutations. (basically the provided combinereducers but with the main block wrapped by "withMutations"

I wouldn't expect a method optimized for immutablejs to be provided by this repo, but as long as it's being mentioned in the readme, it might be worthing pointing out that performance caveat so people know they should probably be writing their own.

I think something like this would work well

function immutableJsCombineReducers(reducerObj) {
  const reducerKeys = Object.keys(reducerObj);

  return (rootState = Map(), action) => {
    let hasChanged = false;
    let effects = [];

    const model = rootState.withMutations(initialState => {
      reducerKeys.reduce((previousState, reducerName) => {
        const currentChildState = previousState.get(reducerName);
        let nextChildState = reducerObj[reducerName](currentChildState, action);

        if (isLoop(nextChildState)) {
          effects.push(getEffect(nextChildState));
          nextChildState = getModel(nextChildState);
        }

        hasChanged = hasChanged || nextChildState !== currentChildState;
        return previousState.set(reducerName, nextChildState);
      }, initialState);
    });

    return loop(
      hasChanged ? model : state,
      optimizeBatch(effects)
    );
  };
}

Suggestion: Add Effects.generator

Hi, after using redux-sagaI have fallen in love with JS generator functions. Would it be possible to have a generator function effect that runs the generator function until it is done?

server-side rendering strategy

As a part of redux-loop 3.0 I want to have a solid strategy that can be communicated about how to use reducers that return effects as a means of implementing the initial data fetch for server-rendered apps.

The prior discussions, proposed code changes, and outstanding issues around this to date are:

There are two techniques I want to propose to start, my favorite as of now is the first:

  1. Lean on existing functionality and write documentation on how to use it for server rendering
  • No additional methods are added to the store
  • Passing a loop to the initialState argument of createStore is explicitly disallowed by throwing an error if it is detected
  • Recommend using an INITIALIZE_APP action that returns the appropriate effects for getting the store where it needs to be for rendering to be considered complete
  • Rely on the fact that store.dispatch() returns a Promise for when the effect tree of the dispatched action has completed
  1. Allow subscriptions to effect completion
  • Either through the work @bardt has already done in #25 or
  • By adding a new onCmdQueueEmpty function that can be subscribed to separately
  • Users can still pass loops as initialState in this scenario

Thoughts?
@bardt @mocheng @bdwain

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.