Skip to content

Instantly share code, notes, and snippets.

@faceyspacey
Last active December 10, 2019 14:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save faceyspacey/500cfe0c682fe6a2a276e5dc8ccb8ed7 to your computer and use it in GitHub Desktop.
Save faceyspacey/500cfe0c682fe6a2a276e5dc8ccb8ed7 to your computer and use it in GitHub Desktop.
how Respond Framework solves the same stuff as Concurrent React, but better without an API
createModule({
  home: '/',
  login: '/login',
  dashboard: {
    path: '/dashboard',
    module: createModule({
      settings: '/settings',
      myAccount: '/my-account',
    }, {
      reducers,
      components,
      middleware: [],
    })
  }
}, {
  reducers,
  components,
  middleware: [],
})




const middlewares = [
  call('beforeEnter'),
  commitEnter,
  call('thunk', { cache: true }),
]

const middlewares = [
  call('beforeEnter', { commitEnter: true }),
  commitEnter(call('thunk', { cache: true })),
]

const middlewares = [
  commitEnter({ start: true }),
  call('beforeEnter'),
  call('thunk', { cache: true }),
  commitEnter(),
]

const middlewares = [
  // short-circuiting middleware:
  serverRedirect(),      
  anonymousThunk(),
  pathlessEvent('thunk'),

  // routing pipeline starts here:
  transformEvent(),
  call('beforeLeave', { prev: true }),
  call('beforeLoading'),
  commitLoading(),    // loading event dispatched to store, state.loading updated
  call('beforeEnter', { cancelOnReturnFalse: true }),
  call('thunk', { cache: true }),
  commitEnter(),      // enter event dispatched to store, state.location updated
  changePageTitle(),
  call('onLeave', { prev: true }),
  call('onEnter'),
  call('onComplete'),
]

// OPTION 1 (TRADITIONAL WAY -- "fetch-on-render" -- waterfall)

function App(props, { components, loading, location }) {
  const { event } = loading || location
  const Component = components[event]
  return Component() // component shows before transition...
}

// ...and a child can CHOOSE to show the loading state
function Page(props, state) {
  // could be a nested module with its own independent state.loading
  return state.loading ? Loading() : state.components.Page()
}


// OPTION 1b (TRADITIONAL WAY - WITH NESTED MODULE)

// if the components are being dynamically loaded from a child module...
function App(props, { modules: { foo } }) {
  const { event } = foo.loading || foo.location
  const Component = foo.components[event]
  return Component ? Component() : Loading() // ...you might still need a loading indicator (cuz u dont have the component yet)
}


// OPTION 2 ("fetch-then-render")

function App(props, { components, location }) {
  const Component = components[location.type]
  return Container(
    Sidebar(),
    Component(), // component shows after transition...
  )
}

function Page(props, state, actions) {
  return Container(
    H1('Page'),
    Button({ onClick: actions.nextPage() }),
    state.loading && Loading(), // ...but--optionally--somewhere on the page shows loading ahead of time
  )
}

// OPTION 3

https://reactjs.org/blog/2019/11/06/building-great-user-experiences-with-concurrent-mode-and-suspense.html

  1. Parallel data and view trees
  2. Fetch in event handlers
  3. Load data incrementally
  4. Treat code like data

there is no option 3 -- the whole goal of Suspense is to be able to incrementally load data (and code) in parallel. We support that via modules. We also support "parallel data and view trees" via pairing components to route-based modules.

And of course we're all about "fetch in event handlers" since that's one of the primary SSR Redux takeways (no lifecycles, use routes + events).

And treat data like code is one we're also better positioned to handled since we can load code just as easily as data from our routes map.

..But back to the first point about "loading data incrementally." Basically, our components allow for fine-grained detecting of what's loading via state.loading and from parent modules: parent.childModule.loading to determine anywhere it wants, any way it wants, whether to show a loading indicator or the previous thing. Suspense to us seems like a problem of its own creation.

Since React is all about deeply nested components, they had to find a way to communicate quickly to the top, like throwing errors, which is what Suspense is essentially. So they also have starTransition which is about "throwing" up the boolean as to whether to be in a loading state or not (and for how long). We simply don't have these problems because components everywhere can access the 2nd state argument and make the decision what to do on their own. The default looks like "fetch then render," but since individual components can decide on their own what to do, they can bypass it and show a loading indicator if they want.

