Skip to content

Instantly share code, notes, and snippets.

@good-idea
Last active March 29, 2023 21:00
Show Gist options
  • Save good-idea/b36f9715aedee44b1ba44b938dc02224 to your computer and use it in GitHub Desktop.
Save good-idea/b36f9715aedee44b1ba44b938dc02224 to your computer and use it in GitHub Desktop.
SWR Lazy Hook

A simple hook to create a "lazy request" hook using swr.

! This isn't perfect !

The problem with the example below is that you need to know your variables when you call useLazyRequest. This might not work in all situations, for instance, if your component is getting the variables from state that might change. This could be fixed by putting the call to useLazyRequest within a child component that receives those variables as props.

See the typescript example below.

Not fully tested, I just wrote this on the fly. If you have any suggestions, please add them in a comment!

import useSWR from 'swr'
import { print } from 'graphql/language/printer'
import { request } from 'graphql-request'
const apiUrl = 'MY_API_URL'
/* Turns a graphQL query into a string.
* If it is already a string, return it as is.
* If it is a DocumentNode (created by gql),
* use print() to turn it into a string. */
const asString = (query) => (typeof query === 'string' ? query : print(query))
export const useLazyRequest = (query, variables = {}, options = {}) => {
const queryString = asString(query)
const response = useSWR(
/* Use the GraphQL Query string as the key for this SWR request */
[queryString, JSON.stringify(variables)],
/* Our initial "fetch" will just return null as the data since this is lazy */
() => null,
/* with any other SWR options we want */
options,
)
const executeQuery = async (variables) => {
/*
* Later you will call executeQuery(variables)
* This will send the "real" request. In this example we are using
* graphql-request but you could use fetch or anything else.
*/
const result = await request(apiUrl, queryString, variables)
/*
* Once we get the result, we "mutate" the response to this request.
* This will update the `response` object that was created by `useSwr`
* and our component will receive the updated data.
*/
response.mutate(result, false)
}
return [executeQuery, response]
}
/* Usage */
import * as React from 'react'
import gql from 'graphql-tag'
import { useLazyRequest } from './useLazyRequest'
const query = gql`
query UserQuery($userId: String!) {
user(id: $userId) {
id
name
}
}
`
export const MyComponent = (props) => {
const variables = {
userId: props.userId,
}
/* call this to create the query. `response.data` will be null
* when the component first mounts.
*/
const [executeQuery, response] = useLazyRequest(query, variables)
function loadUser() {
executeQuery(variables)
}
/* This will render on the first mount */
if (response.data === null) {
return (
<button onClick={loadUser} type="button">
Click To Load
</button>
)
}
/* This will render once our request has returned the data */
const user = response.data ? response.data.user : null
if (user === null) return <p>No user with id {props.userId} was found</p>
return <p>Hello, {user.name}!</p>
}
/* See plain javascript version below 👇 */
import useSWR, {
ConfigInterface,
responseInterface as ResponseInterface,
} from 'swr'
import { DocumentNode } from 'graphql'
import { print } from 'graphql/language/printer'
import { Variables } from 'graphql-request/dist/src/types'
import { request } from 'graphql-request'
const apiUrl = 'MY_API_URL'
type LazyRequestTuple<R, V extends Variables = Variables> = [
(v?: V) => Promise<void>,
ResponseInterface<R | null, Error>,
]
interface RequestArgs<R, V> {
query: DocumentNode | string
variables?: V
options?: ConfigInterface<R>
}
/* Turns a graphQL query into a string.
* If it is already a string, return it as is.
* If it is a DocumentNode (created by gql),
* use print() to turn it into a string. */
const asString = (query: string | DocumentNode): string =>
typeof query === 'string' ? query : print(query)
export const useLazyRequest = <R, V extends Variables = Variables>(
query: DocumentNode | string,
variables: V = {},
options: ConfigInterface<R> = {},
): LazyRequestTuple<R, V> => {
const queryString = asString(query)
const response = useSWR<R | null>(
/* Use the GraphQL Query string as the key for this SWR request */
[queryString, JSON.stringify(variables)],
/* Our initial "fetch" will just return null as the data since this is lazy */
() => null,
/* with any other SWR options we want */
options,
)
const executeQuery = async (variables?: V) => {
/*
* Later you will call executeQuery(variables)
* This will send the "real" request. In this example we are using
* graphql-request but you could use fetch or anything else.
*/
const result = await request<R>(apiUrl, queryString, variables)
/*
* Once we get the result, we "mutate" the response to this request.
* This will update the `response` object that was created by `useSwr`
* and our component will receive the updated data.
*/
response.mutate(result, false)
}
return [executeQuery, response]
}
/* Usage */
import * as React from 'react'
import gql from 'graphql-tag'
import { useLazyRequest } from './useLazyRequest'
const query = gql`
query UserQuery($userId: String!) {
user(id: $userId) {
id
name
}
}
`
interface User {
id: string
name: string
}
interface QueryResponse {
user: User
}
interface QueryVariables {
userId: string
}
interface MyComponentProps {
userId: string
}
export const MyComponent = (props: MyComponentProps) => {
const variables = {
userId: props.userId,
}
/* call this to create the query. `response.data` will be null
* when the component first mounts.
*/
const [executeQuery, response] = useLazyRequest<
QueryResponse,
QueryVariables
>(query, variables)
function loadUser() {
executeQuery(variables)
}
/* This will render on the first mount */
if (response.data === null) {
return (
<button onClick={loadUser} type="button">
Click To Load
</button>
)
}
/* This will render once our request has returned the data */
const user = response.data ? response.data.user : null
if (user === null) return <p>No user with id {props.userId} was found</p>
return <p>Hello, {user.name}!</p>
}
@stivsk
Copy link

stivsk commented Jul 25, 2022

Hello @good-idea, I was working on something similar but I needed to be compatible with Suspense. In my case I used conditional fetching to solve it.
Thanks, your idea helped me a lot, I would like to share mine with you:
https://gist.github.com/stivsk/44854eb08884cb7d876c8e549afaa343

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment