Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do we return a Promise from a store.dispatch in Redux - saga so that we can wait for the resolve and then render in SSR?

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

like image 245
Rahul Singh Avatar asked Oct 15 '22 04:10

Rahul Singh


1 Answers

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.

like image 116
oozywaters Avatar answered Oct 26 '22 22:10

oozywaters