Skip to content

Instantly share code, notes, and snippets.

@chamatt
Last active September 12, 2023 23:31
Show Gist options
  • Save chamatt/57c3743fdf782feb1fa946a9ddfea187 to your computer and use it in GitHub Desktop.
Save chamatt/57c3743fdf782feb1fa946a9ddfea187 to your computer and use it in GitHub Desktop.
Apollo Refetch Manager - Control apollo client refetch behavior from the parent component
import { PropsWithChildren, createContext, useContext, useMemo } from "react"
import { DocumentNode } from "@apollo/client"
import { getOperationName } from "@apollo/client/utilities"
import { groupBy } from "lodash-es"
type MutationQueryPair = {
mutation: DocumentNode
queries: DocumentNode[]
}
export const buildRefetchOperation = (
mutation: DocumentNode,
queries: DocumentNode[],
): MutationQueryPair => ({
mutation,
queries,
})
type RefetchContextType = {
/**
* The operations pair of mutation and query
* @type {MutationQueryPair[]}
* @example
* buildRefetchOperation(MY_MUTATION, [MY_QUERY])
*/
operations: MutationQueryPair[]
}
export const RefetchContext = createContext<RefetchContextType>({
operations: [],
})
const useInternalRefetchContext = () => useContext(RefetchContext)
export const useRefetchManager = (mutation: DocumentNode) => {
const { operations } = useInternalRefetchContext()
const operation = useMemo(
() => operations.find(op => getOperationName(op.mutation) === getOperationName(mutation)),
[operations, mutation],
)
if (!operation) {
throw new Error(
`You must wrap your component in a RefetchManagerProvider and set the queries to refetch for the mutation ${getOperationName(
mutation,
)}\n\n
Example:\n\n
<RefetchManagerProvider operations={[buildRefetchOperation(MY_MUTATION, [MY_QUERY])]}>\n
<MyComponent />\n
</RefetchManagerProvider>\n
`,
)
}
return {
queries: operation?.queries || [],
}
}
/**
* Provides a context for refetching queries in Apollo Client.
*
* @param {Object} props - The props for the context provider.
* @param {ReactNode} props.children - The child components to render.
* @param {MutationQueryPair[]} [props.operations=[]] - The operations pairs to refetch, each containing a mutation and an array of queries.
* @returns {ReactNode} The context provider component.
*
* @example
* <RefetchManagerProvider operations={[buildRefetchOperation(MY_MUTATION, [MY_QUERY])]}>
* <MyComponent />
* </RefetchManagerProvider>
*
* @description
* The `RefetchManagerProvider` component provides a context for refetching queries in Apollo Client.
* It accepts an `operations` prop, which is an array of `MutationQueryPair` objects representing the pairs of mutation and queries to refetch.
* The providers can be nested, and the `operations` array will be merged together for the child provider.
*
* @example
* // MyComponent.tsx
* import { useRefetchManager } from "~/shared/contexts/RefetchContext"
* const MyComponent = () => {
* const { queries } = useRefetchManager(MY_MUTATION)
* const [mutate] = useMutation(MY_MUTATION, {
* refetchQueries: queries
* })
* return (...);
* }
*
* // ParentComponentOne.tsx
* const ParentComponentOne = () => {
* return (
* <RefetchManagerProvider operations={[buildRefetchOperation(MY_MUTATION, [MY_QUERY])]}>
* <MyComponent />
* </RefetchManagerProvider>
* )
* }
*
* // ParentComponentTwo.tsx
* const ParentComponentTwo = () => {
* return (
* <RefetchManagerProvider operations={[buildRefetchOperation(MY_MUTATION, [MY_OTHER_QUERY])]}>
* <MyComponent />
* </RefetchManagerProvider>
* )
* }
*
*
* You can also use the `createRefetchManager` helper to create a provider with the given operations.
* @example
* const RefetchManager = createRefetchManager([buildRefetchOperation(MY_MUTATION, [MY_QUERY])])
* const ParentComponent = () =>
* <RefetchManager>
* <MyComponent />
* </RefetchManager>
*/
export const RefetchManagerProvider = ({
children,
operations = [],
}: PropsWithChildren<{ operations: MutationQueryPair[] }>) => {
const { operations: parentOperations } = useInternalRefetchContext()
const grouppedMutations = groupBy([...parentOperations, ...operations], ({ mutation }) =>
getOperationName(mutation),
)
const mergedOperations = Object.values(grouppedMutations).map(operations => {
return {
mutation: operations[0].mutation,
queries: operations.flatMap(({ queries }) => queries),
}
})
return (
<RefetchContext.Provider value={{ operations: mergedOperations }}>
{children}
</RefetchContext.Provider>
)
}
/**
* Creates a RefetchManagerProvider component with the given operations.
* This is just a helper to avoid having a big JSX footprint
* @param {MutationQueryPair[]} operations - The operations pairs to refetch, each containing a mutation and an array of queries.
* @returns {ReactNode} The context provider component.
* @example
* const RefetchManager = createRefetchManager([buildRefetchOperation(MY_MUTATION, [MY_QUERY])])
*/
export const createRefetchManager = (operations: MutationQueryPair[]) => {
return ({ children }: PropsWithChildren<unknown>) => (
<RefetchManagerProvider operations={operations}>{children}</RefetchManagerProvider>
)
}
@chamatt
Copy link
Author

