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:
1// fetch one post by id2const [{ data, fetching, error }, refetch] = useFindOne(api.post, "10");34// fetch the first 10 posts5const [{ data, fetching, error }, refetch] = useFindMany(api.post, { first: 10 });67// fetch the first post with the slug field equal to "example-slug", throw if it isn't found8const [{ data, fetching, error }, refetch] = useFindFirst(api.post, { where: { slug: "example-slug" } });910// fetch the first post with the slug field equal to "example-slug", return null if it isn't found11const [{ data, fetching, error }, refetch] = useMaybeFindFirst(api.post, { where: { slug: "example-slug" } });1213// fetch a realtime-updated view of the first 10 posts from the backend14const [{ data, fetching, error }, refetch] = useFindMany(api.post, { live: true, first: 10 });1516// fetch the user's current session17const [{ data, fetching, error }, refetch] = useGet(api.currentSession);
1// fetch one post by id2const [{ data, fetching, error }, refetch] = useFindOne(api.post, "10");34// fetch the first 10 posts5const [{ data, fetching, error }, refetch] = useFindMany(api.post, { first: 10 });67// fetch the first post with the slug field equal to "example-slug", throw if it isn't found8const [{ data, fetching, error }, refetch] = useFindFirst(api.post, { where: { slug: "example-slug" } });910// fetch the first post with the slug field equal to "example-slug", return null if it isn't found11const [{ data, fetching, error }, refetch] = useMaybeFindFirst(api.post, { where: { slug: "example-slug" } });1213// fetch a realtime-updated view of the first 10 posts from the backend14const [{ data, fetching, error }, refetch] = useFindMany(api.post, { live: true, first: 10 });1516// fetch the user's current session17const [{ 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:
1import { useFindMany } from "@gadgetinc/react";2import { ReactNode } from "react";3import { api } from "../api";45export const PostsList = () => {6 const [{ data, fetching, error }, _refetch] = useFindMany(api.post, { first: 10 });7 if (fetching) {8 return <div>Loading...</div>;9 }10 if (error) {11 return <div>Error: {error.message}</div>;12 }1314 return (15 <ul>16 {data.map((post) => (17 <li key={post.id}>{post.title}</li>18 ))}19 </ul>20 );21};
1import { useFindMany } from "@gadgetinc/react";2import { ReactNode } from "react";3import { api } from "../api";45export const PostsList = () => {6 const [{ data, fetching, error }, _refetch] = useFindMany(api.post, { first: 10 });7 if (fetching) {8 return <div>Loading...</div>;9 }10 if (error) {11 return <div>Error: {error.message}</div>;12 }1314 return (15 <ul>16 {data.map((post) => (17 <li key={post.id}>{post.title}</li>18 ))}19 </ul>20 );21};
For more documentation on the @gadgetinc/react
hooks library, see the React reference.
Writing data to models
The @gadgetinc/react
React hooks library includes the useActionForm
hook and the useAction
hook for writing data to models by running Actions. useActionForm
is suitable for building forms that give users inputs to control the input data, and useAction
is a lower level hook for calling actions directly.
Building forms
With the useActionForm
hook, you can manage form state and call actions easily. For example, we can build a form for a post
model with useActionForm
:
1import { useActionForm } from "@gadgetinc/react";2import { api } from "../api";34const PostForm = () => {5 const { register, submit } = useActionForm(api.post.create);67 return (8 <form onSubmit={submit}>9 <label htmlFor="title">Title</label>10 <input id="title" type="text" {...register("post.title")} />1112 <label htmlFor="content">Content</label>13 <textarea id="content" {...register("post.content")} />14 <input type="submit" />15 </form>16 );17};
1import { useActionForm } from "@gadgetinc/react";2import { api } from "../api";34const PostForm = () => {5 const { register, submit } = useActionForm(api.post.create);67 return (8 <form onSubmit={submit}>9 <label htmlFor="title">Title</label>10 <input id="title" type="text" {...register("post.title")} />1112 <label htmlFor="content">Content</label>13 <textarea id="content" {...register("post.content")} />14 <input type="submit" />15 </form>16 );17};
See the Forms guide for comprehensive docs on building forms with useActionForm
.
Calling Actions directly
If you aren't building a form, or if you need lower-level control, the useAction
hook can be used to call actions directly. 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, with a post
model on the backend, we can create a new post record by calling the act
function returned by the useAction
hook:
1import { useAction } from "@gadgetinc/react";2import { api } from "../api";34const [{ data, fetching, error }, act] = useAction(api.post.create);5// when ready, run the `act` function to trigger the action6act({ title: "Example Post", body: "some post content" });
1import { useAction } from "@gadgetinc/react";2import { api } from "../api";34const [{ data, fetching, error }, act] = useAction(api.post.create);5// when ready, run the `act` function to trigger the action6act({ title: "Example Post", body: "some post content" });
The same approach is used for updating data with useAction(api.post.update)
, but you must pass the id
of the record you want to update:
1import { useAction } from "@gadgetinc/react";2import { api } from "../api";34const [{ data, fetching, error }, act] = useAction(api.post.update);5// when ready, run the `act` function to trigger the action6act({ id: "123", title: "Example Post", body: "some new post content" });
1import { useAction } from "@gadgetinc/react";2import { api } from "../api";34const [{ data, fetching, error }, act] = useAction(api.post.update);5// when ready, run the `act` function to trigger the action6act({ id: "123", title: "Example Post", body: "some new post content" });
For more details on the useAction
hook, see the @gadgetinc/react
reference.
Calling global actions
The @gadgetinc/react
React hooks library includes support for building forms for global actions with the useActionForm
hook, or calling global actions directly with the useGlobalAction
hook.
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:
1import { useGlobalAction } from "@gadgetinc/react";2import { api } from "../api";34const [{ data, fetching, error }, act] = useGlobalAction(api.syncData);5// when ready, run the `act` function to trigger the action6act({7 // any params the global action might expect8});
1import { useGlobalAction } from "@gadgetinc/react";2import { api } from "../api";34const [{ data, fetching, error }, act] = useGlobalAction(api.syncData);5// when ready, run the `act` function to trigger the action6act({7 // any params the global action might expect8});
We can use the useGlobalAction
hook in a React component that calls the action when a button is clicked:
1import { useState } from "react";2import { useGlobalAction } from "@gadgetinc/react";3import { api } from "../api";45export const CreatePostForm = () => {6 const [title, setTitle] = useState("");7 const [body, setBody] = useState("");89 const [{ data, fetching, error }, act] = useGlobalAction(api.syncData);1011 return (12 <>13 <button disabled={fetching} onClick={() => void act()}>14 Sync Data15 </button>16 {error && <div>Error: {error.message}</div>}17 </>18 );19};
1import { useState } from "react";2import { useGlobalAction } from "@gadgetinc/react";3import { api } from "../api";45export const CreatePostForm = () => {6 const [title, setTitle] = useState("");7 const [body, setBody] = useState("");89 const [{ data, fetching, error }, act] = useGlobalAction(api.syncData);1011 return (12 <>13 <button disabled={fetching} onClick={() => void act()}>14 Sync Data15 </button>16 {error && <div>Error: {error.message}</div>}17 </>18 );19};
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 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 api/routes/GET-hello.js
file that sends a JSON reply in our backend like this:
const route: RouteHandler = async ({ request, reply }) => {reply.send({ message: "Hello from the backend!" });};export default route;
const route: RouteHandler = async ({ request, reply }) => {reply.send({ message: "Hello from the backend!" });};export default route;
We can call this route in a frontend React component:
1import { useFetch } from "@gadgetinc/react";23const Component = () => {4 const [{ data, fetching, error }, send] = useFetch("/hello", { json: true });56 // will start out null, then when the data arrives, { message: "Hello from the backend!" }7 console.log(data);8};
1import { useFetch } from "@gadgetinc/react";23const Component = () => {4 const [{ data, fetching, error }, send] = useFetch("/hello", { json: true });56 // will start out null, then when the data arrives, { message: "Hello from the backend!" }7 console.log(data);8};
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:
const result = await api.fetch("/hello").json();console.log(result);// { message: "Hello from the backend!" }
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.
Here's an example user component that uses useFetch
to make a request to a api/routes/users/GET-me.js
backend Gadget route:
1export function UserByEmail() {2 const [{ data, fetching, error }, refresh] = useFetch("/users/me", {3 method: "GET",4 json: true,5 });67 if (error) return <>Error: {error.toString()}</>;8 if (fetching) return <>Fetching...</>;9 if (!data) return <>No user found</>;1011 return <div>{data.name}</div>;12}
1export function UserByEmail() {2 const [{ data, fetching, error }, refresh] = useFetch("/users/me", {3 method: "GET",4 json: true,5 });67 if (error) return <>Error: {error.toString()}</>;8 if (fetching) return <>Fetching...</>;9 if (!data) return <>No user found</>;1011 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.
1// GET request will be sent immediately, can be refreshed by calling `refresh()` again2const [{ data, fetching, error }, refresh] = useFetch("/some/route", { method: "GET" });34// ... sometime later56// `data` will now be populated7data;
1// GET request will be sent immediately, can be refreshed by calling `refresh()` again2const [{ data, fetching, error }, refresh] = useFetch("/some/route", { method: "GET" });34// ... sometime later56// `data` will now be populated7data;
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.
1// POST requests are not sent immediately. They will only be sent when `send()` is called23const [{ data, fetching, error }, send] = useFetch("/some/route", { method: "POST" });45useEffect(() => {6 async function callRoute() {7 // make request to '/some/route' http route8 await send();9 }1011 void callRoute();12}, []);1314// will be undefined until the useEffect is run15// and send() is called and request is completed16console.log(data);
1// POST requests are not sent immediately. They will only be sent when `send()` is called23const [{ data, fetching, error }, send] = useFetch("/some/route", { method: "POST" });45useEffect(() => {6 async function callRoute() {7 // make request to '/some/route' http route8 await send();9 }1011 void callRoute();12}, []);1314// will be undefined until the useEffect is run15// and send() is called and request is completed16console.log(data);
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.
1import { useFetch } from "@gadgetinc/react";23export function User() {4 const [{ data, fetching, error }, refresh] = useFetch("/users/me", {5 method: "GET",6 json: true,7 });89 if (error) return <>Error: {error.toString()}</>;10 if (fetching) return <>Fetching...</>;11 if (!data) return <>No user found</>;1213 // no need to JSON.parse the result14 return <div>{data.name}</div>;15}
1import { useFetch } from "@gadgetinc/react";23export function User() {4 const [{ data, fetching, error }, refresh] = useFetch("/users/me", {5 method: "GET",6 json: true,7 });89 if (error) return <>Error: {error.toString()}</>;10 if (fetching) return <>Fetching...</>;11 if (!data) return <>No user found</>;1213 // no need to JSON.parse the result14 return <div>{data.name}</div>;15}
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:
1import { useFetch } from "@gadgetinc/react";23export function UpdateUser() {4 const [{ data, fetching, error }, send] = useFetch("/users/update", {5 method: "POST",6 headers: {7 "content-type": "application/json",8 },9 });1011 // sometime later in an event handler12 return (13 <button14 onClick={() => {15 void send({ body: JSON.stringify({ name: "some name" }) });16 }}17 >18 Send19 </button>20 );21}
1import { useFetch } from "@gadgetinc/react";23export function UpdateUser() {4 const [{ data, fetching, error }, send] = useFetch("/users/update", {5 method: "POST",6 headers: {7 "content-type": "application/json",8 },9 });1011 // sometime later in an event handler12 return (13 <button14 onClick={() => {15 void send({ body: JSON.stringify({ name: "some name" }) });16 }}17 >18 Send19 </button>20 );21}
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 api/routes/GET-example.js
file:
1import { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ reply }) => {4 reply.send({ message: "Hello from the backend!" });5};67export default route;
1import { RouteHandler } from "gadget-server";23const route: RouteHandler = async ({ reply }) => {4 reply.send({ message: "Hello from the backend!" });5};67export default route;
Next, install swr
into your application by adding the following to package.json
and clicking the Run Yarn button:
package.jsonjson{"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:
1import useSWR from "swr";2import { api } from "../api";34const fetcher = (args: RequestInfo | URL) => api.fetch(args).then((res) => res.json());56function Profile() {7 const { data, error } = useSWR("/example", fetcher);89 if (error) return <div>failed to load</div>;10 if (!data) return <div>loading...</div>;11 return <div>Backend message: {data.message}!</div>;12}
1import useSWR from "swr";2import { api } from "../api";34const fetcher = (args: RequestInfo | URL) => api.fetch(args).then((res) => res.json());56function Profile() {7 const { data, error } = useSWR("/example", fetcher);89 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:
import imgUrl from "./images/hero.png";export const Hero = () => {return <img src={imgUrl} alt="a hero image" />;};
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 can host assets that aren't transformed by Vite, like your app's favicon
or a robots.txt
file. Instead of being minified and cached by vite, these files will be served as-is from your app's filesystem.
You can store these assets in a public
folder at the root level of your Gadget project.
For example, if you have a public folder in your application like this:
public/favicon.icorobots.txtfoo/bar.txt
You can access your robots.txt
file by making a request to https://example-app--development.gadget.app/robots.txt
.
The public
folder also supports sub-folders. You can access the public/foo/bar.txt
file by making a request to
https://example-app--development.gadget.app/foo/bar.txt
.
Vite configuration
Gadget exposes the vite.config.mjs
file that powers the Vite integration hosting your frontend. In vite.config.mjs
, 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.jsonjson{"@mdx-js/rollup": "^2.3.0"}
Then, in our vite.config.mjs
, we can add the plugin to the list of plugins:
1import react from "@vitejs/plugin-react";2import mdx from "@mdx-js/rollup";3import { defineConfig } from "vite";45export default defineConfig({6 plugins: [7 // newly added mdx plugin, configured as MDX recommends in their docs: https://mdxjs.com/docs/getting-started/#vite8 { enforce: "pre", ...mdx() },9 // leave the existing react plugin in place10 react(),11 ],12 base: "/",13});
1import react from "@vitejs/plugin-react";2import mdx from "@mdx-js/rollup";3import { defineConfig } from "vite";45export default defineConfig({6 plugins: [7 // newly added mdx plugin, configured as MDX recommends in their docs: https://mdxjs.com/docs/getting-started/#vite8 { enforce: "pre", ...mdx() },9 // leave the existing react plugin in place10 react(),11 ],12 base: "/",13});
More Vite plugins can be found in awesome-vite.
Strict mode
By default, your React frontends will be in strict mode.
This is set in the web/main.jsx
file with <React.StrictMode>
.
Strict mode helps you catch bugs in development by automatically re-rendering your app and re-running your useEffect
hooks. This only occurs in development and will not impact production apps.