7.6.1-23-g168e6e2 • Published 7 years ago

restful-react v7.6.1-23-g168e6e2

Weekly downloads
10,485
License
MIT
Repository
github
Last release
7 years ago

RESTful React

Building React apps that interact with a backend API presents a set of questions, challenges and potential gotchas. This project aims to remove such pitfalls, and provide a pleasant developer experience when crafting such applications. It can be considered a thin wrapper around the fetch API in the form of a React component.

As an abstraction, this tool allows for greater consistency and maintainability of dynamic codebases.

Overview

At its core, RESTful React exposes a component, called Get. This component retrieves data, either on mount or later, and then handles error states, caching, loading states, and other cases for you. As such, you simply get a component that gets stuff and then does stuff with it. Here's a quick overview what it looks like.

Edit Restful React demos

import React from "react";
import Get from "restful-react";

const MyComponent = () => (
  <Get path="https://dog.ceo/api/breeds/image/random">
    {randomDogImage => <img alt="Here's a good boye!" src={randomDogImage && randomDogImage.message} />}
  </Get>
);

export default MyComponent;

Getting Started

To install and use this library, simply yarn add restful-react, or npm i restful-react --save and you should be good to go. Don't forget to import Get from "restful-react" or similar wherever you need it!

Features

Restful React ships with the following features that we think might be useful.

Global Configuration

API endpoints usually sit alongside a base, global URL. As a convenience, the RestfulProvider allows top-level configuration of your requests, that are then passed down the React tree to Get components.

Consider,

Edit Restful React demos

import React from "react";
import { RestfulProvider } from "restful-react";

import App from "./App.jsx";

const MyRestfulApp = () => (
  <RestfulProvider base="https://dog.ceo/api">
    <App />
  </RestfulProvider>
);

export default MyRestfulApp;

Meanwhile, in ./App.jsx,

import React from "react";
import Get from "restful-react";

const MyComponent = () => (
  /* Make a request to https://dog.ceo/api/breeds/image/random" */
  <Get path="/breeds/image/random">
    {randomDogImage => <img alt="Here's a good boye!" src={randomDogImage.message} />}
  </Get>
);

export default MyComponent;

Naturally, the request will be sent to the full path https://dog.ceo/api/breeds/image/random. The full API of the RestfulProvider is outlined below. Each configuration option is composable and can be overriden by Get components further down the tree.

RestfulProvider API

Here's a full overview of the API available through the RestfulProvider, along with its defaults.

// Interface
interface RestfulProviderProps<T> {
  /** The backend URL where the RESTful resources live. */
  base: string;
  /**
   * A function to resolve data return from the backend, most typically
   * used when the backend response needs to be adapted in some way.
   */
  resolve?: ResolveFunction<T>;
  /**
   * Options passed to the fetch request.
   * This can be a function if you want dynamically computed options each time.
   */
  requestOptions?: (() => Partial<RequestInit>) | Partial<RequestInit>;
}

// Usage
<RestfulProvider
  base="String!"
  resolve={data => data}
  requestOptions={authToken => ({ headers: { Authorization: authToken } })}
/>;

Here's some docs about the RequestInit type of request options.

Composability

Get components can be composed together and request URLs are an accumulation of their collective path props. Consider,

Edit Restful React demos

// Assuming we're using a RestfulProvider with base="https://my.web/api somewhere,
import React from "react";
import Get from "restful-react";

export default () => (
  {/* Send a request to "https://my.web/api/breeds */}
  <Get path="/breeds">
    {data => {
      return (
        <div>
          <h1>Random Image</h1>
          {/*
            Composes path with parent: sends request to https://my.web/api/breeds/image/random.
            This happens because there is no preceding / to the path.
          */}
          <Get path="image/random">
            {image => <img alt="Random Image" src={image && image.message} />}
          </Get>

          <h1>All Breeds</h1>

          {/*
            Composes path with parent: sends request to https://my.web/api/list
            The preceding slash (/) ALWAYS queries the ROOT of the RestfulProvider's base.
          */}
          <Get path="/list">
            {list => (
              <ul>{list && list.message.map(dogName => <li>{dogName}</li>)}</ul>
            )}
          </Get>
        </div>
      );
    }}
  </Get>
);

From the above example, not only does the path compose based on the nesting of each Get, but each Get can override its parent with other props as well: including having specific requestOptions if there was a valid use case.

To opt-out of this behavior Get components can use an alternative URL as their base prop.

Full Get Component API

Loading and Error States

Get components pass down loading and error states to their children, to allow for state handling. Consider,

Edit Restful React demos

const MyAnimalsList = props => (
  <Get path={`/${props.animal}`}>
    {(animals, { loading, error }) =>
      loading ? (
        <Spinner />
      ) : (
        <div>
          You should only see this after things are loaded.
          {error ? (
            "OH NO!"
          ) : (
            <>
              <h1>
                Here are all my {props.animal}
                s!
              </h1>
              <ul>
                {animals.map(animal => (
                  <li>{animal}</li>
                ))}
              </ul>
            </>
          )}
        </div>
      )
    }
  </Get>
);

Within Operational UI, all of our <Progress /> components support an error prop. For even better request state handling, we can write:

const MyAnimalsList = props => (
  <Get path={`/${props.animal}`}>
    {(animals, { loading, error }) =>
      loading ? (
        <Progress error={error} />
      ) : (
        <div>
          You should only see this after things are loaded.
          <h1>
            Here are all my {props.animal}
            s!
          </h1>
          <ul>
            {animals.map(animal => (
              <li>{animal}</li>
            ))}
          </ul>
        </div>
      )
    }
  </Get>
);

Lazy Fetching

It is possible to render a Get component and defer the fetch to a later stage. This is done with the lazy boolean prop. This is great for displaying UI immediately, and then allowing parts of it to be fetched as a response to an event: like the click of a button, for instance. Consider,

Edit Restful React demos

<Get path="/unicorns" lazy>
  {(unicorns, states, { get }) => (
    <div>
      <h1>Are you ready?</h1>
      <p>Are you ready to unleash all the magic? If yes, click this button!</p>
      <button onClick={get}>GET UNICORNS!!!!!!</button>

      {unicorns && (
        <ul>
          {unicorns.map((unicorn, index) => (
            <li key={index}>{unicorn}</li>
          ))}
        </ul>
      )}
    </div>
  )}
</Get>

The above example will display your UI, and then load unicorns on demand.

Response Resolution

Sometimes, your backend responses arrive in a shape that you might want to adapt, validate, or reshape. Other times, maybe your data consistently arrives in a { data: {} } shape, with data containing the stuff you want.

At the RestfulProvider level, or on the Get level, a resolve prop will take the data and do stuff to it, providing the final resolved data to the children. Consider,

Edit Restful React demos

const myNestedData = props => (
  <Get
    path="/this-should-be-simpler"
    resolve={response => response.data.what.omg.how.map(singleThing => singleThing.name)}
  >
    {data => (
      <div>
        <h1>Here's all the things I want</h1>
        <ul>
          {data.map(thing => (
            <li>{thing}</li>
          ))}
        </ul>
      </div>
    )}
  </Get>
);

Debouncing Requests