What we dont have is a timing mechanism to say when it's enough, but it's so easy it could possibly just exist in userland. Here it is:

const useRespondTransition = (dep, ms = 1000) => {
   const [tooLong, setTooLong) = useState(false)
   const [currentDep, setDep] = useState(dep)
   
   useEffect(() => {
    if (dep !== currentDep) {
      setTooLong(false)
      setDep(dep)
    }
    
    if (!tooLong) setTimeout(() => setTooLong(true), ms)
   }, [dep, currentDep, tooLong])
   
   return tooLong
}
const MyComponent = (props, state) {
   const tooLong = useRespondTransition(state.currentId)

   return tooLong && state.loading
    ? Loading()
    : Something()
}

And since Respond lets you check state.loading at any level in the component tree (i.e. in parents of components that actually need the data like Suspense!), we're good to go with just this. THIS IS OUR useTransition. And it's a lot simpler--u dont have to do anything! The hook only returns a single non-function variable.

How Suspense Product Goals are Achieved in Respond World

So really, the Suspense docs over-complicates things. It does so by mixing tons of information about their implementation and the business goals. If we skip the implementation details, we come to the primary product achievements unlocked:

  1. incrementaly loading
  2. showing the current page while loading the next

That's it. For React proper, this has been a problem because the primary pattern of loading data is by virtue of a component rendering, which means waterfalls ("fetch-on-render")

And for tools like Relay in the past, they chose to gather up all data dependencies, hoist em to the route level and "fetch-then-render." They didn't have to do that though. What's the other option? Do what Apollo does, which again is the waterfall "fetch-on-render" approach, whose queries they batch in a short buffer, which still leads to a waterfall when components depend on data from parents. So we're back to square one. Facebook has a very specific case where they are able to gather all their queries for a route statically upfront, cuz of their babelplugin, way of using fragments, QueryRenderer, etc. They just wanted to be able to "stream" them all down separately--which by the way is now possible thanks to new browser APIs (they likely fall back to non-streaming strategies where support is lacking). Now that said, "streaming" is just a fancy way of doing what we've all done for a long time now with things like Promise.all. But rather than use something that looks like a Node stream in the browser, again, we use Promise.all. Or, somehow the data fetching requests are triggered in separately and run in parallel. Either way, it's really not that fancy. Facebook could have chosen to send each GraphQL query corresponding to a single route down to the server in multiple requests like us peasants if it chose. It doesn't because they are trying to squeeze out every last bit of performance, and they want a single request for all the queries. But for the rest of us, that literally doesn't matter. And sending a few parallel fetch requests works great. I'm not saying we shouldn't all start move to streaming solutions (cuz we should, and it can be easily done with Apollo's batching approach). But what I am saying is you need some centralised mechanism to fire off the parallel requests, like a router. If not, they will continue to waterfall. There needs to be some master controller involved. Or actually that's not it exactly. It's not that a centralized router is necessary--it's this: they fetching requests cannot be fired from within components. They can come from ANYWHERE ELSE, not necessarily a router. In summary: you must shift to the strategy of triggering data fetching from event-handlers, which is exactly what redux-first-router and Respond Framework have pioneered! That's the key. You basically fire ur data fetching request, and it needs to be able to tell multiple places in the component tree that its in a loading state; and when the data returns it needs to tell multiple places that it has the data. In React these places are Suspense Boudnaries (and the components that trigger the events). In Respond, the notfication is literally as easy as it gets since any component you want has a state argument which has access to loading states (and the data). So it's just a matter of notifying the centralized--though namespaced--store and routing system (via dispatched events). Suspense components are not needed because any component can know its loading state. In addition, since our system is based on modules, parents can even know the loading state of children via fully namespaced property access, eg: state.moduleName.loading! And that's ultimately the key to incremental loading. Because of our modules, and ability for each module to make its own decision as to whether its in a loading state or not, as well as parents ability to do it for children, we have a system literally built around "incremental loading."

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