Skip to main content

Promise Helpers

Overview

To make operations with promises in views easier, Gauntlet provides set of helper hooks

usePromise

Helper to run promises in a context of React view

Examples

Basic

import { Detail } from "@project-gauntlet/api/components";
import { ReactElement } from "react";
import { usePromise } from "@project-gauntlet/api/hooks";

export default function UsePromiseBasic(): ReactElement {
const { data, error, isLoading } = usePromise(
async (_data) => await wait(),
["default"]
);

printState(data, error, isLoading)

return (
<Detail isLoading={isLoading}>
<Detail.Content>
<Detail.Content.Paragraph>
{"Data " + data}
</Detail.Content.Paragraph>
</Detail.Content>
</Detail>
)
}

async function wait(): Promise<string> {
return new Promise(resolve => {
setTimeout(() => resolve(`${Math.random()}`), 2000);
})
}

function printState(data: any, error: unknown, isLoading: boolean) {
console.log("")
console.dir(isLoading)
console.dir(data)
console.dir(error)
}

Revalidate

Usually the promise will be executed only on initial load of the view. It is often desired to manually refresh the state of the hook which can be done by calling revalidate()

import { ActionPanel, Detail } from "@project-gauntlet/api/components";
import { ReactElement } from "react";
import { usePromise } from "@project-gauntlet/api/hooks";

export default function UsePromiseRevalidate(): ReactElement {
const { data, error, isLoading, revalidate } = usePromise(
async (_data) => await wait(),
["default"]
);

printState(data, error, isLoading)

return (
<Detail
actions={
<ActionPanel>
<ActionPanel.Action label="Revalidate" onAction={() => revalidate()}/>
</ActionPanel>
}
isLoading={isLoading}
>
<Detail.Content>
<Detail.Content.Paragraph>
{"Data " + data}
</Detail.Content.Paragraph>
</Detail.Content>
</Detail>
)
}

async function wait(): Promise<string> {
return new Promise(resolve => {
setTimeout(() => resolve(`${Math.random()}`), 2000);
})
}

function printState(data: any, error: unknown, isLoading: boolean) {
console.log("")
console.dir(isLoading)
console.dir(data)
console.dir(error)
}

Abortable

Occasionally, revalidation can be started while the main promise is still in progress, if you want to detect when that happens abortable can be provided

import { ActionPanel, Detail } from "@project-gauntlet/api/components";
import { ReactElement, useRef } from "react";
import { usePromise } from "@project-gauntlet/api/hooks";

export default function UsePromiseAbortableRevalidate(): ReactElement {
const abortable = useRef<AbortController>();

const { data, error, isLoading, revalidate } = usePromise(
async (_data) => await wait(),
["default"],
{
abortable,
}
);

// this event will be fired when in-progress promise is supposed to be
// aborted when revalidation (or other reason) causes new promise to start,
// not fired if promise has already resolved
abortable.current?.signal.addEventListener("abort", () => {
console.log("")
console.log("> aborted")
})

printState(data, error, isLoading)

return (
<Detail
actions={
<ActionPanel>
<ActionPanel.Action label="Revalidate" onAction={() => revalidate()}/>
</ActionPanel>
}
isLoading={isLoading}
>
<Detail.Content>
<Detail.Content.Paragraph>
{"Data " + data}
</Detail.Content.Paragraph>
</Detail.Content>
</Detail>
)
}

async function wait(): Promise<string> {
return new Promise(resolve => {
setTimeout(() => resolve(`${Math.random()}`), 2000);
})
}

function printState(data: any, error: unknown, isLoading: boolean) {
console.log("")
console.dir(isLoading)
console.dir(data)
console.dir(error)
}

No execution on load

If execute option is set to false, the promise will be executed only on revalidation

import { Detail } from "@project-gauntlet/api/components";
import { ReactElement } from "react";
import { usePromise } from "@project-gauntlet/api/hooks";

export default function UsePromiseExecuteFalse(): ReactElement {
const { data, error, isLoading } = usePromise(
async (_data) => await wait(),
["default"],
{
execute: false
}
);

printState(data, error, isLoading)

return (
<Detail isLoading={isLoading}>
<Detail.Content>
<Detail.Content.Paragraph>
{"Data " + data}
</Detail.Content.Paragraph>
</Detail.Content>
</Detail>
)
}

async function wait(): Promise<string> {
return new Promise(resolve => {
setTimeout(() => resolve(`${Math.random()}`), 2000);
})
}

function printState(data: any, error: unknown, isLoading: boolean) {
console.log("")
console.dir(isLoading)
console.dir(data)
console.dir(error)
}

Mutate

"Mutation" is an action that is supposed to change the returned value of the main promise. E.g. if the main promise is the fetch() to backend to get list of items, the "mutation" would be an action that adds or removes items of that list.

After the mutation is executed, this helper will automatically refresh the state of the hook by calling revalidate()

import { ActionPanel, Detail } from "@project-gauntlet/api/components";
import { ReactElement } from "react";
import { usePromise } from "@project-gauntlet/api/hooks";

export default function UsePromiseMutate(): ReactElement {
const { data, error, isLoading, mutate } = usePromise(
async (_data) => await wait(),
["default"]
);

printState(data, error, isLoading)

return (
<Detail
actions={
<ActionPanel>
<ActionPanel.Action
label="Mutate"
onAction={async () => {
// do "mutation" request, e.g. create item on server
mutate(wait())
}}
/>
</ActionPanel>
}
isLoading={isLoading}
>
<Detail.Content>
<Detail.Content.Paragraph>
{"Data " + data}
</Detail.Content.Paragraph>
</Detail.Content>
</Detail>
)
}

async function wait(): Promise<string> {
return new Promise(resolve => {
setTimeout(() => resolve(`${Math.random()}`), 2000);
})
}

function printState(data: any, error: unknown, isLoading: boolean) {
console.log("")
console.dir(isLoading)
console.dir(data)
console.dir(error)
}

Mutate Without Revalidations

But sometimes it is not desirable to do revalidation after mutation, so it can be disabled

import { ActionPanel, Detail } from "@project-gauntlet/api/components";
import { ReactElement } from "react";
import { usePromise } from "@project-gauntlet/api/hooks";

export default function UsePromiseMutateNoRevalidate(): ReactElement {
const { data, error, isLoading, mutate } = usePromise(
async (_data) => await wait(),
["default"]
);

printState(data, error, isLoading)

return (
<Detail
actions={
<ActionPanel>
<ActionPanel.Action
label="Mutate"
onAction={async () => {
mutate(
wait(),
{
shouldRevalidateAfter: false
}
)
}}
/>
</ActionPanel>
}
isLoading={isLoading}
>
<Detail.Content>
<Detail.Content.Paragraph>
{"Data " + data}
</Detail.Content.Paragraph>
</Detail.Content>
</Detail>
)
}

async function wait(): Promise<string> {
return new Promise(resolve => {
setTimeout(() => resolve(`${Math.random()}`), 2000);
})
}

function printState(data: any, error: unknown, isLoading: boolean) {
console.log("")
console.dir(isLoading)
console.dir(data)
console.dir(error)
}

Mutate With Optimistic Update

If the promise is for example the call to backend, it will usually take some time. And because the update is done from the current UI, it often can be assumed that the update (aka mutation) will be successful (i.e. we are optimistic that it will succeed), and the UI can be updated right away to get much more responsive experience

import { ActionPanel, Detail } from "@project-gauntlet/api/components";
import { ReactElement } from "react";
import { usePromise } from "@project-gauntlet/api/hooks";

export default function UsePromiseMutateOptimistic(): ReactElement {
const { data, error, isLoading, mutate } = usePromise(
async (_data) => await wait(),
["default"]
);

printState(data, error, isLoading)

return (
<Detail
actions={
<ActionPanel>
<ActionPanel.Action
label="Mutate"
onAction={async () => {
mutate(
wait(),
{
optimisticUpdate: oldData => oldData + " optimistic",
}
)
}}
/>
</ActionPanel>
}
isLoading={isLoading}
>
<Detail.Content>
<Detail.Content.Paragraph>
{"Data " + data}
</Detail.Content.Paragraph>
</Detail.Content>
</Detail>
)
}

async function wait(): Promise<string> {
return new Promise(resolve => {
setTimeout(() => resolve(`${Math.random()}`), 2000);
})
}

function printState(data: any, error: unknown, isLoading: boolean) {
console.log("")
console.dir(isLoading)
console.dir(data)
console.dir(error)
}

Mutate With Optimistic Update And Rollback

When using optimistic update, if the mutation promise was rejected the value will be automatically reverted to what it was before the start of mutation. This behavior can be disabled by setting rollbackOnError option to false or, if set to a function, custom behaviour can be executed instead

import { ActionPanel, Detail } from "@project-gauntlet/api/components";
import { ReactElement } from "react";
import { usePromise } from "@project-gauntlet/api/hooks";

export default function UsePromiseMutateOptimisticRollback(): ReactElement {
const { data, error, isLoading, mutate } = usePromise(
async (_data) => await wait(),
["default"]
);

printState(data, error, isLoading)

return (
<Detail
actions={
<ActionPanel>
<ActionPanel.Action
label="Mutate"
onAction={async () => {
mutate(
wait(true),
{
optimisticUpdate: oldData => oldData + " optimistic",
rollbackOnError: oldData => oldData + " failed",
}
)
}}
/>
</ActionPanel>
}
isLoading={isLoading}
>
<Detail.Content>
<Detail.Content.Paragraph>
{"Data " + data}
</Detail.Content.Paragraph>
</Detail.Content>
</Detail>
)
}

