v0.9: Collections, DevTools, and Legacy Cleanup
Collections are the highlight of this release - automatically managing list updates when creating, updating, or deleting entities. Combined with Resource.extend(), building CRUD operations has never been simpler.
const TodoResource = createResource({
path: '/todos/:id',
schema: Todo,
});
// POST /todos - automatically adds to all matching collections
ctrl.fetch(TodoResource.getList.push, { title: 'My new todo', userId });
We've also improved the developer experience with a devtools button that appears in development mode, better DevTools history, and new controller methods like controller.expireAll() and controller.fetchIfStale().
Breaking Changes:
- Remove all /next exports
- makeCacheProvider removed
- DELETE -> INVALIDATE action type
- Legacy schema support dropped
- Schema Serializers must support function calls
- Action types prefixed with 'rdc'
Collections
Collections automatically update when mutations occur, eliminating the need for
manual Endpoint.update. When you use createResource,
the getList endpoint automatically uses a Collection schema.
const TodoResource = createResource({
path: '/todos/:id',
schema: Todo,
});
// getList.push creates and adds to the collection
ctrl.fetch(TodoResource.getList.push, { title: 'My new todo', userId: 1 });
// getList.unshift adds to the beginning
ctrl.fetch(TodoResource.getList.unshift, { title: 'Priority todo', userId: 1 });
Collections intelligently match based on arguments, so a todo created with userId: 1 will only
appear in collections that were fetched with that same user filter.
Collection with FormData
Collections can filter based on FormData arguments. f95dbc6
ctrl.fetch(getPosts.push, { group: 'react' }, new FormData(e.currentTarget));
If the FormData contains an author field, the newly created item will be properly added to
collections filtered by that author.
Pagination with Collections
Use paginationField for easy pagination support. c8c557
const TodoResource = createResource({
path: '/todos/:id',
schema: Todo,
paginationField: 'page',
});
// Fetches page 2 and appends to the collection
ctrl.fetch(TodoResource.getList.getPage, { page: '2' });
Resource.extend()
Resource.extend() provides three powerful ways to customize resources. 51b4b0d
Add new endpoints
const UserResource = createResource({
path: '/users/:id',
schema: User,
}).extend('current', {
path: '/users/current',
});
Override existing endpoints
const CachedArticleResource = ArticleResource.extend({
getList: {
dataExpiryLength: 10 * 60 * 1000, // 10 minutes
},
});
Derive from base endpoints
const IssueResource = createResource({
path: '/issues/:id',
schema: Issue,
}).extend(Base => ({
byRepo: Base.getList.extend({
path: '/repos/:owner/:repo/issues',
}),
}));
Controller Methods
controller.expireAll()
Sets all matching responses to stale, triggering background refetch while showing existing data. #2802
// Mark all article lists as stale - they'll refetch when rendered
controller.expireAll(ArticleResource.getList);
This differs from invalidateAll() which removes the data entirely. expireAll() keeps showing
the cached data while fetching fresh data in the background.
controller.fetchIfStale()
Fetches only if data is considered stale; otherwise returns the cached data. #2804
// Perfect for prefetching - won't overfetch fresh data
const resolveData = async (controller, { owner, repo }) => {
await controller.fetchIfStale(IssueResource.getList, { owner, repo });
};
DevTools Improvements
DevTools Button
A floating button now appears in development mode to quickly open the Redux DevTools extension. #2803
Configure or disable it via the devButton prop:
// Disable the button
<CacheProvider devButton={null}>
<App />
</CacheProvider>
// Position it differently
<CacheProvider devButton="top-right">
<App />
</CacheProvider>
Persistent History
DevTools no longer forgets history if not open on page load. 2d2e941
Better State Tracking
Since React 18 batches updates, the real state can sometimes update from multiple actions. When devtools are open, a shadow state accurately reflects changes from each action for easier debugging. c9ca31f
Endpoint Properties Visible
Endpoint properties are now fully visible in the devtool inspector. a7da00e
Other Improvements
- Add
classNameto ErrorBoundary anderrorClassNameto AsyncBoundary #2785 - New
getDefaultManagers()export for explicit manager control #2791 - Replace BackupBoundary with UniversalSuspense + BackupLoading #2803
- Entity.process() receives endpoint
argsas fourth parameter a8936f5 nonFilterArgumentKeysfor Collection to exclude sort/order params from filtering 318df89- Support
+and*modifiers in RestEndpoint.path a6b4f4a - Support
{}grouping in path for optional segments a6b4f4a
Migration Guide
This upgrade requires updating all package versions simultaneously.
- NPM
- Yarn
- pnpm
- esm.sh
yarn add @data-client/react@^0.9.0 @data-client/rest@^0.9.0 @data-client/test@^0.9.0 @data-client/img@^0.9.0 @data-client/hooks@^0.9.0
npm install --save @data-client/react@^0.9.0 @data-client/rest@^0.9.0 @data-client/test@^0.9.0 @data-client/img@^0.9.0 @data-client/hooks@^0.9.0
pnpm add @data-client/react@^0.9.0 @data-client/rest@^0.9.0 @data-client/test@^0.9.0 @data-client/img@^0.9.0 @data-client/hooks@^0.9.0
<script type="module">
import * from 'https://esm.sh/@data-client/react@^0.9.0';
import * from 'https://esm.sh/@data-client/rest@^0.9.0';
import * from 'https://esm.sh/@data-client/test@^0.9.0';
import * from 'https://esm.sh/@data-client/img@^0.9.0';
import * from 'https://esm.sh/@data-client/hooks@^0.9.0';
</script>
Remove /next exports
All /next subpath exports have been removed. Features previously in /next are now
the standard exports. f65cf83
import { useController } from '@data-client/react/next';
import { useController } from '@data-client/react';
makeCacheProvider removed
Use the provider component directly with makeRenderDataClient. #2787
import { makeCacheProvider } from '@data-client/react';
const renderDataClient = makeRenderDataClient(makeCacheProvider);
import { CacheProvider } from '@data-client/react';
const renderDataClient = makeRenderDataClient(CacheProvider);
DELETE -> INVALIDATE action type
The DELETE action type has been renamed to INVALIDATE for clarity. #2784
import { DELETE_TYPE } from '@data-client/react';
import { INVALIDATE_TYPE } from '@data-client/react';
Legacy schema support dropped
All support for legacy schemas has been removed. Ensure you're using the current Entity class. #2784
entity.expiresAtremoved- All Entity overrides for backwards compatibility removed
Schema Serializers must support function calls
Schema Serializers must now support
function calls. Date no longer works directly. #2795
class MyEntity extends Entity {
createdAt = new Date();
static schema = {
createdAt: Date,
};
}
class MyEntity extends Entity {
createdAt = new Date();
static schema = {
createdAt: (iso: string) => new Date(iso),
};
}
Alternatively, use Temporal:
import { Temporal } from '@js-temporal/polyfill';
class MyEntity extends Entity {
createdAt = Temporal.Instant.fromEpochSeconds(0);
static schema = {
createdAt: Temporal.Instant.from,
};
}
Action types prefixed with 'rdc'
All action types are now prefixed with 'rdc' for better namespacing. #2781
receive -> set action names
All 'receive' action names have been renamed to 'set'. #2782
import { ReceiveAction, RECEIVE_TYPE } from '@data-client/react';
controller.receive(endpoint, args, response);
import { SetAction, SET_TYPE } from '@data-client/react';
controller.set(endpoint, args, response);
Middleware API simplified
Middleware no longer receives controller as a destructured property - it receives
controller directly. #2786
class LoggingManager implements Manager {
getMiddleware = (): Middleware => ({ controller }) => next => async action => {
console.log('before', action, controller.getState());
await next(action);
console.log('after', action, controller.getState());
};
cleanup() {}
}
class LoggingManager implements Manager {
getMiddleware = (): Middleware => controller => next => async action => {
console.log('before', action, controller.getState());
await next(action);
console.log('after', action, controller.getState());
};
cleanup() {}
}
getDefaultManagers()
CacheProvider elements no longer share default managers. Use getDefaultManagers() for explicit control.
#2791
import { getDefaultManagers, CacheProvider } from '@data-client/react';
const managers = getDefaultManagers();
<CacheProvider managers={managers}>
<App />
</CacheProvider>
RestEndpoint.getRequestInit returns Promise
If you override getRequestInit(), it now returns a Promise.
#2792
class AuthdEndpoint<O extends RestGenerics = any> extends RestEndpoint<O> {
getRequestInit(body: any): RequestInit {
return {
...super.getRequestInit(body),
credentials: 'same-origin',
};
}
}
class AuthdEndpoint<O extends RestGenerics = any> extends RestEndpoint<O> {
async getRequestInit(body: any): Promise<RequestInit> {
return {
...(await super.getRequestInit(body)),
credentials: 'same-origin',
};
}
}
Upgrade support
As usual, if you have any troubles or questions, feel free to join our or file a bug
