react-frontload

Datetime:2017-04-19 05:40:01         Topic: React          Share        Original >>
Here to See The Original Article!!!

react-frontload

Bind data requests from your API to your React Components

Tl;dr

In React applications, a common requirement is to render data which is dynamically loaded from an API into a component, on both server and client renders.

react-frontload is a library that lets you declaratively bind custom data loading logic to your components, and then automatically fires the data loading when the component renders on both server and client.

It integrates easily into your existing React stack, and requires no changes to your existing data API .

github: https://github.com/davnicwil/react-frontload

npm package: react-frontload

Please read on to explore the problem, how react-frontload solves it, and see some code examples.

A Typical Use Case

In the most straightforward use case, you have a view component which displays some dynamic data for some URL, which contains the parameters required to load the data from your API.

example

www.myreactapp.co/profiles/davnicwil shows the profile view for user davnicwil

To load davnicwil’s profile data, a GET request is made to api.myreactapp.co/users/davnicwil

Everything up to and including the initial render of the component is synchronous. The data request, however, is asynchronous. The React component rendering lifecycle is synchronous by design. There is no way, once component rendering has begun, to wait until the data request has resolved before continuing. The asynchronous data request and the component render must be decoupled.

This decoupling can be achieved in a straightforward way by passing required data to a view component via props, and rendering a loader if these props are empty.

example

const ProfileView = (props) => (
  props.profile
    ? <div>{props.profile.fullName}'s profile</div>
    : <div>loading..</div>
)

The Problem

The important question is: when is the data request fired?

On the client, it doesn’t matter when the data request is fired. It can happen before or after the initial render of the component. All that matters is that when the request resolves, the results are fed into the component’s props. React will then re-render the component, and you’re done. On the client, components can be thought of as long-lived state machines, sitting there indefinitely waiting to re-render whenever their props are updated.

example

class ProfileContainer extends React.Component {
componentDidMount () {
    // asynchronously load the profile 
    // into this.props.boundProfile
    // via your state management solution of choice,   
    getProfileAsync(this.props.username)
  }
render () {
    // this.props.boundProfile will at some point
    // contain the loaded profile. For the initial
    // render it will be undefined
    return (
      <ProfileView profile={this.props.boundProfile} />
    )
  }
}

On the server, order matters. That’s because for each individual page request, our same view component is created, rendered exactly once with whatever initial props you provide, and then discarded. Any data passed via props must be loaded before this single render occurs. On the server the component is better thought of as a pure function than a long-lived state machine.

example

const handleServerPageRequest = async (req, res) => { 
  // data request must happen first.
  // wait until it's done before continuing
  const profile = await getProfileAync(req.body.username)
// render component passing loaded data via props
  // note that this is a plain synchronous function call
  const markup = ReactDOM.renderToString(
    <ProfileContainer profile={profile} />
  )
// send the rendered markup in the response
  res.send(markup)
}

Both patterns for client and server are straightforward, but require different wiring to get the request and the subsequent component (re-)render to fire.

The high-level intent in both cases is the same: to specify that some data dependencies will be loaded into props for the component to render — synchronously on the server and asynchronously on the client.

There is another important feature that the patterns above don’t accommodate. When server-rendering a component with data, we probably don’t want to re-load the data as soon as the component renders on the client. We need the component to know that it has been server rendered, and then only reload the data on updates to the parameters which are used for the data request. For example, if the username in the URL changes on an in-app navigation, and the component needs to load and render a different user’s profile, when the username prop updates. Of course we can implement this feature on top of the patterns above, but it involves a lot of small details that are easy to get wrong, even with the help of a state management solution.

After implementing the wiring code for these patterns time and again for my components, I eventually found myself really needing an abstraction for it. The wiring code is not all that complex by itself, but there are lots of details and so the usual DRY rule applies — it just stops being manageable once you need to repeat it for more than a few components.

A Solution

After a few attempts at writing a library using various patterns and falling into pitfalls as I went along, I eventually uncovered my desired feature set:

  • Declarative, co-located, plain JS data-loading logic: A component’s data-loading logic should just a regular JS function, returning a Promise for async. The function should be co-located with the component.
  • Idiomatic React: The data-loading function should interface with the rest of the application only via component props, so it can play nicely with arbitrary libraries in the rest of the stack without any implicit dependencies or integrations beyond React itself.
  • Component encapsulation & independence : A component’s data-loading function should work the same regardless of where the component is placed in the app’s component tree.
  • Configurable component data-loading behaviour : It should be possible to fine-tune when the data loading function runs so that special cases can be handled. The defaults should set up the component for the most common use-case.

I implemented those features into a library: react-frontload. Here’s the result, with the same example as before, using react-frontload.

import { frontloadConnect } from 'react-frontload'
// assuming here that getProfileAsync returns a Promise that
// resolves when the profile is loaded into props.boundProfile
const frontload = (props) => (
  getProfileAsync(props.username)
)
// all available options, set to the default values
const options = {
  noServerRender: false,
  onMount: true,
  onUpdate: true
}
// just decorate the same underlying component from earlier
const ProfileView =
frontloadConnect(frontload, options)((props) => (
props.profile
? <div>{props.profile.fullName}'s profile</div>
: <div>loading..</div>
))

With just those few lines of code, the profile data is loaded in and rendered on both client and server, and we’ll also get the nice feature discussed earlier where the request is not unecessarily re-fired on the first client render immediately after a server render.

All that remains is two tiny changes, to make your application react-frontload aware.

1. Wrap your application with the Frontload provider

