Redux-Bundler Async Resources
This package provides bundle factories for `redux-bundler`, specializing in the management of asynchronous data resources. It offers `createAsyncResourceBundle` for handling single remote resources and `createAsyncResourcesBundle` for managing collections of async resources, each with its own lifecycle, including loading, staleness, and expiration. Key features include configurable `staleAfter` and `expireAfter` durations to manage data freshness and automatic removal, a `dependencyKey` mechanism for conditional fetching and automatic cache invalidation based on upstream selector changes, and support for `doAdjust` actions to optimistically update resource state after mutations. The current stable version is 2.0.1. Releases appear to be ad-hoc, driven by new features or bug fixes, rather than a strict time-based cadence. It differentiates itself by providing a more robust and opinionated approach to async resource management within the `redux-bundler` ecosystem, extending beyond `redux-bundler`'s native capabilities.
Common errors
-
TypeError: Cannot read properties of undefined (reading 'fetchHotCarDeals')
cause The `shopApi` object or similar dependency required by `getPromise` was not injected into the `redux-bundler` context.fixPass the necessary API client or service into `composeBundles` when initializing the store, e.g., `composeBundles(myBundle, { getShopApi: () => shopApiInstance })`. -
ReferenceError: require is not defined
cause Attempting to use CommonJS `require()` syntax in an ESM-only context or a mixed environment not properly configured for CommonJS.fixUpdate imports to use ES Modules syntax: `import { createAsyncResourceBundle } from 'redux-bundler-async-resources';`. -
Error: A selector 'selectMyResourceNameIsPendingForFetch' could not be found. Check your bundle definition.
cause The `name` option in `createAsyncResourceBundle` does not match the expected selector name used in `createSelector` or `useConnect`, or the bundle itself is not correctly added to the root bundler.fixVerify the `name` property passed to `createAsyncResourceBundle` exactly matches the base name used in selectors (e.g., 'hotCarDeals' for `selectHotCarDealsIsPendingForFetch`). Ensure the bundle is included in your `composeBundles` call.
Warnings
- breaking Version 1.1.0 introduced a reimplementation of `createAsyncResourceBundle`. While aiming for 'same naming conventions, implementation logic, and same extra features', internal behavior or edge cases might have changed. Users upgrading from versions prior to 1.1.0 should thoroughly test their existing bundles.
- gotcha Misunderstanding `staleAfter` vs. `expireAfter` can lead to unexpected data persistence or removal. `staleAfter` marks data for refresh but keeps it, while `expireAfter` completely removes it from the store if not refreshed.
- gotcha The `dependencyKey` mechanism introduced in v1.2.0 will force-clear a resource bundle when its associated selector's value changes. If the dependency selector frequently changes for non-material reasons, it can lead to excessive re-fetching.
- gotcha Integration with `redux-bundler-async-resources-hooks` (especially for `v1.2.1`) requires specific versions. Mismatched versions between the two packages can lead to unexpected behavior or runtime errors.
- gotcha The `getPromise` function requires context parameters (e.g., `shopApi`). If these are not provided to your `redux-bundler` store (e.g., through `composeBundles({ getShopApi: () => myApi })`), the promise will fail to execute or throw an error.
Install
-
npm install redux-bundler-async-resources -
yarn add redux-bundler-async-resources -
pnpm add redux-bundler-async-resources
Imports
- createAsyncResourceBundle
const createAsyncResourceBundle = require('redux-bundler-async-resources').createAsyncResourceBundleimport { createAsyncResourceBundle } from 'redux-bundler-async-resources' - createAsyncResourcesBundle
import createAsyncResourcesBundle from 'redux-bundler-async-resources'
import { createAsyncResourcesBundle } from 'redux-bundler-async-resources' - makeAsyncResourceBundleKeys
import * as bundleKeys from 'redux-bundler-async-resources'
import { makeAsyncResourceBundleKeys } from 'redux-bundler-async-resources' - makeAsyncResourcesBundleKeys
import { makeAsyncResourcesBundleKeys } from 'redux-bundler-async-resources'
Quickstart
import { createSelector } from 'redux-bundler';
import { createAsyncResourceBundle } from 'redux-bundler-async-resources';
import React from 'react';
// Assume redux-bundler-hook is installed and configured for useConnect
import { useConnect } from 'redux-bundler-hook';
// Mock shopApi for demonstration purposes
const shopApi = {
fetchHotCarDeals: () => {
return new Promise(resolve => {
setTimeout(() => {
const deals = [{ id: 1, name: 'Sedan Deal', price: 25000 }, { id: 2, name: 'SUV Offer', price: 35000 }];
console.log('Fetched hot car deals:', deals);
resolve(deals);
}, 1500); // Simulate network delay
});
}
};
// bundles/hotCarDeals.js
const hotCarDealsBundle = {
...createAsyncResourceBundle({
name: 'hotCarDeals',
staleAfter: 180000, // refresh every 3 minutes
expireAfter: 60 * 60000, // delete if not refreshed in an hour
getPromise: ({ shopApi: apiContext }) => apiContext.fetchHotCarDeals(),
}),
reactShouldFetchHotCarDeals: createSelector(
'selectHotCarDealsIsPendingForFetch',
shouldFetch => {
if (shouldFetch) {
return { actionCreator: 'doFetchHotCarDeals' };
}
}
),
};
// Component usage example (HotCarDeals.js)
const ErrorMessage = ({ error }) => <div style={{ color: 'red' }}>Error: {error?.message}</div>;
const Spinner = () => <div>Loading...</div>;
const CarDealsList = ({ deals }) => (
<div>
<h3>Hot Car Deals</h3>
<ul>
{deals.map(deal => (
<li key={deal.id}>{deal.name}: ${deal.price}</li>
))}
</ul>
</div>
);
export default function HotCarDealsComponent() {
// In a real app, 'shopApi' would be part of your main bundle's context
const { hotCarDeals, hotCarDealsError } = useConnect(
'selectHotCarDeals',
'selectHotCarDealsError'
);
if (!hotCarDeals && hotCarDealsError) {
return <ErrorMessage error={hotCarDealsError} />;
}
if (!hotCarDeals) {
return <Spinner />;
}
return <CarDealsList deals={hotCarDeals} />;
}
// To run this:
// 1. Create a root bundler (e.g., composeBundles(hotCarDealsBundle, { getShopApi: () => shopApi }))
// 2. Wrap your app in <BundlerProvider bundler={store} />
// 3. Render <HotCarDealsComponent />