Last active
September 12, 2023 23:31
-
-
Save chamatt/57c3743fdf782feb1fa946a9ddfea187 to your computer and use it in GitHub Desktop.
Apollo Refetch Manager - Control apollo client refetch behavior from the parent component
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
) | |
} |
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
Simple usage example:
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
: