15.9.4 • Published 2 years ago

restful-react v15.9.4

Weekly downloads
10,485
License
MIT
Repository
github
Last release
2 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 single 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.

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.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

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,

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 = () => (
  <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.
   */
  requestOptions?: Partial<RequestInit>;
}

// Usage
<RestfulProvider base="String!" resolve={data => data} requestOptions={{}} />;

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

Composability

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

// Assuming we're using a RestfulProvider with base={HOST} somewhere,
<Get path="/cats">
  {data => {
    return (
      <div>
        <h1>Here are my cats!</h1>
        {data.map(cat => <img alt={cat.name} src={cat.photoUrl} />)}

        {/* Request BASE/cats/persian */}
        <Get path="/persian">
          {persianCats => {
            return (
              <div>
                <h2>Here are my persian cats!</h2>
                {persianCats.map(cat => <img alt={cat.name} src={cat.photoUrl} />)}
              </div>
            );
          }}
        </Get>
      </div>
    );
  }}
</Get>

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

Loading and Error States

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

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>
);

Mutations

Get components pass mutation functions as the third argument to their children. Consider,

const Movies = ({ dispatch }) => (
  <ul>
    <Get path="/movies">
      {(movies, states, actions) =>
        movies.map(movie => (
          <li>
            {movie.name}

            {/* Will send a DELETE request to BASE/movies/:movie.id */}
            <button
              onClick={_ =>
                actions
                  .delete(movie.id)
                  .then(returnedData => dispatch({ type: "DELETED_MOVIE", payload: returnedData }))
              }
            >
              Delete!
            </button>
          </li>
        ))
      }
    </Get>
  </ul>
);

The same mutation objects exist for all HTTP verbs, including get, post, put, and patch. Methods post, put, and patch all expect a body as their first argument, and all mutation functions receive requestOptions as their optional second argument.

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.

Mutations API

Here are the functions passed as the second argument to children of Get with their signatures.

interface Mutations<T> {
  get: (path?: string, requestOptions?: Partial<RequestInit>) => Promise<T | null>;
  destroy: (id?: string, requestOptions?: Partial<RequestInit>) => Promise<T | null>;
  post: (data?: string, requestOptions?: Partial<RequestInit>) => Promise<T | null>;
  put: (data?: string, requestOptions?: Partial<RequestInit>) => Promise<T | null>;
  patch: (data?: string, requestOptions?: Partial<RequestInit>) => Promise<T | null>;
}

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,

<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,

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>
);

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

Polling

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 and Get 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.

Poll API

Below is the full Poll component API.

interface Poll<T> {
  /**
   * What path are we polling on?
   */
  path: GetComponentProps<T>["path"];
  /**
   * A function that gets polled data, the current
   * states, meta information, and various actions
   * that can be executed at the poll-level.
   */
  children: (data: T | null, states: States<T>, actions: Actions, meta: Meta) => React.ReactNode;
  /**
   * How long do we wait between requests?
   * Value in milliseconds.
   * Defaults to 1000.
   */
  interval?: number;
  /**
   * A stop condition for the poll that expects
   * a boolean.
   *
   * @param data - The data returned from the poll.
   * @param response - The full response object. This could be useful in order to stop polling when !response.ok, for example.
   */
  until?: (data: T | null, response: Response | null) => boolean;
  /**
   * Are we going to wait to start the poll?
   * Use this with { start, stop } actions.
   */
  lazy?: GetComponentProps<T>["lazy"];
  /**
   * Should the data be transformed in any way?
   */
  resolve?: GetComponentProps<T>["resolve"];
  /**
   * We can request foreign URLs with this prop.
   */
  base?: GetComponentProps<T>["base"];
  /**
   * Any options to be passed to this request.
   */
  requestOptions?: GetComponentProps<T>["requestOptions"];
}

/**
 * Actions that can be executed within the
 * component.
 */
interface Actions {
  start: () => void;
  stop: () => void;
}

/**
 * States of the current poll
 */
interface States<T> {
  /**
   * Is the component currently polling?
   */
  polling: boolean;
  /**
   * Is the initial request loading?
   */
  loading: boolean;
  /**
   * Has the poll concluded?
   */
  finished: boolean;
  /**
   * Is there an error? What is it?
   */
  error?: string;
}

/**
 * Meta information returned from the poll.
 */
interface Meta extends GetComponentMeta {
  /**
   * The entire response object.
   */
  response: Response | null;
}

Caching

This doesn't exist yet. Please contribute a solution here until something happens.

There's a general idea of checking if the results are a collection or a resource, and then:

  • If collection, cache.
  • If resource,

    • Is resource in cached collection?
      • update cached resource.

Contributing

If you'd like to actively develop or maintain this project, clone the repo and then yarn watch to get into dev mode. 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

2 years ago

15.9.3

2 years ago

15.9.2

3 years ago

15.9.0

3 years ago

15.8.0

3 years ago

15.7.0

3 years ago

15.5.0

3 years ago

15.5.1

3 years ago

15.6.0

3 years ago

15.4.2

3 years ago

15.4.1-g1d6980d

3 years ago

15.4.1

3 years ago

15.4.0

3 years ago

15.3.0

3 years ago

15.2.0

3 years ago

15.1.3

3 years ago

15.1.2

3 years ago

15.1.1

3 years ago

15.1.0

3 years ago

15.0.0

3 years ago

15.0.0-alpha

3 years ago

14.5.0

4 years ago

14.5.1

4 years ago

14.4.0

4 years ago

14.3.0

4 years ago

14.2.0

4 years ago

14.2.1

4 years ago

14.1.1

4 years ago

14.1.0

4 years ago

14.0.1

4 years ago

14.0.2

4 years ago

14.0.0

4 years ago

13.0.0

4 years ago

12.0.0

4 years ago

11.2.0

4 years ago

11.1.0

4 years ago

11.0.0

4 years ago

10.0.1

4 years ago

10.0.0

4 years ago

9.12.1

4 years ago

9.12.0

4 years ago

9.11.1

4 years ago

9.11.0

4 years ago

9.10.1

4 years ago

9.10.0

4 years ago

9.9.1

4 years ago

9.9.0

4 years ago

9.8.0

4 years ago

9.7.1-8-g1e2e40f

4 years ago

9.7.1-7-g2609a65

4 years ago

9.7.1-6-g0969004

4 years ago

9.7.1-3-gc0047cb

4 years ago

9.7.1

4 years ago

9.7.0

4 years ago

9.6.2

4 years ago

9.6.1

4 years ago

9.6.0

4 years ago

9.5.0

4 years ago

9.4.2

4 years ago

9.4.1

4 years ago

9.4.0

4 years ago

9.3.0

4 years ago

9.2.0-gb9a3f52

4 years ago

9.2.0

4 years ago

9.1.0

4 years ago

9.0.1-cache.3

5 years ago

9.0.1-cache.2

5 years ago

9.0.1-cache.1

5 years ago

9.0.1

5 years ago

9.0.0

5 years ago

8.1.4

5 years ago

8.1.3

5 years ago

8.1.2

5 years ago

8.1.1

5 years ago

8.1.0

5 years ago

8.0.0

5 years ago

7.6.4

5 years ago

7.6.3

5 years ago

7.6.2

5 years ago

7.6.1

5 years ago

7.6.0

5 years ago

7.3.1

5 years ago

7.5.0

5 years ago

7.4.0

5 years ago

7.3.0

5 years ago

7.2.4

5 years ago

7.2.3

5 years ago

7.2.2

5 years ago

7.2.1

5 years ago

7.2.0

5 years ago

7.2.0-5feeea4

5 years ago

7.1.1

5 years ago

7.1.0

5 years ago

7.0.1

5 years ago

7.0.1-0

5 years ago

7.0.0

5 years ago

6.2.0-openapi-16

5 years ago

6.2.0-openapi-15

5 years ago

6.2.0-openapi-14

5 years ago

6.2.0-openapi-13

5 years ago

6.2.0-openapi-12

5 years ago

6.2.0-openapi-11

5 years ago

6.2.0-openapi-10

5 years ago

6.2.0-openapi-9

5 years ago

6.2.0-openapi-8

5 years ago

6.2.0-openapi-7

5 years ago

6.2.0-openapi-6

5 years ago

6.2.0-openapi-5

5 years ago

6.2.0-openapi-4

5 years ago

6.2.0-openapi-3

5 years ago

6.2.0-openapi-2

5 years ago

6.2.0-openapi-1

5 years ago

6.1.1

5 years ago

6.2.0-openapi-0

5 years ago

6.1.0

5 years ago

6.0.2

5 years ago

6.0.1

5 years ago

6.0.1-1

5 years ago

6.0.0

5 years ago

5.2.1

6 years ago

5.2.0

6 years ago

5.1.1

6 years ago

5.1.0

6 years ago

5.0.1

6 years ago

5.0.0

6 years ago

4.1.3

6 years ago

4.1.3-0

6 years ago

4.1.2

6 years ago

4.1.1

6 years ago

4.1.0

6 years ago

4.0.2

6 years ago

4.0.1

6 years ago

4.0.0

6 years ago

4.0.0-10

6 years ago

4.0.0-9

6 years ago

4.0.0-8

6 years ago

4.0.0-7

6 years ago

4.0.0-6

6 years ago

4.0.0-5

6 years ago

4.0.0-4

6 years ago

4.0.0-3

6 years ago

3.0.4

6 years ago

4.0.0-2

6 years ago

4.0.0-1

6 years ago

3.0.3

6 years ago

3.1.0

6 years ago

3.0.2

6 years ago

3.0.1

6 years ago