reduxjs / redux

A JS library for predictable global state management
https://redux.js.org
MIT License
60.9k stars 15.27k forks source link

Handling async data by making reducers behave like Elm signals #1205

Closed Zacqary closed 8 years ago

Zacqary commented 8 years ago

Forgive me if this has already been figured out; I've looked through the Redux docs and at some middleware and I haven't found a solution myself.

Here's the problem I've had to solve:

  1. User action updates the Redux state of a property called metric.
  2. Re-render the view with the value of that metric, and populate a selection box with the metric's corresponding context options.
  3. Those context options need to be fetched asynchronously from the server. If we haven't cached the contexts for this metric yet, request them. Meanwhile, render a "loading" symbol in the selection box.
  4. Re-render the view once the contexts have been fetched

In Elm, this would be a simple act of making context a Signal. Since Redux recommends Elm-like architecture, I'd like to replicate that pattern.

I have a solution that I'm a little uncomfortable with. After the metric gets updated, it gets passed to the React view. The view sees that the store has no context for that metric yet, and then calls an action creator called requestContext. This uses redux-thunk to asynchronously fetch the contexts, and then dispatches an action to add those contexts to a lookup table in the store. After that, the view is re-rendered, and it can now look up state.context[state.metric].

But the thunk first has to dispatch an action to set those contexts to a value indicating that they're loading. Otherwise the view could keep requesting the contexts over and over again if it re-renders before they're fetched from the server.

So that's the problem: I'd much rather be able to keep that action dispatching/updating logic out of the view. I just want to pass a value to the view, and have it display that value, whether it's a loading state or retrieved data.

I'm not sure if redux-promise is an effective solution for this either — it seems to be about delaying the dispatch of an action just like redux-thunk, but with a different syntax.

I suppose the better solution would be for the reducer handling context to make the request, return a loading state, and then return the result of the request. But that would mean having to dispatch an action from the reducer, right? I don't think that's possible without middleware — maybe something like thunk (which exposes store.dispatch), but for reducers instead of actions?

lukewestby commented 8 years ago

It sounds like what you're really looking for is the ability to return something like the StartApp style (Model, Effects Action) in redux, yes?

winstonewert commented 8 years ago

I recently proposed an alternative approach here: https://github.com/rackt/redux/issues/1182. I think it addresses your concerns about putting this logic into the view.

I'd like to make it a library, but that's not yet a thing. But perhaps the example I showed there will give you some ideas.

Zacqary commented 8 years ago

@winstonewert So let me see if I understand how this would work with reactions:

  1. The SET_METRIC action triggers the reducer for context.
  2. The context reducer checks to see if state contains a key corresponding to the action's metric. If it doesn't, it sets that key's value to something like ....
  3. The context reactor checks to see if any keys have values of ..., and if they do, it sends an XHR to find out what their values should be.
  4. When an XHR returns, the reactor dispatches a SET_CONTEXT action to update the store.

Is that about the gist of it? I noticed your implementation is actually dispatching an action to a doReactions function, so there's an extra step in there — I guess if you don't assume that all reactions necessarily trigger an XHR, then the extra step of doReactions isn't necessary?

I saw redux-saga mentioned in your thread too. It seems like it's more complicated but maybe more bulletproof? Gotta look into that more later.

winstonewert commented 8 years ago

I guess if you don't assume that all reactions necessarily trigger an XHR, then the extra step of doReactions isn't necessary?

Actually, doReactions is pretty key to what's going on. doReactions keeps track of the pending requests. That's what prevents the system from starting the same request over and over again. That lets the reactions function simply worry about which data is missing right now, without being concerned about whether or not a request has been made for it.

The context reducer checks to see if state contains a key corresponding to the action's metric. If it doesn't, it sets that key's value to something like .... The context reactor checks to see if any keys have values of ..., and if they do, it sends an XHR to find out what their values should be.

You could. Or you could simply if check if state.context[state.metric] is undefined.

I saw redux-saga mentioned in your thread too. It seems like it's more complicated but maybe more bulletproof? Gotta look into that more later.

redux-saga has the benefit of actually being available as a library right now. I wouldn't think it was more bulletproof, but its more complicated and allows more complex interactions then my approach.

winstonewert commented 8 years ago

I should note you can do something simpler then me, I was trying to be more generic. You could probably do something like:

var contextRequests = {};
store.subscribe(() => {
    var state = store.getState();
    if(!state.context[state.metric] && !contextRequests[state.metric]) {
       contextRequests[state.metric] = fetch('/api/context/' + state.metric')
             .then((result) => store.dispatch({type: 'SET_CONTEXT', metric: state.metric, context: result});
    }
});
Zacqary commented 8 years ago

You could. Or you could simply if check if state.context[state.metric] is undefined.

I could if I were passing the entire state object. I'm approaching this from the line of thinking that reducers are only supposed to receive the property of state that they act on, though. My assumption is that each reducer with a corresponding reactor of the same name will check the result of its reducer, and then trigger a side effect if need be.

Maybe that's a little limiting. I guess it makes more sense to pass in the entire state and destructure it.

This is why I was thinking of a reducer version of thunk. Since the context reducer can handle an action that's supposed to set metric, it gets the necessary value without having access to other parts of state.

winstonewert commented 8 years ago

In my view, you shouldn't be managing the different properties in different reducers. Splitting it up into different reducers is intended for when you have independent pieces of state. In this case, those pieces of state aren't independent.

gaearon commented 8 years ago

I think we have too many discussion threads about this. Please use any of the existing ones. (You can search for "side effects".)