I am trying React SSR using Redux and Redux-saga.
I am able to get the Client Rendering to work but the server store never seems to get the data / wait for the data before rendering the HTML.
server.js
import express from 'express';
import renderer from "./helpers/renderer";
import createStore from "./helpers/createStore";
import {matchRoutes} from 'react-router-config';
import Routes from "./client/Routes";
import rootSaga from "./client/saga";
const app = express();
app.use(express.static('public'));
app.get('*', (req, res) => {
const store = createStore();
const branch = matchRoutes(Routes, req.path);
const promises = branch.map(({ route }) => {
return route.loadData ? route.loadData(store) : Promise.resolve(null);
});
store.runSaga(rootSaga).done.then(() => {
res.send(renderer(req, store)) // helper method to load static router and provider
}) // Not required after Update 2 , Promise.All should work ..
});
app.listen(3000, () => {
console.log('Listening on Port 3000');
})
small saga
import { put, takeEvery, all, call } from 'redux-saga/effects'
import {FETCH_USERS, SAVE_USERS} from "../actions";
import axios from 'axios';
export function* fetchUsers() {
const res = yield call(getUsers);
yield put({ type: SAVE_USERS, payload: res.data});
}
const getUsers = () => {
const response = axios.get('http://react-ssr-api.herokuapp.com/users');
return response;
}
export function* actionWatcher() {
yield takeEvery(FETCH_USERS, fetchUsers)
}
export default function* rootSaga() {
yield all([
actionWatcher()
])
}
error
TypeError: Cannot read property 'then' of undefined at /Users/rahsinwb/Documents/Mine/code/SSR/build/bundle.js:309:39
What am I missing here? Or is there any proven way we can listen to the saga ending? I was thinking my loadData helper in the component with store.dispatch
for initial action call will return promise but that never works.
LoadData
const loadData = (store) => {
store.dispatch({type: FETCH_USERS});
}
Update
Added this to my index.js File
store.runSaga(rootSaga).toPromise().then(() => {
branch.map(({ route }) => {
return route.loadData ? route.loadData(store) : Promise.resolve(null);
});
res.send(renderer(req, store))
})
Now the code compiles without any error but the problem is the saga dispatches an action from loadData method i.e. FetchData, before the fetchData calls the Save_DATA action in the saga mentioned above the call gets cancelled my assumption, and the reducer is left empty which causes the html to not load with data.
How do we know that the data load is completed for the generator function starting from the action initiated to the action for loading data ? to the reducer as only post that i should render component on the server
Update2
Also had checked a lib, but for some odd reason it throws regenerator issues but i do-not think a lib is need for such a purpose as i plan on reusing the same saga's for client Redux calls to and want to keep changes separated out . index.js
console.log(promises); // no promise returned undefined
//setTimeout((function() {res.send(renderer(req, store))}), 2000);
Promise.all(promises).then(
res.send(renderer(req, store))
)
Adding Set Timeout helps and i am able to fetch data in server and then render so now the CRUX of the problem is that we need to some how wait for the generator function to resolve then call render.
Update 3
Added a way around this problem as an answer but i do-not feel it is the best solve and can be abstract this logic out
TypeError: Cannot read property 'then' of undefined at
.done
getter is deprecated since v1, so .toPromise()
is the way to listen for a saga resolve
As for waiting sagas to complete, there is a special END
action that you can dispatch to terminate saga when all its child tasks are complete, causing store.runSaga(rootSaga).toPromise()
promise to resolve.
When the promise is resolved (meaning that all required data is already there), you should send result HTML received from renderToString
in response. Notice that server-rendered components aren't really mounted, so componentDidMount()
method won't be triggered, thus your data won't be fetched twice.
So you can modify your code as follows:
// server.js
...
import { END } from 'redux-saga';
app.get('*', (req, res) => {
const store = createStore();
const branch = matchRoutes(Routes, req.path);
store.runSaga(rootSaga).toPromise().then(() => {
res.send(renderer(req, store))
});
// trigger data fetching
branch.forEach(({ route }) => {
route.loadData && route.loadData(store)
});
// terminate saga
store.dispatch(END);
});
Also, you can follow this example.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With