Nimble Porridge Muncher

    @gadgetinc/react
    TypeScript icon, indicating that this package has built-in type declarations

    0.7.2 • Public • Published

    Gadget logo

    GitHub CI status npm version

    React hooks for use with application APIs built using the Gadget platform.

    Features

    1. An auto-suggested select option for all hooks, showing all of the available fields on various models
    2. A typed and hydrated result based on any given select option

    This library wraps urql to communicate with a Gadget-generated API, so it benefits from all of the same features as urql.

    Installation

    This is a companion package to the JavaScript client package generated for your Gadget app, so you must install the JS client for your app, and then install this package.

    To install the JS client for your app, you must set up the Gadget NPM registry, and then install the client:

    npm config set @gadget-client:registry https://registry.gadget.dev/npm
    
    # then
    
    yarn add @gadget-client/my-app-slug
    # or
    npm install @gadget-client/product-tagger-8000
    

    Full installation instructions can be found in the Gadget docs at https://docs.gadget.dev/api/<my-app-slug>/installing.

    Once you have your JS client installed, you can install the React hooks library with yarn or npm:

    yarn add @gadgetinc/react react
    # or
    npm install --save @gadgetinc/react react
    

    And finally, you must instantiate an instance of your API client, and then wrap your application in the Provider component exported from this library. In your root-most React component, you can add a Provider around the other components of your application:

    // import the API client for your specific application from your client package, see your app's installing instructions
    import { Client } from "@gadget-client/my-gadget-app";
    // import the required Provider object and some example hooks from this package
    import { Provider } from "@gadgetinc/react";
    
    // instantiate the API client for our app
    const api = new Client({ authenticationMode: { browserSession: true } });
    
    export const MyApp = (props) => {
      // wrapp the application in the <Provider> so the hooks can find the current client
      return <Provider value={api.connection.currentClient}>{props.children}</Provider>;
    };

    Example usage

    // import the API client for your specific application from your client package, see your app's installing instructions
    import { Client } from "@gadget-client/my-gadget-app";
    // import the required Provider object and some example hooks from this package
    import { Provider, useAction, useFindMany } from "@gadgetinc/react";
    import React from "react";
    
    // instantiate the API client for our app
    const api = new Client({ authenticationMode: { browserSession: true, }});
    
    export function MyComponent() {
      // ensure any components which use the @gadgetinc/react hooks are wrapped with the provider, and passed the current api client
      return (
        <Provider value={api.connection.currentClient}>
          <WidgetDeleter />
        </Provider>
      );
    }
    
    function WidgetDeleter() {
      // `useFindMany` executes a backend fetch to get a list of widgets from the backend. we select only the id and name fields of each widget to demonstrate type-safe sub selection
      const [result, refresh] = useFindMany(api.widget, {
        select: {
          id: true,
          name: true,
        },
      });
    
      // `useAction` sets up a mutation we can run to delete a specific widget when a user clicks a button
      const [_, deleteWidget] = useAction(api.widget.delete);
    
      // handle all the different states the data fetch can be in
      if (result.error) return <>Error: {result.error.toString()}</>;
      if (result.fetching && !result.data) return <>Fetching...</>;
      if (!result.data) return <>No widgets found</>;
    
      return (
        <>
          {result.data.map((widget) => (
            <button
              onClick={(event) => {
                event.preventDefault();
                void deleteWidget({ id: widget.id }).then(() => refresh());
              }}
            >
              Delete {widget.name}
            </button>
          ))}
        </>
      );
    }

    Find more examples in the https://github.com/gadget-inc/examples repo.

    API Documentation

    @gadgetinc/react contains a variety of React hooks for working with your Gadget application's API from a React application. Specific code examples for your application's API can be found within your application's docs at https://docs.gadget.dev/api/

    Setup

    Your React application must be wrapped in the Provider component from this library for the hooks to function properly. No other wrappers (like urql's) are necessary.

    useFindOne(manager: ModelFinder, id: string, options: SingleFinderOptions = {}): [{data, fetching, error}, refetch]

    useFindOne fetches one record from your Gadget database with a given id. 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(api.widget, 42) is the React equivalent of await api.widget.findOne(42). Options:

    • manager: The model manager you for the model you want to find a record of. The value always comes from an instance of your application's API client instantiated elsewhere. 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 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

    useFindOne returns two values: a result object with the data, fetching, and error keys for inspecting in your React 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 record isn't found by id. See the Errors section.

    Without any options, useFindOne will fetch the record and cause your component to rerender as the fetch happens and when the data or error arrives:

    import React from "react";
    import { api } from "../api"; // some file that instantiates @gadget-client/your-app-slug
    
    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>;
    };

    useFindOne can take options which allow the customization of which fields are returned:

    import React from "react";
    import { api } from "../api"; // some file that instantiates @gadget-client/your-app-slug
    
    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>
      );
    };

    See the refetch function docs for more information on refreshing the returned server side value.

    useMaybeFindOne(manager: ModelFinder, id: string, options: SingleFinderOptions = {}): [{data, fetching, error}, refetch]

    useMaybeFindOne fetches one record from your Gadget database with a given id. 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.

    useMaybeFindOne(api.widget, 42) is the React equivalent of await api.widget.maybeFindOne(42).

    useFindMany(manager: ModelFinder, options: ManyFinderOptions = {}): [{data, fetching, error}, refetch]

    useFindMany fetches a page of records from your Gadget database, optionally sorted, filtered, or searched. useFindMany(api.widget, { ... }) is the React equivalent of await api.widget.findMany({ ... }). Options:

    • manager: The model manager you for the model you want to find a page of records for. The value always comes from an instance of your application's API client instantiated elsewhere. 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 Model Filtering section in your application's API documentation to see the available filters for your models.
      • search: A search string to match backend records against. Optional. See the Model Searching section in your application's API documentation to see the available search syntax.
      • sort: A sort order to return backend records by. Optional. See the Sorting section 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 Model 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 Model Pagination section in your application's API documentation for more info.
      • 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

    useFindMany returns two values: a result object with the data, fetching, and error keys for use in your React component's output, and a refetch function to trigger a refresh of the hook's data.

    • data: GadgetRecord[] | 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. As the fetch happens, the hook will cause your component to rerender when the data or error arrives:

    import React from "react";
    import { api } from "../api"; // some file that instantiates @gadget-client/your-app-slug
    
    export const ShowWidgetNames = () => {
      const [{ data, fetching, error }, _refetch] = useFindMany(api.widget);
    
      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 the select option to allow customization of which fields are returned:

    export const OnlySomeWidgetFields = (props: { id: string }) => {
      // 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:

    // fetch only the widgets created recently
    const [{ data, fetching, error }, _refetch] = useFindMany(api.widget, {
      filter: {
        createdAt: { greaterThan: new Date(2022, 01, 01) },
      },
    });

    See your application's API documentation 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:

    // 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:

    // return the most recently created widgets first
    const [{ data, fetching, error }, _refetch] = useFindMany(api.widget, {
      search: "penny",
    });

    See your application's API documentation for more information on the search query syntax and which fields are searchable.

    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.

    // 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",
    });
    
    // the current page's start and end cursor are available for use to then make later requests for different pages
    data.endCursor; // used for forward pagination, pass to the `after:` variable
    data.startCursor; // used for backwards pagination, pass to the `before:` variable

    An easy way to do pagination is using React state, or for a better user experience, using the URL with whatever router system works for your application. We use React state to demonstrate pagination in this example:

    import React from "react";
    import { api } from "../api"; // some file that instantiates @gadget-client/your-app-slug
    
    export const WidgetPaginator = () => {
      // store the current cursor in the backend data
      const [cursor, setCursor] = useState(null);
    
      // 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(manager: ModelFinder, options: FindManyOptions = {}): [{data, fetching, error}, refetch]

    useFindFirst fetches the first record from your backend Gadget database that matches the given filter, sort, and search parameters. useFindFirst(api.widget, { ... }) is the React equivalent of await api.widget.findFirst({ ... }). useFindFirst expects a record to be found in the backend, so if no record is found matching the conditions, then the returned result object will data: null and a MissingDataError for the error object. useFindFirst accepts the following arguments:

    • manager: The model manager you for the model you want to find a page of records for. The value always comes from an instance of your application's API client instantiated elsewhere. 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 Model Filtering section in your application's API documentation to see the available filters for your models.
      • search: A search string to find a record matching. Optional. See the Model Searching section in your application's API documentation to see the available search syntax.
      • sort: A sort order to order the backend records by. useFindFirst will only return the first record matching the given search and filter, so sort can be used to break ties and select a specific record. Optional. See the Sorting section in your application's API documentation for more info.
      • 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

    useFindFirst returns two values: a result object with the data, fetching, and error keys for inspecting in your React 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.

    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:

    import React from "react";
    import { api } from "../api"; // some file that instantiates @gadget-client/your-app-slug
    
    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>;
    };

    useFindFirst can take options which allow the customization of which fields are returned:

    const [_result, _refetch] = useFindOne(api.widget, props.id, {
      // fetch only the widget id and name fields
      select: {
        id: true,
        name: true,
      },
    });

    useFindBy(findFunction: ModelFindFunction, fieldValue: any, options: FindByOptions = {}): [{data, fetching, error}, refetch]

    useFindBy fetches one record from your backend looked up by a specific field and value. useFindBy uses the existing specific-field-finder functions generated within your application's API client, like api.user.findByEmail or similar. These functions are generated for Gadget fields that have Unique Validations added to them.

    useFindBy expects to be passed a field-specific finder function as the first argument, and then the value of the field to find by as the second argument, and then optionally accepts a third options object. If no record is found matching the conditions, then the returned object will have null for the data. useFindBy(api.widget.findByEmail, "hi@gadget.dev") is the React equivalent of api.widget.findByEmail("hi@gadget.dev"). Options:

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

    useFindBy returns two values: a result object with the data, fetching, and error keys for inspecting in your React 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.

    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:

    import React from "react";
    import { api } from "../api"; // some file that instantiates @gadget-client/your-app-slug
    
    // 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 which allow the customization of which fields are returned:

    // 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(manager: ModelFinder, options: FindManyOptions = {}): [{data, fetching, error}, refetch]

    useMaybeFindFirst fetches the first record from your backend Gadget database that matches the given filter, sort, and search parameters. useMaybeFindFirst(api.widget, { ... }) is the React equivalent of await api.widget.maybeFindFirst({ ... }). 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.

    useAction(actionFunction: ModelActionFunction, options: UseActionOptions = {}): [{data, fetching, error}, refetch]

    useAction is a hook for running a backend action on one record of a Gadget model. useAction(api.widget.create) is the React equivalent of await api.widget.create({...}). useAction doesn't immediately dispatch a request to run an action server side, but instead returns a result object and a function which runs the action, similar to urql's useMutation hook. useAction must be passed an action function from an instance of your application's generated API client. Options:

    • 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

    useAction returns two values: a result object with the data, fetching, and error keys for inspecting in your React component's output, and a act function to actually run the backend action. useAction is a rule-following React hook that wraps action execution, which means it doesn't just run the action as soon as the hook is invoked. Instead, useAction returns a configured function that will actually run the action, which you need to call in response to some user event. The act function accepts the action inputs as arguments -- not useAction itself. useAction's result will return the data, fetching, and error details for the most recent execution of the action.

    • data: GadgetRecord | null: The record fetched from the backend after a mutation. Is null while before the mutation is run and while it is currently ongoing.
    • fetching: boolean: A boolean describing if the hook is currently making a request to the backend.
    • error: Error | null: An error from the client or server side, if encountered during the mutation. Will contain an error if the client passed invalid data, if server failed to complete the mutation, or if a network error was encountered. See the Errors section.

    For example, we can create a button which creates a post when clicked:

    import React from "react";
    import { api } from "../api"; // some file that instantiates @gadget-client/your-app-slug
    
    export const CreatePost = () => {
      // invoke the `useAction` hook, getting back a result object and an action runner function every render
      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={() => {
              // run the action when the button is clicked
              // the action runner function accepts action inputs in the same format as api.blogPost.create, and the GraphQL API
              createBlogPost({
                blogPost: {
                  title: "New post created from React",
                },
              });
            }}
          >
            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:

    import React from "react";
    import { api } from "../api"; // some file that instantiates @gadget-client/your-app-slug
    
    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={() => {
            // run the action with the action runner function when the form is submitted
            // pass the id of the blog post we're updating as one parameter, and the new post attributes as another
            updateBlogPost({
              id: props.id,
              post: {
                title,
              },
            });
          }}
        >
          <label>Title</label>
          <input type="text" value={title} onChange={(event) => setTitle(event.target.value)} />
          <input type="submit">Submit</input>
        </form>
      );
    };

    useAction returns an error object that is an ErrorWrapper. The error object can have a .networkError, a list of .executionErrors, or a list of .validationErrors which is useful for building client side forms driven by server side validations.

    useAction can take options which allow the customization of which fields are returned:

    // fetch only a post id and title fields
    const [{ data, fetching, error }, _refetch] = useFindBy(api.blogPost.findBySlug, "some-slug", { select: { id: true, title: true } });

    useGlobalAction(actionFunction: GlobalActionFunction, options: UseGlobalActionOptions = {}): [{data, fetching, error}, refetch]

    useGlobalAction is a hook for running a backend Global Action. useGlobalAction(api.widget.create) is the React equivalent of await api.someGlobalAction({...}). useGlobalAction doesn't immediately dispatch a request to run an action server side, but instead returns a result object and a function which runs the action, similar to urql's useMutation hook. useGlobalAction must be passed one of the global action functions from an instance of your application's generated API client. Options:

    • 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

    useGlobalAction returns two values: a result object with the data, fetching, and error keys for inspecting in your React component's output, and a act function to actually run the backend global action. useGlobalAction is a rule-following React hook that wraps action execution, which means it doesn't just run the action as soon as the hook is invoked. Instead, useGlobalAction returns a configured function which you need to call in response to some event. This act function accepts the action inputs as arguments. useGlobalAction's result will return the data, fetching, and error details for the most recent execution of the action.

    • data: Record<string, any> | null: The data returned by the global action from the backend. Is null while before the mutation is run and while it is currently ongoing.
    • fetching: boolean: A boolean describing if the hook is currently making a request to the backend.
    • error: Error | null: An error from the client or server side, if encountered during the mutation. Will contain an error if the client passed invalid data, if server failed to complete the mutation, or if a network error was encountered. See the Errors section.

    For example, we can create a button which runs a global action called purgeData when clicked

    import React from "react";
    import { api } from "../api"; // some file that instantiates @gadget-client/your-app-slug
    
    export const PurgeData = () => {
      // invoke the `useGlobalAction` hook, getting back a result object and an action runner function every render
      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={() => {
              // run the global action when the button is clicked
              // the action runner function accepts action inputs in the same format as api.purgeData, and the GraphQL API
              purgeData({ foo: "bar" });
            }}
          >
            Purge data
          </button>
        );
      } else {
        return "Purge completed";
      }
    };

    useGet(singletonModelManager: SingletonModelManager, options: GetOptions = {}): [{data, fetching, error}, refetch]

    useGet fetches a singleton record for an api.currentSomething style model manager. useGet fetches one global record, which is most often the current session. useGet doesn't require knowing the record's ID in order to fetch it. useGet(api.currentSession) is the React equivalent of await api.currentSession.get(). useGet accepts the following options:

    • 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

    useGet returns two values: a result object with the data, fetching, and error keys for inspecting in your React 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

    import React from "react";
    import { api } from "../api"; // some file that instantiates @gadget-client/your-app-slug
    
    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} is {data.state}
        </span>
      );
    };

    useGet can take options which allow the customization of which fields are returned on the selected record:

    // fetch only the session id and state fields
    const [{ data, fetching, error }, _refetch] = useGet(api.currentSession, { select: { id: true, state: 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.

    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:

    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:

    export const BlogWithComments = (props: { id: string }) => {
      // fetch only the widget id and name fields
      const [{ data, fetching, error }, _refetch] = useFindOne(api.blogPost, props.id, {
        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;
      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.

    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:

    import React from "react";
    import { api } from "../api"; // some file that instantiates @gadget-client/your-app-slug
    
    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 colspan="2">
            <button onClick={() => void refetch()}>Refresh</button>
          </tr>
        </table>
      );
    };

    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:

    import React from "react";
    import { api } from "../api"; // some file that instantiates @gadget-client/your-app-slug
    import { Provider, useQuery } from "@gadgetinc/react";
    
    export const ShowWidgetNames = () => {
      // find all widgets and the most recently created gizmo related to the widget
      const [{ data, fetching, error }, refetch] = useQuery({
        query: `
    query GetWidgets {
      widgets {
        edges {
          node {
            id
            name
            gizmos(first: 1, sort:{ createdAt: Descending }) {
              edges {
                node {
                  createdAt
                }
              }
            }
          }
        }
      }
    }
        `
      });
    
      if (fetching) return "Loading...";
      if (error) return `Error loading data: ${error}`;
    
      return (
        <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 colspan="2">
            <button onClick={() => void refetch()}>Refresh</button>
          </tr>
        </table>
      );
    };
    
    export const App = () => (
      <Provider value={api.connection.currentClient}>
        <ShowWidgetNames />
      </Provider>
    );

    Keywords

    none

    Install

    npm i @gadgetinc/react

    DownloadsWeekly Downloads

    463

    Version

    0.7.2

    License

    MIT

    Unpacked Size

    155 kB

    Total Files

    44

    Last publish

    Collaborators

    • scott-rc
    • alexangelini
    • gedge
    • kristianpd
    • kira-bruneau
    • airhorns