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

SSR whenComplete not helping #139

Open
yyynnn opened this issue Jan 9, 2019 · 10 comments
Open

SSR whenComplete not helping #139

yyynnn opened this issue Jan 9, 2019 · 10 comments
Labels

Comments

@yyynnn
Copy link

yyynnn commented Jan 9, 2019

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
Copy link
Owner

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
Copy link
Owner

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
Copy link
Author

yyynnn commented Feb 6, 2019

@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
Copy link

kshepelev commented Feb 11, 2019

I have this problem too

@yyynnn
Copy link
Author

yyynnn commented Feb 21, 2019

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

@jeffbski
Copy link
Owner

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
Copy link
Author

yyynnn commented Feb 25, 2019

@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):

  • firtst with async calls to get data, prerender store
  • second to actually get async data as initial data from prerendered store.

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
Copy link
Author

yyynnn commented Apr 19, 2019

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
Copy link

therealgilles commented Jan 6, 2021

@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
Copy link

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

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

No branches or pull requests

4 participants