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
How to hydrate the store on first SSR page load with sagas without using getInitialProps #336
Comments
Yup, there is simply no way, because there shall not be This is by design of Next.js as far as I understood. There is a need of lifecycle methods within _app.js (not getInitialProps). EDIT: Actually there is a way, will follow up asap |
@mercteil any findings? |
You can use per-page approach. W/o wrapping the |
I also has this question. Same stack NextJS + Redux + Saga, per page approach. saga
page
UserPage div content is always empty :/ If I console log the actions to the reducer, I can see the data in the reducer (on SSR) but after a second gets undefined: Log for a user page load:
Hydrate
Maybe I am misinterpreting some part of it? Thank you |
EDIT: next-redux-wrapper v7.xx One will have to curry the store to make it work with v7. Meaning everywhere you call using next-redux-wrapper 6.xx Sorry for the late reply. Totally forgot. There is a relatively simple way to solve the problem by composing getServerSideProps and use them across all pages where needed. First of all my actual problem was the initial hydration of the store only when the user requests the initial application load. All client side navigation do not have to process the same action every time. And of course I need this data prior the initial page load, meaning if the request has i.e. a token the user should be loaded on the server and hydrated/served to the client initially. My solution:
Lets say you have an application which would only use getServerSideProps across all the pages except some static pages without state like 404.js or 500.js error pages you can generate a general Wrapper for the gSSPs and compose/pipe logic on every request. Therefore I created this wrapper which lets me compose any amount of separated gSSP functions. The wrapper at the end receives the original gSSP per page and waits for the context which is provided on page load/navigation. import { gSSPWithRedux } from '@client/store';
import { compose, curry } from 'lodash/fp';
import gSSPWithCSP from '../gSSPWithCSP';
import gSSPWithCSRFlag from '../gSSPWithCSRFlag';
import gSSPWithReduxSaga from '../gSSPWithReduxSaga';
import gSSPWithSSRProps from '../gSSPWithSSRProps';
const gSSPWrapper = curry((gSSP, context) => {
if (gSSP && context) {
// !!IMPORTANT!! The order of execution after providing gSSP is top down
// which means gSSPWithRedux is executed first and decorates the context with the store
// => order of execution is crucial!!!
const wrappedGSSPs = compose(
gSSPWithRedux,
gSSPWithCSRFlag,
gSSPWithReduxSaga,
.... any other function
)(gSSP);
return Promise.resolve(wrappedGSSPs(context));
}
throw Error('Either context or gSSP is not provided');
});
export default gSSPWrapper; The with redux wrapper is basically this package: import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware, { END } from 'redux-saga';
import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';
import { createWrapper } from 'next-redux-wrapper';
...
const bindMiddleware = (middlewares) => {
if (isDev) {
return composeWithDevTools(applyMiddleware(...middlewares));
}
return applyMiddleware(...middlewares);
};
const initStore = () => {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, bindMiddleware([sagaMiddleware]));
store.execTasks = (tasks = []) => {
return ((isArray(tasks) && tasks) || [tasks]).filter(Boolean).forEach(store.dispatch);
};
store.stopSagas = async () => {
if (store.currentSagas) {
store.dispatch(END);
await store.currentSagas.toPromise();
}
};
store.runSagas = () => {
if (!store.currentSagas) {
store.currentSagas = sagaMiddleware.run(rootSaga);
}
};
store.runSagas();
return store;
};
export const wrapper = createWrapper(initStore, { debug: false });
export const gSSPWithRedux = wrapper.getServerSideProps; The withCSR function decorates the context with a flag that indicates whether the client is navigating or whether it is the initial application load. This step is curcial for the further logic to work and also a pretty nice feature to separate logic which is not needed for client navigation. You can read about it here (also copied from there): vercel/next.js#13910 CSR decodes to Client Side Request (or Client Side Navigation with an R) import { curry } from 'lodash/fp';
const gSSPWithCSRFlag = curry((gSSP, context) => {
if (gSSP && context) {
// Check and set a flag to context that checks if the request
// is coming from client side routing/navigation
context.isCSR = Boolean(context.req.url.startsWith('/_next'));
return Promise.resolve(gSSP(context));
}
throw Error('Either context or gSSP is not provided');
});
export default gSSPWithCSRFlag; And finally the solution of the initial problem: const gSSPWithReduxSaga = curry((gSSP, context) => {
if (gSSP && context?.store) {
// IMPORTANT!!
// store should already be within context, watch out for the order of gSSP wrapping
// means next-redux-wrapper puts the store in context and should do it prior this HoC
// !!IMPORTANT!!
// In order to execute server side logic on page load/request
// you will have to fire sagas every time the server is requested
// (prior hydration) on the server side.
// This does not work with getStaticProps because there is no req object in context
// which means there is no token etc. provided (logically since not known prior app build)
// Step 1: Execute server side sagas whenever a req is incoming, store is created and root sagas are started
// Step 2: Every app component has to implement getServerSideProps (store is provided as an argument by next-redux-wrapper)
// Step 3: When you need to execute sagas for a page do it within getServerSideProps with store.execTasks(...)
// Step 4: !!!Always!!! stop the sagas in getServerSideProps by await store.stopSagas(). Sagas are stopped here.
// Generally if you do not need to interact with the store (i.e. store.getState() in gSSP)
// you can let the wrapper stop them here. Otherwise do it manually in gSSP prior store interaction.
if (!context.isCSR) {
// isCSR indicates whether it is a consequent client req or an initial server req
// HERE
// THE INITIAL PROBLEM OF THIS THREAD IS SOLVED BY EXECUTING GENERAL SSR LOGIC ONCE
// HERE
context.store.execTasks([...)]);
}
return Promise.resolve(gSSP(context)).then(async (pipedProps) => {
await context.store.stopSagas();
return pipedProps;
});
}
throw Error('Either context or gSSP is not provided');
});
export default gSSPWithReduxSaga; Everything else is super easy. You write some page specific gSSP function and let us say some gSSP_for_SEO which works with the props to prepare SEO and at the end you wrap it with the wrapper described above. export const getServerSideProps = gSSPWrapper(withGSSPSEOData(async ({locale, req, res, store, isCSR,...}) => {
...
})); And do not forget to hydrate the store... import { createReducer } from 'redux-act';
import { HYDRATE } from 'next-redux-wrapper';
export default createReducer(
{
[some action from redux]: (state, payload) => ({}),
[some other action from redux]: (state) => ({ ...state}),
[HYDRATE]: (state, payload) => {
// compare state and payload and decide what goes inside the store
},
},
initialState
); |
There was no activity in this issue for quite a long time, closing. If you feel it needs to be reopened, please reach out to me. |
What I look forward to is to get rid of
getInitialProps
in_app.js
in order to remove my custom server and use static opt.There is one single problem left for me to deal with and I am kind of stuck. I use Next + Redux + Saga. Usually what I was doing is to compose getInitialProps from pages to _app.js and then render everything as it was normal before the fancy getServer and getStaticProps. I now figured everything out to get rid of getInitialProps in _app but here is the problem:
_app.js
withReduxSaga HoC
Ultimately the question is:
How is it possible to hydrate the store within SSR on the first page load without using getInitialProps in the _app.js?
The text was updated successfully, but these errors were encountered: