Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing SSR in large commercial project. #683

Open
KoderFPV opened this issue Oct 24, 2019 · 13 comments
Open

Implementing SSR in large commercial project. #683

KoderFPV opened this issue Oct 24, 2019 · 13 comments

Comments

@KoderFPV
Copy link

KoderFPV commented Oct 24, 2019

Hi guys,
We have quite large commerce app. We need to implement SSR however our whole communication is going through redux-observable.
Previous developer recommend to rewrite all epics to redux-saga. We have them more then 50.

What do you think, should we do this migration or we maybe we should develop own solution based on this PR?

Or maybe this solution would be still working?

I also found react-redux-epic library.
Guy here said 18 days ago that he is still using it and its working.

Our epics are in most cases are very simple. Mostly we do some ajax calls and fill store with data.
Sometimes we do some transformations or we dispatch other actions, but in general they are pretty straightforward.

All we need to do on server is to dispatch few actions (the same we have in componentDidMount) and wait for other few actions to be dispatched (success actions).

Maybe you know how to listen on action$ in server function?

What do you think we should start migration of 50 (or even more) epics to redux-saga or we should spend few days on getting working redux-observable event if ssr is not nativly supported?

I am using redux-observable for few years now but I don't feel strong enough to make decision at least now.

Btw redux-saga based on promise but I always though that streams are just more advanced promises. :)

Have a good day and thanks for any response :)

@jayphelps
Copy link
Member

Hey @Tarvald, I hear ya, but this probably isn't the right place to ask whether you should stop using redux-observable 😆

SSR is possible today with redux-observable, but indeed depending on exactly what SSR means for your use case, you have to provide some custom code yourself. That caveat has been one of the big reasons we've never shipped anything: I've seen several different opinions on how it should work, and in some cases they conflict and are incompatible with each other. e.g. whether you want to wait for epics to shut down gracefully, or force them to shutdown.

Whether we'll ever provide helpers around this is not clear, but there will likely be improvements to some things that will make it easier e.g. ability to have state$ complete(), which isn't possible today without rolling your own state$ Observable that you wrap ours with and provide to your root epic. There's no time frame on this though, unfortunately. I'm not currently doing SSR myself--but if anyone reading this knows what to do, they're welcome to PR if they want to get something in more quickly. Ultimately though, remember it is is still possible even without any changes in redux-observable_. It just isn't as easy as it could be.

Wish you and your team the best. 👍

@japrescott
Copy link

@jayphelps I have been reading all the PR (539 and 412) and the discussions and am very interested into this topic as well. My use-case is very easy but I haven't gotten SSI to work without modifying redux-observable itself as the PR requests suggest.

Since you say it is is still possible even without any changes in redux-observable, could you point me to a working example or into the right direction? That would be awesome

@aplassen
Copy link

aplassen commented Oct 26, 2019

@japrescott, @Tarvald

Look at this comment under #539 with a stackblitz example.

This pattern should provide a basic way to use redux-observable with SSR.

@japrescott
Copy link

@aplassen Thank you for your reply! I have been looking at the stackblitz implementation from jayphelps and have tried to adapt it into my project. But as far as far I can see, it provides only the possibility to stop further actions from being waited for and then resolves the promise after the already dispatched actions are "complete".

Maybe my usecase is not as simple as I assume, but my flow of action is 'get_data' -> rxjs.ajax -> 'data_done'. And I expect to know when 'data_done' has been called.
My epic looks like this;

export const fetchData: Epic<AnyAction> = (action$) => action$.pipe(
    ofAction(actions.fetchData.started),
    mergeMap((param) =>
        ajax.getJSON(`https://somewhereovertherainbow.com`).pipe(
            map(data => {
                return actions.fetchData.done({
                    params: param.payload,
                    result: { data },
                })
            }),
            catchError(error =>
                Observable.of(actions.fetchData.failed({
                    params: param.payload,
                    error: error,
                })),
            ),
        ),
    ),
)

@KoderFPV KoderFPV reopened this Oct 28, 2019
@aplassen
Copy link

@japrescott , so if I understand it correctly, you have another epic that is listening for the action "actions.fetchData.done"?

@KoderFPV
Copy link
Author

KoderFPV commented Oct 28, 2019

Hi Guys
I am not sure but I just implemented it and it works fine.
It "probably" waits for all ajax to be completed. I am going to continue testing and I let you know ;)

However It is only works one time.
On my next re render shutdownEpics() resolving without waiting for actions.
Do I have to re init whole redux store object each time when shutdownEpics resolve?

@aplassen
Copy link

@Tarvald, on the second render-pass, no epics should be listening. For this method to work, whatever the epics need to do, they need to do it before the second render-pass.

In the example shutdownEpics() is triggered when all epics are done doing work in progress.

@KoderFPV
Copy link
Author

@aplassen
Alright but my server need to provide initial state with some actions dispatch for every call to server.

I think this problem is related to our application structure, we are creating store in runtime so its not reinit on call to server.

However I am not sure if I should recreate store from scratch each time even if every user need init store it does not mean he / she needs brand new store object.

So It would be great to reset above approach on demand :)

But so far so good!

@japrescott
Copy link

@aplassen thank you for your reply. I noticed that in my setup the shutdown$ subject was declared outside my store which caused it to resolve early. Everything works now. thank you very much!

@Tarvald you will need to dispatch your actions manually for each route and cant rely on useEffect hook

@KoderFPV
Copy link
Author

@japrescott
Yes you are right.
When I put action in componentWillUnmount it was dispatched after app rendered.
We were going to take care of each route on SSR server, we don't have hooks and our all calls are placed on componentDidMount

@aplassen
Copy link

aplassen commented Oct 28, 2019

@japrescott, cool that you got it to work 👍

@Tarvald

However I am not sure if I should recreate store from scratch each time even if every user need init store it does not mean he / she needs brand new store object.

Be wary of memory leaks and "data leaks" between requests. It is important that each request get their own independent copy.

What you can do is this:

  • initiate a standard state on the server that does not change between requests
  • on each request you hydrate the request store with a deep copy of this state when you configure your store with createStore

@aplassen
Copy link

aplassen commented Oct 28, 2019

Also, to address the concerns that @jayphelps had relating to the risk of epics not actually completing, leaving the request "hanging":

It's also possible for some epic to never terminate, so I'd highly advise adding a timeout.

One could wrap the epics that are expected to run on the server with a takeUntil-trap that triggers once the application dispatches an APP_SHUTDOWN_IN_PROGRESS (or something like that) right after the shutdown() is called. Then, the epic has some amount of time ( UNIVERSAL_EPIC_TIMEOUT in the example) before the universalEpic unsubscribes from the epic being wrapped.

const myUniversalEpic = universalEpic(($action) => (
  $action
    .pipe(
      ofType('PING'),
      switchMap(() => (
        of({ type: 'PONG' })
          .pipe(
            delay(120 * 1000), // running way too long
          )
      )),
    )
));

What universalEpic might look like:

import { ofType } from 'redux-observable';
import { of } from 'rxjs';
import { delay, takeUntil, tap, catchError } from 'rxjs/operators';

const UNIVERSAL_EPIC_TIMEOUT = 10 * 1000;

function universalEpic(epic) {
  function _epic(action$, ...args) {
    return epic(action$, ...args)
      .pipe(
        catchError((error) => (
          of({ type: 'APP_EPIC_UNCAUGHT_ERROR', error })
        )),
        takeUntil(
          action$
            .pipe(
              ofType('APP_SHUTDOWN_IN_PROGRESS'),
              delay(UNIVERSAL_EPIC_TIMEOUT),
              map(() => ({ type: 'APP_SHUTDOWN_IN_PROGRESS_END' })),
              tap(() => {
                console.error(`SSR: Epic "${epic.name || '<anonymous>'}" timed out, pulling the plug!`);
              }),
            ),
        ),
      );
  }

  // Enable us to only include universal epics on serverSide
  _epic.isUniversalEpic = true;

  return _epic;
}

export default universalEpic;

@KoderFPV
Copy link
Author

KoderFPV commented Jan 15, 2020

Hi Again!
We are currently using below setup for iyr store.

export function configureStore() {
const shutdown$ = new Subject();
const epicMiddleware = createEpicMiddleware({
dependencies: services,
});

const serverRootEpic: Epic<Action, Action, void, any> = (action$, state$, deps) => {
    const output$ = rootEpic()(action$.pipe(takeUntil(shutdown$)), state$.pipe(takeUntil(shutdown$)), deps);
    return output$.pipe(
        finalize(() => {
            shutdown$.complete();
        })
    );
};

// Grab the state from a global variable injected into the server-generated HTML
const preloadedState = window.__PRELOADED_STATE__;
// Allow the passed state to be garbage-collected
delete window.__PRELOADED_STATE__;

// Configure middlewares
const middlewares = [epicMiddleware, routerMiddleware(getHistory()), createSentryMiddleware(), logger];
// Compose enhancers
const enhancer = composeEnhancers(applyMiddleware(...middlewares));

// Create store, enhancer is the 3rd argument which is related with epics, do we need them?
store = createStore(rootReducer(), preloadedState, enhancer);

window.state = store.getState;

epicMiddleware.run(serverRootEpic);

return {
    store,
    shutdownEpics() {
        shutdown$.next();
        return shutdown$.toPromise();
    },
};

}

It was working flawlessly since we had to implement the last route to be fully rendered on the server. (seriously!)

On the server, we are doing two renders, by launching renderToString method twice.
First to launch all async actions from constructors, and second after shutdownEpics() will be resolved.

But we have one case where async action can reveal another constructor with extra async action that does not count during the first render.

Do you know how to deal with it?
I really looking forward to your opinion!

Best regards

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants