Load Third-Party Scripts With React-Query

main

Let's load a third-partly script library with react-query!

I use a library to manage subscriptions for my product, and the only way to access their SDK is to load a script.

Here we have a usePaddleSdk hook that returns the library. It leverages react-query to handle asynchronous code. The query function loads the script and returns Paddle SDK.

import { reportError } from "errors/errorMonitoring"
import { useQuery } from "react-query"
import { assertEnvVar } from "shared/assertEnvVar"
import { createScript, getScriptBySrc, loadScript } from "shared/helpers/dom"

import { PaddleSdk } from "../PaddleSdk"

export const paddleQueryKey = "paddle"
const paddleScriptSource = "https://cdn.paddle.com/paddle/paddle.js"
const paddleVendorId = Number(assertEnvVar("REACT_APP_PADDLE_VENDOR_ID"))

export const usePaddleSdk = () => {
  return useQuery(
    paddleQueryKey,
    async () => {
      if (!window.Paddle) {
        const paddleScript = getScriptBySrc(paddleScriptSource)
        try {
          if (paddleScript) {
            await loadScript(paddleScript)
          } else {
            const script = createScript(paddleScriptSource)
            await loadScript(script)
          }
          const paddleSdk = (window.Paddle as unknown) as PaddleSdk
          paddleSdk.Setup({ vendor: paddleVendorId })
        } catch (err) {
          reportError(err, { context: "Fail to load Paddle" })
        }
      }

      return window.Paddle as PaddleSdk
    },
    {
      keepPreviousData: true,
      refetchOnWindowFocus: false,
      refetchOnMount: false,
      refetchOnReconnect: false,
      staleTime: Infinity,
    }
  )
}

If the library is not in the window, we check if the script is already in the DOM. If it's present, we wait for it to load. Otherwise, we create a script element and append it to the body. Then we call our previous function - loadScript.

To escape TypeScript errors, I have a declaration file that sets PaddleSdk as a potential window property. I don't have all types for the library, but I fill it up as needed.

import { PaddleSdk } from "./PaddleSdk"

export declare global {
  interface Window {
    Paddle?: PaddleSdk
  }
}

I store the react-query cache in the local storage, but we don't want to cache the library. To achieve that, we exclude the paddle query from dehydration.

import { paddleQueryKey } from "membership/paddle/hooks/usePaddleSdk"
import { QueryClient, QueryKey } from "react-query"
import { createWebStoragePersistor } from "react-query/createWebStoragePersistor-experimental"
import { persistQueryClient } from "react-query/persistQueryClient-experimental"
import { MS_IN_DAY } from "utils/time"

const cacheTime = MS_IN_DAY * 5

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      cacheTime,
    },
  },
})

const localStoragePersistor = createWebStoragePersistor({
  storage: window.localStorage,
})

const doNotPersistQueries: QueryKey[] = [paddleQueryKey]

persistQueryClient({
  queryClient,
  persistor: localStoragePersistor,
  maxAge: cacheTime,
  hydrateOptions: {},
  dehydrateOptions: {
    shouldDehydrateQuery: ({ queryKey }) => {
      return !doNotPersistQueries.includes(queryKey)
    },
  },
})