Skip to main content

v0.9: Collections, DevTools, and Legacy Cleanup

· 8 min read
Nathaniel Tucker
Creator of Reactive Data Client

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().

Migration guide

Breaking Changes:

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 className to ErrorBoundary and errorClassName to AsyncBoundary #2785
  • New getDefaultManagers() export for explicit manager control #2791
  • Replace BackupBoundary with UniversalSuspense + BackupLoading #2803
  • Entity.process() receives endpoint args as fourth parameter a8936f5
  • nonFilterArgumentKeys for 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 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

Remove /next exports

All /next subpath exports have been removed. Features previously in /next are now the standard exports. f65cf83

Before
import { useController } from '@data-client/react/next';
After
import { useController } from '@data-client/react';

makeCacheProvider removed

Use the provider component directly with makeRenderDataClient. #2787

Before
import { makeCacheProvider } from '@data-client/react';
const renderDataClient = makeRenderDataClient(makeCacheProvider);
After
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

Before
import { DELETE_TYPE } from '@data-client/react';
After
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.expiresAt removed
  • 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

Before
class MyEntity extends Entity {
createdAt = new Date();

static schema = {
createdAt: Date,
};
}
After
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

Before
import { ReceiveAction, RECEIVE_TYPE } from '@data-client/react';
controller.receive(endpoint, args, response);
After
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

Before
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() {}
}
After
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

Before
class AuthdEndpoint<O extends RestGenerics = any> extends RestEndpoint<O> {
getRequestInit(body: any): RequestInit {
return {
...super.getRequestInit(body),
credentials: 'same-origin',
};
}
}
After
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 Chat or file a bug