Security News
GitHub Removes Malicious Pull Requests Targeting Open Source Repositories
GitHub removed 27 malicious pull requests attempting to inject harmful code across multiple open source repositories, in another round of low-effort attacks.
react-lazy-data
Advanced tools
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+.
There are two APIs:
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 );
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>;
}
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.
(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.
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.
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.
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.
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.
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`
This module follows semver. Breaking changes will only be made in major version updates.
Use npm test
to run the tests. Use npm run cover
to check coverage.
See changelog.md
If you discover a bug, please raise an issue on Github. https://github.com/overlookmotel/react-lazy-data/issues
Pull requests are very welcome. Please:
FAQs
Lazy-load data with React Suspense
We found that react-lazy-data demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
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.
Security News
GitHub removed 27 malicious pull requests attempting to inject harmful code across multiple open source repositories, in another round of low-effort attacks.
Security News
RubyGems.org has added a new "maintainer" role that allows for publishing new versions of gems. This new permission type is aimed at improving security for gem owners and the service overall.
Security News
Node.js will be enforcing stricter semver-major PR policies a month before major releases to enhance stability and ensure reliable release candidates.