问题
I am implementing server-side rendering using redux-saga.
I am following the "real world" example provided in the redux-saga repository.
- node.js entry-point uses react.js
renderToString
to render the application. - rendering the application triggers
componentWillMount
, which dispatches actionsGET_GEOLOCATION
andGET_DATE
. These async actions will resolve withSET_GEOLOCATION
andSET_DATE
. renderToString
finishes rendering the application;END
action terminates the saga listeners
The problem is that SET_GEOLOCATION
and SET_DATE
themselves are used to put
a new action GET_MOVIES
. However, by the time the SET_GEOLOCATION
and SET_DATE
are called, the saga listeners are no longer active (we terminated it after renderToString
). Therefore, while GET_MOVIES
will be dispatched, the GET_MOVIES
action will not be picked and SET_MOVIE
will never happen.
Server code:
app.get('*', (req, res) => {
const history = createMemoryHistory({
initialEntries: [
req.url
]
});
const store = configureStore(undefined, history);
const context = {};
const rootComponent = <Provider store={store}>
<StaticRouter context={context} location={req.url}>
<Route component={RootRoute} />
</StaticRouter>
</Provider>;
store
.runSaga(rootSaga).done
.then(() => {
const body = renderToString(rootComponent);
const response = renderHtml(body, store);
res
.send(response);
})
.catch((error) => {
res
.status(500)
.send(error.message);
});
// Force componentWillMount to issue saga effects.
renderToString(rootComponent);
store.close();
});
Sagas:
const watchNewSearchCriteria = function *(): Generator<*, *, *> {
yield takeLatest([
SET_GEOLOCATION,
SET_DATE
], function *() {
const {
coordinates,
date
} = yield select((state) => {
return {
coordinates: state.movieEventsView.location ? state.movieEventsView.location.coordinates : null,
date: state.movieEventsView.date
};
});
if (!coordinates || !date) {
return;
}
yield put(getMovies({
coordinates,
date
}));
});
};
const watchGetMovies = function *() {
yield takeLatest(GET_MOVIES, function *(action) {
const result = yield call(getMovies, action.payload);
yield put(setMovies(result));
});
};
How to delay store.close
until after there are no sagas that are in the state other than take
?
回答1:
How to delay
store.close
until after there are no sagas that are in the state other thantake
?
To answer my own question, I need to observe resolution of anything thats been put. I can do this using the Saga Monitor.
Saga Monitor can be configured at the time of creating the redux-saga
middleware. For our use case, it needs to track whenever an action has been put
and remove it from the index when it has been resolved/ rejected/ cancelled.
const activeEffectIds = [];
const watchEffectEnd = (effectId) => {
const effectIndex = activeEffectIds.indexOf(effectId);
if (effectIndex !== -1) {
activeEffectIds.splice(effectIndex, 1);
}
};
const sagaMiddleware = createSagaMiddleware({
sagaMonitor: {
effectCancelled: watchEffectEnd,
effectRejected: watchEffectEnd,
effectResolved: watchEffectEnd,
effectTriggered: (event) => {
if (event.effect.CALL) {
activeEffectIds.push(event.effectId);
}
}
}
});
We need to access this from the consumer of the store, therefore I assign activeEffectIds
to the store instance:
store.runSaga = sagaMiddleware.run;
store.close = () => {
store.dispatch(END);
};
store.activeEffectIds = activeEffectIds;
Then instead of synchronously stopping the saga...
renderToString(rootComponent);
store.close();
we need to delay store.close
until store.activeEffectIds.length
is 0.
const realDone = () => {
setImmediate(() => {
if (store.activeEffectIds.length) {
realDone();
} else {
store.close();
}
});
};
// Force componentWillMount to issue saga effects.
renderToString(rootComponent);
realDone();
Now store.close
is called only when all the asynchronous effects are resolved.
来源:https://stackoverflow.com/questions/43834756/how-to-handle-multiple-interdependent-sagas-when-rendering-server-side