Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

modeling side-effects #455

Closed
rt2zz opened this issue Aug 11, 2015 · 25 comments
Closed

modeling side-effects #455

rt2zz opened this issue Aug 11, 2015 · 25 comments

Comments

@rt2zz
Copy link

rt2zz commented Aug 11, 2015

re: https://twitter.com/dan_abramov/status/630894436810158080

I believe the idea here is, lets give the store control over executing side effects. Reducers, or reducer like things can provide side effects based on incoming actions. See http://debug.elm-lang.org/ #purity

Continuing conversation in comments...

@rt2zz
Copy link
Author

rt2zz commented Aug 11, 2015

Giving the store control over executing side effects has some really nice benefits (see the bullet points here: https://github.com/rt2zz/redux-remotes). Something like redux-devtools would skip side-effects during replay.

@gaearon based on your tweet it sounds like you are thinking reducers might provide the side effects. I imagine side effects are functions that would look something like:

function sideEffect (action, dispatch){ 
  /* do some work, fire some new actions */ 
}

Let me know if this is in the ballpark of what you are thinking. This is the first time I have considered these concepts.

@confiks
Copy link

confiks commented Aug 11, 2015

My understanding (out of those 140 character bites) is that the sideEffect functions would (conditionally, depending on "whatever calls reducer") be executed by the reducer. If that is the case, either those side effects are supplied in an action/actionCreator, or an action would be modified by the middleware to contain those side effects (much like 'thunk-async' or the promise middleware out of issue #99 now immediately executes them).

I don't immediately see on what grounds a reducer would or would not execute such supplied side effects.

(as an aside, I was wondering why in #99 (comment), in the callbacks of promise.then(), the next function is called, instead of dispatching a new action via store.dispatch({...}). I'm now using this fetch middleware instead, and I wonder if I'm missing something. )

@rt2zz
Copy link
Author

rt2zz commented Aug 11, 2015

@confiks next is basically dispatch, but if you call store.dispatch it will run through the entire middelware chain again, whereas if you call next it will be handed to the next middleware rather than back to the beginning.

@gaearon
Copy link
Contributor

gaearon commented Aug 11, 2015

cc @vladar @vladap

@gaearon
Copy link
Contributor

gaearon commented Aug 11, 2015

that the sideEffect functions would (conditionally, depending on "whatever calls reducer") be executed by the reducer

No, they would be (optionally) returned from it. Imagine reducer's return value is next state and, optionally, a side effect. The store may choose to invoke it or not, based on whether it's in regular flow (yes), or is doing something like replaying previously recorded actions (no).

It's still very unclear to me but I share the concerns about putting too much in a middleware. I'm not sure this is the solution but it's something Elm seems to recommend, and Elm usually has great ideas, so we should definitely look into making a proof of concept.

Open questions:

  1. How can this be used to better express async calls and common scenarios?
  2. Syntactically how would we even "optionally return" a function? No tuples in JS :-(
  3. How does that work with splitting reducers? Do effects just accumulate? Is there anything interesting going on here?

@rt2zz
Copy link
Author

rt2zz commented Aug 11, 2015

  1. It is awesome because among other things, it forces you to think about your imperative async code as an ecapsulated reaction.
  2. :(
  3. Effects would accumulate. This is actually one of the exciting things about hanging side effects off of the store. One action might trigger multiple side effects, or many actions might trigger one side effect.

The big challenge here is the API. Returning tuples is impossible. If we can come up with a solution for how to return side effects, I will gladly put together a proof of concept.

@acdlite
Copy link
Collaborator

acdlite commented Aug 11, 2015

If we can come up with a solution for how to return side effects

Perhaps with a callback?

reducer(state, action, createSideEffect)

@rt2zz
Copy link
Author

rt2zz commented Aug 11, 2015

Ya, @acdlite now that you write that out, it feels that is the only realistic way to accomplish this.

But something about putting side effects in reducers feels very wrong. While strictly speaking reducers will still be deterministic since they are not executing side effects, the mental model will change. My thought is to hang this off a store as a first class concept that gets applied before the reducer. This could be achieved either in redux createStore or as a contrib store enhancer.

What might that look like? For the sake of this example a function that returns side effects is called an effector
createStore.js

//...
function dispatch(action) {
    //...
    sideEffects = currentEffector(currentState, action);
    if(!isReplay){
      runSideEffects(sideEffects)
    }
    isDispatching = true;
    currentState = currentReducer(currentState, action);
    //...
  }

Side effects would have a parallel registration process to reducers. In fact if people want to colocate their side effects they could export both a reducer and a effector from a single file.

@rt2zz
Copy link
Author

rt2zz commented Aug 12, 2015

Ok I have read a bit more about elm side-effects and cerebral signals, and it seems to me these concepts are roughly what we are trying to achieve - but in a redux consistent way. Again I will call this macro concept effectors.

Another interesting possibility here is opening up the potential for effector/reducer combination factories. For example

import {createEntityHandlers} = require('redux-entity')
const {profileEffector, profileReducer} = createEntityHandlers('profile', uri)

//... later in an action creator
loadProfiles = () => ({
  type: 'LOAD_PROFILES',
  meta: {
    entityLoader: true,
    entity: 'profile',
  }
})

Then based on one well modeled action, the entity effector can load data from a url and the entity reducer can receive that data.

@gaearon
Copy link
Contributor

gaearon commented Aug 12, 2015

I made a real world example: https://github.com/gaearon/redux/tree/breaking-changes-1.0/examples/real-world

Right now it uses middleware for async. Feel free to fork Redux and rewrite it with your model in mind, and let's compare benefits!

@leoasis
Copy link
Contributor

leoasis commented Aug 12, 2015

One problem that I see is how to track side effects for cancellation. For example clicking rapidly to fetch a the same user, will trigger a lot of request that ideally will be cancelled when a new one happens. That in the world of side effects in action creators would be handled just there, but how would it be handled in this case? Is there a generic way to handle this or does it depend on the use case? If it's the latter, how does the reducer (or something else) trigger a cancellation to a previous side effect?

@ghost
Copy link

ghost commented Aug 12, 2015

I'd really like to understand this discussion because I have a feeling it is pretty important. What problem/use case is being solved?

@gaearon
Copy link
Contributor

gaearon commented Aug 13, 2015

This is

#351
#343
#307

all over again :-)

Until we have a proof of concept there's little sense keeping discussing it in my view.

@rt2zz
Copy link
Author

rt2zz commented Aug 13, 2015

lol, I see this path has been travelled before

@leoasis async cancellation is a hard problem (see all the troubles promises went through). I would handle it by having a stateful object available to the side effect function which can be used to cancel or take action upon an in progress side effect, basically the same as you would for action creators.

@danmartinez101 the basic question is, is there a better way to organize side effects other thank using async middleware like redux-thunk.

The more I think about this, the more I believe it should exist in user land. There are many possible approaches here, and it feels way to early to prescribe any one of them. The only approach that cannot be done in userland is reducer generated side-effects, but I also think that is the least exciting approach. I will continue experimentation at https://github.com/rt2zz/redux-remotes

@rt2zz rt2zz closed this as completed Aug 13, 2015
@gaearon
Copy link
Contributor

gaearon commented Aug 13, 2015

👍 let us know when you figure it out!

@gaearon
Copy link
Contributor

gaearon commented Aug 13, 2015

async cancellation is a hard problem (see all the troubles promises went through)

Use Observables (e.g. Rx), that's what they do best.

@vladar
Copy link

vladar commented Aug 13, 2015

Idea of having side effects as a separate layer is quite interesting.

I did some reasearch. And what we discuss here is a well-known in academia world as "Finite state machines with output". In theory they are described by two functions - transition function and output function:

transition: (state, input) => state
output: (state, input) => output

Technically you can combine those into one as we dsicussed:
(state, input) => (state, output)

Alternatively we can do something similar to what author proposes - add another layer. But I would argue that it has to be executed AFTER reducer.

There are two flavors of FSMs with output: Moore and Mealey.

Mealey type: (nextState, previousState, input)
Moore type: (nextState, previousState) (only function of state, not input)

In general this approach is quite similar to React hooks. They help to deal with imperfectness and imperative nature of DOM. What we are trying to do here is quite similar.

Framework then can choose to not execute any hooks on replay.

The only problem is that Reducer is a function, not class, so this way of doing things is harder in Redux than it is in React.

But the advantage is that such change may be non-breaking one. So it might be worth considering.

@rt2zz
Copy link
Author

rt2zz commented Aug 14, 2015

@vladar interesting stuff. In some sense having access to (state, action) is isomorphic to (previousState, nextState) in that action contains the information needed to construct nextState.

@vladar
Copy link

vladar commented Aug 19, 2015

@rt2zz actually we can develop this idea further. Whole typical React/Flux app is state machine with output, where output function is transformation of state to new React vdom.

And it's Moore type of output, because React only gets new state, not intial input that caused state transition.

So we can possibly treat side-effects as another type of output and model it similar to react components.

Say we could have "effects" (or "tasks") that react to state changes, get (prevState, nextState) and issue side effects.

Those tasks can be stateful by themeselves - similar to React which holds previous vdom state internally. But that is transient state, that shouldn't be in app state.

For example in this model you can abort http requests easily; dispatch new sync actions (because it is outside of previous dispatch chain).

And it will work nice with replay, if you can manage to disable effects on replay.

I guess this can work with any Flux implementation and doesn't require much changes in frameworks.

@maxfrigge
Copy link

My gut is somehow telling my that just separating sync from async and thereby throwing many side effects in the same box could not be sufficient and just be a short lived solution.

Quick note: I am just getting into react, rfp etc. but I wanted to share my thoughts anyway - so bare that in mind while reading.

Has anyone considered a concept where you have isolated channels (per side effect/side effect groups) and each channel processes actions triggered by a message (signal/event..). Each message could trigger actions on one or more channels (or none). This separation of side effects is handled nicely in cycle.js (they call it drivers). It would add complexity, but one upside would be that each channel could have it's own "time travel strategy".

I picture it like this:

The most obvious channel would be called window - a combination of window.document (dom) and window.location. In redux words this would be the reducers working on the state, which in return results in (v)dom updates and maybe updates to the address bar. This channel would get the time travel strategy reproduce - meaning every action on this channel would be reproduced during time travel.

Another channel would be http. This channel would obviously process async actions which would issue http request, process their responses and eventually send some messages that would be processed on the window channel. In most cases it would not make much sense to reproduce these actions during time travel, thus they would get a different time travel strategy simulate. This strategy would skip executing the actions (ignore all messages that went in), but emit all the messages that where recorded on this channel and thereby simulate this channel/side effect rather then reproducing it.

Other time travel strategies could be:

  • confirm: Ask the user to decide what to do. Maybe you want a request to be send again - how knows?
  • ignore: Completely ignore the channels input and output - a tricky one but why not?

This way we would get more control over side effects during time travel and new strategies could be implemented when new use cases pop up. You could even switch a channel's strategy during time travel - we all know that could result in multiple (parallel) universes but I think it could be an exciting journey and since we are talking about time travel it's something you might want to take into account.

Does this make sense at all or will it just add unnecessary complexity without any real benefits?

@panayi
Copy link

panayi commented Nov 19, 2015

In the redux logger context, I don't think separating the side effects is enough. Consider disabling an action ADD_TODO in the logger. The logger has all the information to compute the state, and (I think) what it does is replay all the actions except for that specific action.

However, assume that a certain action START_SERVICE involves calling service.start(). When disabling that action, the logger will replay all the actions except that one. But that isn't reverting the effect of START_SERVICE; the service is still running. For this reason, I think that redux should at a minimum know how to revert a side effect. The API for defining a side-effect should be a pair: forward path and backward path:

function forward() {
  service.start();
}

function backward() {
  service.stop();
}

When calling an action, check for attached side-effect calls and for each side-effect call the forward action. When disabling an action (or going back in history), call the backward action for each attached side-effect.

I'm new to redux, and I apologize if I repeat something from above or from some other issue.

@tomkis
Copy link
Contributor

tomkis commented Nov 25, 2015

Has anyone ever been considering using generators for side effects? Something like this:

// You can defer the side-effect execution
const sideEffect = appState => dispatch => localStorage.setItem('counter', appState);

function plainOldReducer(appState) {
  return appState + 1;
}

// Composition simply work
function nestedReducer*(appState, action) {
  if (action === FOO) {
    yield sideEffect(appState);
    return plainOldReducer(appState);
  } else {
    return appState;
  }
}

function rootReducer*(appState = 0, action) {
  return yield* nestedReducer(appState, action);
}

Store enhancer should be responsible for iterating over the generator and executing all the side effects.

@tomkis
Copy link
Contributor

tomkis commented Dec 3, 2015

Nobody seems to be interested in side effects anymore? It seems to me like the most discussed topic lately. Anyway, here's the approach described above https://github.com/salsita/redux-side-effects

@gaearon
Copy link
Contributor

gaearon commented Dec 22, 2015

I really like Redux Saga projects which also uses generators: #1139. Feel free to contribute to that discussion!

@gaearon
Copy link
Contributor

gaearon commented Mar 18, 2016

Relevant new discussion: #1528

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants