Skip to main content

createResource

Resources are a collection of RestEndpoints that operate on a common data by sharing a schema

Usage

api/TodoResource.ts
export class Todo extends Entity {
id = '';
title = '';
completed = false;
pk() {
return this.id;
}
static key = 'Todo';
}

const TodoResource = createResource({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
schema: Todo,
});
Resources start with 6 Endpoints
const todo = useSuspense(TodoResource.get, { id: '5' });
const todos = useSuspense(TodoResource.getList);
controller.fetch(TodoResource.getList.push, {
title: 'finish installing reactive data client',
});
controller.fetch(
TodoResource.update,
{ id: '5' },
{ ...todo, completed: true },
);
controller.fetch(TodoResource.partialUpdate, { id: '5' }, { completed: true });
controller.fetch(TodoResource.delete, { id: '5' });

Arguments

{
path: string;
schema: Schema;
urlPrefix?: string;
body?: any;
searchParams?: any;
paginationField?: string;
optimistic?: boolean;
Endpoint?: typeof RestEndpoint;
Collection?: typeof Collection;
} & EndpointExtraOptions

path

Passed to RestEndpoint.path

schema

Passed to RestEndpoint.schema

urlPrefix

Passed to RestEndpoint.urlPrefix

searchParams

Passed to RestEndpoint.searchParams for getList and getList.push

body

Passed to RestEndpoint.body for getList.push update and partialUpdate

paginationField

If specified, will add Resource.getList.getPage method on the Resource.

optimistic

true makes all mutation endpoints optimistic

Endpoint

Class used to construct the members.

import { RestEndpoint } from '@data-client/rest';

export default class AuthdEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
getRequestInit(body: any): RequestInit {
return {
...super.getRequestInit(body),
credentials: 'same-origin',
};
}
}
const TodoResource = createResource({
path: '/todos/:id',
schema: Todo,
Endpoint: AuthdEndpoint,
});

Collection

Collection Class used to construct getList schema.

import { schema, createResource } from '@data-client/rest';

class MyCollection<
S extends any[] | PolymorphicInterface = any,
Parent extends any[] = [urlParams: any, body?: any],
> extends schema.Collection<S, Parent> {
// getList.push should add to Collections regardless of its 'orderBy' argument
// in other words: `orderBy` is a non-filtering argument - it does not influence which results are returned
nonFilterArgumentKeys(key: string) {
return key === 'orderBy';
}
}
const TodoResource = createResource({
path: '/todos/:id',
searchParams: {} as { userId?: string; orderBy?: string } | undefined,
schema: Todo,
Collection: MyCollection,
});

EndpointExtraOptions

Members

These provide the standard CRUD endpointss common in REST APIs. Feel free to customize or add new endpoints based to match your API.

get

Retrieve a singular item.

  • method: 'GET'
  • path: path
  • schema: schema
// GET //test.com/api/abc/xyz
createResource({ urlPrefix: '//test.com', path: '/api/:group/:id' }).get({
group: 'abc',
id: 'xyz',
});

Commonly used with useSuspense(), Controller.invalidate

getList

Retrieve a list of items.

  • method: 'GET'
  • args: PathToArgs(shortenPath(path)) & searchParams
    • Removes the last argument:
      createResource({ path: '/:first/:second' }).getList.path === '/:first';
      createResource({ path: '/:first' }).getList.path === '/';
  • schema: new schema.Collection([schema])
// GET //test.com/api/abc?isExtra=xyz
createResource({ urlPrefix: '//test.com', path: '/api/:group/:id' }).getList({
group: 'abc',
isExtra: 'xyz',
});

Commonly used with useSuspense(), Controller.invalidate

getList.push

push creates a new entity and pushes it to the end of getList.

// POST //test.com/api/abc
// BODY { "title": "winning" }
createResource({
urlPrefix: '//test.com',
path: '/api/:group/:id',
}).getList.push({ group: 'abc' }, { title: 'winning' });

Commonly used with Controller.fetch

getList.unshift

unshift creates a new entity and pushes it to the beginning of getList.

// POST //test.com/api/abc
// BODY { "title": "winning" }
createResource({
urlPrefix: '//test.com',
path: '/api/:group/:id',
}).getList.push({ group: 'abc' }, { title: 'winning' });

Commonly used with Controller.fetch

getList.getPage

getPage retrieves another page appending to getList ensuring there are no duplicates.

// GET //test.com/api/abc?isExtra=xyz&page=2
createResource({
urlPrefix: '//test.com',
path: '/api/:group/:id',
paginationField: 'page',
}).getList.getPage({
group: 'abc',
isExtra: 'xyz',
page: '2',
});

Commonly used with Controller.fetch

update

Update an item.

  • method: 'PUT'
  • path: path
  • schema: schema
// PUT //test.com/api/abc/xyz
// BODY { "title": "winning" }
createResource({ urlPrefix: '//test.com', path: '/api/:group/:id' }).update(
{ group: 'abc', id: 'xyz' },
{ title: 'winning' },
);

Commonly used with Controller.fetch

partialUpdate

Update some subset of fields of an item.

  • method: 'PATCH'
  • path: path
  • schema: schema
// PATCH //test.com/api/abc/xyz
// BODY { "title": "winning" }
createResource({
urlPrefix: '//test.com',
path: '/api/:group/:id',
}).partialUpdate({ group: 'abc', id: 'xyz' }, { title: 'winning' });

Commonly used with Controller.fetch

delete

Deletes an item.

  • method: 'DELETE'
  • path: path
  • schema: new schema.Invalidate(schema)
  • process:
    (value, params) {
    return value && Object.keys(value).length ? value : params;
    },
// DELETE //test.com/api/abc/xyz
createResource({ urlPrefix: '//test.com', path: '/api/:group/:id' }).delete({
group: 'abc',
id: 'xyz',
});

Commonly used with Controller.fetch

extend()

createResource builds a great starting point, but often endpoints need to be further customized.

extend() is polymorphic with three forms:

Batch extension of known members

export const CommentResource = createResource({
path: '/repos/:owner/:repo/issues/comments/:id',
schema: Comment,
}).extend({
getList: { path: '/repos/:owner/:repo/issues/:number/comments' },
update: { body: { body: '' } },
});

Adding new members

export const UserResource = createGithubResource({
path: '/users/:login',
schema: User,
}).extend('current', {
path: '/user',
schema: User,
});

Function form (to get BaseResource/super)

export const IssueResource= createResource({
path: '/repos/:owner/:repo/issues/:number',
schema: Issue,
pollFrequency: 60000,
searchParams: {} as IssueFilters | undefined,
}).extend(BaseResource => ({
search: BaseResource.getList.extend({
path: '/search/issues\\?q=:q?%20repo\\::owner/:repo&page=:page?',
schema: {
results: {
incompleteResults: false,
items: BaseIssueResource.getList.schema.results,
totalCount: 0,
},
link: '',
},
})
)});

More Demos

Function Inheritance Patterns

To reuse code around Resource design, you can create your own function that calls createResource(). This has similar effects as class-based inheritance.

import {
createResource,
RestEndpoint,
type EndpointExtraOptions,
type RestGenerics,
type ResourceGenerics,
type ResourceOptions,
} from '@data-client/rest';

export class AuthdEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
getRequestInit(body: any): RequestInit {
return {
...super.getRequestInit(body),
credentials: 'same-origin',
};
}
}

export function createMyResource<O extends ResourceGenerics = any>({
schema,
Endpoint = AuthdEndpoint,
...extraOptions
}: Readonly<O> & ResourceOptions) {
return createResource({
Endpoint,
schema,
...extraOptions,
}).extend({
getList: {
schema: {
results: new schema.Collection([schema]),
total: 0,
limit: 0,
skip: 0,
},
},
});
}

More Demos