Building frontends 

Reading data from models 

Gadget provides the @gadgetinc/react library of React hooks for reading and writing data from your Gadget models. The useFindOne, useFindMany, useFindBy, and useGet hooks read data from your backend models and render UI with React.

Each of these hooks returns an object with the requested data, the fetching state, and an error if one was encountered, as well as a refetch function for refreshing the data if need be.

For example, if you have a model named Post, we can fetch post records in a variety of ways:

jsx
1// fetch one post by id
2const [{ data, fetching, error }, refetch] = useFindOne(api.post, 10);
3
4// fetch the first 10 posts
5const [{ data, fetching, error }, refetch] = useFindMany(api.post, { first: 10 });
6
7// fetch the first post with the slug field equal to "example-slug", throw if it isn't found
8const [{ data, fetching, error }, refetch] = useFindFirst(api.post, { where: { slug: "example-slug" } });
9
10// fetch the first post with the slug field equal to "example-slug", return null if it isn't found
11const [{ data, fetching, error }, refetch] = useMaybeFindFirst(api.post, { where: { slug: "example-slug" } });
12
13// fetch the user's current session
14const [{ data, fetching, error }, refetch] = useGet(api.currentSession);

Each of these hooks must be wrapped in a React component to render. For example, we can use useFindMany to display a list of posts in a component:

jsx
1import { useFindMany } from "@gadgetinc/react";
2import { api } from "../api";
3
4export const PostsList = (props) => {
5 const [{ data, fetching, error }, _refetch] = useFindMany(api.post, { first: 10 });
6
7 if (fetching) {
8 return <div>Loading...</div>;
9 }
10
11 if (error) {
12 return <div>Error: {error.message}</div>;
13 }
14
15 return (
16 <ul>
17 {data.map((post) => (
18 <li key={post.id}>{post.title}</li>
19 ))}
20 </ul>
21 );
22};

For more documentation on the @gadgetinc/react hooks library, see the reference.

Writing data to models 

The @gadgetinc/react React hooks library includes the useAction hook for writing data to models by running Actions. useAction returns two values: a result object with the data from running, the fetching state, and the error if one was encountered, as well as an act function to run the backend action.

For example, if you have a model named Post, we can create a new post by calling the act function returned by the useAction hook:

jsx
const [{ data, fetching, error }, act] = useAction(api.post.create);
// when ready, run the `act` function to trigger the action
act({ title: "Example Post", body: "some post content" });

We can use the useAction hook in a React component that calls the action with data from a form:

jsx
1import { useState } from "react";
2import { useAction } from "@gadgetinc/react";
3import { api } from "../api";
4
5export const CreatePostForm = (props) => {
6 const [title, setTitle] = useState("");
7 const [body, setBody] = useState("");
8
9 const [{ data, fetching, error }, act] = useAction(api.post.create);
10
11 if (fetching) {
12 return <div>Saving...</div>;
13 }
14
15 if (error) {
16 return <div>Error: {error.message}</div>;
17 }
18
19 return (
20 <form
21 onSubmit={() => {
22 // run the action function when the form is submitted
23 // the component will re-render with `fetching: true` initially, and then when the response arrives, render again with the result in `data`.
24 void act({ title, body });
25 }}
26 >
27 <label>Title</label>
28 <input value={title} onChange={(e) => setTitle(e.target.value)} />
29 <label>Body</label>
30 <textarea onChange={(e) => setTitle(e.target.value)}>{body}</textarea>
31 <input type="submit" />
32 </form>
33 );
34};

The same approach is used for updating data with useAction(api.post.update). If you're presenting a form to the user, you often need to fetch the initial data to show in the form before letting the user edit. This can be done with a useFindOne hook to read the data, and then a useAction hook to write the data when the user is ready to submit the form.

For example, we can build an update form for the Post model like this:

jsx
1import { useEffect, useState } from "react";
2import { useAction } from "@gadgetinc/react";
3import { api } from "../api";
4
5// expects a postId prop to decide which post we're editing
6export const UpdatePostForm = (props) => {
7 const [title, setTitle] = useState("");
8 const [body, setBody] = useState("");
9
10 const [{ data: initialData, fetching, error }] = useFindOne(api.post, props.postId);
11 const [{ fetching: saving, error: mutationError }, act] = useAction(api.post.create);
12
13 useEffect(() => {
14 // when the initial data is loaded, set it into the React state
15 if (initialData) {
16 setTitle(initialData.title);
17 setBody(initialData.body);
18 }
19 }, [initialData]);
20
21 return (
22 <form
23 onSubmit={() => {
24 void act(props.postId, { title, body });
25 }}
26 >
27 {error || (mutationError && <div>Error: {(error || mutationError).message}</div>)}
28 {fetching && <div>Loading...</div>}
29 {initialData && (
30 <>
31 <label>Title</label>
32 <input value={title} onChange={(e) => setTitle(e.target.value)} />
33 <label>Body</label>
34 <textarea onChange={(e) => setBody(e.target.value)}>{body}</textarea>
35 <input disabled={saving} type="submit" />
36 </>
37 )}
38 </form>
39 );
40};

For more details on the useAction hook, see the @gadgetinc/react reference.

Advanced form management

Managing forms with React can grow to be challenging when using only React's basic state management tooling. For complicated forms, Gadget recommends adopting a client-side state management library like react-hook-form for managing state and validation.

Calling Global Actions 

The @gadgetinc/react React hooks library includes the useGlobalAction hook for invoking Global Actions. useGlobalAction returns two values: a result object with the data from running, the fetching state, and the error if one was encountered, as well as an act function to run the backend global action.

For example, if you have a Global Action named syncData, we can run this action by calling the act function returned by the useGlobalAction hook:

jsx
const [{ data, fetching, error }, act] = useGlobalAction(api.syncData);
// when ready, run the `act` function to trigger the action
act({
// any params the global action might expect
});

We can use the useGlobalAction hook in a React component that calls the action when a button is clicked:

jsx
1import { useState } from "react";
2import { useGlobalAction } from "@gadgetinc/react";
3import { api } from "../api";
4
5export const CreatePostForm = (props) => {
6 const [title, setTitle] = useState("");
7 const [body, setBody] = useState("");
8
9 const [{ data, fetching, error }, act] = useGlobalAction(api.syncData);
10
11 return;
12 <>
13 <button disabled={fetching} onClick={() => void act()}>
14 Sync Data
15 </button>
16 {error && <div>Error: {error.message}</div>}
17 </>;
18};

For more details on the useGlobalAction hook, see the @gadgetinc/react reference.

Calling HTTP routes 

Backend HTTP Routes are available for calling from your frontend codebase. Calling these routes can be done with any HTTP client, but within the frontend, Gadget recommends using the useFetch hook. useFetch provides a pleasant React wrapper around the built-in browser fetch function, and includes automatic authentication support.

In a frontend React component, useFetch will make a request to a backend HTTP route. For example, if we have a routes/GET-hello.js file that sends a JSON reply in our backend like this:

routes/GET-example.js
JavaScript
module.exports = async (request, reply) => {
reply.send({ message: "Hello from the backend!" });
};

We can call this route in a frontend React component:

JavaScript
1import { useFetch } from "@gadgetinc/react";
2
3const Component = () => {
4 const [{ data, fetching, error }, send] = useFetch("/hello", { json: true });
5
6 console.log(data); // will start out null, then when the data arrives, { message: "Hello from the backend!" }
7};

Calling fetches imperatively 

If you aren't using React, or would like to await a request like you might with the built-in browser fetch function, you can use the api.fetch function:

For example, we can call the /hello route from above like this:

JavaScript
const result = await api.fetch("/hello").json();
console.log(result); // { message: "Hello from the backend!" }

api.fetch is appropriate for use in an imperative context, like a server-side script, or other places where you don't need to give the user feedback about what's happening. useFetch is appropriate when you need to show the user feedback as the fetching or error state changes.

Maintaining session state when calling HTTP routes 

Different apps use different mechanisms to authenticate requests from the client to your Gadget backend. When making raw HTTP calls to your backend, you need to ensure that the correct authentication headers are passed to your backend. The api client object provided by Gadget sends these headers automatically for GraphQL requests and requests made by the React hooks.

The useFetch hook and api.fetch function implements this same automatic authentication header setting but for any HTTP request to the backend. useFetch and api.fetch wrap the browser built-in fetch, but add the headers required to implement whichever authentication mode is active for your app.

The different authentication modes are documented in your API reference.

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.

useFetch accepts the following arguments:

  • 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

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.

Here's an example user component that uses useFetch to make a request to a routes/users/GET-me.js backend Gadget route:

JavaScript
1export function UserByEmail(props) {
2 const [{ data, fetching, error }, refresh] = useFetch("/users/me", {
3 method: "GET",
4 json: true,
5 });
6
7 if (error) return <>Error: {error.toString()}</>;
8 if (fetching && !data) return <>Fetching...</>;
9 if (!data) return <>No user found with id={props.id}</>;
10
11 return <div>{data.name}</div>;
12}

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.

JavaScript
1// GET request will be sent immediately, can be refreshed by calling `refresh()` again
2const [{ data, fetching, error }, refresh] = useFetch("/some/route", {
3 method: "GET",
4});
5// ... sometime later
6data; // => will be populated

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.

JavaScript
// POST request will not be sent immediately, will only be sent the first time when `send()` is called
const [{ data, fetching, error }, send] = useFetch("/some/route", { method: "GET" });

Retrieving JSON 

useFetch has a handy json: true option for automatically parsing a JSON response from the server. If you know your route will return JSON, you can set this option to true and the response will be parsed and returned as an object.

JavaScript
1export function User(props) {
2 const [{ data, fetching, error }, refresh] = useFetch("/users/me", {
3 method: "GET",
4 json: true,
5 });
6
7 if (error) return <>Error: {error.toString()}</>;
8 if (fetching && !data) return <>Fetching...</>;
9
10 // no need to JSON.parse the result
11 return <div>{data.name}</div>;
12}

The json: true option does not affect what is sent with your request, and only affects how the response is processed.

Sending JSON 

To send a JSON formatted request with the useFetch hook, JSON.stringify your request body and set the content-type: application/json header:

JavaScript
1export function UpdateUser(props) {
2 const [{ data, fetching, error }, send] = useFetch("/users/update", {
3 method: "POST",
4 headers: {
5 "content-type": "application/json",
6 },
7 });
8
9 // sometime later in an event handler
10 return (
11 <button
12 onClick={() => {
13 void send({ body: JSON.stringify({ name: "some name" }) });
14 }}
15 >
16 Send
17 </button>
18 );
19}

Fetching with other React hooks 

The @gadgetinc/react hooks library includes a useFetch hook, but if you'd like to use your preferred HTTP hook library, you can! By wrapping api.fetch with one of the great existing React libraries for making HTTP calls, like use-http, swr or react-query, you can continue passing the same authentication state and headers to your backend.

For example, we call a /example route in the backend with the swr library. First, create the route by adding the routes/GET-example.js file:

routes/GET-example.js
JavaScript
module.exports = async (request, reply) => {
reply.send({ message: "Hello from the backend!" });
};

Next, install swr into your application by adding the following to package.json and clicking the Run Yarn button:

package.json
json
{
"swr": "^2.0.4"
}

Then, in your React component, import the useSWR hook from swr and the api.fetch function from your Gadget API client:

jsx
1import useSWR from "swr";
2import { api } from "../api";
3
4const fetcher = (...args) => api.fetch(...args).then((res) => res.json());
5
6function Profile() {
7 const { data, error } = useSWR("/example", fetcher);
8
9 if (error) return <div>failed to load</div>;
10 if (!data) return <div>loading...</div>;
11 return <div>Backend message: {data.message}!</div>;
12}

Static asset handling 

Gadget's frontend hosting supports serving static assets in a robust, CDN-friendly way using Vite. You can add static assets anywhere within your frontend directory, and then import them into your code. For example, if you add an image at frontend/images/hero.png, you can import it in your frontend code like this:

jsx
import imgUrl from "./images/hero.png";
export const Hero = () => {
return <img src={imgUrl} alt="a hero image" />;
};

If you import from a static asset in your frontend code, Vite will take over serving this file with a robust production cache expiry mechanism, which will change the URL used to access that file in production.

Production asset links

When building assets for production, Vite will transform imported filenames to add a caching-friendly hash, and Gadget will upload these renamed files to Gadget's CDN for optimal serving. Your production Gadget app will make use of the hash generated by Vite to perform cache busting and will save and load assets to and from the disk cache between browser sessions. If you change these assets, Vite will generate a new hash and the updated asset will be loaded from Gadget's CDN on the next page render.

Non-transformed public assets 

Gadget's frontend hosting also allows for the hosting of frontend assets that aren't transformed by Vite. A good example of this is your app's favicon or a robots.txt file. Instead of being minified and cached, you just want it to be non-transformed and hosted for your frontend.

You can store these assets in a public folder at the root level of your Gadget project.

Vite configuration 

Gadget exposes the vite.config.js file that powers the Vite integration hosting your frontend. In vite.config.js, you can adjust the set of Vite plugins that power your application.

For example, we can add [MDX] support to a Gadget frontend with the @mdx-js/rollup plugin. First, install the plugin by adding the following to package.json and clicking the Run Yarn button:

package.json
json
{
"@mdx-js/rollup": "^2.3.0"
}

Then, in our vite.config.js, we can add the plugin to the list of plugins:

frontend/vite.config.js
JavaScript
1import react from "@vitejs/plugin-react";
2import mdx from "@mdx-js/rollup";
3import { defineConfig } from "vite";
4
5export default defineConfig({
6 plugins: [
7 // newly added mdx plugin, configured as MDX recommends in their docs: https://mdxjs.com/docs/getting-started/#vite
8 { enforce: "pre", ...mdx() },
9 // leave the existing react plugin in place
10 react(),
11 ],
12 base: "/",
13});

More Vite plugins can be found in awesome-vite.