# `@gadgetinc/react`
The `@gadgetinc/react` package provides React hooks for calling your Gadget app's [auto-generated API](https://docs.gadget-canary.xyz/api/example-app/development/).
```tsx
import { useFindMany, useAction } from "@gadgetinc/react";
// your app's auto-generated API client
import { api } from "../api";
function WidgetDeleter() {
// `useFindMany` executes a backend fetch to get a list of widgets from the backend.
const [{ data, fetching, error }, refresh] = useFindMany(api.widget, {
select: {
id: true,
name: true,
},
});
// `useAction` sets up a mutation we can run to delete a specific widget when a user clicks a button
const [_, deleteWidget] = useAction(api.widget.delete);
if (fetching) return "Loading...";
return (
{data?.map((widget) => (
{widget.name}
))}
);
}
```
## Key features
1. Rule-obeying hooks for reading and writing data from a backend that handle all request lifecycle and auth like `useFindOne`, `useAction`, and `useFetch`
2. Full type safety for inputs and outputs driven by each Gadget app's backend schema, including over dynamic selections
3. A full-featured, GraphQL-powered nested object selection system using the
4. Data hydrations that return useful objects like `Date`
## Installation
See the [installation instructions](https://docs.gadget-canary.xyz/api/example-app/development/installing) for your app's API.
### Provider Setup
Your React application must be wrapped in the `Provider` component from this library for the hooks to function properly. No other wrappers (like `urql`'s) are necessary.
Gadget provisions applications with the required `` component already in place! If using the frontend hosting built into Gadget, no action is required as this step is already done.
Example:
```tsx
// import the API client for your specific application from your client package, be sure to replace this package name with your own
import { ExampleAppClient } from "@gadget-client/example-app";
// import the required Provider object and some example hooks from this package
import { Provider } from "@gadgetinc/react";
// instantiate the API client for our app
const api = new ExampleAppClient({ authenticationMode: { browserSession: true } });
export const MyApp = (props: { children: React.ReactNode }) => {
// wrap the application in the so the hooks can find the current client
return {props.children};
};
```
### Client side vs Server side React
The `@gadgetinc/react` package is intended for use on the **client-side only**. The hooks provided by this package make requests from your app's frontend directly to your Gadget app's backend from the browser. If you want to use server-side rendering, you can use your framework's server-side data loader support and make imperative calls with your API client object.
## Hooks
### `useFindOne()`
`useFindOne(manager: ModelFinder, id: string, options: SingleFinderOptions = {}): [{data, fetching, error}, refetch]`
`useFindOne` fetches one record from your Gadget database with a given `id`.
```tsx
import React from "react";
import { useFindOne } from "@gadgetinc/react";
import { api } from "../api";
export const ShowWidgetName = (props: { id: string }) => {
const [{ data, fetching, error }, _refetch] = useFindOne(api.widget, props.id);
if (fetching) return "Loading...";
if (error) return `Error loading data: ${error}`;
return {data?.name};
};
```
##### Parameters
* `manager`: The model manager for the model you want to find a record of. Required. Example: `api.widget`, or `api.shopifyProduct`
* `id`: The backend id of the record you want to find. Required.
* `options`: Options for making the call to the backend. Not required, and all keys are optional.
* `select`: A list of fields and subfields to select. See
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Set to true to disable this hook. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `suspense`: Should this hook suspend when fetching data. See
* `select`: A list of fields and subfields to select. See
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Set to true to disable this hook. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `suspense`: Should this hook suspend when fetching data. See
##### Returns
`useFindOne` returns two values: a result object with the `data`, `fetching`, and `error` keys for inspecting in your React component's output, and a to trigger a refresh of the hook's data.
* `data: GadgetRecord | null`: The record fetched from the backend. Is `null` while the data is being loaded, or if the record wasn't found.
* `fetching: boolean`: A boolean describing if the hook is currently requesting data from the backend.
* `error: Error | null`: An error from the client or server side, if encountered during the request. Will contain an error if the record isn't found by `id`. See the .
`useFindOne` expects a record with the given id to be found in the backend database, and will return an error in the `error` property if no record with this id is found.
`useFindOne` can select only some fields from the backend model with the option:
```tsx
import React from "react";
import { useFindOne } from "@gadgetinc/react";
import { api } from "../api";
export const OnlySomeWidgetFields = (props: { id: string }) => {
// fetch only the widget id and name fields
const [{ data, fetching, error }, _refetch] = useFindOne(api.widget, props.id, {
select: {
id: true,
name: true,
},
});
return (
{data?.id}: {data?.name}
);
};
```
### `useMaybeFindOne()`
`useMaybeFindOne(manager: ModelFinder, id: string, options: SingleFinderOptions = {}): [{data, fetching, error}, refetch]`
`useMaybeFindOne` fetches one record from your Gadget database with a given `id`.
```tsx
import React from "react";
import { useMaybeFindOne } from "@gadgetinc/react";
import { api } from "../api";
export const ShowWidgetName = (props: { id: string }) => {
const [{ data, fetching, error }, _refetch] = useMaybeFindOne(api.widget, props.id);
if (fetching) return "Loading...";
if (error) return `Error loading data: ${error}`;
if (data) {
return {data.name};
} else {
return "No widget found";
}
};
```
`useMaybeFindOne` will return `data: null` and `error: null` if no record with the given `id` is found in the backend database. `useMaybeFindOne` otherwise behaves identically to , and accepts the same options.
### `useFindMany()`
`useFindMany(manager: ModelFinder, options: ManyFinderOptions = {}): [{data, fetching, error}, refetch]`
`useFindMany` fetches a page of records from your Gadget database, optionally sorted, filtered, or searched.
```tsx
import React from "react";
import { useFindMany } from "@gadgetinc/react";
import { api } from "../api";
export const ShowWidgetNames = () => {
const [{ data, fetching, error }, _refetch] = useFindMany(api.widget);
if (fetching) return "Loading...";
if (error) return `Error loading data: ${error}`;
return (
{data?.map((widget) => (
{widget.name}
))}
);
};
```
##### Parameters
* `manager`: The model manager for the model you want to find a page of records for. Required. Example: `api.widget`, or `api.shopifyProduct`
* `options`: Options for making the call to the backend. Not required and all keys are optional.
* `select`: A list of fields and subfields to select. See the docs.
* `filter`: A list of filters to limit the set of returned records. Optional. See the Model Filtering section in your application's API documentation to see the available filters for your models.
* `search`: A search string to match backend records against. Optional. See the Model Searching section in your application's API documentation to see the available search syntax.
* `sort`: A sort order to return backend records by. Optional. See the [sorting section](https://docs.gadget-canary.xyz/api/example-app/development/sorting-and-filtering#sorting) in your application's API documentation for more info.
* `first` & `after`: Pagination arguments to pass to fetch a subsequent page of records from the backend. `first` should hold a record count and `after` should hold a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `last` & `before`: Pagination arguments to pass to fetch a subsequent page of records from the backend. `last` should hold a record count and `before` should hold a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `live`: Should this hook re-render when data changes on the backend. See the .
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `suspense`: Should this hook suspend when fetching data. See for more info
* `select`: A list of fields and subfields to select. See the docs.
* `filter`: A list of filters to limit the set of returned records. Optional. See the Model Filtering section in your application's API documentation to see the available filters for your models.
* `search`: A search string to match backend records against. Optional. See the Model Searching section in your application's API documentation to see the available search syntax.
* `sort`: A sort order to return backend records by. Optional. See the [sorting section](https://docs.gadget-canary.xyz/api/example-app/development/sorting-and-filtering#sorting) in your application's API documentation for more info.
* `first` & `after`: Pagination arguments to pass to fetch a subsequent page of records from the backend. `first` should hold a record count and `after` should hold a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `last` & `before`: Pagination arguments to pass to fetch a subsequent page of records from the backend. `last` should hold a record count and `before` should hold a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `live`: Should this hook re-render when data changes on the backend. See the .
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `suspense`: Should this hook suspend when fetching data. See for more info
##### Returns
`useFindMany` returns two values: a result object with the `data`, `fetching`, and `error` keys for use in your React component's output, and a to trigger a refresh of the hook's data.
* `data: GadgetRecordList | null`: The resulting page of records fetched from the backend for your model, once they've arrived
* `fetching: boolean`: A boolean describing if the hook is currently making a request to the backend.
* `error: Error | null`: An error from the client or server side, if encountered during the request. See the .
Without any options, `useFindMany` will fetch the first page of backend records sorted by id.
`useFindMany` accepts the `select` option to allow customization of which fields are returned:
```tsx
import React from "react";
import { useFindMany } from "@gadgetinc/react";
import { api } from "../api";
export const OnlySomeWidgetFields = () => {
// fetch only the widget id and name fields
const [{ data, fetching, error }, _refetch] = useFindMany(api.widget, {
select: {
id: true,
name: true,
},
});
if (fetching) return "Loading...";
if (error) return `Error loading data: ${error}`;
return (
{data?.map((widget) => (
{widget.name}
))}
);
};
```
`useFindMany` accepts a `filter` option to limit which records are returned from the backend. For example, we can filter to return only widgets created since the start of 2022:
```tsx
// fetch only the widgets created recently
const [{ data, fetching, error }, _refetch] = useFindMany(api.widget, {
filter: {
createdAt: { greaterThan: new Date(2022, 1, 1) },
},
});
```
See [your app's API reference](https://docs.gadget-canary.xyz/api/example-app/development/) for more information on which filters are available on what models.
`useFindMany` accepts a `sort` option to change the order of the records that are returned. For example, we can sort returned widgets by the `createdAt` field:
```tsx
// return the most recently created widgets first
const [{ data, fetching, error }, _refetch] = useFindMany(api.widget, {
sort: {
createdAt: "Descending",
},
});
```
`useFindMany` accepts a `search` option to limit the fetched records to only those matching a given search query. For example, we can search all the backend widgets for those matching the string "penny" in any searchable field:
```tsx
// return widgets with "penny" in any searchable field
const [{ data, fetching, error }, _refetch] = useFindMany(api.widget, {
search: "penny",
});
```
See [your app's API reference](https://docs.gadget-canary.xyz/api/example-app/development/) for more information on the search query syntax and which fields are searchable.
`useFindMany` accepts a option to subscribe to changes in the backend data returned, which will trigger re-renders of your react components as that data changes. For example, we can show an up-to-date view of the first page of backend widgets:
```tsx
// will update when new widgets are created or on-screen widgets are updated
const [{ data, fetching, error }, _refetch] = useFindMany(api.widget, {
live: true,
});
```
`useFindMany` accepts pagination arguments for getting the second, third, etc page of results from the backend beyond just the first page. Gadget applications use [Relay Cursor style GraphQL pagination](https://graphql.org/learn/pagination/#pagination-and-edges), where a second page is fetched by asking for the next x many results after a cursor returned with the first page.
```tsx
// return the first 10 results after some cursor from somewhere else
const [{ data, fetching, error }, _refetch] = useFindMany(api.widget, {
first: 10,
after: "some-cursor-value",
});
// data is a GadgetRecordList object, which has extra properties for inquiring about the pagination state
// the current page's start and end cursor are available for use to then make later requests for different pages
const {
// string used for forward pagination, pass to the `after:` variable
endCursor,
// string used for backwards pagination, pass to the `before:` variable
startCursor,
// `data` also reports if there are more pages for fetching
// boolean indicating if there is another page to fetch after the `endCursor`
hasNextPage,
// boolean indicating if there is another page to fetch before the `startCursor`
hasPreviousPage,
} = data;
```
An easy way to do pagination is using React state, or for a better user experience, using the URL with whatever router system works for your application. We use React state to demonstrate pagination in this example:
```tsx
import React, { useState } from "react";
import { useFindMany } from "@gadgetinc/react";
import { api } from "../api";
export const WidgetPaginator = () => {
// store the current cursor in the backend data
const [cursor, setCursor] = useState(undefined);
// pass the current cursor to the `after:` variable, telling the backend to return data after this cursor
const [{ data, fetching, error }, _refetch] = useFindMany(api.widget, {
first: 20,
after: cursor,
});
if (fetching) return "Loading...";
if (error) return `Error loading data: ${error}`;
return (
<>
{data?.map((widget) => (
{widget.name}
))}
>
);
};
```
### `useFindFirst()`
`useFindFirst(manager: ModelFinder, options: FindManyOptions = {}): [{data, fetching, error}, refetch]`
`useFindFirst` fetches the first record from your backend Gadget database that matches the given `filter`, `sort`, and `search`.
```tsx
import React from "react";
import { useFindFirst } from "@gadgetinc/react";
import { api } from "../api";
export const MostRecentPublishedPost = (props: { id: string }) => {
// request the first record from the backend where publishedAt is set and with the most recent publishedAt value
const [{ data, fetching, error }, _refetch] = useFindFirst(api.blogPost, {
filter: { publishedAt: { isSet: true } },
sort: { publishedAt: "Descending" },
});
if (fetching) return "Loading...";
if (error) return `Error loading data: ${error}`;
return Check out our most recent blog post titled {data?.title};
};
```
##### Parameters
* `manager`: The model manager for the model you want to find a page of records for. Required. Example: `api.widget`, or `api.shopifyProduct`
* `options`: Options for making the call to the backend. Not required and all keys are optional.
* `select`: A list of fields and subfields to select. See the docs. Optional.
* `filter`: A list of filters to find a record matching. Optional. See the Model Filtering section in your application's API documentation to see the available filters for your models.
* `search`: A search string to find a record matching. Optional. See the Model Searching section in your application's API documentation to see the available search syntax.
* `sort`: A sort order to order the backend records by. `useFindFirst` will only return the first record matching the given `search` and `filter`, so `sort` can be used to break ties and select a specific record. Optional. See the [sorting section](https://docs.gadget-canary.xyz/api/example-app/development/sorting-and-filtering#sorting) in your application's API documentation for more info.
* `live`: Should this hook re-render when data changes on the backend. See the .
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `suspense`: Should this hook suspend when fetching data. See for more info
* `select`: A list of fields and subfields to select. See the docs. Optional.
* `filter`: A list of filters to find a record matching. Optional. See the Model Filtering section in your application's API documentation to see the available filters for your models.
* `search`: A search string to find a record matching. Optional. See the Model Searching section in your application's API documentation to see the available search syntax.
* `sort`: A sort order to order the backend records by. `useFindFirst` will only return the first record matching the given `search` and `filter`, so `sort` can be used to break ties and select a specific record. Optional. See the [sorting section](https://docs.gadget-canary.xyz/api/example-app/development/sorting-and-filtering#sorting) in your application's API documentation for more info.
* `live`: Should this hook re-render when data changes on the backend. See the .
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `suspense`: Should this hook suspend when fetching data. See for more info
##### Returns
`useFindFirst` returns two values: a result object with the `data`, `fetching`, and `error` keys for inspecting in your React component's output, and a to trigger a refresh of the hook's data.
* `data: GadgetRecord | null`: The record fetched from the backend. Is `null` while the data is being loaded, or if a matching record wasn't found.
* `fetching: boolean`: A boolean describing if the hook is currently making a request to the backend.
* `error: Error | null`: An error from the client or server side, if encountered during the request. Will contain an error if the first record isn't found. See the .
If no record is found matching the conditions, `useFindFirst` will return `{data: null, error: new MissingDataError}`.
Without any options, `useFindFirst` will fetch the first matching record and cause your component to rerender as the fetch happens and when the data or error arrives.
`useFindFirst` can only select some of the fields from the backend model with :
```tsx
// fetch the first upside down widget, and only it's id and name fields
const [{ data }] = useFindFirst(api.widget, {
filter: {
state: { equals: "upsideDown" },
},
select: {
id: true,
name: true,
},
});
```
`useFindFirst` can subscribe to changes in the returned data from the backend with the option, and re-render when the backend data changes:
```tsx
// fetch the first upside down widget, and re-render if it's data changes
const [{ data }] = useFindFirst(api.widget, {
filter: {
state: { equals: "upsideDown" },
},
live: true,
});
```
### `useFindBy()`
`useFindBy(findFunction: ModelFindFunction, fieldValue: any, options: FindByOptions = {}): [{data, fetching, error}, refetch]`
`useFindBy` fetches one record from your backend looked up by a specific field and value. `useFindBy` requires a by-field record finder like `.findBySlug` or `.findByEmail` to exist for your model, which are generated by adding a [Unique Validations](https://docs.gadget-canary.xyz/guides/models/fields#uniqueness-validations) to a field.
```tsx
import React from "react";
import { useFindBy } from "@gadgetinc/react";
import { api } from "../api";
// get a slug from the URL or similar, and look up a post record by this slug
export const PostBySlug = (props: { slug: string }) => {
const [{ data, fetching, error }, _refetch] = useFindBy(api.blogPost.findBySlug, props.slug);
if (fetching) return "Loading...";
if (error) return `Error loading data: ${error}`;
return (
<>
{data?.title}
{data?.body}
>
);
};
```
##### Parameters
* `findFunction`: The model finder function from your application's API client for finding records by a specific field. Gadget generates these finder functions for the fields where they are available. Changes to your Gadget backend schema may be required to get these to exist. Required. Example: `api.widget.findBySlug`, or `api.user.findByEmail`.
* `fieldValue`: The value of the field to search for a record using. This is which slug or email you'd pass to `api.widget.findBySlug` or `api.user.findByEmail`.
* `options`: Options for making the call to the backend. Not required and all keys are optional.
* `select`: A list of fields and subfields to select. See the docs.
* `live`: Should this hook re-render when data changes on the backend. See the .
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `suspense`: Should this hook suspend when fetching data. See for more info
* `select`: A list of fields and subfields to select. See the docs.
* `live`: Should this hook re-render when data changes on the backend. See the .
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `suspense`: Should this hook suspend when fetching data. See for more info
##### Returns
`useFindBy` returns two values: a result object with the `data`, `fetching`, and `error` keys for inspecting in your React component's output, and a to trigger a refresh of the hook's data.
* `data: GadgetRecord | null`: The record fetched from the backend. Is `null` while the data is being loaded, or if a matching record wasn't found for the given `fieldValue`.
* `fetching: boolean`: A boolean describing if the hook is currently making a request to the backend.
* `error: Error | null`: An error from the client or server side, if encountered during the request. Will contain an error if a matching record isn't found. See the .
If no record is found matching the conditions, then the returned object will have `null` for the `data`. `useFindBy(api.widget.findByEmail, "hi@gadget.dev")` is the React equivalent of `api.widget.findByEmail("hi@gadget.dev")`
Without any options, `useFindBy` will fetch the record with the given field value, and cause your component to rerender as the fetch happens and when the data or error arrives:
```tsx
import React from "react";
import { useFindBy } from "@gadgetinc/react";
import { api } from "../api";
// get a slug from the URL or similar, and look up a post record by this slug
export const PostBySlug = (props: { slug: string }) => {
const [{ data, fetching, error }, _refetch] = useFindBy(api.blogPost.findBySlug, props.slug);
if (fetching) return "Loading...";
if (error) return `Error loading data: ${error}`;
return (
<>
{data?.title}
{data?.body}
>
);
};
```
`useFindBy` can take options that allow the customization of which fields are returned:
```tsx
// fetch only a post id and title fields
const [{ data, fetching, error }, _refetch] = useFindBy(api.blogPost.findBySlug, "some-slug", { select: { id: true, title: true } });
```
The `refetch` function returned as the second element can be executed in order to trigger a refetch of the most up to date data from the backend. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/basics/react-preact/#reexecuting-queries) on re-executing queries for more information.
### `useMaybeFindFirst()`
`useMaybeFindFirst(manager: ModelFinder, options: FindManyOptions = {}): [{data, fetching, error}, refetch]`
`useMaybeFindFirst` fetches the first record from your backend Gadget database that matches the given `filter`, `sort`, and `search` parameters.
```tsx
import React from "react";
import { useMaybeFindFirst } from "@gadgetinc/react";
import { api } from "../api";
export const MostRecentPublishedPost = (props: { id: string }) => {
// request the first record from the backend where publishedAt is set and with the most recent publishedAt value
const [{ data, fetching, error }, _refetch] = useMaybeFindFirst(api.blogPost, {
filter: { publishedAt: { isSet: true } },
sort: { publishedAt: "Descending" },
});
if (fetching) return "Loading...";
if (error) return `Error loading data: ${error}`;
if (data) {
return Check out our most recent blog post titled {data.title};
} else {
// no first record found
return null;
}
};
```
`useMaybeFindFirst` returns `data: null` if no record is found in the backend database, and otherwise works identically to `useFindFirst`. See for more details on the options `useMaybeFindFirst` accepts.
### `useAction()`
`useAction(actionFunction: ModelActionFunction, options: UseActionOptions = {}): [{data, fetching, error}, refetch]`
```tsx
import React from "react";
import { useAction } from "@gadgetinc/react";
import { api } from "../api";
export const CreatePost = () => {
const [{ data, fetching, error }, createBlogPost] = useAction(api.blogPost.create);
return (
);
};
```
`useAction` is a hook for running a backend action on one record of a Gadget model. `useAction` must be passed an action function from an instance of your application's generated API client. Options:
##### Parameters
* `actionFunction`: The model action function from your application's API client for acting on records. Gadget generates these action functions for each action defined on backend Gadget models. Required. Example: `api.widget.create`, or `api.user.update` or `api.blogPost.publish`.
* `options`: Options for making the call to the backend. Not required and all keys are optional.
* `select`: A list of fields and subfields to select. See the docs.
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `suspense`: Should this hook suspend when fetching data. See for more info
* `select`: A list of fields and subfields to select. See the docs.
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `suspense`: Should this hook suspend when fetching data. See for more info
##### Returns
`useAction` returns two values: a result object with the `data`, `fetching`, and `error` keys for inspecting in your React component's output, and a `act` function to actually run the backend action. `useAction` is a rule-following React hook that wraps action execution, which means it doesn't just run the action as soon as the hook is invoked. Instead, `useAction` returns a configured function that will actually run the action, which you need to call in response to some user event. The `act` function accepts the action inputs as arguments -- not `useAction` itself.
`useAction`'s result will return the `data`, `fetching`, and `error` details for the most recent execution of the action.
* `data: GadgetRecord | null`: The record fetched from the backend after a mutation. Is `null` while before the mutation is run and while it is currently ongoing.
* `fetching: boolean`: A boolean describing if the hook is currently making a request to the backend.
* `error: Error | null`: An error from the client or server side, if encountered during the mutation. Will contain an error if the client passed invalid data, if the server failed to complete the action, or if a network error was encountered. See the .
For example, we can create a button that creates a post when clicked, and then shows the post once it has been created:
```tsx
import React from "react";
import { useAction } from "@gadgetinc/react";
import { api } from "../api";
export const CreatePost = () => {
const [{ data, fetching, error }, createBlogPost] = useAction(api.blogPost.create);
// deal with all the states of the result object
if (fetching) return "Loading...";
if (error) return `Error loading data: ${error}`;
if (!data) {
return (
);
} else {
return (
<>
{data.title}
{data.body}
>
);
}
};
```
We can also run actions on existing models by passing the `id:` in with the action parameters:
```tsx
import React, { useState } from "react";
import { useAction } from "@gadgetinc/react";
import { api } from "../api";
export const UpdatePost = (props: { id: string }) => {
// invoke the `useAction` hook, getting back a result object and an action runner function every render
const [{ data, fetching, error }, updateBlogPost] = useAction(api.blogPost.update);
const [title, setTitle] = useState("");
if (fetching) return "Loading...";
if (error) return `Error loading data: ${error}`;
return (
);
};
```
`useAction` can take options that allow the customization of which fields are returned on the acted-upon record:
```tsx
// fetch only a post id and title fields
const [{ data, fetching, error }, updateBlogPost] = useAction(api.blogPost.update, { select: { id: true, title: true } });
```
### `useGlobalAction()`
`useGlobalAction(actionFunction: GlobalActionFunction, options: UseGlobalActionOptions = {}): [{data, fetching, error}, refetch]`
`useGlobalAction` is a hook for running a backend Global Action.
```tsx
import React from "react";
import { useGlobalAction } from "@gadgetinc/react";
import { api } from "../api";
export const PurgeData = () => {
const [{ data, fetching, error }, runPurge] = useGlobalAction(api.purgeData);
return (
);
};
```
##### Parameters
* `globalActionFunction`: The action function from your application's API client. Gadget generates these global action functions for each global action defined in your Gadget backend. Required. Example: `api.runSync`, or `api.purgeData` (corresponding to Global Actions named `Run Sync` or `Purge Data`).
* `options`: Options for making the call to the backend. Not required and all keys are optional.
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
##### Returns
`useGlobalAction` returns two values: a result object with the `data`, `fetching`, and `error` keys for inspecting in your React component's output, and an `act` function to actually run the backend global action. `useGlobalAction` is a rule-following React hook that wraps action execution, which means it doesn't just run the action as soon as the hook is invoked. Instead, `useGlobalAction` returns a configured function which you need to call in response to some event. This `act` function accepts the action inputs as arguments. `useGlobalAction`'s result will return the `data`, `fetching`, and `error` details for the most recent execution of the action.
* `data: Record<string, any> | null`: The data returned by the global action from the backend. Is `null` while before the mutation is run and while it is currently ongoing.
* `fetching: boolean`: A boolean describing if the hook is currently making a request to the backend.
* `error: Error | null`: An error from the client or server side, if encountered during the mutation. Will contain an error if the client passed invalid data, if server failed to complete the mutation, or if a network error was encountered. See the .
For example, we can create a button that runs a Global Action called `purgeData` when clicked, and shows the result after it has been run:
```tsx
import React from "react";
import { useGlobalAction } from "@gadgetinc/react";
import { api } from "../api";
export const PurgeData = () => {
const [{ data, fetching, error }, runPurge] = useGlobalAction(api.purgeData);
// deal with all the states of the result object
if (fetching) return "Loading...";
if (error) return `Error running global action: ${error}`;
if (!data) {
return (
);
} else {
return "Purge completed";
}
};
```
### `useView()`
`useView(view: F | string, variablesOrOptions?: VariablesT | Omit, options: Omit): [{data, fetching, error}, refetch]`
`useView` is a hook for calling both named and inline computed views.
```tsx
export function Leaderboard() {
const [result, _refetch] = useView(api.leaderboard);
if (result.error) return <>Error: {result.error.toString()}>;
if (result.fetching && !result.data) return <>Fetching...>;
if (!result.data) return <>No data found>;
return (
<>
{result.data.players.map((player) => (
{player.name}: {player.totalScore}
))}
>
);
}
```
##### Parameters
* `view`: The view function from your application's API client or a stringified Gelly snippet. Gadget generates view functions for each named computed view defined in your Gadget backend. Required.
* `variables`: Variables to pass to the computed view. Only required if the view expects variables.
* `options`: Options for making the call to the backend. Not required and all keys are optional.
* `select`: A list of fields and subfields to select. See the docs.
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `suspense`: Should this hook suspend when fetching data. See for more info
* `select`: A list of fields and subfields to select. See the docs.
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `suspense`: Should this hook suspend when fetching data. See for more info
##### Returns
`useView` returns two values: a result object with the `data`, `fetching`, and `error` keys for inspecting in your React component's output, and a to trigger a refresh of the hook's data.
* `data: ViewResult | null`: The result of the computed view query fetched from the backend. Is `null` while the data is being loaded, or if the record wasn't found.
* `fetching: boolean`: A boolean describing if the hook is currently requesting data from the backend.
* `error: Error | null`: An error from the client or server side, if encountered during the request. Will contain an error if the record isn't found by `id`. See the .
Inline views can be run with `useView` by passing in a Gelly query as a string:
```tsx
export function Leaderboard() {
const [result, _refetch] = useView(`{
players {
name
totalScore: sum(results.score)
[order by sum(results.score) desc limit 5]
}
}`);
if (result.error) return <>Error: {result.error.toString()}>;
if (result.fetching && !result.data) return <>Fetching...>;
if (!result.data) return <>No data found>;
return (
<>
{result.data.players.map((player) => (
{player.name}: {player.score}
))}
>
);
}
```
Computed view parameters can also be passed into both named and inline computed views executed using the `useView` hook:
```tsx
export function Leaderboard() {
// will only return the top 2 players
const [{ data, fetching, error }] = useView(
`($numTopPlayers: Integer) {
players {
name
totalScore: sum(results.score)
[order by sum(results.score) desc limit $numTopPlayers]
}
}`,
{ numTopPlayers: 2 }
);
if (result.error) return <>Error: {result.error.toString()}>;
if (result.fetching && !result.data) return <>Fetching...>;
if (!result.data) return <>No data found>;
return (
<>
{result.data.players.map((player) => (
{player.name}: {player.totalScore}
))}
>
);
}
```
You should always pass dynamic values to inline computed views using the `variables` parameter. It allows for better escaping of values with spaces or special characters, and improved performance under the hood.
### `useGet()`
`useGet(singletonModelManager: SingletonModelManager, options: GetOptions = {}): [{data, fetching, error}, refetch]`
`useGet` fetches a singleton record for an `api.currentSomething` style model manager. `useGet` fetches one global record, which is most often the current session.
If you'd like to access the current session on the frontend, use the hook
```tsx
import React from "react";
import { useGet } from "@gadgetinc/react";
import { api } from "../api";
export const CurrentSessionId = () => {
const [{ data, fetching, error }, _refetch] = useGet(api.currentSession);
if (fetching) return "Loading...";
if (error) return `Error loading data: ${error}`;
return Current session: {data?.id};
};
```
##### Parameters
* `singletonModelManager`: The singleton model manager available on the generated API client for your application. The passed model manager _must_ be one of the `currentSomething` model managers. `useGet` can't be used with other model managers that don't have a `.get` function. Example: `api.currentSession`.
* `options`: Options for making the call to the backend. Not required and all keys are optional.
* `select`: A list of fields and subfields to select. See the docs.
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `suspense`: Should this hook suspend when fetching data. See for more info
* `select`: A list of fields and subfields to select. See the docs.
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `suspense`: Should this hook suspend when fetching data. See for more info
##### Returns
`useGet` returns two values: a result object with the `data`, `fetching`, and `error` keys for inspecting in your React component's output, and a to trigger a refresh of the hook's data.
* `data: GadgetRecord | null`: The record fetched from the backend. Is `null` while the data is being loaded, or if the record wasn't found.
* `fetching: boolean`: A boolean describing if the hook is currently making a request to the backend.
* `error: Error | null`: An error from the client or server side, if encountered during the request. Will contain an error if the singleton record isn't found. See the .
`useGet(api.currentSession)` retrieves the current global session for the current browser
```tsx
import React from "react";
import { useGet } from "@gadgetinc/react";
import { api } from "../api";
export const CurrentSessionId = () => {
const [{ data, fetching, error }, _refetch] = useGet(api.currentSession);
if (fetching) return "Loading...";
if (error) return `Error loading data: ${error}`;
return Current session: {data?.id};
};
```
`useGet` can take options which allow the customization of which fields are returned on the selected record:
```tsx
// fetch only the session id and state fields
const [{ data, fetching, error }, _refetch] = useGet(api.currentSession, {
select: {
id: true,
createdAt: true,
},
});
```
The `refetch` function returned as the second element can be executed to trigger a refetch of the most up-to-date data from the backend. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/basics/react-preact/#reexecuting-queries) on re-executing queries for more information.
### `useEnqueue()`
`useEnqueue(actionFunction: ModelActionFunction || GlobalActionFunction, actionInput, backgroundActionOptions = {}): [{error, fetching, handle}, enqueue]`
`useEnqueue` facilitates the enqueuing of actions to be executed in the background.
```tsx
import React from "react";
import { useEnqueue } from "@gadgetinc/react";
import { api } from "../api";
export function CreateUserButton(props: { name: string; email: string }) {
const [{ error, fetching, handle }, enqueue] = useEnqueue(api.user.create);
const onClick = () =>
enqueue(
{
// actionInput
name: props.name,
email: props.email,
},
{
// backgroundActionoptions
id: `send-email-action-${props.email}`,
}
);
return (
<>
{error && <>Failed to enqueue user create: {error.toString()}>}
{fetching && <>Enqueuing action...>}
{handle && <>Enqueued action with background action id={handle.id}>}
>
);
}
```
##### Parameters
* `actionFunction: ModelActionFunction || GlobalActionFunction`: Either a model or global action.
* `actionInput: Input | null`: The parameters or data passed to a model or global action.
* `backgroundActionOptions (optional)`: The options for how the background action runs, for more information check out the [reference here](https://docs.gadget-canary.xyz/reference/gadget-server#enqueue-action-input-options).
##### Returns
`useEnqueue` doesn't submit the background action when invoked but instead returns a function for enqueuing the action in response to the event.
### `useActionForm()`
`useActionForm(actionFunction: ModelActionFunction, options: UseActionFormOptions = {}): UseActionFormResult`
`useActionForm` manages form state for calling actions in your Gadget backend. `useActionForm` can fetch the record for editing, manage the state of the fields as the user changes them in a form, track validations and errors, and then call the action with the form data when the user submits the form. `useActionForm` can call Actions on models as well as Global Actions.
`useActionForm` wraps the excellent [`react-hook-form` library](https://react-hook-form.com/) and provides all the same state management primitives that `react-hook-form` does, in addition to Gadget-specific goodies like automatic record fetching, automatic action calling and type safety.
```tsx
import React from "react";
import { useActionForm } from "@gadgetinc/react";
import { api } from "../api";
const PostForm = () => {
const { register, submit } = useActionForm(api.post.create);
return (
);
};
```
Read more about building forms in the [Frontend Forms guide](https://docs.gadget-canary.xyz/guides/frontend/forms).
##### Parameters
* `action`: the Model Action or Global Action to call when submitting. Required.
* `options`: the configuration for the form
The `options` options object accepts the following options:
| Name | Type | Description |
| --- | --- | --- |
| `defaultValues` | `Partial` | Default values to seed the form inputs with. Can be omitted. Mutually exclusive with the `findBy`. option. |
| `findBy` | `string` or `{ [field: string]: any }` | Details for automatically finding a record to seed the form values with. When passed as a string, will look up a record with that `id`. When passed an object, will call a `findBy` function on the `api` object to retrieve a record by that field. The field must have a uniqueness validation in order to function. |
| `mode` | `"onChange"` or `"onBlur"` or `"onSubmit"` or `"onTouched"` or `"all"` | Validation strategy before submitting behavior |
| `reValidateMode` | `"onChange"` or `"onBlur"` or `"onSubmit"` | Validation strategy after submitting behavior |
| `resetOptions` | [`ResetOptions`](https://react-hook-form.com/docs/useform/reset) | Default options to use when calling `reset`. For more details, see the [`reset` function](https://react-hook-form.com/docs/useform/reset) docs |
| `criteriaMode` | `"firstError"` or `"all"` | Display all validation errors or one at a time. |
| `shouldFocusError` | `boolean` | Enable or disable built-in focus management. |
| `delayError` | `number` | Delay errors by this many milliseconds to avoid them appearing instantly |
| `shouldUseNativeValidation` | `boolean` | Use browser built-in form constraint API. |
| `shouldUnregister` | `boolean` | Enable and disable input unregister after unmount. |
| `select` | `RecordSelection` | Which fields to select from the backend when retrieving initial data with `findBy`. Can also mark fields as `ReadOnly` to exclude them from being sent during an update. See docs on for more information. |
| `send` | `string[]` | Which fields to send from the form values to the backend for the action. Useful if you want to include fields in your form state for driving UI that shouldn't be sent with the submission |
| `onSubmit` | `() => void` | Callback called right before data is submitted to the backend action |
| `onSuccess` | `(actionResult: ActionResultData) => void` | Callback called after a successful submission to the backend action. Passed the action result, which is the object with `{data, error, fetching}` keys |
| `onError` | `(error: Error \| FieldErrors) => void` | Callback called after an error occurs finding the initial record or during submission to the backend action. Passed the error, which can be a transport error from a broken network, or a list of validation errors returned by the backend |
`useActionForm`'s `props` input is very similar to `useForm`'s from `react-hook-form`. For more docs on these props, see the [`react-hook-form` docs](https://react-hook-form.com/docs/useform).
##### Returns
`useActionForm` returns a variety of functions and state for managing your form that most users destructure as they call it:
```tsx
const { register, submit, error, reset, ...rest } = useActionForm(api.post.update, {
defaultValues: {
id: "123",
},
});
```
The available result values are:
| Name | Type | Description |
| --- | --- | --- |
| `register` | [`Function`](https://react-hook-form.com/docs/useform/register) | A function for registering uncontrolled inputs with the form [docs](https://react-hook-form.com/docs/useform/register) |
| `unregister` | [`Function`](https://react-hook-form.com/docs/useform/unregister) | A function for de-registering uncontrolled inputs with the form. Needed when dynamically adjusting the form elements on screen. [docs](https://react-hook-form.com/docs/useform/unregister) |
| `formState` | | Current state of the form, like validations, errors, submission details, etc [docs](https://react-hook-form.com/docs/useform/formstate) |
| `submit` | `(event?: React.Event) => Promise` | A function to call that submits the form to the backend. Returns a promise for the `ActionResult` object containing the `{data, error, fetching}` triple returned by the backend action. |
| `watch` | `(names?: string \| string[] \| (data, options) => void) => unknown` | A function for observing form values for easily changing how a form is displayed. [docs](https://react-hook-form.com/docs/useform/watch) |
| `reset` | `` ``(values?: T \| ResetAction``, options?: `Record`) => void `` | Function for resetting the entire form state to specific values. [docs](https://react-hook-form.com/docs/useform/reset) |
| `resetField` | ``(name: string, options?: `Record`) => void`` | Function for resetting one field in the values to a specific value. [docs](https://react-hook-form.com/docs/useform/resetfield) |
| `setError` | `(name: string, error: FieldError, { shouldFocus?: boolean }) => void` | Function for manually adding errors to fields. [docs](https://react-hook-form.com/docs/useform/seterror) |
| `clearErrors` | `(name?: string \| string[]) => void` | Function for removing all the errors or errors on one field. [docs](https://react-hook-form.com/docs/useform/clearErrors) |
| `setValue` | `(name: string, value: unknown, config?: Object) => void` | Function for setting one field to a specific value. [docs](https://react-hook-form.com/docs/useform/setValue) |
| `setFocus` | `(name: string, options: SetFocusOptions) => void` | Function for imperatively focusing one field. [docs](https://react-hook-form.com/docs/useform/setFocus) |
| `getValues` | `(payload?: string \| string[]) => Object` | Function for retrieving all the values from within the form state. [docs](https://react-hook-form.com/docs/useform/getValues) |
| `getFieldState` | `(name: string, formState?: Object) => ({isDirty, isTouched, invalid, error})` | Function for returning the state of one individual field from within the form state. [docs](https://react-hook-form.com/docs/useform/getFieldState) |
| `trigger` | `` (name?: string \| string[]) => `Promise` `` | Manually triggers form or input validation. This method is also useful when you have dependant validation (input validation depends on another input's value). [docs](https://react-hook-form.com/docs/useform/trigger) |
| `control` | `FormControl` | Context object for passing to `` components wrapping `ref`\-less or controlled components |
| `error` | `Error \| null` | Any top-level Error objects encountered during processing. Will contain transport level errors as well as field validation errors returned by the backend. This value is for deeply inspecting the error if you want more than just the message, but the `formState.errors` object should be preferred if not. |
| `actionData` | `` `{`data: Result \| null, error: Error \| null, fetching: false `}` `` | The `ActionResult` triple returned by the inner `useAction` hook. Will be populated with the action execution result after submission. |
| `originalFormMethods` | [`UseFormReturn`](https://react-hook-form.com/ts#UseFormReturn) | These are the original methods returned by the useForm hook. They are used to take advantage of useFormContext. |
#### `FormState` object
The `FormState` object returned by `useActionForm` includes the following properties:
| Name | Type | Description |
| --- | --- | --- |
| `isDirty` | `boolean` | `true` if the user has modified any inputs away from the `defaultValues`, and `false` otherwise |
| `dirtyFields` | `Record` | A map of fields to the dirty state for each field. Each field's property on the object `true` if the user has modified this field away from the default and `false` otherwise |
| `touchedFields` | `Record` | A map of fields to the touched state for each field. Each field's property on the object `true` if the user has modified this field at all and `false` otherwise |
| `defaultValues` | `Record` | The default values the form started out with, or has been `reset` to |
| `isSubmitted` | `boolean` | `true` if the form has ever been submitted, `false` otherwise |
| `isSubmitSuccessful` | `boolean` | `true` if the form has completed a submission that encountered no errors, `false` otherwise |
| `isSubmitting` | `boolean` | `true` if the form is currently submitting to the backend, `false` otherwise |
| `isLoading` | `boolean` | `true` if the form is currently loading data from the backend to populate the initial values or another input, `false` otherwise |
| `submitCount` | `number` | Count of times the form has been submitted |
| `isValid` | `boolean` | `true` if the form has no validation errors currently, `false` otherwise |
| `isValidating` | `boolean` | `true` if the form is currently validating data, `false` otherwise |
| `errors` | `Record` | A map of any validation errors currently present on each field |
The `FormState` object managed by `useActionForm` (and `react-hook-form` underneath) is a `Proxy` object that tracks which properties are accessed during rendering to avoid excessive re-renders. Make sure you read its properties within a component render function to properly track which properties should trigger re-renders.
For more on this proxy object, see the [`react-hook-form` docs](https://react-hook-form.com/docs/useform/formstate)
For more on the `FormState` object, see the [`react-hook-form` docs](https://react-hook-form.com/api/useform/formstate).
#### Missing options from `react-hook-form`
Unlike `react-hook-form`, `useActionForm` manages the submission process to your backend action. You _don't_ need to manually make a call to your action -- instead, call the `submit` function returned by `useActionForm` when you are ready to submit the form, and `useActionForm` will call the action.
Because the submission process is managed, `useActionForm` does not accept the `handleSubmit` option that `react-hook-form` does.
#### ``
`useActionForm`'s `register` function only works with uncontrolled components that conform to normal DOM APIs. For working with controlled components, like those from popular UI libraries such as `@shopify/polaris`, you must register these input components with a `` instead.
```tsx
import { TextField } from "@shopify/polaris";
import { useActionForm, Controller } from "@gadgetinc/react";
import { api } from "../api";
const AllowedTagForm = () => {
const { submit, control } = useActionForm(api.allowedTag.create);
return (
);
};
```
`` accepts the following props:
| Name | Type | Description |
| --- | --- | --- |
| `name` | `string \| FieldPath` | The key of this input's data within your form's field values |
| `control` | `FormControl` | Context object returned by `useActionForm`, must be passed to each `` to tie them to the form |
| `render` | `(props: ControllerProps) => ReactElement` | A function that returns a React element and provides the ability to attach events and value into the component. This simplifies integrating with external controlled components with non-standard prop names. Provides `onChange`, `onBlur`, `name`, `ref` and `value` as props for sending to the child component, and also a `fieldState` object which contains specific input state. |
| `defaultValue` | `unknown` | A default value for applying to the inner input. |
| `rules` | `object` | Validation options for applying to the form value. Accepts the same format of options as the `register` function. |
| `shouldUnregister` | `boolean` | Should this input's values/validations/errors be removed on unmount. |
| `disabled` | `boolean` | Is this input currently disabled such that it can't be edited |
For more information on ``, see the [`react-hook-form` docs](https://react-hook-form.com/docs/usecontroller/controller)
#### `useFormContext()`
The `useFormContext` hook is also available for use within components that have access to a `FormProvider` context powered by `useActionForm`. `useFormContext` allows you to access the form state and methods from a parent form component in a child component. In order to use `useFormContext`, you must wrap your form in a `FormProvider`.
```tsx
import { api } from "../api";
import { useActionForm, FormProvider } from "@gadgetinc/react";
import PostFormChild from "./PostFormChild";
import { TextField } from "@shopify/polaris";
export default function () {
const { originalFormMethods } = useActionForm(api.post.create);
return (
);
}
```
```tsx
import { useFormContext, Controller } from "@gadgetinc/react";
export default function () {
const { control } = useFormContext();
return (
}
/>
);
}
```
#### `useFieldArray()`
`useFieldArray(fieldArrayProps: UseFieldArrayProps): UseFieldArrayResult`
`useActionForm` also supports the [`useFieldArray` hook](https://react-hook-form.com/api/usefieldarray) from `react-hook-form` for managing form state on dynamic forms and for related models.
For examples of using `useFieldArray` to manage related models, see the [Forms guide](https://docs.gadget-canary.xyz/guides/frontend/forms#registering-fields-on-related-models).
`useFieldArray` accepts the following props:
| Name | Type | Description |
| --- | --- | --- |
| `name` | `string` | **Required**. Name of the field array |
| `control` | `FormControl` | Context object returned by `useActionForm`, must be passed to `useFieldArray` to tie array state to the form |
| `rules` | `object` | Validation options for applying to the form value. Accepts the same format of options as the `register` function |
| `shouldUnregister` | `boolean` | Should this input's values/validations/errors be removed on unmount |
##### Returns
`useFieldArray` returns the managed form state, and a variety of functions for manipulating the array:
| Name | Type | Description |
| --- | --- | --- |
| `fields` | `object & { id: string }` | Contains the default value and key for component array |
| `append` | `(obj: object \| object[], focusOptions) => void` | Append input to the **end** of your fields and focus. The input value will be registered during this action |
| `prepend` | `(obj: object \| object[], focusOptions) => void` | Prepend input to the **start** of your fields and focus. The input value will be registered during this action |
| `insert` | `(index: number, value: object \| object[], focusOptions) => void` | Insert at particular position and focus |
| `swap` | `(from: number, to: number) => void` | Swap array element's position |
| `move` | `(from: number, to: number) => void` | Move array element to another position |
| `update` | `(index: number, obj: object) => void` | Update input at a particular position. The updated fields will get unmounted and remounted, if this is not the desired behavior, use `setValue` API instead |
| `replace` | `(obj: object[]) => void` | Replace all values currently in the field array |
| `remove` | `(index?: number \| number[]) => void` | Remove input at a particular position or remove all when no index provided |
### `useList()`
`useList(manager: ModelFinder, options?: ManyFinderOptions = {}): [{ data, fetching, page, search, error }, refresh]`
`useList` handles the logic of creating a paginated, searchable list of records from your Gadget backend. The `useList` hook takes the same parameters as useFindMany (with the addition of the `pageSize` param). Refer to [useFindMany](https://docs.gadget-canary.xyz/reference/react#usefindmany) for examples of how to query the data that will populate your list.
##### Parameters
* `manager`: The model manager for the model you want to find a page of records for. Required. Example: `api.widget`, or `api.shopifyProduct`
* `options`: Options for making the call to the backend. Not required and all keys are optional.
* `select`: A list of fields and subfields to select. See the docs.
* `filter`: A list of filters to limit the set of returned records. Optional. See [filtering](https://docs.gadget-canary.xyz/api/example-app/development/sorting-and-filtering#filtering) in your application's API documentation for more info.
* `search`: A search string to match backend records against. Optional. See the model searching section in your application's API documentation for the available search syntax.
* `sort`: A sort order to return backend records by. Optional. See [sorting](https://docs.gadget-canary.xyz/api/example-app/development/sorting-and-filtering#sorting) in your application's API documentation for more info.
* `first` & `after`: Pagination arguments to pass to fetch a subsequent page of records from the backend. `first` should hold a record count and `after` should hold a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `last` & `before`: Pagination arguments to pass to fetch a subsequent page of records from the backend. `last` should hold a record count and `before` should hold a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `live`: Should this hook re-render when data changes on the backend. See the docs.
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/).
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/).
* `suspense`: Should this hook suspend when fetching data. See for more info.
* `pageSize`: The page size of the paginated data. Optional, defaults to 50.
* `select`: A list of fields and subfields to select. See the docs.
* `filter`: A list of filters to limit the set of returned records. Optional. See [filtering](https://docs.gadget-canary.xyz/api/example-app/development/sorting-and-filtering#filtering) in your application's API documentation for more info.
* `search`: A search string to match backend records against. Optional. See the model searching section in your application's API documentation for the available search syntax.
* `sort`: A sort order to return backend records by. Optional. See [sorting](https://docs.gadget-canary.xyz/api/example-app/development/sorting-and-filtering#sorting) in your application's API documentation for more info.
* `first` & `after`: Pagination arguments to pass to fetch a subsequent page of records from the backend. `first` should hold a record count and `after` should hold a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `last` & `before`: Pagination arguments to pass to fetch a subsequent page of records from the backend. `last` should hold a record count and `before` should hold a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `live`: Should this hook re-render when data changes on the backend. See the docs.
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/).
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/).
* `suspense`: Should this hook suspend when fetching data. See for more info.
* `pageSize`: The page size of the paginated data. Optional, defaults to 50.
##### Returns
`useList` returns two values: a result object with the `data`, `fetching`, `page`, `search`, and `error` keys for use in your React component's output, and a `refresh` function to trigger a refresh of the hook's data.
* `data: GadgetRecord | null`: The record fetched from the backend. Is `null` while the data is being loaded, or if the record wasn't found.
* `fetching: boolean`: A boolean describing if the hook is currently requesting data from the backend.
* `page: PaginationResult`: A collection of variables and functions to handle pagination.
* `page.hasNextPage: boolean | undefined`: Whether the paginated data has a next page.
* `page.hasPreviousPage: boolean | undefined`: Whether the paginated data has a previous page.
* `page.variables: { first?: number; after?: string; last?: number; before?: string; }`:
* `first` holds a record count and `after` holds a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `last` holds a record count and `before` holds a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `page.pageSize: number`: Page size of the paginated data. Defaults to 50.
* `page.goToNextPage: () => void`: Function to load the next page of paginated data.
* `page.goToPreviousPage: () => void`: Function to load the last page of paginated data.
* `search: SearchResult`: A collection of variables and functions to handle pagination.
* `value: string`: The current value of the input, possibly changing rapidly as the user types.
* `debouncedValue: string`: The value that has been processed by the debounce function, updating less frequently than value. [Learn more about debouncing](https://dev.to/anjankarmakar/mastering-event-debouncing-in-javascript-a-guide-with-practical-example-19oe).
* `set: (value: string) => void`: A function to update the value.
* `clear: () => void`: A function to clear the value.
* `error: Error | null`: An error from the client or server side, if encountered during the request. Will contain an error if the record isn't found by `id`. See the .
* `page.hasNextPage: boolean | undefined`: Whether the paginated data has a next page.
* `page.hasPreviousPage: boolean | undefined`: Whether the paginated data has a previous page.
* `page.variables: { first?: number; after?: string; last?: number; before?: string; }`:
* `first` holds a record count and `after` holds a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `last` holds a record count and `before` holds a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `page.pageSize: number`: Page size of the paginated data. Defaults to 50.
* `page.goToNextPage: () => void`: Function to load the next page of paginated data.
* `page.goToPreviousPage: () => void`: Function to load the last page of paginated data.
* `first` holds a record count and `after` holds a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `last` holds a record count and `before` holds a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `value: string`: The current value of the input, possibly changing rapidly as the user types.
* `debouncedValue: string`: The value that has been processed by the debounce function, updating less frequently than value. [Learn more about debouncing](https://dev.to/anjankarmakar/mastering-event-debouncing-in-javascript-a-guide-with-practical-example-19oe).
* `set: (value: string) => void`: A function to update the value.
* `clear: () => void`: A function to clear the value.
#### Paginated list
Here's how you can create a paginated list that is synced with your data in Gadget:
```tsx
import { useList } from "@gadgetinc/react";
// your app's auto-generated API client
import { api } from "../api";
export const PaginatedList = () => {
// Passing the same filter/sort/search params to useList as useFindMany:
const [{ data, fetching, error, page }] = useList(api.customer, { sort: { createdAt: "Descending" } });
if (fetching) return "Loading list...";
if (error) return `An error occurred: ${error.message}`;
return (
<>
{/* easy pagination! */}
{data?.map((customer) => (
{customer.email}
))}
>
);
};
```
#### Searchable list
The fun continues! The `useList` hook also supports search:
```tsx
import { useList } from "@gadgetinc/react";
// your app's auto-generated API client
import { api } from "../api";
export const SearchableList = () => {
const [{ data, fetching, error, search }] = useList(api.customer);
const handleSearch = (event: React.ChangeEvent) => {
event.preventDefault();
search.set(event.target.value);
};
if (error) return `An error occurred: ${error.message}`;
return (
<>
{fetching ? (
Loading list...
) : (
{data?.map((customer) => (
{customer.email}
))}
)}
>
);
};
```
### `useTable()`
`useTable(manager: ModelFinder, options?: ManyFinderOptions = {}): [{ data, fetching, page, search, error }, refresh]`
`useTable` is a headless React hook for powering a table. The table shows a page of Gadget records from the backend. The hook returns an object with optional params for sorting, filtering, searching, and data selection. `useTable` returns an object with `data`, `fetching`, and `error` keys, and a `refetch` function. `data` is a `GadgetRecordList` object holding the list of returned records and pagination info.
This hook is like [useList](https://docs.gadget-canary.xyz/reference/react#uselist). The difference is it divides each field into columns, and orders the records into rows.
```tsx
import React from "react";
import { useTable } from "@gadgetinc/react";
import { api } from "../api";
export const ExampleTable = () => {
const [{ rows, columns, fetching, error }] = useTable(api.customer);
if (fetching) return "Loading table...";
if (error) return `There was an error loading the table: ${error.message}`;
return (
{/* Create a column header for each column name in `columns` */}
{columns?.map((col) => (
{col.header}
))}
{/* Create a row in the HTML table for each `row` in `rows` */}
{rows?.map((row) => (
{/* Index the row object using the column API identifier to populate each cell */}
{columns.map((col) => (
{String(row[col.identifier])}
))}
))}
);
};
```
##### Parameters
* `manager`: The model manager for the model you want to find a page of records for. Required. Example: `api.widget`, or `api.shopifyProduct`
* `options`: Options for making the call to the backend. Not required and all keys are optional.
* `select`: A list of fields and subfields to select. See the docs.
* `filter`: A list of filters to limit the set of returned records. Optional. See the [model filtering section](https://docs.gadget-canary.xyz/api/example-app/development/sorting-and-filtering#filtering) in your application's API documentation to see the available filters for your models.
* `search`: A search string to match backend records against. Optional. See the model searching section in your application's API documentation to see the available search syntax.
* `live`: Should this hook re-render when data changes on the backend. See the .
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `suspense`: Should this hook suspend when fetching data. See for more info
* `pageSize`: The page size of the paginated data. Optional, defaults to 50.
* `initialCursor`: A string cursor; `useTable` handles pagination, so this prop is only needed if you get the cursor hash from the returned `after` or `before` variables, and want to set the cursor to a custom spot.
* `initialSort`: An object of type `{ [column: string]: "Ascending" | "Descending" }` that sets the initial sort order for the table.
* `columns`: A list of field API identifiers and custom column renderer objects to be returned. Custom cell renderers have type `{header: string; render: (props: {record: GadgetRecord, index: number}) => ReactNode; style?: React.CSSProperties;}`
* `select`: A list of fields and subfields to select. See the docs.
* `filter`: A list of filters to limit the set of returned records. Optional. See the [model filtering section](https://docs.gadget-canary.xyz/api/example-app/development/sorting-and-filtering#filtering) in your application's API documentation to see the available filters for your models.
* `search`: A search string to match backend records against. Optional. See the model searching section in your application's API documentation to see the available search syntax.
* `live`: Should this hook re-render when data changes on the backend. See the .
* `requestPolicy`: The `urql` request policy to make the request with. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `pause`: Should the hook make a request right now or not. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/api/urql/)
* `suspense`: Should this hook suspend when fetching data. See for more info
* `pageSize`: The page size of the paginated data. Optional, defaults to 50.
* `initialCursor`: A string cursor; `useTable` handles pagination, so this prop is only needed if you get the cursor hash from the returned `after` or `before` variables, and want to set the cursor to a custom spot.
* `initialSort`: An object of type `{ [column: string]: "Ascending" | "Descending" }` that sets the initial sort order for the table.
* `columns`: A list of field API identifiers and custom column renderer objects to be returned. Custom cell renderers have type `{header: string; render: (props: {record: GadgetRecord, index: number}) => ReactNode; style?: React.CSSProperties;}`
```tsx
// Only returns the `name` and `createdAt` columns
const [{ rows, columns, fetching, error, page }] = useTable(api.customer, {
columns: ["name", "createdAt"],
});
```
##### Returns
* `rows: Record[]`: A list of records indexed by each column's API identifier. If the column displays a field of type number, then the `ColumnValueType` type will take on the number type.
* `columns: RecordTableColumnValue[]`: `columns` is a list of objects with the following attributes:
* `identifier: string`: The identifier of the field. For Gadget fields, this is the field's API identifier. For custom columns, this is a UUID.
* `header: string`: The header of the column. If the current column does not have a custom configuration, the Gadget field's name will be used.
* `field: string`: The field's API identifier. For Gadget relationship fields, this is the dot separated path to the displayed field on the related model.
* `type: string`: The field type (string, number, json, etc) of the displayed value
* `relationshipType?: string`: The parent field type for relationship fields (belongs to, has one, has many, has many through). The type of the displayed value is represented in `type`.
* `sortable: boolean`: Whether the column can be sorted.
* `includeTime?: boolean`: `true` when the field is a date / time field with `includeTime` enabled.
* `render: (props: {record: GadgetRecord, index: number}) => ReactNode`: Given a record and the row index, `render` returns the ReactNode that will render in the table's cell.
* `style?: React.CSSProperties`: A CSS style object for the current column forwarded from from the corresponding column entry in the `columns` option in `useTable`.
* `metadata: ModelMetadata`: An array of objects that describe each field in the model.
* `identifier: string`: The identifier of the field. For Gadget fields, this is the field's API identifier. For custom columns, this is a UUID.
* `header: string`: The header of the column. If the current column does not have a custom configuration, the Gadget field's name will be used.
* `field: string`: The field's API identifier. For Gadget relationship fields, this is the dot separated path to the displayed field on the related model.
* `type: string`: The field type (string, number, json, etc) of the displayed value
* `relationshipType?: string`: The parent field type for relationship fields (belongs to, has one, has many, has many through). The type of the displayed value is represented in `type`.
* `sortable: boolean`: Whether the column can be sorted.
* `includeTime?: boolean`: `true` when the field is a date / time field with `includeTime` enabled.
* `render: (props: {record: GadgetRecord, index: number}) => ReactNode`: Given a record and the row index, `render` returns the ReactNode that will render in the table's cell.
* `style?: React.CSSProperties`: A CSS style object for the current column forwarded from from the corresponding column entry in the `columns` option in `useTable`.
```json
// in example ModelMetadata object
[
{
"name": "Id",
"apiIdentifier": "id",
"fieldType": "ID",
"requiredArgumentForInput": true,
"sortable": true,
"filterable": true,
"__typename": "GadgetModelField",
"configuration": {
"__typename": "GadgetGenericFieldConfig",
"fieldType": "ID",
"validations": [
{
"__typename": "GadgetGenericFieldValidation",
"name": "Uniqueness",
"specID": "gadget/validation/unique"
},
{
"__typename": "GadgetGenericFieldValidation",
"name": "Required",
"specID": "gadget/validation/required"
}
]
}
}
]
```
* `fetching: boolean`: A boolean describing if the hook is currently requesting data from the backend.
* `page: PaginationResult`: A collection of variables and functions to handle pagination.
* `page.hasNextPage: boolean | undefined`: Whether the paginated data has a next page.
* `page.hasPreviousPage: boolean | undefined`: Whether the paginated data has a previous page.
* `page.variables: { first?: number; after?: string; last?: number; before?: string; }`:
* `first` holds a record count and `after` holds a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `last` holds a record count and `before` holds a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `page.pageSize: number`: Page size of the paginated data. Defaults to 50.
* `page.goToNextPage: () => void`: Function to load the next page of paginated data.
* `page.goToPreviousPage: () => void`: Function to load the last page of paginated data.
* `search: SearchResult`: A collection of variables and functions to handle pagination.
* `value: string`: The current value of the input, possibly changing rapidly as the user types.
* `debouncedValue: string`: The value that has been processed by the debounce function, updating less frequently than value. [Learn more about debouncing](https://dev.to/anjankarmakar/mastering-event-debouncing-in-javascript-a-guide-with-practical-example-19oe).
* `set: (value: string) => void`: A function to update the value.
* `clear: () => void`: A function to clear the value.
* `sort: (colName: string, sortDirection: "Ascending" | "Descending") => void`: A function that sorts by column name. If sort is `undefined`, no sorting is applied to the table.
* `error: Error | null`: An error from the client or server side, if encountered during the request. Will contain an error if the record isn't found by `id`. See the .
* `page.hasNextPage: boolean | undefined`: Whether the paginated data has a next page.
* `page.hasPreviousPage: boolean | undefined`: Whether the paginated data has a previous page.
* `page.variables: { first?: number; after?: string; last?: number; before?: string; }`:
* `first` holds a record count and `after` holds a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `last` holds a record count and `before` holds a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `page.pageSize: number`: Page size of the paginated data. Defaults to 50.
* `page.goToNextPage: () => void`: Function to load the next page of paginated data.
* `page.goToPreviousPage: () => void`: Function to load the last page of paginated data.
* `first` holds a record count and `after` holds a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `last` holds a record count and `before` holds a string cursor retrieved from the `pageInfo` of the previous page of results. See the pagination section in your application's API documentation for more info.
* `value: string`: The current value of the input, possibly changing rapidly as the user types.
* `debouncedValue: string`: The value that has been processed by the debounce function, updating less frequently than value. [Learn more about debouncing](https://dev.to/anjankarmakar/mastering-event-debouncing-in-javascript-a-guide-with-practical-example-19oe).
* `set: (value: string) => void`: A function to update the value.
* `clear: () => void`: A function to clear the value.
#### Paginated table
`useTable` handles pagination in the same way as the `useList` hook.
```tsx
import React from "react";
import { useTable } from "@gadgetinc/react";
import { api } from "../api";
export const PaginatedTable = () => {
const [{ rows, columns, fetching, error, page }] = useTable(api.customer);
if (error) return `An error occurred: ${error.message}`;
return (
<>
{fetching ? (
Loading table...
) : (
{/* Create a column header for each column name in `columns` */}
{columns?.map((col) => (
{col.header}
))}
{/* Create a row in the HTML table for each `row` in `rows` */}
{rows?.map((row) => (
{/* Index the row object using the column API identifier to populate each cell */}
{columns.map((col) => (
{String(row[col.identifier])}
))}
))}
)}
>
);
};
```
#### Searchable table
`useTable` returns a `search` object that allows you to search through the first page of data returned by the hook. Simply set the search value to the user's search query, and the table will automatically rerender:
```tsx
import React from "react";
import { useTable } from "@gadgetinc/react";
import { api } from "../api";
export const SearchableTable = () => {
const [{ rows, columns, fetching, error, search }] = useTable(api.customer);
const handleSearch = (event: React.ChangeEvent) => {
event.preventDefault();
search.set(event.target.value);
};
if (error) return `An error occurred: ${error.message}`;
return (
<>
{fetching ? (
Loading table...
) : (
{/* Create a column header for each column name in `columns` */}
{columns?.map((col) => (
{col.header}
))}
{/* Create a row in the HTML table for each `row` in `rows` */}
{rows?.map((row) => (
{/* Index the row object using the column API identifier to populate each cell */}
{columns.map((col) => (
{String(row[col.identifier])}
))}
))}
)}
>
);
};
```
#### Sortable table
A `sort` object is also returned by the hook, this allows you to sort any column in your table from the backend, with one line of code:
```tsx
import React from "react";
import { useTable } from "@gadgetinc/react";
import { api } from "../api";
export const SortableTable = () => {
const [{ rows, columns, fetching, error, sort }] = useTable(api.customer);
if (error) return `An error occurred: ${error.message}`;
return (
<>
{fetching ? (
Loading table...
) : (
{/* Create a column header for each column name in `columns` */}
{columns?.map((col) => (
{col.header}
))}
{/* Create a row in the HTML table for each `row` in `rows` */}
{rows?.map((row) => (
{/* Index the row object using the column API identifier to populate each cell */}
{columns.map((col) => (
{String(row[col.identifier])}
))}
))}
)}
>
);
};
```
### `useFetch()`
`useFetch(path: string, options: RequestInit = {})`
`useFetch` is a low-level hook for making an HTTP request to your Gadget backend's HTTP routes. `useFetch` preserves client-side authentication information by using `api.fetch` under the hood, which means fetches will use the same request identity as other GraphQL API calls using the other hooks.
Gadget apps get an auto-generated API for reading and writing data to your models, which is often faster and easier to use than `useFetch`. See your [app's API reference](https://docs.gadget-canary.xyz/api/example-app/development/) for more information.
```tsx
import { useFetch } from "@gadgetinc/react";
export function UserByEmail(props: { id: string }) {
const [{ data, fetching, error }, refresh] = useFetch("/users/me", {
method: "GET",
headers: {
"content-type": "application/json",
},
json: true,
});
if (error) return <>Error: {error.toString()}>;
if (fetching && !data) return <>Fetching...>;
if (!data) return <>No user found with id={props.id}>;
return
{data.name}
;
}
```
##### Parameters
* `path`: the server-side URL to fetch from. Corresponds to an HTTP route defined on in your backend Gadget app's `routes` folder
* `options`: options configuring the `fetch` call, corresponding exactly to those you might send with a normal `fetch`.
* `method`: the request method, like `"GET"`, `"POST"`, etc. Defaults to `"GET"`
* `headers`: the request headers, like `{ "content-type": "application/json" }`
* `body`: the request body to send to the server, like `"hello"` or `JSON.stringify({foo: "bar"})`
* `json`: If true, expects the response to be returned as JSON, and parses it for convenience
* `stream`:
* If `true`, response will be a `ReadableStream` object, allowing you to work with the response as it arrives
* If `"string"`, will decode the response as a string and update `data` as the response arrives; this is useful when streaming responses from LLMs
* `onStreamComplete`: a callback function that will be called with the final content when the streaming response is complete; this is only available when the `stream: "string"` option is set
* `sendImmediately`: If true, sends the first fetch on component mount. If false, waits for the `send` function to be called to send a request. Defaults to `true` for GET requests and `false` for any other HTTP verbs.
* See all the `fetch` options on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/fetch)
* `method`: the request method, like `"GET"`, `"POST"`, etc. Defaults to `"GET"`
* `headers`: the request headers, like `{ "content-type": "application/json" }`
* `body`: the request body to send to the server, like `"hello"` or `JSON.stringify({foo: "bar"})`
* `json`: If true, expects the response to be returned as JSON, and parses it for convenience
* `stream`:
* If `true`, response will be a `ReadableStream` object, allowing you to work with the response as it arrives
* If `"string"`, will decode the response as a string and update `data` as the response arrives; this is useful when streaming responses from LLMs
* `onStreamComplete`: a callback function that will be called with the final content when the streaming response is complete; this is only available when the `stream: "string"` option is set
* `sendImmediately`: If true, sends the first fetch on component mount. If false, waits for the `send` function to be called to send a request. Defaults to `true` for GET requests and `false` for any other HTTP verbs.
* See all the `fetch` options on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/fetch)
* If `true`, response will be a `ReadableStream` object, allowing you to work with the response as it arrives
* If `"string"`, will decode the response as a string and update `data` as the response arrives; this is useful when streaming responses from LLMs
##### Returns
`useFetch` returns a tuple with the current state of the request and a function to send or re-send the request. The state is an object with the following fields:
* `data`: the response data, if the request was successful
* `fetching`: a boolean describing if the fetch request is currently in progress
* `streaming`: a boolean describing if the fetch request is currently streaming. This is only set when the option `{ stream: "string" }` is set
* `error`: an error object if the request failed in any way
The second return value is a function for sending or resending the fetch request.
#### Request method
By default, `GET` requests are sent as soon as the hook executes. `GET` requests can also be refreshed by calling the second return value to re-send the fetch request and fetch fresh data.
```tsx
// GET request will be sent immediately, can be refreshed by calling `send()` again
const [{ data, fetching, error }, send] = useFetch("/some/route", { method: "GET" });
// ... sometime later
// `data` will be populated
data;
```
Other request methods like `POST`, `DELETE`, etc will not be sent automatically. The request will only be sent when the `send` functions is called explicitly, often in a click handler or similar.
```tsx
// POST requests will not be sent until `send` is called explicitly
const [{ data, fetching, error }, send] = useFetch("/some/route", {
method: "POST",
body: JSON.stringify({}),
});
// ... sometime later
// `data` will still be undefined
data;
await send();
// `data` will now be populated
data;
```
#### Sending request bodies
`useFetch` supports sending data to the server in the request body using the `body` option. Bodies are only supported for HTTP verbs which support them, like `POST`, `PUT`, `PATCH`, and `DELETE`.
To send a request body, pass a string, Buffer, or Stream object in the `body` key, and set the `content-type` header to match the type of data you are sending.
```tsx
const [{ data, fetching, error }, send] = useFetch("/some/path", {
method: "POST",
body: JSON.stringify({ foo: "bar" }),
headers: {
"content-type": "application/json",
},
});
```
This will send your data to a backend `routes/some/POST-path.js` file, where `request.body` will be `{ foo: "bar" }`.
#### Parsing the response
Unlike the `useFind` hooks, `useFetch` doesn't automatically parse and return rich data from your HTTP route. By default, `useFetch` returns a string of the response for the `data`. But, there's a couple convenience options for quickly parsing the response into the shape you need.
Pass the `{ json: true }` option to expect a JSON response from the server, and to automatically parse the response as JSON.
Pass the `{ stream: true }` to get a `ReadableStream` object as a response from the server, allowing you to work with the response as it arrives. Otherwise, the response will be returned as a `string` object.
Pass the `{ stream: "string" }` to decode the `ReadableStream` as a string and update data as it arrives. If the stream is in an encoding other than utf8 pass the encoding i.e. `{ stream: "utf-16" }`. When `{ stream: "string" }` is used, the `streaming` field in the state will be set to `true` while the stream is active, and `false` when the stream is complete. You can use this to show a loading indicator while the stream is active. You can also pass an `onStreamComplete` callback that will be called with the final string content when the stream is complete.
```tsx
const [{ data, fetching, streaming }, sendChat] = useFetch("/chat", {
method: "POST",
headers: {
"content-type": "application/json",
},
stream: "string",
// optional callback
// onStreamComplete: (content) => console.log(content)
});
```
#### When to use `useFetch` vs `useFindMany` etc
When possible, the hooks which make requests to your structured GraphQL API should be preferred. Your app's GraphQL API is auto-generated and full of useful features, which means you don't need to wire up custom routes on your backend to serve data. The API hooks provide built in type safety, error handling, caching, and `useFetch` does not.
#### Calling third-party APIs with `useFetch`
`@gadgetinc/react`'s `useFetch` hook calls `fetch` under the hood both client side and server side, which means you can use it to make HTTP requests to services other than your Gadget backend. You don't have to use `useFetch` to make calls elsewhere, but it is handy for avoiding adding other dependencies to your frontend code.
For example, we can call a third-party JSON API at `dummyjson.com`:
```tsx
import { useFetch } from "@gadgetinc/react";
export function DummyProducts() {
const [{ data, fetching, error }, resend] = useFetch("https://dummyjson.com/products", {
method: "GET",
json: true,
});
if (error) return <>Error: {error.toString()}>;
if (fetching || !data) return <>Fetching...>;
return
{JSON.stringify(data.products)}
;
}
```
`useFetch` will **not** send your Gadget API client's authentication headers to third party APIs. It will behave like a normal browser `fetch` call, just with the added React wrapper and `json: true` option for easy JSON parsing.
#### When the request gets sent
By default, `useFetch` will immediately issue HTTP requests for `GET`s when run. This makes it easy to use `useFetch` to retrieve data for use rendering your component right away.
```tsx
import { useFetch } from "@gadgetinc/react";
export function GetRequest() {
// will automatically send the request when the component renders the first time, as it is a GET
const [{ data, fetching, error }, resend] = useFetch("/products");
// fetching will be `true`
}
```
`useFetch` will _not_ immediately issue HTTP requests for HTTP verbs other than `GET`, like `POST`, `PUT`, etc. The HTTP request will only be sent when you call the returned `send` function.
```tsx
import { useFetch } from "@gadgetinc/react";
export function PostRequest() {
// will not automatically send the request when the component renders, call `send` to issue the request
const [{ data, fetching, error }, send] = useFetch("/products", { method: "POST" });
// fetching will be `false`
}
```
This behavior can be overridden with the `sendImmediately` option. You can avoid sending GET requests on render by passing `sendImmediately: false`:
```tsx
import { useFetch } from "@gadgetinc/react";
export function DelayedGetRequest() {
// will not automatically send the request when the component renders, call `send` to issue the request
const [{ data, fetching, error }, send] = useFetch("/products", { sendImmediately: false });
// fetching will be `false`
}
```
You can also have `POST` or `PUT` requests immediately issued by passing `sendImmediately: true`:
```tsx
import { useFetch } from "@gadgetinc/react";
export function ImmediatePutRequest() {
// will automatically send the request when the component renders the first time
const [{ data, fetching, error }, send] = useFetch("/products", { method: "POST", sendImmediately: true });
// fetching will be `true`
}
```
### `useSession()`
Returns the current session for the user viewing the app.
```tsx
import { useSession } from "@gadgetinc/react";
import { api } from "../api";
const WhoAmI = () => {
const session = useSession(api);
return <>{session ? `You are signed in with session id ${session.id} as ${session.user?.email}` : "You are not signed in"}>;
};
```
`useSession` will suspend your application if session data hasn't been loaded yet. See [React's Suspense docs](https://react.dev/reference/react/Suspense) for more information.
### `useUser()`
Returns the current user of the session, if present. For unauthenticated sessions, returns `null`.
```tsx
import { useUser } from "@gadgetinc/react";
import { api } from "../api";
const WhoAmI = () => {
const user = useUser(api);
return <>{user ? `You are signed in as ${user.email}` : "You are not signed in"}>;
};
```
`useUser` will suspend your application if session data hasn't been loaded yet. See [React's Suspense docs](https://react.dev/reference/react/Suspense) for more information.
### `useAuth()`
Returns an object representing the current authentication state of the session.
```tsx
import { useUser, useAuth } from "@gadgetinc/react";
import { api } from "../api";
function App() {
const user = useUser(api);
const { isSignedIn, configuration } = useAuth(api);
if (isSignedIn) {
return
Hello, {user.name}
;
} else {
return Please sign in;
}
}
```
##### Returns
* `user` - the current `User`, if signed in. Similar to `useUser`.
* `session` - the current `Session`. Similar to `useSession`.
* `isSignedIn` - set to `true` if the session has a user associated with it (signed in), `false` otherwise.
### The `select` option
The `select` option allows you to choose which fields and subfields are returned by your Gadget app's GraphQL API. Your app's API supports returning only some fields of each model you request, as well as fields of related models through the Gadget relationship field types. The `select` option is an object with keys representing the `apiIdentifier` of fields in your Gadget models, and values holding a boolean describing if that field should be selected or not, or a subselection for object-typed fields.
For example, you can limit the fields selected by a finder to only return some fields, lowering the amount of bandwidth used and making your requests faster:
```tsx
import { useFindOne } from "@gadgetinc/react";
import { api } from "../api";
export const OnlySomeWidgetFields = (props: { id: string }) => {
// fetch only the widget id and name fields
const [{ data, fetching, error }, _refetch] = useFindOne(api.widget, props.id, { select: { id: true, name: true } });
return (
{data?.id}: {data?.name}
);
};
```
You can also use the `select` option for selecting fields of related models. For example, if we have a backend **Blog Post** model which has a `HasMany` field to a **Comment** model, we can fetch a blog post and it's related comments:
```tsx
import { useFindOne } from "@gadgetinc/react";
import { api } from "../api";
export const BlogWithComments = (props: { id: string }) => {
// fetch only the blogPost id and name fields ...
const [{ data, fetching, error }, _refetch] = useFindOne(api.blogPost, props.id, {
select: {
id: true,
title: true,
body: true,
// and fetch the post's `comments` HasMany relationship field on the post
comments: {
edges: {
node: {
id: true,
body: true,
// and fetch the author's BelongsTo User relationship field also
author: { email: true },
},
},
},
},
});
if (!data) return null;
return (
<>
{data.title}
{data.comments.edges.map((edge) => (
{edge.node.author?.email} says {edge.node.body}
))}
>
);
};
```
**Note**: The shape of the options you pass in the `select` option matches exactly the shape of the GraphQL API for your application. Gadget applications use Relay-style GraphQL pagination, which means lists of records are accessed using the `relatedField: { edges: { node: true } }` style. BelongsTo and HasOne field types are accessed without any intermediate fields.
For TypeScript users, the `select` option is fully typesafe, allowing you to typecheck which fields you're fetching from the backend as well as ensure that the fields you render in your components are actually selected.
#### The `select` option for `useActionForm`
The select option for `useActionForm` is slightly different from the select option for other actions. For `useActionForm`, the `select` option allows you to mark fields as `ReadOnly` to indicate that they should not be forwarded to the backend when submitting the form. For information, see the [useActionForm guide](https://docs.gadget-canary.xyz/guides/frontend/forms#submitting-the-form-and-excluding-fields).
```tsx
import { useActionForm } from "@gadgetinc/react";
import { api } from "../api";
const PostForm = () => {
const { register, submit } = useActionForm(api.post.update, {
findBy: "1",
select: {
id: true,
title: true,
category: "ReadOnly",
},
});
return (
);
};
```
### The `live` option
The `live` option makes your component re-render when the data it is showing changes for any reason in the database. Passing `live: true` sets up a special `@live` query from your frontend to the Gadget backend which subscribes to changes in the on-screen data, and will continue streaming in those changes as they happen while the component remains mounted.
For example, we can show users a live view of the list of widgets from the backend with `useFindMany(api.widget, { live: true })`:
```tsx
import { useFindOne } from "@gadgetinc/react";
import { api } from "../api";
export const LiveWidgets = (props: { id: string }) => {
const [{ data, fetching, error }] = useFindOne(api.widget, props.id, { live: true });
// will re-render when widgets change
return (
{data?.id}: {data?.name}
);
};
```
`live: true` can be combined with the other options you might pass hooks, like the `select` option for selecting fields of related models, or the `filter` and `sort` options for limiting the result set. Hooks passed `live: true` will respect the given selection, filtering, sorting, and pagination, and only trigger re-renders when the relevant backend data changes.
```tsx
import { useFindOne } from "@gadgetinc/react";
import { api } from "../api";
export const BlogWithComments = (props: { id: string }) => {
const [{ data, fetching, error }, _refetch] = useFindOne(api.blogPost, props.id, {
live: true,
select: {
id: true,
title: true,
body: true,
// fetch the post's `comments` HasMany relationship field on the post
comments: {
edges: {
node: {
id: true,
body: true,
// fetch the author's BelongsTo User relationship field also
author: {
email: true,
},
},
},
},
},
});
if (!data) return null;
// will re-render when the blog post changes, any of its comments change, or any of the comment authors' emails change
return (
<>
{data.title}
{data.comments.edges.map((edge) => (
{edge.node.author?.email} says {edge.node.body}
))}
>
);
};
```
#### Live query result values
When using `live: true`, your hook will the same thing the non-live variants do, which is a tuple with:
* a `data` object containing the up-to-date result from the backend
* a `fetching` boolean describing if the initial data fetch has completed or not
* an `error` object describing any errors encountered during execution at any point
When the `live: true` hook mounts, it will fetch some initial data, then keep it up to data over time by subscribing to the backend. The `fetching` boolean describes if the _initial_ data fetch has happened. Once the initial fetch is complete, the `fetching` boolean will be false, and new data will appear in the `data` object.
### Errors from the returned `error` object
Running queries or mutations can produce a few different kinds of errors your client side should handle:
* network errors where the browser is unable to connect to the server at all
* validation errors where the client sent information to the server successfully, but the server deemed it invalid and rejected it
* server side errors where the client sent information to the server but the server failed to process it due to a bug or transient issue.
Each of these error cases is broken out on the `error` object returned by `useAction` (and any of the other hooks). The `error` object is an `ErrorWrapper` object, which has a number of properties for figuring out exactly what went wrong:
* `error.message: string` - A top level error message which is always present
* `error.networkError: Error | undefined` - An error thrown by the browser when trying to communicate with the server
* `error.executionErrors: (GraphQLError | GadgetError)[] | undefined` - Any errors thrown by the GraphQL API, like missing parameters or invalid selections, and any errors thrown by the server concerning invalid data or backend processing errors.
* `error.validationErrors: { apiIdentifier: string, message: string }[] | undefined` - Any validation errors returned by the server. A shortcut to accessing the `.validationErrors` property of the first `InvalidRecordError` in the `.executionErrors` of the outer `ErrorWrapper` object. Useful for building form validations.
### Default selections
Gadget makes a default selection when you don't pass the `select` option to a finder, which will include all the model's scalar fields and a small representation of its related records. This default is also type safe, so you can rely on the returned objects from default finder methods returning type safe results conforming to the default shape. To figure out exactly what your client will select by default for a model, see the documentation for that model in your generated API documentation.
### The `refetch` function
The `refetch` function returned as the second return value for some hooks can be executed in order to trigger a refetch of the most up to date data from the backend. This is useful for powering refresh buttons in user-facing UI, or for periodically updating the client side data. See [`urql`'s docs](https://formidable.com/open-source/urql/docs/basics/react-preact/#reexecuting-queries) on re-executing queries for more information.
As an example, we could use the `refetch` function to power a refresh button in a table:
```tsx
import React from "react";
import { useFindMany } from "@gadgetinc/react";
import { api } from "../api";
export const ShowWidgetNames = () => {
// get the second return value of `useFindMany`, which is the refetch function
const [{ data, fetching, error }, refetch] = useFindMany(api.widget);
if (fetching) return "Loading...";
if (error) return `Error loading data: ${error}`;
return (
ID
Name
{data?.map((widget) => (
{widget.id}
{widget.name}
))}
);
};
```
### Suspense
`@gadgetinc/react` supports two modes for managing loading states: the `fetching` return value, which will be true when making requests under the hood, as well as using ``, React's next generation tool for managing asynchrony. Read more about `` in the [React docs](https://react.dev/reference/react/Suspense).
To suspend rendering when fetching data, pass the `suspense: true` option to the `useFind*` hooks.
```tsx
import React from "react";
import { useFindMany } from "@gadgetinc/react";
import { api } from "../api";
export const Posts = () => {
// pass suspense: true, and the component will only render once data has been returned
const [{ data, error }, refresh] = useFindMany(api.post, { suspense: true });
// note: no need to inspect the fetching prop
return (
<>
{data!.map((post) => (
{post.title}
))}
>
);
};
```
All the read hooks support suspense: `useFindOne`, `useMaybeFindOne`, `useFindMany`, `useFindFirst`, `useMaybeFindFirst`, and `useGet`.
`suspense: true` is most useful when a parent component wraps a suspending-child with the `` component for rendering a fallback UI while the child component is suspended:
```tsx
const PostsContainer = () => {
return (
);
};
```
With this wrapper in place, the fallback prop will be rendered while the data is being fetched, and once it's available, the `` component will render with data.
Read more about `` in the [React docs](https://react.dev/reference/react/Suspense). `suspense: true` uses `urql`'s suspense support under the hood.
### Request caching
Under the hood, your Gadget app's API client and `@gadgetinc/react` use a powerful, production-grade GraphQL client called [urql](https://formidable.com/open-source/urql/docs/). urql has a great client-side data caching feature built-in called [Document Caching](https://formidable.com/open-source/urql/docs/basics/document-caching/) which allows React components issuing GraphQL requests for the same data to de-duplicate requests and share client-side state. `@gadgetinc/react` enables this functionality by default.
`@gadgetinc/react` runs urql's Document Caching with a default [`requestPolicy`](https://formidable.com/open-source/urql/docs/basics/document-caching/#request-policies) of `cache-and-network`, which means your React hooks will re-render data with any cached results from the in-memory store, and then make an underlying HTTP request to fetch the most up to date data.
If you want to change the default `requestPolicy` that your Gadget API client and React hooks use, you can pass the `requestPolicy` option to your API client constructor.
```tsx
// instantiate the API client for our app that will make network calls for every query, regardless of cache state
const api = new ExampleAppClient({
requestPolicy: "network-only",
});
```
There are four different request policies that you can use:
* `cache-first` prefers cached results and falls back to sending an API request when no prior result is cached.
* `cache-and-network` (the default) returns cached results but also always sends an API request, which is perfect for displaying data quickly while keeping it up-to-date.
* `network-only` will always send an API request and will ignore cached results.
* `cache-only` will always return cached results or null.
For more information on `urql`'s built-in client-side caching, see [`urql`'s docs](https://formidable.com/open-source/urql/docs/basics/document-caching/).
### `urql` exports
Since this library uses `urql` behind the scenes, it provides a few useful exports directly from `urql` so that it does not need to be installed as a peer dependency should you need to write custom queries or mutations.
The following are exported from `urql`:
* `Provider`
* `Consumer`
* `Context`
* `useQuery`
* `useMutation`
Example usage:
```tsx
import React from "react";
import { api } from "../api";
import { Provider, useQuery } from "@gadgetinc/react";
export const ShowWidgetNames = () => {
// find all widgets and the most recently created gizmo related to the widget
const [{ data, fetching, error }, refetch] = useQuery({
query: `
query GetWidgets {
widgets {
edges {
node {
id
name
gizmos(first: 1, sort:{ createdAt: Descending }) {
edges {
node {
createdAt
}
}
}
}
}
}
}
`,
});
if (fetching) return "Loading...";
if (error) return `Error loading data: ${error}`;
return (
{widget.gizmos.edges.length > 0 ? widget.gizmos.edges[0].node.createdAt : "Does not have Gizmos"}
))}
);
};
export const App = () => (
);
```
## Components
If you are trying to control the layout of your application based on authentication state, it may be helpful to use the Gadget auth React components instead of, or in addition to, the hooks.
### ``
Conditionally renders its children if the current session has a user associated with it, similar to the `isSignedIn` property of the `useAuth()` hook.
```tsx
import { SignedIn } from "@gadgetinc/react";
const SignedInMessage = () => (
Hello, human!
);
```
### ``
Conditionally renders its children if the current session does not have a user associated with it.
```tsx
import { SignedOut } from "@gadgetinc/react";
const SignedOutMessage = () => (
Sign In!
);
```
### ``
Conditionally renders its children if the current session has a user associated with it, or redirects the browser via `window.location.assign` if the user is not currently signed in. This component is helpful for protecting frontend routes.
```tsx
import { Route, RouterProvider, createBrowserRouter, createRoutesFromElements } from "react-router";
import { SignedInOrRedirect } from "@gadgetinc/react";
export const RouterComponent = () => {
const router = createBrowserRouter(
createRoutesFromElements(
}>
} />
}
/>
} />
)
);
return (
<>
>
);
};
```
### ``
Conditionally renders its `children` if the current `Session` is signed out, otherwise redirects the browser to the `path` prop. Uses `window.location.assign` to perform the redirect.
```tsx
import { Route, RouterProvider, createBrowserRouter, createRoutesFromElements } from "react-router";
import { SignedOutOrRedirect } from "@gadgetinc/react";
export const RouterComponent = () => {
const router = createBrowserRouter(
createRoutesFromElements(
}>
}
/>
} />
)
);
return (
<>
>
);
};
```
## Authentication
When working with Gadget auth, there are several hooks and components that can help you manage the authentication state of your application.
The `Provider` component exported from this library accepts an `auth` prop which can be used to configure the relative paths to your app's sign in and sign out endpoints. If you do not provide these paths, the default values of `/` and `/signed-in` will be used.
The hooks use the Gadget client's `suspense: true` option, making it easier to manage the async nature of the hooks without having to deal with loading state.
```tsx
import { MyGadgetAppClient } from "@gadget-client/my-gadget-app";
import { Provider } from "@gadgetinc/react";
import { Suspense } from "react";
import App from "./App";
// instantiate the API client for our app
const api = new MyGadgetAppClient({ authenticationMode: { browserSession: true } });
export function Main() {
// ensure any components which use the @gadgetinc/react hooks are wrapped with the Provider and a Suspense component
return (
Loading...>}>
);
}
```