Skip to content

Notes from Experimenting with SSR

Charles ⚡ edited this page Oct 22, 2018 · 1 revision

Experiments

Login is properly setting the token

The following need to reliably retrieve the token

  • index
  • edit
  • new

using the universal-cookies-middleware neatly attaches the token to the header but i can't nest getInitialProps

adding the token to the query helped

Outstanding Issues Dec 25

on the other machine it's working sort of.

  • need to pass down token, user, id as needed
  • could a HOC actually help with this?
  • what about nested getInitialProps?

Outstanding Issues Dec 26

  • Router.push('/', { query: token }) isn't working right now on successful login...
  • Linking on the client side is showing error before content loads. this seems relatively easy to fix. - Ref: Getting strange 404 before page loads

https://github.com/luisrudge/next.js-auth0 https://github.com/estrada9166/server-authentication-next.js https://github.com/trandainhan/next.js-example-authentication-with-jwt https://github.com/zeit/next.js/issues/2208


Reverting Back to CRA

TODO:

I should really really curate the smarter changes from the Next.js rebuild. Off the top of my head that's this stuff.

  1. collect CSS from next.js branch
  2. grab fixes for empty title in new post
  3. the state updating clean up in edit
  4. use Bolt instead of Lerna

Validate the Token Client-Side

In an instance of componentWillMount fetch a validation endpoint. If it doesn't respond with an error, set the state of off to be true. On the server, with the validation endpoint, use the pre method. If the pre method fails, it should error out on its own, and the controller can reply with whatever.

The non-optimal choice is to decode the token on the client, and check the expiration time against the current time.

Either implementation needs to handle the case for when a fetch request errors out. The user needs feedback.

Or better option just handle the error


April 2018

Working through this with backpack I'm noticing that the modules in redownwrite aren't accepting babel transform. Wonder if taking it into the redownwrite repository would help. Spectrum does this by parsing them from the root of their monorepo.

Okay that worked.

Need to update client-side config to use new ReactLoadablePlugin({ filename: './dist/react-loadable' })

Map through routes next comes from express req, res, next

const active = routes.find(route => matchPath(req.url, route));
const requestInitialData = active.component.requestInitialData && activeRoute.component.requestInitialData();

Promise.resolve(requestInitialData).then(initialData => {
  const context = { initialData };
  const markup = renderToString(
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );
}).catch(next)

Needs to go in renderToString()

<script>window.__initialData__ = ${serialize(initialData)}</script>
  • Use CORS
  • Remove Loadable and start without it and iterate to it.
  • Use universal-cookie and pass that into the context. requestInitialData(context: { bearer token })
  • work through serialized data to pull from the staticContext
constructor(props) {
  super(props)

  let repos
  if (__isBrowser__) {
    repos = window.__INITIAL_DATA__
    delete window.__INITIAL_DATA__
  } else {
    repos = props.staticContext.data
  }
}

The other thing that could be done is the extend Component to a "Container" that preloads the static method() from the component and then renders the component or a HOC that can do something similar

FWIW, Suspense will fix this

Need some mechanism for handing initial state and resolving data-fetching static getInitialProps() to handle and resolve on the client when routes are transitioned to.

<RouteContainer /> This grabs a route array from a route config.

async function loadInitialProps(routes, pathname, ctx) {
  const promises = []
  const match = routes.find(route => {
    const match = matchPath(pathname, route)
    if (match && route.component && route.component.getInitialProps) {
      promises.push(
        route.component.load
          ? route.component
              .load() // load it as well
              .then(() => route.component.getInitialProps({ match, ...ctx }).catch(() => {}))
          : route.component.getInitialProps({ match, ...ctx }).catch(() => {})
      )
    }
    return match
  })
  return {
    match,
    data: (await Promise.all(promises))[0]
  }
}

class RouteContainer extends Component {}

Need to inject default state of auth, which we can check by getting the cookie. We can decode it, can pass everything into the constructor of the Container in unstated


Custom Webpack

So we're going to need somethings

  • We're going to need custom babel
  • We're going to need custom eslint reporting
  • We're going to need custom postcss (which we can add more to)
  • We're going to need custom jest setup
  • We're going to need an HMR express with dev server
  • We're going to need to update the service worker precache plugins
  • We're going to need to use the Offline Plugin
  • We're going to need to use React Loadable
  • We're going to need multiple entry points for server.js and one for client.js
  • We're going to need to proxy the end points
  • We're going to need integration testing (maybe this should be it's own workspace)

Should

  • keep all configs in package.json
  • try and scaffold out the components in redownwrite into UI package

Eventually:

  • We're going to need to handle GraphQL tags in the ES6

But if we go toward the GraphQL end of things we can experiment with another workspace and react scripts

Using an SSR approach would remove the need for working with HTML Plugins for webpack. The renderer template literal will need to account for the #root, the favicon, the manifest file.

`
<!DOCTYPE html>
  <head>
    <meta name='theme-color' content='#4FA5C2' />
    <title>Downwrite</title>
    ${tags}
    ${createLinkTag({ href: `/static/css/${link}` })}
  </head>
  <body>
    <div id="root">
      ${body}
    </div>
    ${bundles.map(src => createScriptTag({ src }))}
    ${createScriptTag({ src: `/static/js/${scripts}` })}
  </body>
</html>
`

Working with Dynamic Imports

React Loadable has an issue, when we wrap the component in the function we no longer have access to the Component.getInitialData() method that we attempt to resolve on the server before we send the initial tree back. There should be a way around this.

[1]

In the config we could specify the path to the component we could do this:

Loadable({
  loader: () => import('./Bar'),
  modules: ['./Bar'],
  webpack: () => [require.resolveWeak('./Bar')],
});

Loadable.Map({
  loader: {
    Bar: () => import('./Bar'),
    i18n: () => fetch('./i18n/bar.json').then(res => res.json()),
  },
  render(loaded, props) {
    let Bar = loaded.Bar.default;
    let i18n = loaded.i18n;
    return <Bar {...props} i18n={i18n}/>;
  },
});

that might fix the issue.

[2]

We can funnel out the Component.getInitialData some other way. that might require a few issues to be resolved first.

[3]

Ditch react-loadable for the preset and iterate on solution 1.

[4]

Consider using react-loadable for components like the Editor, Export and save async routing for Suspense


NOTE:

Initially it might've been easier to map through only the active route.

routes.filter(route => activeRoute.path === route.path)
export const findRoute = (
  routes: Array<RouteObject>,
  authed: boolean,
  initialData: {}
) =>
  routes.map((route, i) => {
    const Route = chooseRoute(route)
    const Cx = chooseComponent(route, authed)
    const SSRCx = props => <Cx {...props} {...initialData} />

    return <Route key={i} {...route} component={SSRCx} />
  })