Skip to content

Best async serverside loading technique? #99

Closed

Description

@erikras

First of all, I love this library and the patterns you guys are using. 👍👍

I'm trying to use redux to build an isomorphic app. It's working beautifully so far, except that I need to figure out how to wait until my stores have loaded (on the server) before returning the initial page load. Ideally, the loading should take place in the stores themselves, but when I call dispatch(userActions.load()), the store has to return the new state (i.e. return { ...state, loading: true };), so it can't return a promise to wait for. dispatch() returns the action passed to it for some reason. I'd really like something like...

dispatch(someAsyncAction, successAction, failureAction) => Promise

...where the promise doesn't resolve until one of the other two actions is dispatched.

Is that the sort of thing that could be enabled with the middleware pattern?

Am I totally off base and there's a simple way to do this already?

Thanks.

Activity

  1. gaearon commented on Jun 15, 2015

    @gaearon
    Contributor

    Hey, thanks!

    Ideally, the loading should take place in the stores themselves

    Redux enforces that Stores are completely synchronous. What you describe should happen in the action creator instead.

    I think it may even be possible with the default thunk middleware. Your action creator would look like:

    export function doSomethingAsync() {
      return (dispatch) => {
        dispatch({ type: SOMETHING_STARTED });
    
        return requestSomething().then(
          (result) =>  dispatch({ type: SOMETHING_COMPLETED, result }),
          (error) =>  dispatch({ type: SOMETHING_FAILED, error })
        );
      };
    }

    and handling the actual (granular) actions in the Store.

    It's also possible to write a custom middleware to remove the boilerplate.

  2. erikras commented on Jun 15, 2015

    @erikras
    Author

    Genius! I figured I was overlooking something obvious. I like that separation of doing and storing.

    I look forward to watching this library grow, although it's pretty doggone complete already. Cheers, @gaearon!

  3. gaearon commented on Jun 15, 2015

    @gaearon
    Contributor

    You can also write a custom middleware like this

    export default function promiseMiddleware() {
      return (next) => (action) => {
        const { promise, ...rest } = action;
        if (!promise) {
          return next(action);
        }
    
        next({ ...rest, readyState: 'request' );
        return promise.then(
          (result) => next({ ...rest, result, readyState: 'success' }),
          (error) => next({ ...rest, error, readyState: 'failure' })
        );
      };
    }

    and use it instead of the default one.

    This will let you write async action creators like

    function doSomethingAsync(userId) {
      return {
        type: SOMETHING,
        promise: requestSomething(userId),
        userId
      };
    }
    

    and have them turn into

    { type: SOMETHING, userId: 2, readyState: 'request' }
    { type: SOMETHING, userId: 2, readyState: 'success' }
    { type: SOMETHING, userId: 2, readyState: 'failure' }
    
  4. erikras commented on Jun 15, 2015

    @erikras
    Author

    Ooh, that's nice as well, and more of what I had in mind when I asked the original question. I can't tell if I like the tradeoff in reducing the number of action constants in exchange for adding ifs to check the readyState inside the store. I think I might prefer having addtional _SUCCESS and _FAILURE versions of each action just to avoid putting an if inside a case.

    Thanks, though.

  5. gaearon commented on Jun 15, 2015

    @gaearon
    Contributor

    Yeah that's totally up to your taste. You could have a similar version that turns types: { request: ..., success: ..., failure: ... } into actions. This is the point of making it a middleware instead of baking into the library: everybody has their own taste to these things.

  6. gaearon commented on Jun 15, 2015

    @gaearon
    Contributor
    // Middleware
    export default function promiseMiddleware() {
      return (next) => (action) => {
        const { promise, types, ...rest } = action;
        if (!promise) {
          return next(action);
        }
    
        const [REQUEST, SUCCESS, FAILURE] = types;
        next({ ...rest, type: REQUEST });
        return promise.then(
          (result) => next({ ...rest, result, type: SUCCESS }),
          (error) => next({ ...rest, error, type: FAILURE })
        );
      };
    }
    
    // Usage
    function doSomethingAsync(userId) {
      return {
        types: [SOMETHING_REQUEST, SOMETHING_SUCCESS, SOMETHING_FAILURE],
        promise: requestSomething(userId),
        userId
      };
    }
  7. erikras commented on Jun 15, 2015

    @erikras
    Author

    Oh man, I love that solution. So much nicer than having the then() and additional calls to dispatch() like the first solution you proposed. Hooray for middleware!

  8. gaearon commented on Jun 15, 2015

    @gaearon
    Contributor

    Let me know how (and whether ;-) it works!
    We have not really tested custom middleware much.

  9. erikras commented on Jun 15, 2015

    @erikras
    Author

    You left out a } (that's -1 point 😀), but it worked like a charm! First time.

  10. gaearon commented on Jun 15, 2015

    @gaearon
    Contributor

    👍

  11. iest commented on Jun 17, 2015

    @iest

    @erikras I'm curios how you implemented waiting for the promises to resolve on the server?

  12. erikras commented on Jun 17, 2015

    @erikras
    Author

    This is just pseudocode, so don't go pasting this anywhere, but I'm using react-router (whose api is changing as fast as redux's) something like this:

    app.get('/my-app', (req, res) => {
      Router.run(routes, req.path, (error, initialState) => {
        Promise.all(initialState.components
          .filter(component => component.fetchData) // only components with a static fetchData()
          .map(component => {
            // have each component dispatch load actions that return promises
            return component.fetchData(redux.dispatch);
          })) // Promise.all combines all the promises into one
          .then(() => {
            // now fetchData() has been run on every component in my route, and the
            // promises resolved, so we know the redux state is populated
            res.send(generatePage(redux));
          });
      });
    });

    Does that clear anything up?

  13. gaearon commented on Jun 17, 2015

    @gaearon
    Contributor

    @iest

    Quoting your problem in Slack:

    I’ve got a route handler with

     static async routerWillRun({dispatch}) {
       return await dispatch(UserActions.fooBar());
     }

    where UserActions.fooBar() is:

    export function fooBar() {
     return dispatch => {
       doAsync().then(() => dispatch({type: FOO_BAR}));
     };
    }

    then in the server render I’m yielding:

     yield myHandler.routerWillRun({dispatch: redux.dispatch});

    but it doesn’t work.

    I think the problem here is you're not actually returning anything from fooBar's nested method.

    Either remove the braces:

    export function fooBar() {
      return dispatch =>
        doAsync().then(() => dispatch({type: FOO_BAR}));
    }

    or add an explicit return statement:

    export function fooBar() {
      return dispatch => {
        return doAsync().then(() => dispatch({type: FOO_BAR}));
      };
    }

    Either way, it might be easier to use a custom promise middleware as suggested above.

  14. mattybow commented on Jun 17, 2015

    @mattybow
    Contributor

    @erikras Regarding your last comment where you are calling the fetchData method on what you have as initialState.components (in the callback of Router.run), the object you get the component references from only return the route handlers matched. What are your thoughts on reaching components that might not be a matched route handler, i.e a child component, but needs to fetch data?

    here's an example of what I'm talking about

    import React from 'react';
    import Router from 'react-router';
    import {Route, RouteHandler, DefaultRoute} from 'react-router';
    
    //imagine Bar needs some data
    const Bar = React.createClass({
      render(){
        return(
          <div>bar</div>);
      }
    });
    
    const Foo = React.createClass({
      render(){
        return (
          <div>
            foo
            <Bar/>
          </div>);
      }
    });
    
    
    const App = React.createClass({
      render(){
        return (
          <div>
            <RouteHandler />
          </div>
        );
      }
    });
    
    const routes = (
      <Route path="/" handler={App} name="App">
        <DefaultRoute handler={Foo} name="Foo"/>
      </Route>
    );
    
    Router.run(routes,'/',function(Root,state){
      console.log(state);
    });

    output:

    { path: '/',
      action: null,
      pathname: '/',
      routes: 
       [ { name: 'App',
           path: '/',
           paramNames: [],
           ignoreScrollBehavior: false,
           isDefault: false,
           isNotFound: false,
           onEnter: undefined,
           onLeave: undefined,
           handler: [Object],
           defaultRoute: [Object],
           childRoutes: [Object] },
         { name: 'Foo',
           path: '/',
           paramNames: [],
           ignoreScrollBehavior: false,
           isDefault: true,
           isNotFound: false,
           onEnter: undefined,
           onLeave: undefined,
           handler: [Object] } ],
      params: {},
      query: {} }

    You won't have access to Bar in Routes

  15. 37 remaining items

  16. ms88privat commented on Feb 17, 2016

    @ms88privat

    @chemoish

    I get around this by preventing the fetch code from executing if the store is already full

    Okey, I got that. Thanks.

    However, the solution posed above requires each page to know the data for all of its children components.

    Maybe I misread your solution, but isn't this a necessary requirement regardless of the server-side rendering? (e.g. it should render the same state, if I refresh on the current route, even if it is a SPA)

  17. chemoish commented on Feb 17, 2016

    @chemoish

    It would, but you may want a nested component manage its own data fetching, for whatever reason.

    For instance, a component that is repeated on many pages, but each page doesn't have much data fetching needs.

  18. ms88privat commented on Feb 17, 2016

    @ms88privat

    @chemoish I'm not sure if we are on the same page. Let me try to explain what my point of view is.

    For example I got three nested components:

    • component1 (static dataFetch1)
      • component2 (static dataFetch2)
        • component3 (static dataFetch3)

    Each of them have there own "componentDidMount" methods, with there own dataFetching declarations (dispatching actions via its static dataFetching method).

    If I do not have sever-side rendering and I refresh the current URL, my components will mount and trigger all the actions needed to load all the required data afterwards.

    With server-side rendering, your match function and renderProps will extract all three components, so I can access all of the static dataFetching methods, which will then allow me to fetch all the data needed for the initial server-side rendering?

    Do you have a reference to your match function from your provided example? Thx.

  19. dominictobias commented on Feb 17, 2016

    @dominictobias

    @ms88privat renderProps.components is an array of router components, it doesn't go deeper than that. @chemoish meant that with his implementation you cannot describe the data fetching needs on deeper components.

  20. ms88privat commented on Feb 17, 2016

    @ms88privat

    @dominictobias thx, do you have a solution to this problem? Is there a possibility to get all the nested components?

  21. sompylasar commented on Feb 17, 2016

    @sompylasar

    Probably this can help? https://github.com/gaearon/react-side-effect
    Used to collect all meta tags from nested elements: https://github.com/nfl/react-helmet

  22. vtambourine commented on Mar 11, 2016

    @vtambourine

    Sorry for bumping this discussion again, but I have recently run into the same problem of prefilling state with async action.

    I can see that @erikras moved his boilerplate project to redux-async-connect. I wonder, if somebody found another solution?

  23. ctrlplusb commented on Mar 18, 2016

    @ctrlplusb

    @vtambourine I have been looking at https://github.com/markdalgleish/redial which is quite helpful

  24. vtambourine commented on Mar 18, 2016

    @vtambourine

    Yes, I have looked through it. But I didn't get, how to make sure data
    fetching hooked will not run second time after reinitialization code to n
    client.
    On Пт, 18 марта 2016 г. at 22:54, Sean Matheson notifications@github.com
    wrote:

    @vtambourine https://github.com/vtambourine I have been looking at
    https://github.com/markdalgleish/redial which is quite helpful

    —
    You are receiving this because you were mentioned.
    Reply to this email directly or view it on GitHub
    #99 (comment)

  25. isTravis commented on May 12, 2016

    @isTravis

    Also curious if anyone has found a stable solution for this challenge. I love @erikras's boilerplate, but as @vtambourine mentioned, it has moved to redux-async-connect which seems like it may not be a stable long-term solution: #81 Is redux-async-connect dead?.

  26. AVVS commented on May 24, 2016

    @AVVS

    @vtambourine there is a fork thats available at https://github.com/makeomatic/redux-connect and is well-maintained. It has similar API with a few changes to it, check it out if you are interested

  27. peter-mouland commented on Sep 3, 2016

    @peter-mouland
    Contributor

    for those interested in a redux solution with middleware as mentioned by @gaearon I have an example project which implements this technique and allows the components themselves request the data they need server-side

    https://github.com/peter-mouland/react-lego-2016#redux-with-promise-middleware

  28. serraventura commented on Oct 18, 2016

    @serraventura

    How to unit test action creator with this approach?

  29. deleted a comment from Acnologiaxx on May 5, 2020
  30. locked as resolved and limited conversation to collaborators on May 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions