@hatchifyjs/react-jsonapi
is an NPM package that takes Schemas and produces an API client that your frontend can use for your JSON:API backend.
The following example uses @hatchifyjs/react-jsonapi
to create and fetch todos from a JSON:API backend. react-jsonapi
will automatically update the list when a create happens.
import { useState } from "react"
import { hatchifyReactRest, createJsonapiClient } from "@hatchifyjs/react-jsonapi"
import { string } from "@hatchifyjs/core"
import type { PartialSchema } from "@hatchifyjs/core"
const Todo = {
name: "Todo",
attributes: {
name: string({ required: true }),
},
} satisfies PartialSchema
const hatchedReactRest = hatchifyReactRest(createJsonapiClient("/api", { Todo }))
function App() {
const [todos] = hatchedReactRest.Todo.useAll()
const [createTodo] = hatchedReactRest.Todo.useCreateOne()
const [name, setName] = useState("")
return (
<div>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<button
type="button"
onClick={() => {
createTodo({ name })
setName("")
}}
>
Create
</button>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.name}</li>
))}
</ul>
</div>
)
}
See the documentation below for each individual function.
@hatchifyjs/react-jsonapi
exports the following:
hatchifyReactRest
- A function that takes aRestClient
, such as the one returned bycreateJsonapiClient
, and returns an object with promise and hook-based functions for each schema.createJsonapiClient
- A function that takes a base URL and a set of schemas and returns aRestClient
object for a JSON:API backend.
import { hatchifyReactRest, createJsonapiClient } from "@hatchifyjs/react-jsonapi"
createJsonapiClient(baseUrl: string, schemas: Schemas) => RestClient
creates a RestClient
which can then be passed into hatchifyReactRest
. A RestClient
is made up of a set of CRUD functions for interacting with a JSON:API backend.
const jsonapiClient = createJsonapiClient("/api", schemas)
hatchifyReactRest(restClient: RestClient) => HatchifyReactRest
is the entry point function. It returns an instance of HatchifyReactRest
, which is an object keyed by each schema that was passed into the createJsonapiClient
function. Each schema has a set of promise and hook-based functions for interacting with a JSON:API backend.
const hatchedReactRest = hatchifyReactRest(jsonapiClient)
const [todos] = await hatchedReactRest.Todo.useAll()
const [users] = await hatchedReactRest.User.useAll()
hatchedReactRest[SchemaName].findAll() => Promise<[RecordObject[], MetaData]>
loads a list of records from the REST client.
This is how you could use the findAll
function to fetch a page of todos. The metadata returned by the server will contain the total count of todos.
const [todos, metadata] = await hatchedReactRest.Todo.findAll({ page: { page: 1, size: 10 } })
Parameters
Property | Type | Details |
---|---|---|
queryList | QueryList? |
An object with optional include, fields, filter, sort, and page. |
Returns
An array with the following properties:
Property | Common Alias | Type | Details |
---|---|---|---|
[0] |
the plural name of the schema, e.g. todos |
RecordObject[] |
An array of records of the given schema. |
[1] |
metadata |
MetaData |
An object with metadata returned by the server, such as the count of records. |
hatchedReactRest[SchemaName].findOne(id: string | QueryOne) => Promise<RecordObject>
The findOne
function can be used to fetch a single record by its id.
const record = await hatchedReactRest.Todo.findOne(UUID)
Optionally, if you'd like to specify the fields to return or the relationships to include, you can pass in a QueryOne object.
const record = await hatchedReactRest.Todo.findOne({
id: UUID,
fields: ["name"],
})
Parameters
Property | Type | Details |
---|---|---|
IdOrQueryOne | string |
The id of the record. |
QueryOne |
The id of the record and an optional include or fields. |
Returns
Type | Details |
---|---|
Promise<RecordObject> |
A record of the given schema. |
hatchedReactRest[SchemaName].createOne(data: Partial<RecordObject>, mutateOptions?: MutateOptions) => Promise<RecordObject>
The createOne
function creates a new record for the given schema, in this case Todo. Only the required attributes need to be passed in.
const createdRecord = await hatchedReactRest.Todo.createOne({
name: "Learn Hatchify",
complete: false,
})
You can also create a record with a relationship by passing in the id of the related record.
const createdRecord = await hatchedReactRest.Todo.createOne({
name: "Learn Hatchify",
complete: false,
user: { id: UUID },
})
If a todo could have many users, you would pass in an array of user ids.
const createdRecord = await hatchedReactRest.Todo.createOne({
name: "Learn Hatchify",
complete: false,
users: [{ id: UUID_1 }, { id: UUID_2 }],
})
If you do not want to notify hooks and components of the User
schema when a Todo
is created, you can pass in the notify
option.
const createdRecord = await hatchedReactRest.Todo.createOne(
{
name: "Learn Hatchify",
complete: false,
user: { id: UUID },
},
{ notify: false },
)
Parameters
Property | Type | Details |
---|---|---|
data | Partial<RecordObject> |
An object containing the data for the new record. |
mutateOptions? | MutateOptions | undefined |
An object used to configure the behavior of a mutation function. |
Returns
Type | Details |
---|---|
Promise<RecordObject> |
The newly created record. |
hatchedReactRest[SchemaName].updateOne(data: Partial<RecordObject>, mutateOptions?: MutateOptions) => Promise<RecordObject>
When using the updateOne
function, the id must be passed in along with only the data that needs to be updated.
const updated = await hatchedReact.model.Todo.updateOne({
id: createdRecord.id,
name: "Master Hatchify",
})
When dealing with relationships, the same rules apply from the createOne
function. If it's a to-one relationship then pass in an object with the id of the related record and if it's a to-many relationship then pass in an array of objects with the ids of the related records.
const updated = await hatchedReact.model.Todo.updateOne({
id: createdRecord.id,
name: "Master Hatchify",
user: { id: UUID },
})
const updated = await hatchedReact.model.Todo.updateOne({
id: createdRecord.id,
name: "Master Hatchify",
users: [{ id: UUID_1 }, { id: UUID_2 }],
})
Parameters
Property | Type | Details |
---|---|---|
data | Partial<RecordObject> |
An object containing the data for the updated record. The id is required to be passed into RecordObject |
mutateOptions? | MutateOptions | undefined |
An object used to configure the behavior of a mutation function. |
Returns
Type | Details |
---|---|
Promise<RecordObject> |
The updated record. |
hatchedReactRest[SchemaName].deleteOne(id: string, mutateOptions?: MutateOptions) => Promise<void>
The deleteOne
function deletes a record by its id.
await hatchedReactRest.Todo.deleteOne(UUID)
Parameters
Property | Type | Details |
---|---|---|
id | string |
The id of the record to delete. |
mutateOptions? | MutateOptions | undefined |
An object used to configure the behavior of a mutation function. |
Returns
Type | Details |
---|---|
Promise<void> |
A promise that resolves when the record is deleted. |
hatchedReactRest[SchemaName].useAll(QueryList?) => [RecordObject[], RequestState]
In this example, we use the useAll
hook to fetch all todos and display them in a list. The hook returns an array with the todos that we map over and display. We use the the RequestState
to determine whether to display a loading spinner or an error message.
function TodosList() {
const [todos, state] = hatchedReactRest.Todo.useAll()
if (state.isPending) {
return <div>Loading...</div>
}
if (state.isRejected) {
return <div>Error: {state.error.message}</div>
}
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.name}</li>
))}
</ul>
)
}
Parameters
Property | Type | Details |
---|---|---|
queryList | QueryList? |
An object with optional include, fields, filter, sort, and page. |
Returns
An array with the following properties:
Property | Common Alias | Type | Details |
---|---|---|---|
[0] |
the plural name of the schema, e.g. todos |
RecordObject[] |
An array of records of the given schema. |
[1] |
state |
RequestState |
An object with request state data. |
hatchedReactRest[SchemaName].useOne(id: string) => [RecordObject, RequestState]
Here we use the useOne
hook to fetch a single todo and display its name and whether it is complete. Using the RequestState
object, we conditionally handle loading and error states. If the record is not found, we display a message to the user.
function ViewTodo({ uuid }: { uuid: string }) {
const [todo, state] = hatchedReactRest.Todo.useOne(uuid)
if (state.isPending) {
return <div>Loading...</div>
}
if (state.isRejected) {
return <div>Error: {state.error.message}</div>
}
if (!todo) {
return <div>Not found</div>
}
return (
<div>
<p>Name: {todo.name}</p>
<p>Complete: {todo.complete ? "Yes" : "No"}</p>
</div>
)
}
Parameters
Property | Type | Details |
---|---|---|
IdOrQueryOne | string |
The id of the record. |
QueryOne |
The id of the record and an optional include or fields. |
Returns
An array with the following properties:
Property | Common Alias | Type | Details |
---|---|---|---|
[0] |
the plural name of the schema, e.g. todos |
RecordObject |
A record of the given schema. |
[1] |
state |
RequestState |
An object with request state data. |
hatchedReactRest[SchemaName].useCreateOne(mutateOptions?: MutateOptions) => [CreateFunction, RequestState, RecordObject?]
Here we use the useCreateOne
hook to create a simple form for creating a new todo. We us the createTodo
function when the form is submitted, the RequestState
object to conditionally handle loading and error states, and we track the created
object to console log the newly created record.
function CreateTodo() {
const [createTodo, state, created] = hatchedReactRest.Todo.useCreateOne()
const [name, setName] = useState("")
useEffect(() => {
console.log("created record:", created)
}, [created])
if (state.isPending) {
return <div>Creating...</div>
}
if (state.isRejected) {
return <div>Error: {state.error.message}</div>
}
return (
<form
onSubmit={(e) => {
e.preventDefault()
createTodo({ name })
setName("")
}}
>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<button type="submit">Create</button>
</form>
)
}
Returns
An array with the following properties:
Property | Common Alias | Type | Details |
---|---|---|---|
[0] |
create{SchemaName} |
CreateFunction |
A function to create a record. |
[1] |
state |
RequestState |
An object with request state data. |
[2] |
created |
RecordObject |
The most recently created record. |
hatchedReactRest[SchemaName].useUpdateOne(mutateOptions?: mutateOptions) => [UpdateFunction, { id: RequestState }, RecordObject?]
Here we use the useUpdateOne
hook to create a simple edit form for updating a todo. We use the updateTodo
function when the form is submitted, the RequestState
object to conditionally handle loading and error states, and we track the updated
object to console log the newly updated record.
function EditTodo({ todo }: { todo: { id: string; name: string } }) {
const [updateTodo, state, updated] = hatchedReactRest.Todo.useUpdateOne()
const [name, setName] = useState(todo.name)
useEffect(() => {
console.log("updated record:", updated)
}, [updated])
if (state[todo.id]?.isPending) {
return <div>Updating...</div>
}
if (state[todo.id]?.isRejected) {
return <div>Error: {state[todo.id]?.error.message}</div>
}
return (
<form
onSubmit={(e) => {
e.preventDefault()
updateTodo({ id: todo.id, name })
}}
>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<button type="submit">Update</button>
</form>
)
}
Returns
An array with the following properties:
Property | Common Alias | Type | Details |
---|---|---|---|
[0] |
update{SchemaName} |
UpdateFunction |
A function to update a record. |
[1] |
state |
RequestState |
An object with request state data. |
[2] |
updated |
RecordObject |
The most recently updated record. |
hatchedReactRest[SchemaName].useDeleteOne(mutateOptions?: mutateOptions) => [DeleteFunction, { id: RequestState }]
Here we use the useDeleteOne
hook to create alongside a list of todos. We use the deleteTodo
function when the delete button is clicked, and the RequestState
object to disable the delete button when the request is pending.
function TodosListWithDelete() {
const [deleteTodo, state] = hatchedReactRest.Todo.useDeleteOne()
const [todos, todosState] = hatchedReactRest.Todo.useAll()
if (todosState.isPending) {
return <div>Loading...</div>
}
if (todosState.isRejected) {
return <div>Error: {todosState.error.message}</div>
}
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.name}
<button disabled={state[todo.id]?.isPending} onClick={() => deleteTodo(todo.id)}>
Delete
</button>
</li>
))}
</ul>
)
}
Returns
An array with the following properties:
Property | Common Alias | Type | Details |
---|---|---|---|
[0] |
delete{SchemaName} |
DeleteFunction |
A function to delete a record. |
[1] |
state |
RequestState |
An object with request state data. |
CreateFunction
is a function that takes an object containing the data for the new record and returns a promise that resolves to the newly created record.
Type | Details |
---|---|
(data: RecordObject) => Promise<RecordObject> |
A function that creates a record, modifies the associated RequestState, and updates the latest created record in the useCreateOne hook. |
For passing in relationships through the RecordObject
, see the example in the createOne function.
DeleteFunction
is a function that takes the id of the record to delete and returns a promise that resolves when the record is deleted.
Type | Details |
---|---|
(id: string) => Promise<void> |
A function that deletes a record and modifies the associated RequestState in the useDeleteOne hook. |
MetaData
is an object with metadata returned by the server, such as the count of records.
Property | Type | Details |
---|---|---|
... | any |
Metadata, for example unpaginatedCount |
MutateOptions
is an object used to configure the behavior of a mutation function.
Property | Type | Details |
---|---|---|
notify? | boolean | SchemaName[] | undefined |
Determines whether hooks and components should refetch data if a create, update, or delete happens |
notify
The notify
property within the MutateOptions
object is used to determine whether to refetch data for hooks (useOne
, useAll
, useDataGridState
), and components (DataGrid
).
-
By default, any mutation (create, update, or delete) will trigger a refetch of all active hooks and components for every schema.
-
If
notify
is omitted or set toundefined
, all active hooks and components will refetch data. -
If
notify
is set tofalse
, only the schema that was mutated will refetch data. For exampple,Todo.createOne({ ... })
will only refetch for hooks and components from the theTodo
schema. -
If
notify
is set to an array of schema names, only the specified schemas will refetch data as well as the mutated schema. For example,Todo.createOne({ ... }, { notify: ["Person"] })
will only refetch for hooks and components from theTodo
andPerson
schema.
QueryList
is an object with the following properties:
Property | Type | Details |
---|---|---|
include | string[]? |
Specify which relationships to include. |
fields | string[]? |
Specify which fields to return. |
filter | { field: string, operator: string, value: any }[]? |
Specify which records to include. |
sort | string? |
Specify how to sort the records. |
page | { page: number, size: number }? |
Specify which page of records to include. |
See JSON:API for more details on querying.
QueryOne
is an object with the following properties:
Property | Type | Details |
---|---|---|
id | string |
The id of the record. |
include | string[]? |
Specify which relationships to include. |
fields | string[]? |
Specify which fields to return. |
RecordObject
is a flat object representing the JSON:API response from the backend. The attributes and relationships are flattened to the top level of the object.
Property | Type | Details |
---|---|---|
id | string |
The id of the record. |
... | any |
The attributes and relationships of the record. |
The expected shape of the RecordObject
in the case of the Todo
and User
schemas would be:
{
id: string,
name: string,
complete: boolean,
user: {
id: string,
email: string,
},
}
RequestState
is an object with the following properties:
Property | Type | Details |
---|---|---|
error | Error? |
An error object if the request failed. |
isPending | boolean |
True if the status is "loading" , false otherwise. |
isRejected | boolean |
True if the status is "error" , false otherwise. |
isResolved | boolean |
True if the status is "success" or "error , false otherwise. |
isSuccess | boolean |
True if status is "success" , false otherwise. |
meta | MetaData |
Metadata returned by the server. |
status | "loading" |
If the promise is pending. |
"error" |
If the promise is rejected. | |
"success" |
If the promise is successfully resolved. |
UpdateFunction
is a function that takes an object containing the data for the new record and returns a promise that resolves to the newly updated record.
Type | Details |
---|---|
(data: Partial<RecordObject>) => Promise<RecordObject> |
A function that updates the record, modifies the associated RequestState, and updates the latest updated record in the useUpdateOne hook. |
For passing in relationships through the RecordObject
, see the example in the updateOne function.