HenrikJoreteg / redux-bundler

Compose a Redux store out of smaller bundles of functionality.
https://reduxbundler.com
583 stars 46 forks source link

Would redux-bundler be capable of handling SSR? #6

Closed acstll closed 6 years ago

acstll commented 6 years ago

Thank you @HenrikJoreteg for getting me into Redux again :-)

The recommended way of doing server-side rendering in Redux's docs is having a static method on the "page" component, ala getInitialProps in next.js or other tools (https://github.com/c8r/x0). But this approach reminds me of this:

Many developers, if using react will use component life-cycle methods like componentDidMount to trigger data fetches required by that component. But this sucks for many reasons

Since redux-bundler is handling the routing. Would it be possible to use the store also for data fetching in the server too? Like, on every request instantiate the store, doUpdateUrl, maybe call another action and then await for data before passing the store to the Provider for rendering. The problem I'm facing is how do I know the data is ready, one would need either a specific event to listen to (non-existing in Redux) or a promise returning from somewhere… right?

Have you thought of a way of using redux-bundler to do SSR?

HenrikJoreteg commented 6 years ago

Hey @acstll. So in terms of the routing you can pass {url: '/somepathname'} as part of your initial state on the server-side and it will handle things.

But, you have to expose URL as a global because it expects to have that available.

You could do this in node:

// essentially polyfill URL for node
global.URL = require('url').URL

// grab what we need and create a simple store
const { composeBundles } = require('redux-bundler')
const createStore = composeBundles()

// start out with a URL as initial state
const store  = createStore({url: 'https://example.com/some/page'})

// now we have our store with selectors so we can grab the url
console.log(store.selectPathname()) // logs: some/page

In terms of knowing when asynchronous things are done fetching, that's a bit trickier. But on the flipside, all of this information is known and centralized on the store, rather than sprinkled throughout components.

Theres a simple convention included for keeping track of how many outstanding asynchronous actions are in progress. It's quite simple, it assumes that your asynchronous actions are named ending with _START, _SUCCESS, and _ERROR. It then simply tracks if there are any unfinished async actions.

The reactors bundle (the one that implements the ability to write selectors that cause other actions) takes an option for a doneCallback. This is triggered when there are no more "reactions" to run and store.selectAsyncActive() comes back false. This is not enabled by default in composeBundles. But if you instead use composeBundlesRaw() which simply doesn't include any of the "standard" bundles, then you have an opportunity to pass a doneCallback that you could wait for., then render the response. You can find this here

So, I haven't done this recently... but if I were doing SSR with redux-bundler I'd probably do something like this. Please note I haven't actually ran this code, so it could be a mess :)

Instead of turning your bundle index into a createStore function you could have it return a promise that resolves for the doneCallback.

/bundles/index.js

export default (initialUrl) => new Promise((resolve) => {
  let store
  const bundles = [
    appTime,
    asyncCount,
    online,
    url(),
    reactors({doneCallback: () => {
      resolve(store)
    }}),
    debug,
    // whatever other ones
    ...
  ]

  const createStore = compose(...bundles)

  store = createStore({url: initialUrl})
})

some-express-js-handler.js

const getStorePromise = require('/bundles/index')

app.get('*', (req, res) => {
   getStorePromise(req.url)
     .then(store => {
       res.send(renderToString(rootComponent(store))
     })
})

Anyway, I don't love SSR just because it has a way of making a mess of things and can sometimes be more trouble when it's worth when my total bundle size is <30kb anyway. But anyway, it's certainly possible. Another approach, that I find is often under appreciated is that many times people implement full SSR with async fetching on the server-side when they're really just trying to grab like one API call. There's nothing saying you can't also make that API call outside of redux and then use it as initial data. /me shrugs.

I should probably try to throw together a working SSR demo at some point...

acstll commented 6 years ago

Thanks @HenrikJoreteg for the detailed explanation. composeBundlesRaw was the piece I was missing. I will try this as soon as I get a chance.

Anyway, I don't love SSR just because it has a way of making a mess of things and can sometimes be more trouble when it's worth when my total bundle size is <30kb anyway.

I can totally relate to this. Where I work now we need SSR mainly because SEO. It's a bit silly IMHO to be building SPAs for "plain old" kind of websites that only display some content, but it's what we do these days, haha /me shrugs too.

I'm trying to develop a setup (àla Sapper), something with svelte and along these lines:

Older server centric application patterns show up again but with a new spin. The pendulum could start to swinging away from strick SPA applications. People will begin to pull back on the complexity of single page applications and return to things like pjax…

(From @codylindley here https://frontendmasters.gitbooks.io/front-end-developer-handbook-2018/content/2018.html)

If now we do SPAs and add SSR on top, the idea is to flip this approach and start building "for the server" and get the SPA for free, if you need it.

One of the things I like the most about redux-bundler is how simple and clean it is to "connect" the views. Just as declarative as something like GraphQL, I guess, but simpler.

If I can get something working and worth sharing I'll post it here. Thanks again!

HenrikJoreteg commented 6 years ago

If I can get something working and worth sharing I'll post it here. Thanks again!

Awesome! Would love that.