Skip to content

Commit 389ac8e

Browse files
docs(vue-query): Add docs page for vue reactivity (#8170)
* add page for vue reactivity * add to structure
1 parent e474f73 commit 389ac8e

File tree

2 files changed

+170
-0
lines changed

2 files changed

+170
-0
lines changed

docs/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@
9999
"label": "TypeScript",
100100
"to": "framework/vue/typescript"
101101
},
102+
{
103+
"label": "Reactivity",
104+
"to": "framework/vue/reactivity"
105+
},
102106
{
103107
"label": "GraphQL",
104108
"to": "framework/vue/graphql"

docs/framework/vue/reactivity.md

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
---
2+
id: reactivity
3+
title: Reactivity
4+
---
5+
6+
Vue uses the [the signals paradigm](https://vuejs.org/guide/extras/reactivity-in-depth.html#connection-to-signals) to handle and track reactivity. A key feature of
7+
this system is the reactive system only triggers updates on specifically watched reactive properties. A consequence of this is you also need to ensure that the queries are updated when values they consume are updated.
8+
9+
# Keeping Queries Reactive
10+
11+
When creating a composable for a query your first choice may be to write it like so:
12+
13+
```ts
14+
export function useUserProjects(userId: string) {
15+
return useQuery(
16+
queryKey: ['userProjects', userId],
17+
queryFn: () => api.fetchUserProjects(userId),
18+
);
19+
}
20+
```
21+
22+
We might consume this composable like so:
23+
24+
```ts
25+
// Reactive user ID ref.
26+
const userId = ref('1')
27+
// Fetches the user 1's projects.
28+
const { data: projects } = useUserProjects(userId.value)
29+
30+
const onChangeUser = (newUserId: string) => {
31+
// Edits the userId, but the query will not re-fetch.
32+
userId.value = newUserId
33+
}
34+
```
35+
36+
This code will not work as intended. This is because we are extracting the value from the userId ref directly. Vue-query is not tracking the `userId` `ref` so it has no way of knowing when the value changes.
37+
38+
Luckily, the fix for this is trivial. The value must be made trackable in the query key. We can Just accept the `ref` directly in the composable and place it in the query key:
39+
40+
```ts
41+
export function useUserProjects(userId: Ref<string>) {
42+
return useQuery(
43+
queryKey: ['userProjects', userId],
44+
queryFn: () => api.fetchUserProjects(userId.value),
45+
);
46+
}
47+
```
48+
49+
Now the query will re-fetch when the `userId` changes.
50+
51+
```ts
52+
const onChangeUser = (newUserId: string) => {
53+
// Query refetches data with new user ID!
54+
userId.value = newUserId
55+
}
56+
```
57+
58+
In vue query any reactive properties within a query key are tracked for changes automatically. This allows vue-query to refetch data whenever the
59+
parameters for a given request change.
60+
61+
## Accounting for Non-Reactive Queries
62+
63+
While far less likely, sometimes passing non-reactive variables is intentional. For example, some entities only need to be fetched once and don't need tracking or we invalidate a mutation a query options object after a mutation.
64+
If we use our custom composable defined above the usage in this case feels a bit off:
65+
66+
```ts
67+
const { data: projects } = useUserProjects(ref('1'))
68+
```
69+
70+
We have to create an intermediate `ref` just to make the parameter type-compatible. We can do better here. Let's instead update our composable to accept both plain values and reactive values:
71+
72+
```ts
73+
export function useUserProjects(userId: MaybeRef<string>) {
74+
return useQuery(
75+
queryKey: ['userProjects', userId],
76+
queryFn: () => api.fetchUserProjects(toValue(userId)),
77+
);
78+
}
79+
```
80+
81+
Now we can use the composable with both plain values and refs:
82+
83+
```ts
84+
// Fetches the user 1's projects, userId is not expected to change.
85+
const { data: projects } = useUserProjects('1')
86+
87+
// Fetches the user 1's projects, queries will react to changes on userId.
88+
const userId = ref('1')
89+
90+
// Make some changes to userId...
91+
92+
// Query re-fetches based on any changes to userId.
93+
const { data: projects } = useUserProjects(userId)
94+
```
95+
96+
## Using Derived State inside Queries
97+
98+
It's quite common to derive some new reactive state from another source of reactive state. Commonly, this problem manifests in situations where you deal with component props. Let's assume our `userId` is a prop passed to a component:
99+
100+
```vue
101+
<script setup lang="ts">
102+
const props = defineProps<{
103+
userId: string
104+
}>()
105+
</script>
106+
```
107+
108+
You may be tempted to use the prop directly in the query like so:
109+
110+
```ts
111+
// Won't react to changes in props.userId.
112+
const { data: projects } = useUserProjects(props.userId)
113+
```
114+
115+
However, similar to the first example, this is not reactive. Property access on `reactive` variables causes reactivity to be lost. We can fix this by making this derived state reactive via a `computed`:
116+
117+
```ts
118+
const userId = computed(() => props.userId)
119+
120+
// Reacts to changes in props.userId.
121+
const { data: projects } = useUserProjects(userId)
122+
```
123+
124+
This works as expected, however, this solution isn't always the most optimal. Aside from the introduction of an intermediate variable, we also create a memoized value that is somewhat unnecessary. For trivial cases of simple property access `computed` is an optimization with no real benefit. In these cases a more appropriate solution is to use [reactive getters](https://blog.vuejs.org/posts/vue-3-3#better-getter-support-with-toref-and-tovalue). Reactive getters are simply functions that return a value based on some reactive state, similar to how `computed` works. Unlike `computed`, reactive getters do not memoize their values so it makes it a good candidate for simple property access.
125+
126+
Let's once again refactor our composable, but this time we'll have it accept a `ref`, plain value, or a reactive getter:
127+
128+
```ts
129+
export function useUserProjects(userId: MaybeRefOrGetter<string>) {
130+
...
131+
}
132+
```
133+
134+
Let's adjust our usage and now use a reactive getter:
135+
136+
```ts
137+
// Reacts to changes in props.userId. No `computed` needed!
138+
const { data: projects } = useUserProjects(() => props.userId)
139+
```
140+
141+
This gives us a terse syntax and the reactivity we need without any unneeded memoization overhead.
142+
143+
## Other tracked Query Options
144+
145+
Above, we only touched one query option that tracks reactive dependencies. However, in addition to `queryKey`, `enabled` also allows
146+
the use of reactive values. This comes in handy in situations where you want to control the fetching of a query based on some derived state:
147+
148+
```ts
149+
export function useUserProjects(userId: MaybeRef<string>) {
150+
return useQuery(
151+
queryKey: ['userProjects', userId],
152+
queryFn: () => api.fetchUserProjects(toValue(userId)),
153+
enabled: () => userId.value === activeUserId.value,
154+
);
155+
}
156+
```
157+
158+
More details on this option can be found on the [useQuery reference](./reference/useQuery.md) page.
159+
160+
# Key Takeaways
161+
162+
- `enabled` and `queryKey` are the two query options that can accept reactive values.
163+
- Pass query option that accept all three types of values in Vue: refs, plain values, and reactive getters.
164+
- If you expect a query to react to changes based on the values it consumes, ensure that the values are reactive. (i.e. pass in refs directly to the query, or use reactive getters)
165+
- If you don't need a query to be reactive pass in a plain value.
166+
- For trivial derived state such as property access consider using a reactive getter in place of a `computed`.

0 commit comments

Comments
 (0)