Home

Awesome

react-jobs 💼

Asynchronously resolve data for your components, with support for server side rendering.

npm MIT License Travis Codecov

export default withJob({
  work: (props) => fetch(`/categories/${props.categoryID}`).then(r => r.json()),
  LoadingComponent: (props) => <div>Loading...</div>, // Optional
  ErrorComponent: ({ error }) => <div>{error.message}</div>, // Optional
})(Category)

TOCs

Introduction

This library provides you with a generic mechanism of attaching jobs to asynchronously resolve data for your React Components.

Features

Installation

npm

npm i react-jobs -S

yarn

yarn add react-jobs

Usage

In the naive example below we will use the fetch API (you may need to polyfill it for older browsers) to retrieve data from an API endpoint.

import React from 'react'
import { withJob } from 'react-jobs' // 👈
import Product from './Product'

// When the work has completed your component will be rendered
// with a "jobResult" prop containing the result of the work.
//                               👇
function Products({ categoryID, jobResult }) {
  return (
    <div>
      { jobResult.map(product =>
          <Product key={product.id} product={product} />
        )}
    </div>
  )
}

// You use the "withJob" function to attach work to your component.
//             👇
export default withJob({
  work: (props) =>
    fetch(`/products/category/${props.categoryID}`)
      .then(response => response.json())
})(Products)

This component can then be used like so:

<Products categoryID={1} />

API

withJob(config)

Attaches a "job" to a target Component.

When the job has completed successfully your component will be rendered and provided a jobResult prop containing the result of the job.

Arguments

Important notes regarding behaviour

The work will fire under the following conditions:

OR

Returns

A React component.

Examples

Asynchronous
export default withJob({
  work: (props) => new Promise('/fetchSomething')
})(YourComponent);
Synchronous
export default withJob({
  work: (props) => 'foo'
})(YourComponent);
Using shouldWorkAgain
export default withJob({
  work: ({ productId }) => getProduct(productId),
  shouldWorkAgain: function (prevProps, nextProps, jobStatus) {
    // We will return true any time the productId changes
    // This will allow our work to re-execute, and the
    // appropriate product data can then be fetched.
    return prevProps.productId !== nextProps.productId;
  }
})(YourComponent);
Naive Caching
let resultCache = null;

export default withJob({
  work: (props) => {
    if (resultCache) {
      return resultCache;
    }
    return new Promise('/fetchSomething')
      .then((result) => {
        resultCache = result;
        return result;
      });
  }
})(YourComponent);
Retrying work that fails

You could use something like @sindresorhus's p-retry within your work.

import pRetry from 'p-retry';

export default withJob({
  work: ({ productId }) => {
    const run = () => fetch(`https://foo.com/products/${productId}`)
      .then(response => {
        // abort retrying if the resource doesn't exist
        if (response.status === 404) {
          throw new pRetry.AbortError(response.statusText);
        }
        return response.json();
      });

    return pRetry(run, {retries: 5}).then(result => {});
  }
})(YourComponent);

Server Side Rendering

This library has been designed for interoperability with react-async-bootstrapper.

react-async-bootstrapper allows us to do a "pre-render parse" of our React Element tree and execute an asyncBootstrap function that are attached to a components within the tree. In our case the "bootstrapping" process involves the resolution of our jobs prior to the render on the server. We use this 3rd party library as it allows interoperability with other libraries which also require a "bootstrapping" process (e.g. code splitting as supported by react-async-component).

Firstly, install react-async-bootstrapper:

npm install react-async-bootstrapper

Now, let's configure the "server" side. You could use a similar express (or other HTTP server) middleware configuration:

import React from 'react'
import { JobProvider, createJobContext } from 'react-jobs' // 👈
import asyncBootstrapper from 'react-async-bootstrapper' // 👈
import { renderToString } from 'react-dom/server'
import serialize from 'serialize-javascript'

import MyApp from './shared/components/MyApp'

export default function expressMiddleware(req, res, next) {
  //    Create the job context for our provider, this grants
  // 👇 us the ability to track the resolved jobs to send back to the client.
  const jobContext = createJobContext()

  // 👇 Ensure you wrap your application with the provider.
  const app = (
    <JobProvider jobContext={jobContext}>
      <MyApp />
    </JobProvider>
  )

  // 👇 This makes sure we "bootstrap" resolve any jobs prior to rendering
  asyncBootstrapper(app).then(() => {
      // We can now render our app 👇
      const appString = renderToString(app)

      // Get the resolved jobs state. 👇
      const jobsState = jobContext.getState()

      const html = `
        <html>
          <head>
            <title>Example</title>
          </head>
          <body>
            <div id="app">${appString}</div>
            <script type="text/javascript">
              // Serialise the state into the HTML response
              //                                 👇
              window.JOBS_STATE = ${serialize(jobsState)}
            </script>
          </body>
        </html>`

      res.send(html)
    });
}

Then on the "client" side you would do the following:

import React from 'react'
import { render } from 'react-dom'
import { JobProvider } from 'react-jobs'

import MyApp from './shared/components/MyApp'

// Get any "rehydrate" state sent back by the server
//                               👇
const rehydrateState = window.JOBS_STATE

// Surround your app with the JobProvider, providing
// the rehydrateState
//     👇
const app = (
  <JobProvider rehydrateState={rehydrateState}>
    <MyApp />
  </JobProvider>
)

// Render 👍
render(app, document.getElementById('app'))

FAQs

Let me know if you have any questions.