jeffbski / redux-logic

Redux middleware for organizing all your business logic. Intercept actions and perform async processing.
MIT License
1.81k stars 107 forks source link

SSR whenComplete not helping #139

Open yyynnn opened 5 years ago

yyynnn commented 5 years ago

Hi there! Great lib. Having trouble getting state from api call while doing server side render. All i get is the initial state of the reducer. How to wait for store to get a new state? (using redux-logic, recompose)

// Component
  const enhancer = compose(
  connect((state, { match }) => {
    return {
      data: pageGeneratorDataSelector(state, match.params.id),
      meta: pageGeneratorMetaSelector(state, match.params.id)
    }
  }),
  withLifecycle({
    onWillMount({ match, dispatch }) { // or DidMount, no matter
      dispatch(actions.data(match.params.id))
    }
  }),
  shouldUpdate(
    ({ data, meta }, { data: dataNext, meta: metaNext }) =>
      !shallowEqual(data, dataNext) || !shallowEqual(meta, metaNext)
  )
)

export const PageGeneratorProvider = enhancer(({ data, meta }) => {
  console.log('​PageGeneratorProvider -> data', data) // here i get empty array from initial data on first render SS
  return <PageGenerator data={data} meta={meta} />
})

// Logic

export const getPageSchemaLogic = createLogic({
  type: actions.data().type,
  debounce: 300,
  latest: true,
  process({ action }, dispatch, done) {
    pageGeneratorApi
      .getPageSchema(action.payload)
      .then(results => dispatch(actions.success(results))) // updating data
      .catch(err => dispatch(actions.fail(err)))
      .then(() => {
        return done() // I guess here whenComplete resolves, but store still not updated.
      })
  }
})

// Server renderer

    const root = (
      <ServerRoot
        location={req.url}
        sheet={sheet.instance}
        store={store}
        context={context}
      />
    )
    store.logicMiddleware.whenComplete(() => {
      const jsx = extractor.collectChunks(root)
      const html = renderToString(jsx)
      const styleTags = sheet.getStyleTags()
      const scriptTags = extractor.getScriptTags()
      const linkTags = extractor.getLinkTags()
      const storeState = store.getState()
      const preloadedState = encodeURIComponent(JSON.stringify(storeState))

      res.status(200).send(
        renderFullPage({
          html,
          styleTags,
          scriptTags,
          linkTags,
          preloadedState,
          meta
        })
      )
    })

Like whenComplete ignore logic completion

jeffbski commented 5 years ago

Thanks for the question, I'll take a closer look at this and see if I can figure out what is going on and how to solve.

jeffbski commented 5 years ago

I wasn't sure in your example whether you have dispatched the action(s) before calling whenComplete on your SSR.

Does your actions.data().type action get dispatched before the store.logicMiddleware.whenComplete() is called? That would be key for this to work, all actions need to be dispatched so things are in flight before whenComplete is called.

yyynnn commented 5 years ago

@jeffbski, well it is expected to be working while i do redux-logic-stuff in <ServerRoot/>, i launch my actions to trigger logics there.

It worked with dispatching actions before <ServerRoot/> explicitly. Like this:

//first
store.dispatch(someActions.actionTODO())
//then
const root = renderToString(
          <ServerRoot
            sizesConfig={sizesConfig}
            location={req.url}
            sheet={sheet.instance}
            store={store}
            context={context}
          />
        )
//boom i got whole route in html with prefetched initial state for redux

But it is kinda bad. Doubling and what if some action names change?

It would be nice to trigger some special logic that could terminate on will:

//first
store.dispatch(globalActions.trigger_special_logic_START())
//then
const root = renderToString(
          <ServerRoot
            sizesConfig={sizesConfig}
            location={req.url}
            sheet={sheet.instance}
            store={store}
            context={context}
          />
        )

store.dispatch(globalActions.trigger_special_logic_END())
// like done() was triggered outside
kshepelev commented 5 years ago

I have this problem too

yyynnn commented 5 years ago

@jeffbski is there anything that can be done with this problem? Or am i doing something wrong?

jeffbski commented 5 years ago

Sorry for the delay on this, slipped off my radar.

I'm not sure if I completely understand what you are proposing? Let me suggest what I think you are wanting to accomplish and if I am missing the mark, let me know.

If you simply want to wait for state to finish changing before continuing then what about something like this?

//first
store.dispatch(globalActions.trigger_special_logic_START())
//then
const root = renderToString(
          <ServerRoot
            sizesConfig={sizesConfig}
            location={req.url}
            sheet={sheet.instance}
            store={store}
            context={context}
          />
        )

// assuming you have put your logicMiddleware on the store when you created it,
// the promise completes when everything in redux-logic is done running
await store.logicMiddleware.whenComplete() 
// now I am ready to deliver for SSR

I want to make this work well with SSR so if I am missing something please explain and we'll try to tackle it.

yyynnn commented 5 years ago

@jeffbski yeah, but the initial render will happen and data will not be in place. To make this work you need to do two renderToString calls (as i recall in redux-saga):

The data flow on server in this case is this (top to bottom):

// first ss render to string WILL MOUNT TRIGGER RENDER. data state is: List [] // initial render. empty data. RENDER TO STRING SYNC END LOGIC START DISPATCH. SIDE EFFECT SUCCESS LOGIC END WHEN COMPLETE TRIGGERED

// second ss render to string WILL MOUNT TRIGGER RENDER. data state is: List [ Map {} ] //data is finally here RENDER TO STRING 2-ND SYNC END // no need for logic call in ssr (could be resolved with a condition) LOGIC START DISPATCH. SIDE EFFECT SUCCESS LOGIC END

with this solution you can't escape the double render but it works with no code duplication.

yyynnn commented 5 years ago

So here is the working boilerplate https://github.com/yyynnn/redux-logic-ssr-boilerplate

You just do the first render to get the data, then you fire signal action to tell the server-side-redux-store, that you are on the second render, and inside logics you could get that value from the store to conditionally do logic and save some requests. Check the renderer.

renderer

therealgilles commented 3 years ago

@jeffbski: I know this is an old thread. Wondering if you had thoughts on the double-render. Does it sound necessary in all situations when using redux-logic, or only for specific ones?

I am also trying to figure out what needs to be waited on with whenComplete(). If I use redux-logic for API calls and websockets, it does not seem any of that logic would get activated during server side rendering and therefore would not need to be waited on (unless the API call needs to happen before the render). Now for something related to authentication, I am not sure.

therealgilles commented 2 years ago

@jeffbski: Any thoughts on this and how would this work with React 18 renderToPipeableStream?