I finished coding a React app created using the create-react-app (CSR) but I'm now rewriting this entire app using the Next.js framework for better SEO performance.
While rewriting it I had some hard times figuring out how to properly deal with redux and redux-saga to do the fetching and storing data process. The main reason to use Next.js in this project is to make use of the getInitialProps method, to fetch the data on the server-side before the first page load happens. But for some reason, I'm not able to "await" for the Redux dispatch to complete and get the fetched data on time.
So what ends up happening is that I dispatch the action to fetch the data, but it doesn't get stored in the redux store on time during the initial server-side page load. But when I change routes using next/link the data comes in, but only on client-side, after the server-side rendering happened.
So it kinda defeats the purpose of using the Next.js.
This new code is very similar to the create-react-app project, with some minor changes to fit the Next.js project requirements.
Here is my code.
./pages/_app.js
import App from 'next/app';
import { Provider } from 'react-redux';
import withRedux from 'next-redux-wrapper';
import withReduxSaga from 'next-redux-saga';
import makeStore from '../store/index';
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
const pageProps = Component.getInitialProps
? await Component.getInitialProps(ctx)
: {};
return { pageProps };
}
render() {
const { Component, pageProps, store } = this.props;
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
}
}
export default withRedux(makeStore)(MyApp);
./pages/index.jsx:
import React from 'react';
import { useRouter } from 'next/router';
import Layout from '../components/Layout';
import SubNavBarCategories from '../components/Pages/Home/SubNavBarCategory';
import * as BlogActions from '../store/actions/blog/categories';
const Blog = (props) => {
const router = useRouter();
const {
blogCategories,
} = props;
return (
<Layout>
<SubNavBarCategories blogCategories={blogCategories} />
</Layout>
);
};
Blog.getInitialProps = async ({ isServer, store }) => {
await store.execSagaTasks(isServer, (dispatch) => {
dispatch(BlogActions.getRecentCategories(5));
});
console.log('await store:', await store.getState().blog.blogCategories);
//output: await store: { data: [], loading: true, fetched: false, error: false }
//expected something like this:
// await store: { data: ['test1', 'category', 'crypto', 'test4', 'Day trade'] loading: false, fetched: true, error: false }
return {
blogCategories: await store.getState().blog.blogCategories,
};
};
export default Blog;
./store/index.js
import {
createStore,
applyMiddleware,
compose,
} from 'redux';
import createSagaMiddleware, { END } from 'redux-saga';
import rootReducer from './reducers';
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
const makeStore = (initialState) => {
const composeEnhancers = (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose;
const store = createStore(
rootReducer,
initialState,
compose(
composeEnhancers(applyMiddleware(sagaMiddleware)),
),
);
store.runSaga = () => {
if (store.saga) {
return;
}
store.sagaTask = sagaMiddleware.run(rootSaga);
};
store.stopSaga = async () => {
if (!store.saga) {
return;
}
store.dispatch(END);
await store.saga.done;
store.saga = null;
};
store.execSagaTasks = async (isServer, tasks) => {
store.runSaga();
tasks(store.dispatch);
await store.stopSaga();
if (!isServer) {
store.runSaga();
}
};
store.runSaga();
return store;
};
export default makeStore;
./store/actions/blog/blog.js
export function getRecentCategories(number) {
return {
type: 'REQUEST_RECENT_CATEGORIES',
payload: {
number,
},
};
}
./store/reducers/blog/blog.js
import update from 'immutability-helper';
const initialState = {
blogCategories: {
data: [],
loading: false,
fetched: false,
error: false,
},
};
export default function blog(state = initialState, action) {
switch (action.type) {
case 'REQUEST_RECENT_CATEGORIES':
return update(state, {
blogCategories: {
loading: { $set: true },
},
});
case 'SUCCESS_RECENT_CATEGORIES':
console.log('actions:', action.payload.data);
//output: actions: blogCategories [ 'test1', 'category', 'crypto', 'test4', 'Day trade' ]
return update(state, {
blogCategories: {
data: { $set: action.payload.data },
loading: { $set: false },
fetched: { $set: true },
error: { $set: false },
},
});
case 'FAILURE_RECENT_CATEGORIES':
return update(state, {
blogCategories: {
fetched: { $set: true },
error: { $set: true },
},
});
default:
return state;
}
}
./store/sagas/blog/getRecentCategories.js
import {
put,
call,
} from 'redux-saga/effects';
import 'isomorphic-fetch';
async function getRecentCategoriesApi(number) {
const res = await fetch(`http://localhost:5000/blog/get/categories/newest/${number}`, {
method: 'GET',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json();
return data;
}
export default function* asyncGetRecentCategoriesApi(action) {
try {
const response = yield call(getRecentCategoriesApi, action.payload.number);
yield put({ type: 'SUCCESS_RECENT_CATEGORIES', payload: { data: response } });
} catch (err) {
yield put({ type: 'FAILURE_RECENT_CATEGORIES' });
}
}
As you can see this app is a pretty ordinary react redux-saga app. Everything else is working as it should besides this getting data from the backend using redux-saga thing.
Is there any way to make getInitialProps method work with redux and redux-saga as intended?