Introduction
GQTY suggests using integration with React in the form of useQuery
, useMutation
hooks, and so on.
But when using a state manager, we face the problem of where to store data and a natural desire to move everything about the data and their loading to the state manager, but this creates a second problem - we have to manually transfer data from gqty hooks to the state manager.
Since our projects use effector as a state manager, we will consider the integration with it. First, you need to configure your local gqty instance. Please follow the original instructions at https://gqty.dev/docs/getting-started.
The differences will be in the Configuring Codegen section, the react property should be switched to false, this will not load your bundle with unused hooks, yes, I propose to completely abandon all gqty hooks. After that, you need to delete the generated files, including index.ts
Integration with effector comes down to using a standalone client inside the effects, documentation, and example at https://gqty.dev/docs/client/fetching-data and in the Core Client section of the gqty documentation. The effects already have load markers and load end events, both successful and error events.
Using with effector
Let's start with an example code to retrieve data (query):
import { query, resolved } from '../../api';
const readUserFx = createEffect((userId: string) => resolved(() => {
const user = query.readUser({ userId })
if (!user) return null;
return {
id: user.id!,
username: user.username!,
avatarUrl: user.avatarUrl!,
tags: user.tags!.map(tag => tag),
posts: user.posts!.map(post => ({
id: post.id!,
title: post.title!,
content: post.content!,
})),
}
}))
Now we can figure out what's going on here and why.
query.readUser({ userId })
doesn't send a query to the server the first time, it only returns a Proxy object so that we can gather the list of fields we need to make a valid query.
In the return expression, we list the fields that we want to get from the query; this is how we describe fields when writing a regular graphQL query.
Exclamation marks in expressions like user.username!
are needed to prove to the typescript that the value in the field is certain, otherwise, it will be a string | undefined
, which is not the case. https://github.com/gqty-dev/gqty/issues/261
resolved()
is a magic function that helps gqty gather the fields the user needs to execute the query. The first time, before executing a query, resolved sets a Proxy instance in the query
variable, which collects all the fields accessed by the developer inside the resolved(callback)
. After the callback is executed, resolved
sends the request to the server and returns Promise
to the developer. When the server returns the response, resolved
substitutes it in the query
variable and calls the callback again, already with real data, and then resolves the promise. Note that this is a rough description of the process necessary to explain what's going on.
Any nested data, you also need to select, as well as arrays, even if they are simple, otherwise, you will fall into the data Proxy-objects, which, to put it mildly, are not very pleasant to work with.
But it doesn't look like a convenient solution! Yes, and there are a few ways to simplify life:
Step 1: Create type-caster functions
import { query, resolved, User, Post } from '../../api';
function getPost(post: Post) {
return {
id: post.id!,
title: post.title!,
content: post.content!,
}
}
function getUser(user: User) {
return {
id: user.id!,
username: user.username!,
avatarUrl: user.avatarUrl!,
tags: user.tags!.map(tag => tag),
posts: user.posts!.map(getPost),
}
}
const readUserFx = createEffect((userId: string) => resolved(() => {
const user = query.readUser({ userId })
if (!user) return null;
return getUser(user)
}))
Here it's simple, just put the repeated object getters into functions and reuse them, it's better to put such getters next to the API definition.
Step 2. Use helper functions from gqty
https://gqty.dev/docs/client/helper-functions
import { selectFields } from 'gqty'
import { query, resolved, User } from '../../api'
function getUser(user: User) {
return selectFields(user, [
'id',
'username',
'avatarUrl',
'tags',
'posts.id',
'posts.title',
'posts.content',
])
}
const readUserFx = createEffect((userId: string) =>
resolved(() => {
const user = query.readUser({userId})
if (!user) return null
return getUser(user)
})
)
It is important to read the documentation and carefully check the operation of gqty methods under different conditions.
Step 3. Put all the effects in a separate API layer.
// api.layer.ts
import { selectFields } from 'gqty'
import { query, resolved, User } from './index'
export function getUser(user: User) {
return selectFields(user, [
'id',
'username',
'avatarUrl',
'tags',
'posts.id',
'posts.title',
'posts.content',
])
}
export const readUserFx = createEffect((userId: string) =>
resolved(() => {
const user = query.readUser({userId})
if (!user) return null
return getUser(user)
})
)
// pages/users/model.ts
import { attach } from 'effector'
import * as api from '../../api/api.layer'
const readUserFx = attach({ effect: api.readUserFx })
Now all models can reuse graphQL queries in the same way, without even thinking about how exactly the query is run and what fields get under the hood. But if they need to query additional fields or perform the query differently, they can easily build their query by reusing getUser-like getters.
Why we need attach
In the example, I used the attach method instead of using api.readUserFx
directly, for one very important reason:
// pages/users/model.ts
import * as api from '../../api/api.layer'
sample({
clock: api.readUserFx.done,
target: showNotification,
})
If we write code without attach
, subscribing directly to any effect events, these events will be triggered every time any other model triggers that effect. And since in an application different models can subscribe to the same effect, all the scripts in which the effect is involved will be triggered, without regard to whether the page is open now or not, or whether a certain script triggered the effect or not.
// pages/users/model.ts
import * as api from '../../api/api.layer'
const readUserFx = attach({ effect: api.readUserFx })
sample({
clock: readUserFx.done,
target: showNotification,
})
Using attach we create a local copy of the original effect. If each model creates a local effect, and only subscribes and runs its local copy, there won't be any problems with overlapping different scripts.
But keep in mind that running the local effect still triggers events and triggers the parent effect, and if someone subscribes to the global api.readUserFx
, they will get all the reactions from all the models, this is useful when building an error handling system.
Top comments (0)