Giter Site home page Giter Site logo

adexot / react-loads Goto Github PK

View Code? Open in Web Editor NEW

This project forked from jxom/react-loads

0.0 1.0 0.0 2.89 MB

A zero-dependency React utility to help with promise state & response data.

Home Page: https://jxom.github.io/react-loads/

License: MIT License

JavaScript 48.78% TypeScript 51.22%

react-loads's Introduction

React Loads

A zero-dependency React utility to help with promise state & response data.

The problem

There are a few concerns in managing async data fetching manually:

  • Managing loading state can be annoying and prone to a confusing user experience if you aren't careful.
  • Managing data persistence across page transitions can be easily overlooked.
  • Flashes of loading state & no feedback on something that takes a while to load can be annoying.
  • Nested ternaries can get messy and hard to read. Example:
<Fragment>
  {isPending ? (
    <p>{hasTimedOut ? 'Taking a while...' : 'Loading...'}</p>
  ) : (
    <Fragment>
      {!error && !response && <button onClick={this.handleLoad}>Click here to load!</button>}
      {response && <p>{response}</p>}
      {error && <p>{error.message}</p>}
    </Fragment>
  )}
</Fragment>

The solution

React Loads comes with a handy set of features to help solve these concerns:

  • Manage your async data & states with a declarative syntax with React Hooks or Render Props
  • Predictable outcomes with deterministic state variables or state components to avoid messy state ternaries
  • Invoke your loading function on initial render and/or on demand
  • Pass any type of promise to your loading function
  • Add a delay to prevent flashes of loading state
  • Add a timeout to provide feedback when your loading function is taking a while to resolve
  • Data caching enabled by default to maximise user experience between page transitions
  • Tell Loads how to load your data from the cache to prevent unnessessary invocations
  • External cache support to enable something like local storage caching
  • Optimistic responses to update your UI optimistically

Table of contents

Installation

npm install react-loads --save

or install with Yarn if you prefer:

yarn add react-loads

Usage

FIRSTLY

Wrap your app in a <LoadsContext.Provider>:

import React from 'react';
import ReactDOM from 'react-dom';
import { LoadsContext } from 'react-loads';

ReactDOM.render(
  <LoadsContext.Provider>
    {/* ... */}
  <LoadsContext.Provider>
)

With Hooks

See the useLoads API

import React from 'react';
import { useLoads } from 'react-loads';

export default function DogApp() {
  const getRandomDog = React.useCallback(() => {
    return axios.get('https://dog.ceo/api/breeds/image/random')
  }, []);
  const { response, error, load, isRejected, isPending, isResolved } = useLoads(getRandomDog);

  return (
    <div>
      {isPending && <div>loading...</div>}
      {isResolved && (
        <div>
          <div>
            <img src={response.data.message} width="300px" alt="Dog" />
          </div>
          <button onClick={load}>Load another</button>
        </div>
      )}
      {isRejected && <div type="danger">{error.message}</div>}
    </div>
  );
}

IMPORTANT NOTE: You must provide useLoads with a memoized promise (via React.useCallback or bounded outside of your function component), otherwise useLoads will be invoked on every render.

If you are using React.useCallback, the react-hooks ESLint Plugin is incredibly handy to ensure your hook dependencies are set up correctly.

Usage with state components

You can also use state components to conditionally render children. Learn more

import React from 'react';
import { useLoads } from 'react-loads';

export default function DogApp() {
  const getRandomDog = React.useCallback(() => {
    return axios.get('https://dog.ceo/api/breeds/image/random')
  }, []);
  const { response, error, load, Pending, Resolved, Rejected } = useLoads(getRandomDog);

  return (
    <div>
      <Pending>
        <div>loading...</div>
      </Pending>
      <Resolved>
        <div>
          <div>
            {response && <img src={response.data.message} width="300px" alt="Dog" />}
          </div>
          <button onClick={load}>Load another</button>
        </div>
      </Resolved>
      <Rejected>
        <div type="danger">{error.message}</div>
      </Rejected>
      <Resolved or={[Pending, Rejected]}>
        This will show when the state is pending, resolved or rejected.
      </Resolved>
    </div>
  );
}

With Render Props

See the <Loads> API

import React from 'react';
import { useLoads } from 'react-loads';

class DogApp extends React.Component {
  getRandomDog = () => {
    return axios.get('https://dog.ceo/api/breeds/image/random');
  }

