Ben
Ben

Reputation: 16524

redux-observable multiple actions after 1st dispatch

I'm the 'registerEpic' utility described here: Is it an efficient practice to add new epics lazily inside react-router onEnter hooks?

Our code needs to be isomoprhic, but on the server side, an action is triggered the 1st time and all is well. However the 2nd time the action is triggered, the epic seems to get 2 copies of that action. Here's my code:

export const fetchEpic = (action$, store) =>
  action$.ofType("FETCH")
    .do((action) => console.log('doing fetch', action.payload))
    .mergeMap(({meta:{type}, payload:[url, options = {}]}) => {

            let defaultHeaders = {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            };

            options.headers = {...defaultHeaders, ...options.headers};

            let request = {
                url,
                method: 'GET',
                responseType: 'json',
                ...options
            };

            //AjaxObservables are cancellable... that's why we use them instead of fetch.  Promises can't be cancelled.

            return AjaxObservable.create(request)
                .takeUntil(action$.ofType(`${type}_CANCEL`))

                .map(({response: payload}) => ({type, payload}))
                .catch(({xhr:{response: payload}}) => (Observable.of({type, payload, error: true})));

        }
    );

 registerEpic(fetchEpic);

so the 1st time I hit a page which triggers this action (server side) everything works fine and i get the 'doing fetch' once in the console.

However, refreshing the page, yields 2 of those console messages and the resulting actions are not triggered.

I added a 'clear' function to my epic registry, but maybe I'm total noob sauce and just don't grok it fully. Here's my middleware:

let epicRegistry = [];
let mw = null;
let epic$ = null;
export const registerEpic = (epic) => {
    // don't add an epic that is already registered/running
    if (epicRegistry.indexOf(epic) === -1) {
        epicRegistry.push(epic);

        if (epic$ !== null) { //this prevents the observable from being used before the store is created.
            epic$.next(epic);
        }
    }
};

export const unregisterEpic =(epic) => {
    const index = epicRegistry.indexOf(epic);
    if(index >= 0) {
        epicRegistry.splice(index, 1);
    }
}

export const clear = () => {
    epic$.complete();
    epic$ = new BehaviorSubject(combineEpics(...epicRegistry));

}

export default () => {

    if (mw === null) {
        epic$ = new BehaviorSubject(combineEpics(...epicRegistry));
        const rootEpic = (action$, store) =>
            epic$.mergeMap(epic => epic(action$, store));

        mw = createEpicMiddleware(rootEpic);
    }

    return mw;
};

Upvotes: 4

Views: 1408

Answers (1)

jayphelps
jayphelps

Reputation: 15401

I don't see a epicMiddleware.replaceEpic anywhere? If you do not replace the currently running epic inside the middleware, it's really tough to truly guarantee that it's not taking actions--which sounds exactly like what is happening here.

Doing serverside rendering with redux-observable (or any async middleware, including redux-saga) is tough because you have to create some convention. e.g. when your app boots, if an epic starts some side effect, when do you say "that's enough" and prematurely cut it off? Do you let them do anything async at all? How do you properly guarantee an epic truly ends all its side effects? It's possible for the middleware to stop subscribing to an epic but it doesn't properly chain its subscribers so continues some side effect anyway.

This is why we have not yet documented how to do SSR in redux-observable, because while we have created code that "does it", we're not confident in the choices.

How ever you decide to handle it, it's very likely you want to eventually call epicMiddleware.replaceEpic(nextRootEpic) to have the middleware stop subscribing to the previous root epic.

If you don't want to let epics do anything at all async during a page render serverside, you would render the page synchronously, then immediately after replace the root epic with the same root epic again. That is, you almost certainly will pass the exact same root epic again, which will effectively "restart" your tree of epics--but again, no guarantees, if you wrote a misbehaving epic.

I'm happy to help further elaborate, if you want to post your actual SSR code, e.g. if you use React and react-router, it would be all your match({ routes, location: req.url } ...etc) stuff.


It's worth noting that many people have the opinion that SSR should not do any side effects, and leave them up to the client. So no AJAX calls, in the redux-observable case with something like React, that means don't dispatch actions that cause side effects until componentDidMount (which isn't run serverside). This is mostly because in many ways this would defeat the possible performance benefits of SSR because you aren't sending the initial HTML as soon as possible, letting the JS come along side. However, what is commonly not mentioned is that if you don't pull any sort of dynamic data, your app will clearly not have as much SEO benefit. This is a trade off you have to decide, or possibly use a mix of both.

Upvotes: 1

Related Questions