Patterns and Tips around migrating from Apollo to React Query #301
Replies: 13 comments 25 replies
-
|
I recently undertook this migration in my own project and can share some of the things I wrote to make this work. I wanted an API very close to Apollo's to make this easier. There are two files that made it possible. I wrote a sample codesandbox where this code can be played with: https://codesandbox.io/s/peaceful-sun-51609
This is how its used: import { GraphQLProvider, useGraphQLQuery } from "./react-query-graphql";
import gql from "graphql-tag";
const POKEMON_QUERY = gql`
query pokemon($name: String) {
pokemon(name: $name) {
id
number
name
attacks {
special {
name
type
damage
}
}
image
}
}
`;
const App = () => {
const [name, setName] = React.useState("pikachu");
const { data, error } = useGraphQLQuery(POKEMON_QUERY, {
variables: { name }
});
return (
<>
<label>Search for pokemon</label>
<input
placeholder="Pokemon name"
style={{ fontSize: 20 }}
value={name}
onChange={e => setName(e.currentTarget.value)}
/>
<br />
{data && data.pokemon && (
<img
height={100}
width={100}
src={data.pokemon.image}
alt={data.pokemon.name}
/>
)}
</>
);
}; |
Beta Was this translation helpful? Give feedback.
-
|
Some of my takeaways were:
|
Beta Was this translation helpful? Give feedback.
-
|
The bundle improvements are massive! More than I initially thought they would be. Apollo must be pretty big... |
Beta Was this translation helpful? Give feedback.
-
|
Hi @nksaraf, maybe you can try https://github.com/hasura/graphqurl instead of your own fetch lib, I did a basic example and that seems good, you can check here https://codesandbox.io/s/react-query-graphqurl-ybiop. |
Beta Was this translation helpful? Give feedback.
-
|
Hello. I literally came across Tanner and react-query today and it's fantastic. I have recently started using Apollo client hooks as well in a new project. I am trying to understand what the best course of action might be. Replace Apollo hooks with react-query entirely? Wrap Apollo hooks with react-query where appropriate (would love ideas on their harmonious existence) or maybe I don't need react-query at all? Thanks for any guidance. |
Beta Was this translation helpful? Give feedback.
-
|
I still cant seem to figure out if react-query can handle all of state management needs or not. React Apollo can completely replace other state management tools. It seems like we will have to use Context API or something else to handle client states and store server states when we need to expose them globally? |
Beta Was this translation helpful? Give feedback.
-
|
In the react-query comparison guide there’s a small blurb about accomplishing auto refetching using a schema and some heuristics on how to consume that schema. Are there more details on how this works? I’m wondering if this same concept could be used to implement a normalized cache like Apollo. @nksaraf mentioned they don’t utilize the normalized cache much but it’s something I find very useful and would love to see if react-query can support it. |
Beta Was this translation helpful? Give feedback.
-
|
I finally did it! Took me quiet some time, mainly because I also took the opportunity to refactor all the custom data hooks and moved them into a separate package for future reuse, but it was definitely worth it! Advantages from my side:
Disadvantages:
For the implementation side, I took the approach from @nksaraf and changed it a little bit so it perfectly suits my use case. Here's an example: // QUERY
interface IGetPlaylistsData {
share: {
id: string
playlists: Playlist[]
}
}
interface IGetPlaylistsVariables {
shareID: string
}
// definition of the graphql query, so we only have to state the data and variable type once for later inferrence
const GET_SHARE_PLAYLISTS = TransformedGraphQLQuery<IGetPlaylistsData, IGetPlaylistsVariables>(gql`
query sharePlaylists($shareID: String!){
share(shareID: $shareID) {
id,
playlists {
${playlistKeys}
}
}
}
`)((data) => data.share.playlists)
// custom query hook using our little graphql client implementation
const useSharePlaylists = (shareID: string, opts?: IGraphQLQueryOpts<typeof GET_SHARE_PLAYLISTS>) => {
// data and variable type is inferred from the query definition
const query = useGraphQLQuery(GET_SHARE_PLAYLISTS, {
variables: { shareID },
...opts,
})
return query
}
// MUTATION
interface ICreatePlaylistVariables {
shareID: string
name: string
}
interface ICreatePlaylistData {
createPlaylist: Playlist
}
const CREATE_PLAYLIST = TransformedGraphQLMutation<ICreatePlaylistData, ICreatePlaylistVariables>(gql`
mutation createPlaylist($shareID: String!, $name: String!){
createPlaylist(shareID: $shareID, name: $name){
${playlistKeys}
}
}
`)((data) => data.createPlaylist)
const useCreatePlaylist = (opts?: IGraphQLMutationOpts<typeof CREATE_PLAYLIST>) => {
const mutation = useGraphQLMutation(CREATE_PLAYLIST, {
...opts,
onSuccess: (data, variables) => {
// type safe update of our query. No dealing with query keys, they are automatically taken from GET_SHARE_PLAYLISTS
typedQueryCache.setTypedQueryData(
{
query: GET_SHARE_PLAYLISTS,
variables: { shareID: variables.shareID },
},
(currentData) => [...(currentData || []), data],
)
if (opts?.onSuccess) opts.onSuccess(data, variables)
},
})
return mutation
}For the full implementation details, have a look here. |
Beta Was this translation helpful? Give feedback.
-
|
This is what i'm doing to support GraphQL queries & mutations using react-query. First I created a reusable fetch function that supports variables. (credentials included because i use httpOnly cookie) async function fetchData(query, { variables } = {}) {
const res = await fetch(process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({
query,
variables,
}),
});
const json = await res.json();
if (json.errors) {
const { message } = json.errors[0] || "Error..";
throw new Error(message);
}
return json.data;
}Heres an example of one of my queries with variables: export async function getUserLetters(key, { sort, limit, start, where }) {
const data = await fetchData(
`
query USER_LETTERS($sort: String, $limit: Int, $start: Int, $where: JSON) {
count: userLetterCount
userLetters(
sort: $sort
limit: $limit
start: $start
where: $where
) {
id
title
slug
status
content
created_at
updated_at
__typename
user {
id
}
}
}
`,
{ variables: { sort, limit, start, where } }
);
return data?.userLetters;
}Heres that query in use: const { data } = useQuery(
["userletters", { limit: 2, start: 0 }],
getUserLetters
);I use the same fetch function for mutations: import { UserFragment } from "../fragments";
/* Sign In Mutation */
export async function signIn({ identifier, password }) {
const data = await fetchData(
`
mutation signIn($identifier: String!, $password: String!) {
signIn(input: { identifier: $identifier, password: $password }) {
authorized
${UserFragment}
}
}
`,
{ variables: { identifier, password } }
);
return data?.signIn;
} |
Beta Was this translation helpful? Give feedback.
-
|
custom plugin // react-query-codegen.js
// react-query-codegen.js
const { concatAST, Kind, visit } = require('graphql');
const {
ClientSideBaseVisitor,
indentMultiline,
} = require('@graphql-codegen/visitor-plugin-common');
module.exports = {
plugin: (schema, documents, config) => {
const allAst = concatAST(documents.map((v) => v.document));
const allFragments = [
...allAst.definitions
.filter((d) => d.kind === Kind.FRAGMENT_DEFINITION)
.map((fragmentDef) => ({
node: fragmentDef,
name: fragmentDef.name.value,
onType: fragmentDef.typeCondition.name.value,
isExternal: false,
})),
...(config.externalFragments || []),
];
const visitor = new TypeScriptDocumentNodesVisitor(schema, allFragments, config, documents);
const visitorResult = visit(allAst, { leave: visitor });
return {
prepend: visitor.getImports(),
content: [
visitor.fragments,
...visitorResult.definitions.filter((t) => typeof t === 'string'),
visitor.queries,
].join('\n'),
};
},
};
class TypeScriptDocumentNodesVisitor extends ClientSideBaseVisitor {
constructor(schema, fragments, config, documents) {
super(schema, fragments, config, {}, documents);
}
operations = [];
buildOperation(
node,
documentVariableName,
operationType,
operationResultType,
operationVariablesTypes
) {
this.operations.push({
node,
documentVariableName,
operationType,
operationResultType,
operationVariablesTypes,
});
return null;
}
get queries() {
return `
import { QueryConfig, useQuery } from 'react-query';
function graphql<Result, Variables>(query: string, variables?: Variables) {
return async (): Promise<Result> => {
const res = await fetch(process.env.GRAPHQL_API, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
authorization: \`Bearer \${localStorage.getItem('token')\}\`,
},
credentials: 'include',
body: JSON.stringify({ query, variables }),
});
const json = await res.json();
if (json.errors) {
const { message } = json.errors[0] || 'Error..';
throw new Error(message);
}
return json.data;
}
}
${this.operations
.map(({ node, operationResultType, documentVariableName, operationVariablesTypes }) => {
const optionalVariables =
!node.variableDefinitions ||
node.variableDefinitions.length === 0 ||
node.variableDefinitions.every(
(v) => v.type.kind !== Kind.NON_NULL_TYPE || v.defaultValue
);
const variables = `variables${optionalVariables ? '?' : ''}: ${operationVariablesTypes}`;
return `
export const use${operationResultType} = (${variables}, options?:QueryConfig<${operationResultType}>) =>
useQuery<${operationResultType}, ${operationVariablesTypes}>(
['${node.name.value}', variables],
graphql<${operationResultType}, ${operationVariablesTypes}>(${documentVariableName}, variables),
options
)`;
})
.map((s) => indentMultiline(s, 2))
.join(',\n')}`;
}
}Config // config.yml
overwrite: true
strict: true
config:
avoidOptionals:
field: true
scalars:
_text: string[]
bigint: number
date: string
float8: number
inet: string
numeric: number
timestamptz: string
uuid: string
namingConvention:
typeNames: change-case#pascalCase
transformUnderscore: true
hooks:
afterAllFileWrite:
- prettier --write
schema: graphql.introspect.json
generates:
src/api.ts:
config:
onlyOperationTypes: true
enumsAsConst: true
addDocBlocks: false
preResolveTypes: true
skipTypename: true
documentMode: 'string'
documents:
- src/**/*.gql
plugins:
- typescript
- typescript-operations
- react-query-codegenHow to use it // index.ts
// index.ts
import React from 'react';
import ReactDOM from 'react-dom';
import { useGetBanksQuery } from './api';
export function App() {
const query1 = useGetBanksQuery({ limit: 3 });
const query1Clone = useGetBanksQuery({ limit: 3 });
const query2 = useGetBanksQuery({ limit: 2 });
// expected only two requests since query1 and query1Clone have same cache key
return (
<>
<div>{JSON.stringify(query1.data?.banks)}</div>
<div>{JSON.stringify(query1Clone.data?.banks)}</div>
<div>{JSON.stringify(query2.data?.banks)}</div>
</>
);
}
ReactDOM.render(<App />, document.getElementById('root'));``` |
Beta Was this translation helpful? Give feedback.
-
|
Has anyone tried graphql code generator with React-Query: https://graphql-code-generator.com/docs/plugins/typescript-react-query I used graphql code generator with apollo and it was really nice experience. I am trying to use it but getting several errors at this moment. |
Beta Was this translation helpful? Give feedback.
-
|
I would love to have a unified solution for custom type serialization/deserialization on the client and server. graphql-code-generator's ScalarsMap gets half of the way there. By defining a custom scalar type in Specifically, I really only want a |
Beta Was this translation helpful? Give feedback.
-
|
Has anyone had success with replacing Apollo's |
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
-
Let's get this going!
Beta Was this translation helpful? Give feedback.
All reactions