I build distributed frontends using components (preact components, but that's not important). Components are a great unit of composition when the structure of a system cannot be statically determined.
In the following examples, I'm going to pretend there exists a common definition for a <SplitPoint>
component:
SplitPoint
invokes an async load()
function (passed as a prop), then renders result as its child.
A horribly naive implementation of this component might look like this:
class SplitPoint extends Component {
constructor({ load }) {
load().then( Child => this.setState({ Child }) )
}
render() {
let { Child } = this.state
let { load, ...props } = this.props
return <Child {...props} />
}
}
We can use this component to lazy-load (webpack-)chunked components on first render. Normally in Webpack this might look like:
<SplitPoint load={ () => import('./SomeComponent') } />
We can also emulate this without any real chunk using a function that returns a Promise resolving to a Component:
// we're emulating a chunk here: a Promise that returns a Component
const loadComponent = () => Promise.resolve(
class SomeComponent extends Component {
render() { return <div>whatever</div> }
}
)
<SplitPoint load={ () => loadComponent() } /> // unnecessary? yes
The main point here is that we can asynchronously pull in new components and render them into the Virtual DOM tree,
simply by wrapping them in a <SplitPoint />
and chunking the Component definition.
Here's an example of what that looks like:
const Sidebar = () => (
<aside>
<SplitPoint load={ () => import('./Ad') } />
<SplitPoint load={ () => import('./Map') } />
<SplitPoint load={ () => import('./Nearby') } />
<SplitPoint load={ () => import('./AnotherAd') } />
</aside>
)
... rendering that <Sidebar />
actually triggers network calls to go get the necessary components, and when they resolve they are rendered in-place.
The same technique is easily used to chunk routes/pages/whatever.
Let's say we're using something like Redux for a centralized store. The components are going to pull initial state from that central store, and subscribe to to changes to update in response.
The components in our Sidebar were Ad
, Map
, Nearby
and AnotherAd
because revenue is important.
Let's ignore the Ads (everyone does) and focus on Map
and Nearby
.
Both of these Components want to use the same location
value from the centralized store.
The location
value isn't free, though. It's existence in that store includes a bunch of code to
- fetch the user's location from an IP address
- geolocate them using browser APIs
- save location preferences to localStorage; or if signed in,
- save an authenticated user's location preferences to a service somewhere
That's a nontrivial amount of code.
If we're building a sufficiently large application, it seems like there would come a time when the functionality associated with values
in the store would be worth splitting out into chunks that we could load on-demand. Or, perhaps we don't know ahead of time (during a build)
if there will even be Components in this application that rely on that location
value at all? Maybe their existence in the UI is
determined at runtime by something like a CMS, authentication status, or flags?
The logical solution here would be to code-split the logic around that location
value so that it's only downloaded and executed when the
application actually needs to use it. However, how do we know that the application needs to access location
?
// perhaps getters on `state`?
@connect( state => ({
location: state.location
})
export default class Map extends Component {
render() {
this.props.location // accessed synchronously
return <div>a map</div>
}
}