async function wait(error: boolean = false): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
const value = `${Math.random()}`;
if (error) {
reject(value)
} else {
resolve(value)
}
}, 2000);
})
}

function printState(data: any, error: unknown, isLoading: boolean) {
console.log("")
console.dir(isLoading)
console.dir(data)
console.dir(error)
}

useCachedPromise

Helper to run promises with caching done automatically

The main difference from usePromise is that the state stored in session storage. So it is reused from previous time this entrypoint view was opened, but not since before the plugin is restarted

Follows "stale-while-revalidate" caching strategy

All options from usePromise are also available in this hook

Examples

Basic

import { Detail } from "@project-gauntlet/api/components";
import { ReactElement } from "react";
import { useCachedPromise } from "@project-gauntlet/api/hooks";

export default function UseCachedPromiseBasic(): ReactElement {
const { data, error, isLoading } = useCachedPromise(
async (_data) => await wait(),
["default"]
);

printState(data, error, isLoading)

return (
<Detail isLoading={isLoading}>
<Detail.Content>
<Detail.Content.Paragraph>
{"Data " + data}
</Detail.Content.Paragraph>
</Detail.Content>
</Detail>
)
}

async function wait(): Promise<string> {
return new Promise(resolve => {
setTimeout(() => resolve(`${Math.random()}`), 2000);
})
}

function printState(data: any, error: unknown, isLoading: boolean) {
console.log("")
console.dir(isLoading)
console.dir(data)
console.dir(error)
}

Initial Value

If the view is opened for the first time, the cache state will be empty. It is possible to assign default initial value of the cache

import { Detail } from "@project-gauntlet/api/components";
import { ReactElement } from "react";
import { useCachedPromise } from "@project-gauntlet/api/hooks";

export default function UseCachedPromiseBasicInitialValue(): ReactElement {
const { data, error, isLoading } = useCachedPromise(
async (_data) => await wait(),
["default"],
{
initialState: () => "initial"
}
);

printState(data, error, isLoading)

return (
<Detail isLoading={isLoading}>
<Detail.Content>
<Detail.Content.Paragraph>
{"Data " + data}
</Detail.Content.Paragraph>
</Detail.Content>
</Detail>
)
}

async function wait(): Promise<string> {
return new Promise(resolve => {
setTimeout(() => resolve(`${Math.random()}`), 2000);
})
}

function printState(data: any, error: unknown, isLoading: boolean) {
console.log("")
console.dir(isLoading)
console.dir(data)
console.dir(error)
}

useFetch

Helper to run fetch() with caching done automatically.

Uses useCachedPromise internally, so all parameters from it are exposed in this hook as well

The state stored in session storage

Follows "stale-while-revalidate" caching strategy

If revalidation is done while fetch() is still in progress it will be canceled automatically

Examples

Basic

import { Detail } from "@project-gauntlet/api/components";
import { ReactElement } from "react";
import { useFetch } from "@project-gauntlet/api/hooks";

export default function UseFetchBasic(): ReactElement {
interface GithubLatestRelease {
// ...
}

const { data, error, isLoading } = useFetch<GithubLatestRelease>(
"https://api.github.com/repos/project-gauntlet/gauntlet/releases/latest"
);

printState(data, error, isLoading)

return (
<Detail isLoading={isLoading}>
<Detail.Content>
<Detail.Content.Paragraph>
{"Data " + data}
</Detail.Content.Paragraph>
</Detail.Content>
</Detail>
)
}

function printState(data: any, error: unknown, isLoading: boolean) {
console.log("")
console.dir(isLoading)
console.dir(data)
console.dir(error)
}

Map the returned value

import { Detail } from "@project-gauntlet/api/components";
import { ReactElement } from "react";
import { useFetch } from "@project-gauntlet/api/hooks";

export default function UseFetchBasic(): ReactElement {
interface GithubLatestRelease {
url: string
// ...
}

const { data, error, isLoading } = useFetch<GithubLatestRelease, string>(
"https://api.github.com/repos/project-gauntlet/gauntlet/releases/latest",
{
map: result => result.url
}
);

printState(data, error, isLoading)

return (
<Detail isLoading={isLoading}>
<Detail.Content>
<Detail.Content.Paragraph>
{"Data " + data}
</Detail.Content.Paragraph>
</Detail.Content>
</Detail>
)
}

function printState(data: any, error: unknown, isLoading: boolean) {
console.log("")
console.dir(isLoading)
console.dir(data)
console.dir(error)
}