The @gadgetinc/preact package provides Preact hooks for calling your Gadget app's auto-generated API.
Preact
import { useFindMany, useAction } from "@gadgetinc/preact";
// 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 to delete a specific widget
// when a user clicks a button
const [_, deleteWidget] = useAction(api.widget.delete);
if (fetching) return "Loading...";
return (
<ul>
{data?.map((widget) => (
<li>
{widget.name}
<button onClick={() => deleteWidget({ id: widget.id })}>Delete</button>
</li>
))}
</ul>
);
}
Key features
Rule-obeying hooks for reading and writing data from a backend that handle all request lifecycle and auth like useFindOne, useAction, and useFetch
Full type safety for inputs and outputs driven by each Gadget app's backend schema, including over dynamic selections
A full-featured, GraphQL-powered nested object selection system using the select option
Data hydrations that return useful objects like Date
Installation
Gadget apps already include the required JS API client.
Install @gadgetinc/preact using your project's package manager. Gadget apps use yarn.
Provider setup
Your Preact components must be wrapped in the Provider component from this library for the hooks to function properly.
Preact
// 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/preact";
import { ComponentChildren } from "preact";
// instantiate the API client for our app
const api = new ExampleAppClient({ authenticationMode: { browserSession: true } });
export const MyApp = (props: { children: ComponentChildren }) => {
// wrap the application in the <Provider> so the hooks can find the current client
return <Provider api={api}>{props.children}</Provider>;
};
Client side vs server side Preact
The @gadgetinc/preact 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. See the Preact docs for instructions on server-side rendering.
useFindOne fetches one record from your Gadget database with a given id.
Preact
import { useFindOne } from "@gadgetinc/preact";
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 <span className="widget-name">{data?.name}</span>;
};
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 select option
requestPolicy: The urql request policy to make the request with. See urql's docs
pause: Set to true to disable this hook. See urql's docs
suspense: Should this hook suspend when fetching data. See suspense
Returns
useFindOne returns two values: a result object with the data, fetching, and error keys for inspecting in your Preact component's output, and a refetch 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.
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 errors section.
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 select option:
Preact
import { useFindOne } from "@gadgetinc/preact";
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 (
<span className="widget-name">
{data?.id}: {data?.name}
</span>
);
};
useMaybeFindOne fetches one record from your Gadget database with a given id.
Preact
import { useMaybeFindOne } from "@gadgetinc/preact";
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 <span className="widget-name">{data.name}</span>;
} 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 useFindOne, and accepts the same options.
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 select option docs.
filter: A list of filters to limit the set of returned records. Optional. See the 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 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 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 live option docs.
requestPolicy: The urql request policy to make the request with. See urql's docs
pause: Should the hook make a request right now or not. See urql's docs
suspense: Should this hook suspend when fetching data. See suspense for more info
Returns
useFindMany returns two values: a result object with the data, fetching, and error keys for use in your Preact component's output, and a refetch function 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 errors section.
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:
Preact
import { useFindMany } from "@gadgetinc/preact";
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 (
<ul>
{data?.map((widget) => (
<li key={widget.id}>{widget.name}</li>
))}
</ul>
);
};
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:
Preact
// 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 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:
Preact
// 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:
Preact
// return widgets with "penny" in any searchable field
const [{ data, fetching, error }, _refetch] = useFindMany(api.widget, {
search: "penny",
});
See your app's API reference for more information on the search query syntax and which fields are searchable.
useFindMany accepts a live option to subscribe to changes in the backend data returned, which will trigger re-renders of your Preact components as that data changes. For example, we can show an up-to-date view of the first page of backend widgets:
Preact
// 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, where a second page is fetched by asking for the next x many results after a cursor returned with the first page.
Preact
// 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 and 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 Preact state, or for a better user experience, using the URL with whatever router system works for your application. We use Preact state to demonstrate pagination in this example:
Preact
import { useState } from "preact/hooks";
import { useFindMany } from "@gadgetinc/preact";
import { api } from "../api";
export const WidgetPaginator = () => {
// store the current cursor in the backend data
const [cursor, setCursor] = (useState < string) | (undefined > 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 (
<>
<ul>
{data?.map((widget) => (
<li key={widget.id}>{widget.name}</li>
))}
</ul>
<button
onClick={() => {
// update the cursor, which will trigger a refetch of the next page and rerender with a new `data` object
setCursor(data?.endCursor);
}}
disabled={!data?.hasNextPage}
>
Next page
</button>
</>
);
};
useFindFirst fetches the first record from your backend Gadget database that matches the given filter, sort, and search.
Preact
import { useFindFirst } from "@gadgetinc/preact";
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 (
<span className="banner">
Check out our most recent blog post titled {data?.title}
</span>
);
};
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 select option docs. Optional.
filter: A list of filters to find a record matching. Optional. See the 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 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 in your application's API documentation for more info.
live: Should this hook re-render when data changes on the backend. See the live option docs.
requestPolicy: The urql request policy to make the request with. See urql's docs
pause: Should the hook make a request right now or not. See urql's docs
suspense: Should this hook suspend when fetching data. See suspense for more info
Returns
useFindFirst returns two values: a result object with the data, fetching, and error keys for inspecting in your Preact component's output, and a refetch 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 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 errors section.
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 select:
Preact
// 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 live option, and re-render when the backend data changes:
Preact
// 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 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 to a field.
Preact
import { useFindBy } from "@gadgetinc/preact";
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 (
<>
<h2>{data?.title}</h2>
<p>{data?.body}</p>
</>
);
};
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 select option docs.
live: Should this hook re-render when data changes on the backend. See the live option docs.
requestPolicy: The urql request policy to make the request with. See urql's docs
pause: Should the hook make a request right now or not. See urql's docs
suspense: Should this hook suspend when fetching data. See suspense for more info
Returns
useFindBy returns two values: a result object with the data, fetching, and error keys for inspecting in your Preact component's output, and a refetch 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 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 errors section.
If no record is found matching the conditions, then the returned object will have null for the data. useFindBy(api.widget.findByEmail, "[email protected]") is the Preact equivalent of api.widget.findByEmail("[email protected]")
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:
Preact
import { useFindBy } from "@gadgetinc/preact";
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 (
<>
<h2>{data?.title}</h2>
<p>{data?.body}</p>
</>
);
};
useFindBy can take options that allow the customization of which fields are returned:
Preact
// 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 on re-executing queries for more information.
useMaybeFindFirst fetches the first record from your backend Gadget database that matches the given filter, sort, and search parameters.
Preact
import { useMaybeFindFirst } from "@gadgetinc/preact";
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 (
<span className="banner">
Check out our most recent blog post titled {data.title}
</span>
);
} 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 useFindFirst for more details on the options useMaybeFindFirst accepts.
import { useAction } from "@gadgetinc/preact";
import { api } from "../api";
export const CreatePost = () => {
const [{ data, fetching, error }, createBlogPost] = useAction(api.blogPost.create);
return (
<button
onClick={() => {
createBlogPost({
title: "New post created from Preact",
});
}}
>
Create a post
</button>
);
};
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 select option docs.
requestPolicy: The urql request policy to make the request with. See urql's docs
pause: Should the hook make a request right now or not. See urql's docs
suspense: Should this hook suspend when fetching data. See suspense for more info
Returns
useAction returns two values: a result object with the data, fetching, and error keys for inspecting in your Preact component's output, and a act function to actually run the backend action.
useAction is a rule-following Preact 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 errors section.
For example, we can create a button that creates a post when clicked, and then shows the post once it has been created:
Preact
import { useAction } from "@gadgetinc/preact";
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 (
<button
onClick={() => {
createBlogPost({
title: "New post created from Preact",
});
}}
>
Create a post
</button>
);
} else {
return (
<>
<h2>{data.title}</h2>
<p>{data.body}</p>
</>
);
}
};
We can also run actions on existing models by passing the id: in with the action parameters:
Preact
import { useState } from "preact/hooks";
import { useAction } from "@gadgetinc/preact";
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 (
<form
onSubmit={() => {
// pass the id of the blog post we're updating as one parameter, and the new post attributes as another
updateBlogPost({
id: props.id,
title,
});
}}
>
<label>Title</label>
<input
type="text"
value={title}
onChange={(event) => setTitle(event.target.value)}
/>
<input type="submit">Submit</input>
</form>
);
};
useAction can take options that allow the customization of which fields are returned on the acted-upon record:
Preact
// fetch only a post id and title fields
const [{ data, fetching, error }, updateBlogPost] = useAction(api.blogPost.update, {
select: { id: true, title: true },
});
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
pause: Should the hook make a request right now or not. See urql's docs
Returns
useGlobalAction returns two values: a result object with the data, fetching, and error keys for inspecting in your Preact component's output, and an act function to actually run the backend global action. useGlobalAction is a rule-following Preact 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 errors section.
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:
Preact
import { useGlobalAction } from "@gadgetinc/preact";
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 (
<button
onClick={() => {
runPurge({ foo: "bar" });
}}
>
Purge data
</button>
);
} else {
return "Purge completed";
}
};
useView is a hook for calling both named and inline computed views.
useView for named computed views
Preact
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) => (
<div>
{player.name}: {player.totalScore}
</div>
))}
</>
);
}
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 select option docs.
requestPolicy: The urql request policy to make the request with. See urql's docs
pause: Should the hook make a request right now or not. See urql's docs
suspense: Should this hook suspend when fetching data. See suspense for more info
Returns
useView returns two values: a result object with the data, fetching, and error keys for inspecting in your Preact component's output, and a refetch function 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 errors section.
Inline views can be run with useView by passing in a Gelly query as a string:
useView for inline computed views
Preact
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) => (
<div>
{player.name}: {player.score}
</div>
))}
</>
);
}
Computed view parameters can also be passed into both named and inline computed views executed using the useView hook:
useView for a computed view with parameters
Preact
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) => (
<div>
{player.name}: {player.totalScore}
</div>
))}
</>
);
}
Avoid using string interpolation on inline computed views
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 fetches a singleton record for an api.currentSomething style model manager. useGet fetches one global record, which is most often the current session.
Preact
import { useGet } from "@gadgetinc/preact";
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 <span>Current session: {data?.id}</span>;
};
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 select option docs.
requestPolicy: The urql request policy to make the request with. See urql's docs
pause: Should the hook make a request right now or not. See urql's docs
suspense: Should this hook suspend when fetching data. See suspense for more info
Returns
useGet returns two values: a result object with the data, fetching, and error keys for inspecting in your Preact component's output, and a refetch 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 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 errors section.
useGet(api.currentSession) retrieves the current global session for the current browser
Preact
import { useGet } from "@gadgetinc/preact";
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 <span>Current session: {data?.id}</span>;
};
useGet can take options which allow the customization of which fields are returned on the selected record:
Preact
// 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 on re-executing queries for more information.
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.
Returns
useEnqueue doesn't submit the background action when invoked but instead returns a function for enqueuing the action in response to the event.
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 for
more information.
Preact
import { useFetch } from "@gadgetinc/preact";
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 <div>{data.name}</div>;
}
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.
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.
Preact
// 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.
Preact
// 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.
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.
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/preact'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:
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 Preact wrapper and json: true option for easy JSON parsing.
When the request gets sent
By default, useFetch will immediately issue HTTP requests for GETs when run. This makes it easy to use useFetch to retrieve data for use rendering your component right away.
Preact
import { useFetch } from "@gadgetinc/preact";
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.
Preact
import { useFetch } from "@gadgetinc/preact";
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:
Preact
import { useFetch } from "@gadgetinc/preact";
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:
Preact
import { useFetch } from "@gadgetinc/preact";
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`
}
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:
Preact
import { useFindOne } from "@gadgetinc/preact";
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 (
<span className="widget-name">
{data?.id}: {data?.name}
</span>
);
};
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:
Preact
import { useFindOne } from "@gadgetinc/preact";
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 (
<>
<h2>{data.title}</h2>
<ul>
{data.comments.edges.map((edge) => (
<li>
{edge.node.author?.email} says {edge.node.body}
</li>
))}
</ul>
</>
);
};
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.
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 }):
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.
Preact
import { useFindOne } from "@gadgetinc/preact";
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 (
<>
<h2>{data.title}</h2>
<ul>
{data.comments.edges.map((edge) => (
<li>
{edge.node.author?.email} says {edge.node.body}
</li>
))}
</ul>
</>
);
};
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 on re-executing queries for more information.
As an example, we could use the refetch function to power a refresh button in a table:
Preact
import { useFindMany } from "@gadgetinc/preact";
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 (
<table>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
{data?.map((widget) => (
<tr key={widget.id}>
<td>{widget.id}</td>
<td>{widget.name}</td>
</tr>
))}
<tr>
<button onClick={() => void refetch()}>Refresh</button>
</tr>
</table>
);
};
Suspense
@gadgetinc/preact 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 <Suspense/>, Preact's next generation tool for managing asynchrony. Read more about <Suspense/> in the Preact docs.
To suspend rendering when fetching data, pass the suspense: true option to the useFind* hooks.
Preact
import { useFindMany } from "@gadgetinc/preact";
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) => (
<div key={post.id}>{post.title}</div>
))}
</>
);
};
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 <Suspense/> component for rendering a fallback UI while the child component is suspended:
With this wrapper in place, the fallback prop will be rendered while the data is being fetched, and once it's available, the <Posts/> component will render with data.
Read more about <Suspense/> in the Preact docs. suspense: true uses urql's suspense support under the hood.
Request caching
Under the hood, your Gadget app's API client and @gadgetinc/preact use a powerful, production-grade GraphQL client called urql. urql has a great client-side data caching feature built-in called Document Caching which allows Preact components issuing GraphQL requests for the same data to de-duplicate requests and share client-side state. @gadgetinc/preact enables this functionality by default.
@gadgetinc/preact runs urql's Document Caching with a default requestPolicy of cache-and-network, which means your Preact 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 Preact hooks use, you can pass the requestPolicy option to your API client constructor.
Preact
// 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.
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:
Preact
import { api } from "../api";
import { Provider, useQuery } from "@gadgetinc/preact";
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 (
<table>
<tr>
<th>ID</th>
<th>Name</th>
<th>Last Gizmo Created</th>
</tr>
{data.widgets.edges.map(({ node: { ...widget } }) => (
<tr key={widget.id}>
<td>{widget.id}</td>
<td>{widget.name}</td>
<td>
{widget.gizmos.edges.length > 0
? widget.gizmos.edges[0].node.createdAt
: "Does not have Gizmos"}
</td>
</tr>
))}
<tr>
<button onClick={() => void refetch()}>Refresh</button>
</tr>
</table>
);
};
export const App = () => (
<Provider api={api}>
<ShowWidgetNames />
</Provider>
);