Improve the cache api #249
-
I was reading through the docs and didn't find a way to hook into the cache setter and getter, I think that would be a good addition to this library. I usually use Apollo in my projects and one of the things that I really like about it is their normalized cache, it's very useful in large scale apps. This is an example of what a normalized cache looks like:
{
todos: [
{
id: 1,
comment: 'something',
user: {
id: 1,
name: 'John'
}
},
{
id: 2,
comment: 'another one',
user: {
id: 2,
name: 'Maria'
}
},
{
id: 2,
comment: 'another comment',
user: {
id: 1,
name: 'John'
}
}
]
}
{
todos: [
{ id: 1, comment: 'something', userId: 1},
{ id: 2, comment: 'another one', userId: 2 },
{ id: 2, comment: 'another comment', userId: 1 },
],
users: [
{ id: 1, name: 'John'},
{ id: 2, name: 'Maria' }
]
} This is highly useful when you have entities/queries sharing the same nested entities. if you, for example, have recently fetched a query that has a user with an updated name, it will update all the queries that are referencing that user. I'm not sure if that's a strategy that we should adopt in the library but it'd be good if we have a way to hook into the cache maybe through functions like |
Beta Was this translation helpful? Give feedback.
Replies: 8 comments 10 replies
-
What I'm doing is normalizing the data in the fetch function: import { normalize, schema } from 'normalizr';
import { path } from 'ramda';
import api from './api';
const userSchema = new schema.Entity('users');
export const fetchUsers = async () => {
const users = await api()
.get('users')
.json();
const normalizedData = normalize(users, [userSchema]);
const data = path(['entities', 'users'], normalizedData) || {};
return data;
}; This way when I use the function with const { status, data } = useQuery('users', fetchUsers); And when an update is made, is easy to update the data this way: import produce from 'immer';
const [userUpdate] = useMutation(updateUser, {
onSuccess: data => {
queryCache.setQueryData(
'users',
oldData => {
return produce(oldData, newData => {
newData[data.id] = data;
});
},
);
},
}); |
Beta Was this translation helpful? Give feedback.
-
That looks good for simple cases, but what if you have a nested entity in your user schema, like e.g With your approach, I don't think you can do that because you'd need a way to store the friends under another key inside the cache, please tell me if I'm wrong. Thank you for your answer. |
Beta Was this translation helpful? Give feedback.
-
The bottom line here is that once you start normalizing, you start making decisions that incur tradeoffs of performance or built-in staleness. You're more than welcome to implement whatever manual operations with the query cache that you want using |
Beta Was this translation helpful? Give feedback.
-
Sorry to bump a very old topic, but if normalizing isn't part of the philosophy of the project is there any discussion or documentation about how to handle situations like @focux brought up? Most larger apps that I've worked on have data entities that are really closely interrelated and I'm sure that's the same for a lot of people. How does react-query (or people who have used it on larger codebases) suggest handling a situation like a user that's represented in a bunch of different queries being updated? |
Beta Was this translation helpful? Give feedback.
-
This is where managing your side effects in a central location is helpful. It’s almost like maintaining a schema of related Query keys. When this key changes, these keys should invalidate. Then just have your mutations reference these central side effects. |
Beta Was this translation helpful? Give feedback.
-
@tannerlinsley if someone wanted to build a third-party integration with normalizr for automatic caching, like I think this is an important topic (even if not addressed within react-query itself) because many teams choose between GraphQL+Apollo or REST+react-query for their server/client architecture. One of the biggest selling points of graphql+apollo is the normalized caching. To get that benefit with REST today, you have to do a lot of complicated and ugly manual stuff with redux/mobx, which is a no-go – and not having a normalized cache can make for a broken user experience (update your profile picture or name and don't see it reflected anywhere in the app); also a no-go. When I worked at Stripe, I helped build a solution that made REST at least as nice to use as GraphQL (basically, an internal version of react-query, before it existed). It autogenerated a function for each operation in our internal openapi spec, complete with types for params and responses, and a normalizr schema for the relations within our app for automatic normalized caching. Having the cache work in a consistent/automated way kept the system relatively predictable, and each use of a query/mutation had to specify whether it should use the cache, which was a simple solution to the general problem of dealing with caching bugs. Today, orval exists to generate @tannerlinsley how would you architect such a library? Can it be done today in an ergonomic way? |
Beta Was this translation helpful? Give feedback.
-
Have you considered using a library intended for high performance and data consistency using data normalization like Reactive Data Client? This normalization lets you perform data mutations with one line and zero extra fetches. No need to create inconsistencies and performance problems with invalidation fetches. 🎮Simplest Todo Demo | 🎮Github Demo For example: import { Entity, createResource } from '@data-client/rest';
export class Todo extends Entity {
id = 0;
userId = 0;
title = '';
completed = false;
pk() {
return `${this.id}`;
}
static key = 'Todo';
}
export const TodoResource = createResource({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
searchParams: {} as { userId?: string | number } | undefined,
schema: Todo,
optimistic: true,
}); import { useSuspense } from '@data-client/react';
import { TodoResource } from './TodoResource';
import TodoItem from './TodoItem';
import CreateTodo from './CreateTodo';
function TodoList() {
const userId = 1;
const todos = useSuspense(TodoResource.getList, { userId });
return (
<div>
{todos.map(todo => (
<TodoItem key={todo.pk()} todo={todo} />
))}
<CreateTodo userId={userId} />
</div>
);
} import { useController } from '@data-client/react';
import { TodoResource } from './TodoResource';
export default function CreateTodo({ userId }: { userId: number }) {
const ctrl = useController();
const handleKeyDown = async e => {
if (e.key === 'Enter') {
ctrl.fetch(TodoResource.getList.push, {
userId,
title: e.currentTarget.value,
});
e.currentTarget.value = '';
}
};
return (
<div className="listItem nogap">
<label>
<input type="checkbox" name="new" checked={false} disabled />
<input type="text" onKeyDown={handleKeyDown} />
</label>
<CancelButton />
</div>
);
} import { useController } from '@data-client/react';
import { TodoResource, type Todo } from './TodoResource';
export default function TodoItem({ todo }: { todo: Todo }) {
const ctrl = useController();
const handleChange = e =>
ctrl.fetch(
TodoResource.partialUpdate,
{ id: todo.id },
{ completed: e.currentTarget.checked },
);
const handleDelete = () =>
ctrl.fetch(TodoResource.delete, {
id: todo.id,
});
return (
<div className="listItem nogap">
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={handleChange}
/>
{todo.completed ? <strike>{todo.title}</strike> : todo.title}
</label>
<CancelButton onClick={handleDelete} />
</div>
);
} You could also integrate normalization into another library using the underlying optimized normalizr library |
Beta Was this translation helpful? Give feedback.
-
I'm building/consuming a backend that follows the JSON:API spec, and it was designed with normalisation of object graphs in mind. I'm just commenting this for now in case anyone else is searching for JSON:API-related keywords. I'm keen to see if I can use this library to build a kind of equivalent of Ember data in Vue. |
Beta Was this translation helpful? Give feedback.
The bottom line here is that once you start normalizing, you start making decisions that incur tradeoffs of performance or built-in staleness. You're more than welcome to implement whatever manual operations with the query cache that you want using
queryCache.setQueryData
or similar utilities, but for now, no built-in mechanisms or affordances for normalized caching will be built in or native to React Query.