Skip to content

Latest commit

 

History

History
634 lines (466 loc) · 27.6 KB

README.md

File metadata and controls

634 lines (466 loc) · 27.6 KB

@hatchifyjs/react-jsonapi

@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.

Exports

@hatchifyjs/react-jsonapi exports the following:

  • hatchifyReactRest - A function that takes a RestClient, such as the one returned by createJsonapiClient, 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 a RestClient object for a JSON:API backend.
import { hatchifyReactRest, createJsonapiClient } from "@hatchifyjs/react-jsonapi"

createJsonapiClient

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

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

findAll

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.

findOne

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.

createOne

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.

updateOne

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.

deleteOne

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.

useAll

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.

useOne

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.

useCreateOne

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.

useUpdateOne

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.

useDeleteOne

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.

Types

CreateFunction

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

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

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

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 to undefined, all active hooks and components will refetch data.

  • If notify is set to false, only the schema that was mutated will refetch data. For exampple, Todo.createOne({ ... }) will only refetch for hooks and components from the the Todo 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 the Todo and Person schema.

QueryList

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

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

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

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

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.