Some requests fire in response to a rapid succession of user events: things like autocomplete or resizing a window. For this reason, users sometimes need to wait until all the keystrokes are typed (until everything's done), before sending a request.

Restful React exposes a debounce prop on Get that does exactly this.

Here's an example:

const SearchThis = props => (
  <Get path={`/search?q=${props.query}`} debounce>
    {data => (
      <div>
        <h1>Here's all the things I search</h1>
        <ul>
          {data.map(thing => (
            <li>{thing}</li>
          ))}
        </ul>
      </div>
    )}
  </Get>
);

Debounce also accepts a number, which tells Get how long to wait until doing the request.

const SearchThis = props => (
-  <Get path={`/search?q=${props.query}`} debounce>
+  <Get path={`/search?q=${props.query}`} debounce={200 /*ms*/}>
    {data => (
      <div>
        <h1>Here's all the things I search</h1>
        <ul>{data.map(thing => <li>{thing}</li>)}</ul>
      </div>
    )}
  </Get>
);

It uses lodash's debounce function under the hood, so you get all the benefits of it out of the box like so!

const SearchThis = props => (
  <Get
    path={`/search?q=${props.query}`}
-   debounce={200}
+   debounce={{ wait: 200, options: { leading: true, maxWait: 300, trailing: false } }}
  >
    {data => (
      <div>
        <h1>Here's all the things I search</h1>
        <ul>{data.map(thing => <li>{thing}</li>)}</ul>
      </div>
    )}
  </Get>
);

TypeScript Integration

One of the most poweful features of RESTful React, each component exported is strongly typed, empowering developers through self-documenting APIs. As for returned data, simply tell your data prop what you expect, and it'll be available to you throughout your usage of children.

Using RESTful React in VS Code

Query Parameters

All components in this library support query params (https://my.site/?query=param) via a queryParams prop. Each Get, Mutate and Poll component is generic, having a type signature of Get<TData, TError, TQueryParams>. If described, the queryParams prop is fully type-safe in usage and provides autocomplete functionality.

Autocompletion on QueryParams

Please note that the above example was built using our OpenAPI generator in order to infer the type of component from the specification and automatically generate the entire type-safe component in a very quick and easy way.

Mutations with Mutate

Restful React exposes an additional component called Mutate. These components allow sending requests with other HTTP verbs in order to mutate backend resources.

Edit Restful React demos

const Movies = ({ dispatch }) => (
  <ul>
    <Get path="/movies">
      {(movies, states, actions) =>
        movies.map(movie => (
          <li>
            {movie.name}
            <Mutate verb="DELETE">
              {(deleteMovie, { loading: isDeleting }) => (
                <button onClick={() => deleteMovie(movie.id).then(() => dispatch("DELETED"))} loading={isDeleting}>
                  Delete!
                </button>
              )}
            </Mutate>
          </li>
        ))
      }
    </Get>
  </ul>
);

Mutate is strongly typed, and provides intelligent autocompletion out of the box, complete with available verbs and other self-documentation.

Mutate

Each mutation returns a promise, that can then be used to update local component state, or dispatch an action, or do something else depending on your use case.

Full Mutate Component API

Polling with Poll

RESTful React also exports a Poll component that will poll a backend endpoint over a predetermined interval until a stop condition is met. Consider,

import { Poll } from "restful-react"

<Poll path="/deployLogs" resolve={data => data && data.data}>
  {(deployLogs: DeployLog[], { loading }) =>
    loading ? (
      <PageSpinner />
    ) : (
      <DataTable
        columns={["createdAt", "deployId", "status", "sha", "message"]}
        orderBy="createdAt"
        data={deployLogs}
        formatters={{
          createdAt: (d: DeployLog["createdAt"]) => title(formatRelative(d, Date.now())),
          sha: (i: DeployLog["sha"]) => i && i.slice(0, 7),
        }}
      />
    )
  }
</Poll>

Note the API similarities that we have already uncovered. In essence, Poll, Get and Mutate have near-identical APIs, allowing developers to quickly swap out <Get /> for <Poll /> calls and have the transition happen seamlessly. This is powerful in the world of an ever-changing startup that may have volatile requirements.

In addition to the Get component API, Poll also supports:

  • an interval prop that will poll at a specified interval (defaults to polling 1 second), and
  • an until prop that accepts a condition expressed as a function that returns a boolean value. When this condition is met, polling will stop.
    • the signature of this function is (data: T, response: Response) => boolean. As a developer, you have access to the returned data, along with the response object in case you'd like to stop polling if response.ok === false, for example.

Below is a more convoluted example that employs nearly the full power of the Poll component.

<Poll path="/status" until={(_, response) => response && response.ok} interval={0} lazy>
  {(_, { loading, error, finished, polling }, { start }) => {
    return loading ? (
      <Progress error={error} />
    ) : (
      <Button
        loading={editorLoading || polling}
        condensed
        icon="ExternalLink"
        color="ghost"
        onClick={() => {
          if (finished) {
            return window.open(editor.url);
          }
          requestEditor();
          start();
        }}
      >
        {finished ? "Launch Editor" : "Request Editor"}
      </Button>
    );
  }}
</Poll>

Note from the previous example, Poll also exposes more states: finished, and polling that allow better flow control, as well as lazy-start polls that can also be programatically stopped at a later stage.

Long Polling

At Contiamo, we have a powerful Long Polling specification in place that allows us to build real-time apps over HTTPS, as opposed to WebSockets. At a glance the specification can be distilled into:

  • Web UI sends a request with a Prefer header that contains:
    • a time, in seconds, to keep requests open (60s), and
    • a polling index that is a server-sent hash ahpiegh.
    • all together, the client sends a request with a header Prefer: wait=60s;index=939192.
  • The backend server responds, either with:
    • an empty response with status 304 Not Modified
    • a successful response with data and a new polling index.

The polling index allow the client and the server to stay in sync: the client says "the last stuff I got was at this index". The server says "oh, let me get you up to speed and send you a new index".

Visually, this is represented as below.

Contiamo Poll.

To get this functionality in Restful React, it is as simple as specifying a wait prop on your Poll component, provided your server implements the specification as well.

Full Poll Component API

Code Generation

Restful React is able to generate type-safe React components from any valid OpenAPI v3 or Swagger v2 specification, either in yaml or json formats.

Usage

Type-safe React data fetchers can be generated from an OpenAPI specification using the following command:

  • restful-react import --file MY_OPENAPI_SPEC.yaml --output my-awesome-generated-types.d.tsx

This command can be invoked by either:

  • Installing restful-react globally and running it in the terminal: npm i -g restful-react, or
  • Adding a script to your package.json like so:
      "scripts": {
        "start": "webpack-dev-server",
        "build": "webpack -p",
+       "generate-fetcher": "restful-react import --file MY_SWAGGER_DOCS.json --output FETCHERS.tsx"
      }

Your components can then be generated by running npm run generate-fetcher. Optionally, we recommend linting/prettifying the output for readability like so:

      "scripts": {
        "start": "webpack-dev-server",
        "build": "webpack -p",
        "generate-fetcher": "restful-react import --file MY_SWAGGER_DOCS.json --output FETCHERS.tsx",
+       "postgenerate-fetcher": "prettier FETCHERS.d.tsx --write"
      }

Validation of the specification

To enforce the best quality as possible of specification, we have integrate the amazing open-api linter from ibm (OpenAPI Validator). We strongly encourage you to setup your custom rules with a .validaterc file, you can find all useful information about this configuration here.

If it's too noisy, you don't have the time or can't control the open-api specification: just add --no-validation flag to the command and this validation step will be skipped :wink:

Import from GitHub

Adding the --github flag to restful-react import instead of a --file allows us to create React components from an OpenAPI spec remotely hosted on GitHub. (how is this real life 🔥 )

To generate components from remote specifications, you'll need to follow the following steps:

  1. Visit your GitHub settings.
  2. Click Generate New Token and choose the following:

    Token Description: (enter anything, usually your computer name)
    Scopes:
        [X] repo
            [X] repo:status
            [X] repo_deployment
            [X] public_repo
            [X] repo:invite
  3. Click Generate token.

  4. Copy the generated string.
  5. Open Terminal and run restful-react import --github username:repo:branch:path/to/openapi.yaml --output MY_FETCHERS.tsx.
  6. You will be prompted for a token.
  7. Paste your token.
  8. You will be asked if you'd like to save it for later. This is entirely up to you and completely safe: it is saved in your node_modules folder and not committed to version control or sent to us or anything: the source code of this whole thing is public so you're safe.

    Caveat: Since your token is stored in node_modules, your token will be removed on each npm install of restful-react.

  9. You're done! 🎉

Transforming an Original Spec

In some cases, you might need to augment an existing OpenAPI specification on the fly, for code-generation purposes. Our CLI makes this quite easy:

  restful-react import --file myspec.yaml --output mybettercomponents.tsx --transformer path/to/my-transformer.js

The function specified in --transformer is pure: it imports your --file, transforms it, and passes the augmented OpenAPI specification to Restful React's generator. Here's how it can be used:

// /path/to/my-transformer.js

/**
 * Transformer function for restful-react.
 *
 * @param {OpenAPIObject} schema
 * @return {OpenAPIObject}
 */
module.exports = inputSchema => ({
  ...inputSchema,
  // Place your augmentations here
  paths: Object.entries(schema.paths).reduce(
    (mem, [path, pathItem]) => ({
      ...mem,
      [path]: Object.entries(pathItem).reduce(
        (pathItemMem, [verb, operation]) => ({
          ...pathItemMem,
          [verb]: {
            ...fixOperationId(path, verb, operation),
          },
        }),
        {},
      ),
    }),
    {},
  ),
});

Caching

This doesn't exist yet. Feel free to contribute a solution here.

An LRU cache would be nice.

Contributing

All contributions are welcome – especially:

  • documentation,
  • bug reports and issues,
  • code contributions.

Code

If you'd like to actively develop or maintain this project, clone the repo and then yarn watch to get into dev mode. There are existing tests against which you can test the library. Typically, this looks like

  • git clone git@github.com:contiamo/restful-react.git
  • cd restful-react
  • yarn install
  • yarn test --watch

From there, you should be able to start developing without problems.

Dogfooding

This project works great when dogfooded: I'd suggest creating a separate project somewhere (or using an existing one), and using your fork in your project. To do so, after cloning and npm i,

  • npm link inside of the root folder of this project,
  • go to your consumer project,
  • npm link restful-react in there, and npm will link the packages.

You can now import Get from "restful-react" and do all the things you'd like to do, including test and develop new features for the project to meet your use case.

Next Steps

We're actively developing this at Contiamo to meet our use cases as they arise. If you have a use case that you'd like to implement, do it! Open an issue, submit a Pull Request, have fun! We're friendly.

15.9.4

4 years ago

15.9.3

4 years ago

15.9.2

5 years ago

15.9.0

5 years ago

15.8.0

5 years ago

15.7.0

5 years ago

15.5.0

5 years ago

15.5.1

5 years ago

15.6.0

5 years ago

15.4.2

5 years ago

15.4.1-g1d6980d

5 years ago

15.4.1

5 years ago

15.4.0

5 years ago

15.3.0

5 years ago

15.2.0

5 years ago

15.1.3

5 years ago

15.1.2

5 years ago

15.1.1

5 years ago

15.1.0

5 years ago

15.0.0

6 years ago

15.0.0-alpha

6 years ago

14.5.0

6 years ago

14.5.1

6 years ago

14.4.0

6 years ago

14.3.0

6 years ago

14.2.0

6 years ago

14.2.1

6 years ago

14.1.1

6 years ago

14.1.0

6 years ago

14.0.1

6 years ago

14.0.2

6 years ago

14.0.0

6 years ago

13.0.0

6 years ago

12.0.0

6 years ago

11.2.0

6 years ago

11.1.0

6 years ago

11.0.0

6 years ago

10.0.1

6 years ago

10.0.0

6 years ago

9.12.1

6 years ago

9.12.0

6 years ago

9.11.1

6 years ago

9.11.0

6 years ago

9.10.1

6 years ago

9.10.0

6 years ago

9.9.1

6 years ago

9.9.0

6 years ago

9.8.0

6 years ago

9.7.1-8-g1e2e40f

6 years ago

9.7.1-7-g2609a65

6 years ago

9.7.1-6-g0969004

6 years ago

9.7.1-3-gc0047cb

6 years ago

9.7.1

6 years ago

9.7.0

6 years ago

9.6.2

6 years ago

9.6.1

6 years ago

9.6.0

6 years ago

9.5.0

6 years ago

9.4.2

6 years ago

9.4.1

6 years ago

9.4.0

6 years ago

9.3.0

6 years ago

9.2.0-gb9a3f52

6 years ago

9.2.0

6 years ago

9.1.0

6 years ago

9.0.1-cache.3

7 years ago

9.0.1-cache.2

7 years ago

9.0.1-cache.1

7 years ago

9.0.1

7 years ago

9.0.0

7 years ago

8.1.4

7 years ago

8.1.3

7 years ago

8.1.2

7 years ago

8.1.1

7 years ago

8.1.0

7 years ago

8.0.0

7 years ago

7.6.4

7 years ago

7.6.3

7 years ago

7.6.2

7 years ago

7.6.1

7 years ago

7.6.0

7 years ago

7.3.1

7 years ago

7.5.0

7 years ago

7.4.0

7 years ago

7.3.0

7 years ago

7.2.4

7 years ago

7.2.3

7 years ago

7.2.2

7 years ago

7.2.1

7 years ago

7.2.0

7 years ago

7.2.0-5feeea4

7 years ago

7.1.1

7 years ago

7.1.0

7 years ago

7.0.1

7 years ago

7.0.1-0

7 years ago

7.0.0

7 years ago

6.2.0-openapi-16

7 years ago

6.2.0-openapi-15

7 years ago

6.2.0-openapi-14

7 years ago

6.2.0-openapi-13

7 years ago

6.2.0-openapi-12

7 years ago

6.2.0-openapi-11

7 years ago

6.2.0-openapi-10

7 years ago

6.2.0-openapi-9

7 years ago

6.2.0-openapi-8

7 years ago

6.2.0-openapi-7

7 years ago

6.2.0-openapi-6

7 years ago

6.2.0-openapi-5

7 years ago

6.2.0-openapi-4

7 years ago

6.2.0-openapi-3

7 years ago

6.2.0-openapi-2

7 years ago

6.2.0-openapi-1

7 years ago

6.1.1

7 years ago

6.2.0-openapi-0

7 years ago

6.1.0

7 years ago

6.0.2

7 years ago

6.0.1

7 years ago

6.0.1-1

7 years ago

6.0.0

7 years ago

5.2.1

8 years ago

5.2.0

8 years ago

5.1.1

8 years ago

5.1.0

8 years ago

5.0.1

8 years ago

5.0.0

8 years ago

4.1.3

8 years ago

4.1.3-0

8 years ago

4.1.2

8 years ago

4.1.1

8 years ago

4.1.0

8 years ago

4.0.2

8 years ago

4.0.1

8 years ago

4.0.0

8 years ago

4.0.0-10

8 years ago

4.0.0-9

8 years ago

4.0.0-8

8 years ago

4.0.0-7

8 years ago

4.0.0-6

8 years ago

4.0.0-5

8 years ago

4.0.0-4

8 years ago

4.0.0-3

8 years ago

3.0.4

8 years ago

4.0.0-2

8 years ago

4.0.0-1

8 years ago

3.0.3

8 years ago

3.1.0

8 years ago

3.0.2

8 years ago

3.0.1

8 years ago