// before
const Application = () => (
  <div>
    {/* application content here */}
  </div>
)
// after: react-frontload aware
const Application = () => (
  <Frontload>
    <div>
      {/* application content here */}
    </div>
  </Frontload>
)

2. Use the special react-frontload serverRender function on the server

// before
const store = initStore(req)
const markup = ReactDOM.renderToString(
  <Application store={store} />
)
res.send(markup)
// after: react-frontload aware
import { serverRender } from 'react-frontload'
...
const store = initStore(req)
// encapsulate exactly the same render code as above in a
// function and pass it serverRender
const markup = serverRender(() =>
ReactDOM.renderToString(<Application store={store} />)
))
res.send(markup)

And that’s all that’s required. It’s ready to use.

Just to comment on the code missing from the example — i.e. binding the asynchronously loaded profile to props.boundProfile, and the username from the URL to props — I left those details out purposefully, as established libraries and patterns exist and react-frontload has no opinion about which you use.

This works because the component’s frontload function interfaces with the component only via props. This was one of the design goals, and hopefully it’s clear how simple it makes it to use react-frontload in any existing stack. You make these bindings to your component props in the same way you always do, and then you can access them in the component’s frontload function. You don’t have to do anything extra to get the bindings to work.

The react-frontload API

frontloadConnect(frontload, [options])(Component)

frontloadConnectis the bridge between the library, and the Component you want to load data into.

Let’s go through the frontload and options parameters.

frontload(props)

A function which is called with the Component props, and returns a Promise which must resolve when all the required data-loading is complete.

The full power and flexibility of the Promise API is therefore available to encapsulate the asynchronosity of the data-loading behaviour into a single returned Promise — that means a single request is fine, but equally if multiple requests are required they can just be chained with .then (if order matters) or parallelised with Promise.all (if they are independent).

[options]

The options object provides three configurations that specify when exactly the frontload function should fire on both client and server. If any particular configuration is left undefined, it takes its default value. Likewise, if the entire options object is left undefined, all three configurations take their default values.

1 noServerRender (boolean) [default false]

When false , the Component ’s frontload function will run on every server render. In the example, this ensures the component server renders with the expected profile, rather than the ‘loading..’ placeholder.

When true , the Component ’s frontload function will not run on server render. In the example, the component would be server rendered with the ‘loading..’ placeholder, since no profile is loaded.

Importantly, when this is true it also turns off the feature that prevents the component from loading data on mount after a server render. Assuming the option to load data on mount (discussed below) is set, the frontload function will fire on first mount after server render, since react-frontload knows that the data was not loaded on the server and the ‘loading..’ placeholder has to be replaced with the loaded in profile.

Note : if you need to turn off server rendering for an entire application, instead of setting this configuration true on every individual component, noServerRender can instead be set as a prop on the application’s Frontload provider like this:

<Frontload noServerRender> 
<div>
{/* app code */}
</div>
</Frontload>

2 onMount (boolean) [default true]

This options toggles whether or not the frontload function should fire when the Component mounts on the client. Again, this only skips when the Component was server rendered immediately before hand— see above for more details.

The default is true and 99% of the time this is what it should be. After all, in almost all cases you want to load the data and render it when the Component is first displayed. It is provided as an option to accommodate edge cases, however.

3 onUpdate (boolean) [default true]

Very similar to onMount , this options toggles whether or not the frontload function should fire when the Component ’s props update on the client.

The default is true and again, for a well designed application component tree with proper restriction of unnecessary updates when props do not change (via shouldComponentUpdate ) it is likely that this is fine.

For edge cases, or to remove the need to change component trees that trigger spurious updates where this is infeasible, it can be set to false .

Note that there is a common case where some but not all props should trigger the frontload function. In our example, we’d always want to fire it when username updates, but what if there was another prop, textColor , that could also update but which would not require the profile to be reloaded.

I thought about making this configurable via another options field, for example by providing prop names that should trigger the frontload function to fire when they update. But I realised that this strayed into DSL territory, with all the loss of power and maintainability issues that brings, and in fact there is already a mechanism for dealing with this with the full power of JS — recall that the frontload function itself receives the Component props. Therefore, we can make any arbitrarily complex decisions about whether to actually fire any expensive requests on Component update in the frontload function itself.

frontloadServerRender(renderFunction)

The frontloadServerRender function can be thought of as a decoration of your existing React server render logic, which you have to encapsulate in a renderFunction which outputs server-rendered React markup. Usually, renderFunction will be a simple wrapper over a ReactDOM.renderToString call.

The output of frontloadServerRender is the same markup that renderFunction produces, which can be returned in the response to the client in the usual way. It is nothing more than a proxy for this function, and exists only to ‘inject’ the plumbing code required to do synchronous loading of all the frontload functions in the application prior to final rendering.

How does it work? (coming soon)

In a future post I’ll explain how react-frontload is implemented. For now, if you’re curious please read the source, which I’ve tried to write as clearly as possible, and is itself commented with some implementation notes.

I hope you found this post interesting. I developed react-frontload to tidy up and improve the code in my own applications, and also as a learning experience after thinking about this problem for a while and trying and failing multiple times to come up with a nice way to solve it.

I put the source and npm package out there in case this solution could be useful for anyone else in the React community. It has decent test coverage, and I’ve been using it for a while in production on my side project https://postbelt.com where it’s being performing without issues.

However, it’s very much written to serve my particular use cases, and it’s quite possible that there are bugs / edge cases that I haven’t caught simply because I’ve not needed to use them yet. I’m more than happy to answer any questions and review PRs on the github repo if that’s the case!

Thanks for taking the time to read this post.








New

Put your ads here, just $200 per month.