chamatt commented Sep 12, 2023

Simple usage example:

// MyComponent.tsx
import { useRefetchManager } from "~/shared/contexts/RefetchManager"
const MyComponent = () => {
 const { queries } = useRefetchManager(MY_MUTATION)
 const [mutate] = useMutation(MY_MUTATION, {
    refetchQueries: [...queries]
 })
 return (...);
}

// ParentComponentOne.tsx
const ParentComponentOne = () => {
 return (
  <RefetchManagerProvider operations={[buildRefetchOperation(MY_MUTATION, [MY_QUERY])]}>
   <MyComponent />
 </RefetchManagerProvider>
 )
}

// ParentComponentTwo.tsx
const ParentComponentTwo = () => {
 return (
  <RefetchManagerProvider operations={[buildRefetchOperation(MY_MUTATION, [MY_OTHER_QUERY])]}>
   <MyComponent />
 </RefetchManagerProvider>
 )
}

PS: The operations is an array because you can pass multiple mutation/queries[] pairs in a single RefetchManager

An alternative API is creating the RefetchProvider outside the component with the utility createRefetchManager:

const RefetchManager = createRefetchManager([
  buildRefetchOperation(MY_MUTATION, [MY_QUERY]),
  buildRefetchOperation(ANOTHER_MUTATION, [ANOTHER_QUERY_TARGETTED_1, ANOTHER_QUERY_TARGETTED_2]),
])

const ParentComponent = () => {
  return (
    <RefetchManager>
      <MyComponent />
    </RefetchManager>
  )
}

@chamatt
Copy link
Author

chamatt commented Sep 12, 2023

You can probably create a wrapper around useMutation and integrate this directly there, so you wouldn't have to call both useRefetchManager and useMutation. But in our company we didn't want to encourage refetching as a primary option, so we kept this little bit of friction.

Here's how it would roughly look like (without typescript):

const useMutation = (mutation, options) => {
   const { queries } = useRefetchManager(mutation)
   const [mutate] = useApolloMutation(mutation, {
      ...options,
      refetchQueries: [...options.refetchQueries, ...queries]
   })
}

Then the component would just become:

// MyComponent.tsx
const MyComponent = () => {
 const [mutate] = useMutation(MY_MUTATION)
 return (...);
}

PS: For this to work you'd have to remove the error check in useRefetchManager, or create a secondary hook just to use inside the custom useMutation, otherwise you'd have to wrap every usage with a <RefetchManager/>

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