  render = () => {
    return (
      <Loads load={this.getRandomDog}>
        {({ response, error, load, isRejected, isPending, isResolved }) => (
          <div>
            {isPending && <div>loading...</div>}
            {isResolved && (
              <div>
                <div>
                  <img src={response.data.message} width="300px" alt="Dog" />
                </div>
                <button onClick={load}>Load another</button>
              </div>
            )}
            {isRejected && <div type="danger">{error.message}</div>}
          </div>
        )}
      </Loads>
    )
  }
}

Usage with state components

You can also use state components to conditionally render children. Learn more

import React from 'react';
import { useLoads } from 'react-loads';

class DogApp extends React.Component {
  getRandomDog = () => {
    return axios.get('https://dog.ceo/api/breeds/image/random');
  }

  render = () => {
    return (
      <Loads load={this.getRandomDog}>
        <Loads.Pending>Loading...</Loads.Pending>
        <Loads.Resolved>
          {({ response, load }) => (
            <div>
              <div>
                <img src={response.data.message} width="300px" alt="Dog" />
              </div>
              <button onClick={load}>Load another</button>
            </div>
          )}
        </Loads.Resolved>
        <Loads.Rejected>
          {({ error }) => (
            <div>{error.message}</div>
          )}
        </Loads.Rejected>
      </Loads>
    )
  }
}

See a demo

More examples

loader = useLoads(load[, config[, inputs]])

returns an object (loader)

load

function(...args, { cachedRecord, setResponse, setError }) | returns Promise | required

The function to invoke. It must return a promise.

The argument cachedRecord is the stored record in the cache (if exists). It uses the context option to retrieve the cache record.

The arguments setResponse & setError are optional and are used for optimistic responses. Read more on optimistic responses.

config

defer

boolean | default: false

By default, the loading function will be invoked on initial render. However, if you want to defer the loading function (call the loading function at another time), then you can set defer to true.

If defer is set to true, the initial loading state will be "idle".

Example:

const getRandomDog = React.useCallback(() => axios.get('https://dog.ceo/api/breeds/image/random'), []);
const { response, error, load, Pending, Resolved, Rejected } = useLoads(getRandomDog, { defer: true });

return (
  <div>
    <Idle>
      <button onClick={load}>Load dog</button>
    </Idle>
    <Pending>
      <div>loading...</div>
    </Pending>
    <Resolved>
      <div>
        <div>
          {response && <img src={response.data.message} width="300px" alt="Dog" />}
        </div>
        <button onClick={load}>Load another</button>
      </div>
    </Resolved>
  </div>
);

delay

number | default: 300

Number of milliseconds before the component transitions to the 'pending' state upon invoking load.

context

string

Unique identifier for the promise (load). Enables the ability to persist the response data. If context changes, then load will be invoked again.

timeout

number | default: 0

Number of milliseconds before the component transitions to the 'timeout' state. Set to 0 to disable.

Note: load will still continue to try an resolve while in the 'timeout' state

loadPolicy

"cache-first" | "cache-and-load" | "load-only" | default: "cache-and-load"

A load policy allows you to specify whether or not you want your data to be resolved from the Loads cache and how it should load the data.

  • "cache-first": If a value for the promise already exists in the Loads cache, then Loads will return the value that is in the cache, otherwise it will invoke the promise.

  • "cache-and-load": This is the default value and means that Loads will return with the cached value if found, but regardless of whether or not a value exists in the cache, it will always invoke the promise.

  • "load-only": This means that Loads will not return the cached data altogether, and will only return the data resolved from the promise.

enableBackgroundStates

boolean | default: false

If true and the data is in cache, isIdle, isPending and isTimeout will be evaluated on subsequent loads. When false (default), these states are only evaluated on initial load and are falsy on subsequent loads - this is helpful if you want to show the cached response and not have a idle/pending/timeout indicator when load is invoked again. You must have a context set to enable background states as it only effects data in the cache.

cacheProvider

Object({ get: function(key), set: function(key, val) })

Set a custom cache provider (e.g. local storage, session storate, etc). See external cache below for an example.

update

function(...args, { setResponse, setError }) | returns Promise | Array<Promise>

A function to update the response from load. It must return a promise. Think of update like a secondary load, which has a different way of fetching/loading data.

IMPORTANT NOTE ON update: It is recommended that your update function resolves with the same response schema as your loading function (load) to avoid erroneous & confusing behaviour in your UI.

Read more on the update function here.

loader

response

any

Response from the resolved promise (load).

error

any

Error from the rejected promise (load).

load

function(...args, { setResponse, setError }) | returns Promise

Trigger to invoke load.

The arguments setResponse & setError are optional and are used for optimistic responses. Read more on optimistic responses.

update

function(...args, { setResponse, setError }) or Array<function(...args, { setResponse, setError })>

Trigger to invoke [update(#update)]

isIdle

boolean

Returns true if the state is idle (load has not been triggered).

isPending

boolean

Returns true if the state is pending (load is in a pending state).

isTimeout

boolean

Returns true if the state is timeout (load is in a pending state for longer than delay milliseconds).

isResolved

boolean

Returns true if the state is resolved (load has been resolved).

isRejected

boolean

Returns true if the state is rejected (load has been rejected).

Idle

ReactComponent

Renders it's children when the state is idle.

See here for an example

Pending

ReactComponent

Renders it's children when the state is pending.

See here for an example

Timeout

ReactComponent

Renders it's children when the state is timeout.

See here for an example

Resolved

ReactComponent

Renders it's children when the state is resolved.

See here for an example

Rejected

ReactComponent

Renders it's children when the state is rejected.

See here for an example

isCached

boolean

Returns true if data exists in the cache.

<Loads> Props

The <Loads> props mimics the useLoads arguments and it's config attributes.

load

See here

inputs

You can optionally pass an array of inputs (or an empty array), which <Loads> will use to determine whether or not to load the loading function. If any of the values in the inputs array change, then it will reload the loading function.

defer

See here

delay

See here

context

See here

timeout

See here

loadPolicy

See here

enableBackgroundStates

See here

cacheProvider

See here

update

See here

<Loads> Render Props

The <Loads> render props mimics the useLoads' loader.

Note: <Loads.Idle>, <Loads.Pending>, <Loads.Timeout>, <Loads.Resolved> and <Loads.Rejected> share the same render props as <Loads>.

response

See here

error

See here

load

See here

update

See here

isIdle

See here

isPending

See here

isTimeout

See here

isResolved

See here

isRejected

See here

Idle

See here

Pending

See here

Timeout

See here

Resolved

See here

Rejected

See here

isCached

See here

cache = useLoadsCache(context)

returns an object (cache)

context

string

The context key of the record to retrieve from cache.

cache

Object

The cached record.

Example

export default function DogApp() {
  const dogRecord = useLoadsCache('dog');
  // dogRecord = { response: { ... }, error: undefined, isIdle: false, isPending: false, isResolved, true, ... }
  
  // ...
}

Caching response data

Basic cache

React Loads has the ability to cache the response and error data. The cached data will persist while the application is mounted, however, will clear when the application is unmounted (on page refresh or window close). Here is an example to use it:

import React from 'react';
import { useLoads } from 'react-loads';

export default function DogApp() {
  const getRandomDog = React.useCallback(() => axios.get('https://dog.ceo/api/breeds/image/random'), []);
  const { response, error, load, Pending, Resolved, Rejected } = useLoads(getRandomDog, { context: 'random-dog' });

  return (
    <div>
      <Pending>
        <div>loading...</div>
      </Pending>
      <Resolved>
        <div>
          <div>
            {response && <img src={response.data.message} width="300px" alt="Dog" />}
          </div>
          <button onClick={load}>Load another</button>
        </div>
      </Resolved>
      <Rejected>
        <div type="danger">{error.message}</div>
      </Rejected>
    </div>
  );
}

External cache

Global cache provider

If you would like the ability to persist response data upon unmounting the application (e.g. page refresh or closing window), a cache provider can also be utilised to cache response data.

Here is an example using Store.js and setting the cache provider on an application level using <LoadsContext.Provider>. If you would like to set a cache provider on a hooks level with useLoads, see Local cache provider.

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { LoadsContext } from 'react-loads';

const cacheProvider = {
  get: key => store.get(key),
  set: (key, val) => store.set(key, val)
}

ReactDOM.render(
  <LoadsContext.Provider cacheProvider={cacheProvider}>
    {/* ... */}
  </LoadsContext.Provider>
)

Local cache provider

A cache provider can also be specified on a component level with useLoads. If a cacheProvider is provided to useLoads, it will override the global cache provider if one is already set.

import React from 'react';
import { useLoads } from 'react-loads';

const cacheProvider = {
  get: key => store.get(key),
  set: (key, val) => store.set(key, val)
}

export default function DogApp() {
  const getRandomDog = React.useCallback(() => axios.get('https://dog.ceo/api/breeds/image/random'), []);
  const { response, error, load, Pending, Resolved, Rejected } = useLoads(getRandomDog, {
    cacheProvider,
    context: 'random-dog'
  });

  return (
    <div>
      <Pending>
        <div>loading...</div>
      </Pending>
      <Resolved>
        <div>
          <div>
            {response && <img src={response.data.message} width="300px" alt="Dog" />}
          </div>
          <button onClick={load}>Load another</button>
        </div>
      </Resolved>
      <Error>
        <div type="danger">{error.message}</div>
      </Error>
    </div>
  );
}

Optimistic responses

React Loads has the ability to optimistically update your data while it is still waiting for a response (if you know what the response will potentially look like). Once a response is received, then the optimistically updated data will be replaced by the response. This article explains the gist of optimistic UIs pretty well.

The setResponse and setError functions are provided as the last argument of your loading function (load). The interface for these functions, along with an example implementation are seen below.

setResponse(data[, opts[, callback]]) / setError(data[, opts[, callback]])

Optimistically sets a successful response or error.

data

Object or function(currentData) {} | required

The updated data. If a function is provided, then the first argument will be the current loaded (or cached) data.

opts

Object{ context }

opts.context

string | optional

The context where the data will be updated. If not provided, then it will use the context prop specified in useLoads. If a context is provided, it will update the responses of all useLoads using that context immediately.

callback

function(currentData) {}

A callback can be also provided as a second or third parameter to setResponse, where the first and only parameter is the current loaded (or cached) response (currentData).

Basic example

import React from 'react';
import { useLoads } from 'react-loads';

export default function DogApp() {
  const getRandomDog = React.useCallback(({ setResponse }) => {
    setResponse({ data: { message: 'https://images.dog.ceo/breeds/doberman/n02107142_17147.jpg' } })
    return axios.get('https://dog.ceo/api/breeds/image/random');
  }, []);
  const { response, error, load, isRejected, isPending, isResolved } = useLoads(getRandomDog);

  return (
    <div>
      {isPending && <div>loading...</div>}
      {isResolved && (
        <div>
          <div>
            <img src={response.data.message} width="300px" alt="Dog" />
          </div>
          <button onClick={load}>Load another</button>
        </div>
      )}
      {isRejected && <div type="danger">{error.message}</div>}
    </div>
  );
}

Example updating another useLoads optimistically

import React from 'react';
import { useLoads } from 'react-loads';

export default function DogApp() {
  const createDog = React.useCallback(async (dog, { setResponse }) => {
    setResponse(dog, { context: 'dog' });
    // ... - create the dog
  }, [])
  const createDogLoader = useLoads(createDog, { defer: true });

  const getDog = React.useCallback(async () {
    // ... - fetch and return the dog
  }, []);
  const getDogLoader = useLoads(getDog, { context: 'dog' });

  return (
    <React.Fragment>
      <button onClick={() => createDogLoader.load({ name: 'Teddy', breed: 'Groodle' })}>Create</button>
      {getDogLoader.response && <div>{getDogLoader.response.name}</div>}
    </React.Fragment>
  )
}

Updating resources

Instead of using multiple useLoads's to provide a way to update/amend a resource, you are able to specify an update function which mimics the load function. In order to use the update function, you must have a load function which shares the same response schema as your update function.

Here's an example of how you could use an update function:

import React from 'react';
import { useLoads } from 'react-loads';

export default function DogApp() {
  const getRandomDog = React.useCallback(() => axios.get('https://dog.ceo/api/breeds/image/random'), []);
  const getRandomDoberman = React.useCallback(() => axios.get('https://dog.ceo/api/breed/doberman/images/random'), []);
  const getRandomPoodle = React.useCallback(() => axios.get('https://dog.ceo/api/breed/poodle/images/random'), []);
  const {
    response,
    load,
    update: [loadDoberman, loadPoodle],
    isPending,
    isResolved
  } = useLoads(getRandomDog, {
    update: [getRandomDoberman, getRandomPoodle]
  });

  return (
    <div>
      {isPending && 'Loading...'}
      {isResolved && (
        <div>
          <div>
            <img src={response.data.message} width="300px" alt="Dog" />
          </div>
          <button onClick={load}>Load another random dog</button>
          <button onClick={loadDoberman}>Load doberman</button>
          <button onClick={loadPoodle}>Load poodle</button>
        </div>
      )}
    </div>
  );
}

Articles

Happy customers

  • "I'm super excited about this package" - Michele Bertoli
  • "Love the API! And that nested ternary-boolean example is a perfect example of how messy React code commonly gets without structuring a state machine." - David K. Piano
  • "Using case statements with React components is comparable to getting punched directly in your eyeball by a giraffe. This is a huge step up." - Will Hackett

License

MIT © jxom

react-loads's People

Contributors

jxom avatar thepenskefile avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.