prateekbh / preact-async-route

Async route component for preact-router
MIT License
138 stars 14 forks source link

Support prefetching of links #5

Closed ajoslin closed 7 years ago

ajoslin commented 7 years ago

I'd like to use preact-async-route to create "prefetching" functionality similar to next.js. This could also belong in a separate library, of course.

We could split this functionality out into separate files so as not to increase bundle size.

The end-user API I have in mind is something like:

import {Component} from 'preact'
import AsyncRouter from 'preact-async-route/router'
import AsyncLink from 'preact-async-route/link'

export default class MyComponent extends Component {
  render () {
    return <AsyncLink href="/foo" prefetch={true} /> // ... can prefetch declaratively
  }
  componentDidMount () {
    AsyncRouter.prefetch('/foo') // ... or imperatively
  }
}

It's expected that the end-user would use <AsyncRouter> in place of <Router>.

AsyncLink would be a component that extends Link. If props.prefetch was given, it would call AsyncRouter.prefetch(this.props.href) in its componentDidMount hook.

AsyncRouter extends Router. It would look something like the following:

// Both of these libraries weigh < 500b
import PromiseQueue from 'queue-that-promise'
import requestIdleCallback from 'ric-shim'

const prefetchQueue = PromiseQueue()
const ROUTERS = []

class AsyncRouter extends Router {
  componentWillMount () {
    super(this)
    ROUTERS.push(this)
  }

  componentWillUnmount () { 
    super(this)
    ROUTERS.splice(ROUTERS.indexOf(this), 1) 
  }

  static prefetch (href) {
    if (typeof window !== 'object') return Promise.resolve() // noop in Node

    // `.add()` auto starts the queue if it's not active
    return prefetchQueue.add(() => {
      return new Promise((resolve) => requestIdleCallback(resolve)))
        .then(runPrefetch)
    })

    function runPrefetch () {
      var prefetchPromises = []
      ROUTERS.forEach(function (router) {
        var routes = router.getMatchingChildren(href)
        routes.forEach(function (route) {
          if (!route instanceof AsyncRoute) return
          prefetchPromises.push(route.loadComponent())
        })
      })
      return Promise.all(prefetchPromises)
    }
}

I'd like your thoughts on this feature. I can open a PR if you like the idea, or a separate library if you think it belongs elsewhere. Mainly, I'd like feedback on the proposed implementation.

prateekbh commented 7 years ago

the idea kinda sounds nice, though i'd also love to hear @developit 's views too here... cuz it overlaps the router itself

ajoslin commented 7 years ago

Sounds good.

If preact-router exposed its ROUTERS array, there wouldn't be a need for the AsyncRouter to be a component. Instead, just a function prefetch(href) could be exported.

However, if exopsing that array we're getting into preact-router exposing its implementation details, and exposing globals, which could be dangerous.

@developit what do you think of the idea of this, and of preact-router exposing its ROUTERS array?

Also, I don't know how well this would work with the nested routers PR that's getting merged soon.

developit commented 7 years ago

I wonder if it'd be possible to just have an alternative <Link /> component that, when mounted, pushes rel="prefetch" tags into <head> manually? Something like import Link from 'prefetching-link'? That would be totally decoupled from the router.

ajoslin commented 7 years ago

The problem is we don't have any guarantee that the promise returned from an AsyncRoute's component prop represents a <script src> call.

And even if the AsyncRoute is doing a Webpack import (the most common case), we have no way of knowing what <script src> path that Webpack import is "linked" to.

So unfortunately, I don't think it would be possible to do it that transparently -- since the AsyncRoute promise can represent any async operation (even a timeout), we need to trigger it with JS to preload it.

developit commented 7 years ago

ah yeah, you're right. it would assume too much about the webpack config. maybe that's the reason to have <Link prefetch> be a separate module, it seems like it'd be specific to a webpack + webpack config.

prateekbh commented 7 years ago

ok I may be a little late on this thread but why <Link prefetch> needs to be dependent on webpack? why cant we just add <link rel='prefetch'..../> to head on componentDidMount?

developit commented 7 years ago

@prateekbh need webpack in order to know the chunk that will be loaded to handle the URL. The URL itself isn't what gets preloaded.

ajoslin commented 7 years ago

FYI I've decided to just use next.js instead of building this out custom.

But yes, it has to be done programmatically unless you want to couple this library to webpack.

developit commented 7 years ago

@ajoslin good to know.