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
- src/basic.tsx
- gauntlet.toml
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()
- src/revalidate.tsx
- gauntlet.toml
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
- src/abortable-revalidate.tsx
- gauntlet.toml
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
- src/execute-false.tsx
- gauntlet.toml
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()
- src/mutate.tsx
- gauntlet.toml
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
- src/mutate-no-revalidate.tsx
- gauntlet.toml
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
- src/mutate-optimistic.tsx
- gauntlet.toml
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
- src/mutate-optimistic-rollback.tsx
- gauntlet.toml
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
- src/cached-basic.tsx
- gauntlet.toml
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
- src/cached-basic-initial-value.tsx
- gauntlet.toml
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
- src/fetch-basic.tsx
- gauntlet.toml
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
- src/fetch-map.tsx
- gauntlet.toml
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)
}