Skip to content

Instantly share code, notes, and snippets.

@faceyspacey
Last active March 23, 2019 04:41
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save faceyspacey/80d90a54e636043487b43182ae445cbb to your computer and use it in GitHub Desktop.
Save faceyspacey/80d90a54e636043487b43182ae445cbb to your computer and use it in GitHub Desktop.
Example of our automatically code-split manifest

Our Manifest

Below is our manifest which has the information of every route the app has. It's statically generated by our babel-plugin at compile time.

It has the absolute minimal amount of information necessary so that it's possible for any route to dispatch actions to any other route.

Since createScene() generates action creators from simply our routesMap types/keys, that's all we need to generate ALL ACTIONS. Well, there is a few small edge cases, but you got the idea.

const { store, firstRoute } createApp({
  main: {
    load: () => import('modules/main'), // so the idea is that routes can build themselves later while the plane is flying!
    routes: {
      HOME: {},
      CHECKOUT: {
        load: () => import('modules/checkout'),
        routes: {
          STEP1: {},
          STEP2: {},
          PAYMENT: {
            load: () => import('modules/stripe'),
            routes: {
              CHARGE: {},
              CONFIRMATION: {}
            }
          }
        },
      }
    }
  },
  auth: {
    load: () => import('modules/auth'),
    routes: {
      LOGIN: {},
      SIGNUP: {}
    }
  }
}, {
  initialState, // redux initial state(createApp does createRouter + createStore!!)
  enhancer, // redux enhancer!
  // ...rest of the rudy options
}, [
  codeSplit('load'
  call('beforeEnter'),
  call('enter')
  call('thunk')
])


const render = App => {
  ReactDOM.hydrate(
    <AppContainer>
      <Provider store={store}>
        <App />
      </Provider>
    </AppContainer>,
    document.getElementById('root')
  )
}

(async function() {
  await store.dispatch(firstRoute())
  render(App)
})()

notice the 2 modules: "main" + "auth"

Collapsing 2 concepts: scene nesting + modules!!!!!

So basically, createScene will no longer be an API the user calls. It's now automaticall called based on the above routesMap tree.

That means actions are generated and automatically inject into context, and as additional arguments to reducers.

E.g:

const MyRespondComponent = (props, state, actions) => ...

the actions are made available via this line:

<Provider store={store}>
  <App />
</Provider>

Our reducers look like this:

const myReducer = (state, action, types) => types.TOGGLE ? !state : state

So similar to how the store/state is provide by context to all components to guarantee the correct store instance across server-side renders, we are going to use the same technique to our actions and types, to guarantee as new ones come over the wire (via code-splitting) *they are right where you need them!

In other words, you won't be importing types and action creators anymore. In fact, YOU DONT MAKE THEM.

You just make the routes map, and generated actions + types are available in both places you need them:

  • the component tree via context
  • reducers passed as a 3rd argument

Why this is important may not be totally clear, but basically, it answers this question:

How do you gain access to the action creators for a code-split route that isn't loaded yet?

See, we could insure all parent modules have the action creators, but that becomes cumbersome, especially in an automatically code-split system like this one. Instead, we make sure the minimum amount of information necessary to create generate action creators is available no matter what page u enter via. And then of course we make all those action creators on first load.

So from there, the only natural way to provide these action creators is essentially via dependency injection using context, PLUS our new modified form of redux reducers. Essentially we always pass to your Redux reducer all your actions as a 3rd arg. Easy peasy.

ABOUT MODULES

So as you can see we now split our app freely by simply grouping routes into module. You can see above we have main and auth .

But also CHECKOUT + PAYMENT. Each nesting is also a module. It's a module that contains all its child routes.

The implications are that the parent route doubles as the name of the module, eg: CHECKOUT === checkout.

Not all modules muste nested of course. If you don't want it nested, put it at the top level like auth of course.

Now that said, since we have first clast nested routes, the same applies at deeper nesting levels as well! E.g. parallel to payment we can have something else like support.

Some of the semantics about capitalization might not be fully correct yet. Perhaps scenes aren't the lowercased version, and simply the route type doubles as the precise module namespace.

That would mean in this case that the main and auth namespaces essentially double as scene prefixes for all their child routes. Yup, thats what I'm thinking.

Anyway, to see how relates to remixx modules at the component level, combine this knowledge with what i said in the #remixx slack channel. Basically, i've devised a plan for these namespaces to be how you access slices of states in components

FINAL WORD ON NAMESPACES

So part of the realization here is that preventing conflicts between modules/namespaces in the component tree follows the same principle as preventing conflicts in the routesmap. Which, btw, is also the same principle of ES6 modules in general:

the parent file imported to dictates how you alias names to avoid conflicts!!!

So you saw in the routesMap that a parent route lets you import() into it, eg:

CHECKOUT: {
  load: () => import('modules/checkout'),

modules/checkout could be anything. It's anonymous. It could be an npm package made by a 3rd party developer for Respond Apps.

What that developer builds and puts on NPM has module names like this:

const moduleNamesByFileName = {
  '/src/components/AuthModule.js': '234345345',
}

Only when built into an app, is that unique ID number translated into this:

const moduleNamesByFileName = {
  '/src/components/AuthModule.js': 'CHECKOUT',
}

Or if there's nesting, into:

const moduleNamesByFileName = {
  '/src/components/AuthModule.js': 'MAIN/CHECKOUT',
}

I explain this in the #remixx thread on slack, but basically they're all anonymous until our babel plugin builds a simple dependency graph of all our Respond modules. That means there is no conflicts.

Then we are able to piece together the minimal, but complete, routesMap tree as above, and resolve the module names, replacing the unique IDs with whatever the developer decides in userland.

Now, imagine you have a team made of many sub-teams. They're all essentially working on MicroFrontEnds in the form of these Respond modules, right. If ur a team working on a sub module, it's the parent module that ultimately dictates the final module name, just like aliases in ES6 modules!!.

Basically, the final module name isn't CHECKOUT, it's MAIN/CHECKOUT. And that's how it's done.

But from within a given module's components and reducers, u can use just state.foo. And something similar in reducers with their action types.

State Mappings

Over here is the complete brainstorm:

https://gist.github.com/faceyspacey/248e6a82780763bf5b48c973e63783db

It's long and redundant. But here's one other cool thing i want u to see:

auth: {
    load: () => import('modules/auth'),
    routes: {
      LOGIN: {},
      SIGNUP: {}
    },
    stateMappings: {
            user: 'session',
            pay: 'charge'
    }
  }

This allows for state.user within a parent module to appear as state.session within a child module. It accomplishes the goal of passing props into a module, but provides that prop in the form of state to all the child components that make up that module. I.e. u dont have to prop drill to pass the user down, eg:

<Auth session={user} />

U dont have to do that.

U can simply do:

<Auth />

And state.session will be available in it. It's kind of like Static Types for the props of a component. But it's a real runtime link to data.

It solves the multiple store problem, allowing us to use a single store that is time-travellable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment