Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

react-lazy-data

Package Overview
Dependencies
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-lazy-data

Lazy-load data with React Suspense

  • 0.1.5
  • Source
  • npm
  • Socket score

Version published
Maintainers
1
Created
Source

NPM version Build Status Dependency Status Dev dependency Status Greenkeeper badge Coverage Status

Lazy-load data with React Suspense

What's it for?

React does not officially support using Suspense for data fetching. This package makes that possible.

NB Does not require experimental builds of React, or "concurrent mode". Tested and working on all versions of React 16.8.0+.

Usage

There are two APIs:

  1. Event-based
  2. Hooks

The moving parts

Resource Factory

A Resource Factory defines the method for fetching data.

You call the Factory's .create() or .use() method to initiate fetching data and create a Resource.

import { createResourceFactory } from 'react-lazy-data';

// Create Resource Factory
const PokemonResource = createResourceFactory(
  id =>
    fetch(`https://pokeapi.co/api/v2/pokemon/${id}`)
      .then(res => res.json())
);
// Create Resource
const pokemonResource = PokemonResource.create( 1 );
Resource

A Resource represents data being fetched. It can be pending, complete, or errored - much like a Promise.

Resources have a .read() method which a component can call to get the underlying data.

If the data is loaded, .read() returns the data. If the data is not ready yet, .read() throws a Promise which bails out of rendering the component and "suspends" the Suspense boundary above it. When the data is loaded, the Promise resolves which causes React to re-render the component. .read() will then return the data. If data-loading fails, .read() will throw the error that the fetch promise rejected with.

This may sound odd, but it's how Suspense works. React.lazy() works the same way - if the lazy component is not loaded yet, it throws a Promise which resolves when it is loaded.

So you can write a component that uses async data in a synchronous style:

function Pokemon( { pokemonResource } ) {
  // Read the resource
  const pokemon = pokemonResource.read();

  // This will only run once the data is ready
  return <div>My name is {pokemon.name}.</div>;
}

Event-based API

This is the approach recommended by React - load data in event handlers.

import { createResourceFactory } from 'react-lazy-data';

const PokemonResource = createResourceFactory(
  id =>
    fetch(`https://pokeapi.co/api/v2/pokemon/${id}`)
      .then(res => res.json())
);

function App() {
  const [id, setId] = useState( 1 );

  const resourceRef = useRef();
  const resource = resourceRef.current || createResource( id );

  function createResource( id ) {
    const resource = PokemonResource.create( id );
    resourceRef.current = resource;
    return resource;
  }

  const next = useCallback( () => {
    const previousResource = resourceRef.current;
    if ( previousResource ) previousResource.dispose();

    setId( id => {
      const newId = id + 1;
      createResource( newId );
      return newId;
    } );
  }, [] );

  return (
    <div>
      <Suspense fallback={ <div>Loading...</div> }>
        <Pokemon resource={ resource } />
      </Suspense>
      <button onClick={ next }>Next Pokemon!</button>
    </div>
  );
}

function Pokemon( { resource } ) {
  const pokemon = resource.read();
  return <div>My name is {pokemon.name}.</div>;
}

As you can see, it's not very ergonomic. You have to manually take care of disposing of the old resource when you're done with it, in the event handler.

Hooks API

(requires React 16.8.0+)

import { createResourceFactory } from 'react-lazy-data';

const PokemonResource = createResourceFactory(
  id =>
    fetch(`https://pokeapi.co/api/v2/pokemon/${id}`)
      .then(res => res.json())
);

function App() {
  const [id, setId] = useState( 1 );
  const resource = PokemonResource.use( id );

  return (
    <div>
      <Suspense fallback={ <div>Loading...</div> }>
        <Pokemon resource={ resource } />
      </Suspense>
      <button onClick={ () => setId( id => id + 1 ) }>Next Pokemon!</button>
    </div>
  );
}

function Pokemon( { resource } ) {
  const pokemon = resource.read();
  return <div>My name is {pokemon.name}.</div>;
}

.use() is a React hook.

The hooks-based API takes care of disposing old resources automatically.

Whenever the argument passed to .use() changes, it disposes the old resource and creates a new one, triggering loading the new data. If the component which called .use() is unmounted, the resource is disposed.

Note that the Pokemon component is exactly the same with either the event-based or hooks-based APIs. The difference is in how the Resource is created, not how it's used.

IMPORTANT: The one rule

Due to how React works, you must not call .use() and .read() in the same component. One component must create the resource with .use(), and then pass the resource to a 2nd component to render it.

// DON'T DO THIS - IT WON'T WORK
function Pokemon( { id } ) {
  const resource = PokemonResource.use( id );
  const pokemon = resource.read();
  return <div>My name is {pokemon.name}.</div>;
}
// This works
function Pokemon( { id } ) {
  const resource = PokemonResource.use( id );
  return <PokemonDisplay resource={ resource } />;
}

function PokemonDisplay( { resource } ) {
  const pokemon = resource.read();
  return <div>My name is {pokemon.name}.</div>;
}

You can use withResources() to remove some of the boilerplate code.

Wait a second! You're not meant to perform effects in the render phase.

Quite right!

It looks like .use() is doing this, but actually it's not. Internally, .use() performs the data loading inside a useEffect() hook. This is what allows it to automatically dispose of defunct resources.

Aborting data fetching

If you are changing the data you load frequently, you may want to abort fetch requests in flight if their results are no longer required.

If the promise returned by the fetch function has an .abort() method, it will be called when the resource is disposed.

e.g. Using fetch() and AbortController:

function abortableFetchJson( url ) {
  const abortController = new AbortController();

  const promise = fetch(
    url,
    { signal: abortController.signal }
  ).then( res => res.json() );

  promise.abort = () => abortController.abort();

  return promise;
}

// Create Resource Factory
const PokemonResource = createResourceFactory(
  id => abortableFetchJson(`https://pokeapi.co/api/v2/pokemon/${id}`)
);

// Create resource - fetching begins
const resource = PokemonResource.create( 1 );

// Dispose resource - fetch request is aborted
resource.dispose();

NB .dispose() does not call the Promise's .abort() method if the Promise has already resolved.

If you're using the hooks-based API, .use() takes care of disposal for you. All you need to do is make sure the promises returned by the createResourceFactory() fetch function have an .abort() method.

const PokemonResource = createResourceFactory(
  id => abortableFetchJson(`https://pokeapi.co/api/v2/pokemon/${id}`)
);

function App() {
  const [id, setId] = useState( 1 );
  const resource = PokemonResource.use( id );

  return (
    <div>
      <Suspense fallback={ <div>Loading...</div> }>
        <Pokemon resource={ resource } />
      </Suspense>
      <button onClick={ () => setId( id => id + 1 ) }>Next Pokemon!</button>
    </div>
  );
}

function Pokemon( { resource } ) {
  const pokemon = resource.read();
  return <div>My name is {pokemon.name}.</div>;
}

If you click "Next Pokemon!" before the previous data has finished loading, the old fetch is aborted before the next fetch commences. So you're not using bandwidth for data which won't be used.

NB The only thing which has changed from previous .use() example is in the fetch function passed to createResourceFactory(). Everything else is unchanged.

Caching

If two components may both require the same data concurrently, and call .create() or .use() with the same argument, you can ensure the fetch is only performed once - to save bandwidth and ensure data consistency.

To enable caching, pass a serialize option to createResourceFactory().

serialize should be a function which serializes the request argument to a string, which will act as the cache key. serialize: true will use the default serializer, JSON.stringify.

If .create() or .use() is called again with an argument which serializes to the same cache key, the resource which is returned from the 2nd call "follows" the original, rather than fetching again.

The cache lives only as long as active resources which are using it. Once all resources relating to a particular request are disposed, the cache is cleared. So a later call to .create() or .use() will fetch again, and get fresh data.

const Resource = createResourceFactory(
  id => fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then(res => res.json()),
  { serialize: true }
);

const resource1 = Resource.create( 1 );
// `fetch()` is called
const resource2 = Resource.create( 1 );
// `fetch()` is not called again - cache ensures no repeat calls

// Each resource is different,
// so they can be disposed individually
resource1 === resource2 // => false

// ...time passes, fetch completes...

// `.read()` returns same result on both resources
resource1.read() // => { id: 1, ... }
resource2.read() // => { id: 1, ... }

// Further calls to `.create()` or `.use()`
// return a resource pre-populated with cached result
const resource3 = Resource.create( 1 );
resource3.read() // => { id: 1, ... }

// Dispose all resources
resource1.dispose();
resource2.dispose();
resource3.dispose();
// Cache is now cleared

// Now fetch will be called again to get fresh data
const resource4 = Resource.create( 1 );
// `fetch()` is called again

withResources()

To avoid having to call .read(), you can instead wrap components which use Resources in withResources().

When a wrapped component is rendered, it checks if any of its props are Resources. If they are, it calls .read() on the Resources before rendering the original component. So then you can just use the data directly, rather than having to call .read() yourself.

Same example as above, but using withResources():

import { withResources } from 'react-lazy-data';

const PokemonDisplay = withResources(
  ( { pokemon } ) => <div>My name is {pokemon.name}.</div>
};

function Pokemon( { id } ) {
  const pokemonResource = PokemonResource.use( id );
  return <PokemonDisplay pokemon={ pokemonResource } />;
}

NB withResources() only does a shallow search of props for Resources. i.e. if props are { pokemon: resource }, the resource will be found and read. But if props are { myStuff: { pokemon: resource } }, it won't.

Usage with React.lazy()

withResources() should be used to wrap a component before it is wrapped with React.lazy().

// DON'T DO THIS
// It will work, but it'll delay loading the Lazy component
// until after data loads.
const Pokemon = withResources(
  React.lazy( () => import( './Pokemon.js' ) )
);
// This works better - Lazy component and data load concurrently

// App.js
const Pokemon = React.lazy( () => import( './Pokemon.js' ) );

// Pokemon.js
export default withResources(
  ( { pokemon } ) => <div>My name is {pokemon.name}.</div>
);

isResource()

Utility function to determine if a value is a Resource.

import { isResource } from 'react-lazy-data';

const resource = PokemonResource.create( 1 );

isResource( resource ) // => `true`
isResource( { foo: 'bar' } ) // => `false`

Versioning

This module follows semver. Breaking changes will only be made in major version updates.

Tests

Use npm test to run the tests. Use npm run cover to check coverage.

Changelog

See changelog.md

Issues

If you discover a bug, please raise an issue on Github. https://github.com/overlookmotel/react-lazy-data/issues

Contribution

Pull requests are very welcome. Please:

  • ensure all tests pass before submitting PR
  • add tests for new features
  • document new functionality/API additions in README
  • do not add an entry to Changelog (Changelog is created when cutting releases)

Keywords

FAQs

Package last updated on 05 Apr 2020

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc