respond-framework / rudy

Rudy Router - MVC style route async controller for react-redux applications.
MIT License
80 stars 8 forks source link

Finalize prefetching and code splitting #13

Open faceyspacey opened 6 years ago

faceyspacey commented 6 years ago

The idea was to finish the code splitting middleware, and then implement a prefetch mechanism that simply calls a route’s pipeline without actually changing the route. The code splitting middleware will automatically acquire necessary chunks, and any data fetched from thunks would be retrieved.

So step 1 is a simple api like: Rudy.prefetch(actionOrPath). And its pipeline is called, but a flag insures the URL doesn’t change. It’s up to the developer to insure that their thunks use Params from actions, not state, as the route won’t have changed yet. This is the primary thing to understand for apps that want to implement prefetching.

faceyspacey commented 6 years ago

Step 2 is our link component can have a prefetch prop like Next.js, and/or we can do one better and add a key to each route such as route.nextRoutes, which is a function that returns an array of actions or paths to prefetch. This function is called after entering the route, after its pipeline is complete.

hedgepigdaniel commented 6 years ago

The prefetching thunks part makes alot of sense to me. I imagine that you could trigger prefetching by dispatching an action like

{
  type: DASHBOARD,
  prefetch: true,
}

or

{
  type: '@@rudy/PREFETCH',
  payload: {
    type: DASHBOARD,
  },
}

So the prefetch action would go through the pipeline, and then if the navigation actually happens, then the commit action would also go through the pipeline.

Then its a case of the middlewares needing some tweaks:

Maybe if there is something about the request, say req.isPrefetching() and req.wasPrefetched() then the middlewares can skip when they need to. There probably also needs to be a way of clearing the prefetch cache - some sane default behaviour and a way for the user to manually clear it.

The problem I often run into when thinking about the chunks part is how do you determine which chunks you need for a route so that the code splitting middleware can load them, and still:

The chunks needed for the thunks is easy enough - I like to do it this way:

// routesMap.js
{
  DASHBOARD: {
    thunk: (dispatch, getState, bag) => import('./loadDashboard').then(
      // Everything needed by loadDashboard is in its own chunk(s) now
      ({ loadDashboard }) => loadDashboard(dispatch, getState, bag))
    ),
  },
}
// loadDashboard.js
import {dashboard } from './reducers/dashboard';

import { Dashboard } from './components/dashboard';

export const loadDashboard = (dispatch, getState, bag) => {
  // Add any reducers you need
  bag.extra.addReducer('dashboard', dashboard);

  // preload any components you need
  Dashboard.preload();

  // This generic component happens to be used by dashboard... do I really need to care about that now?
  LoadableAutoFill.preload();

  return API.request();
};

So that's easy enough - if you run the callbacks, the correct chunks are loaded for the controller and model. I think this needs something extra to hydrate the root reducer if addReducer is called in SSR, but I think that's easily solveable.

For me it feels a little bit dodgy to be importing react components (view code) from the controller. The developer would have to manually keep the react components imported here in sync with the components that end up actually being used in practice to render the route. I wonder if there's a better way to handle that? I think this way can work, its just not very DRY to have a nice automatically code split component tree and then have to hard code all the split points again in the thunk.