Normalization. We either suffer from it or write our own solution with many checks for the existence of an entity in a common repository. Let's try to figure it out and solve this problem!
Description of the problem
Let's imagine the following sequence:
The client application requests a list of users with a request to / users and it gets users with id from 1 to 10
User with id 3 changes his name
The client application requests the user with id 3 using a request to / user / 3
Question: What username with id 3 will be in the application?
Answer: Depends on the component that requested the data. The component that uses the data from the request to / users will display the old name. The component that uses the data from the request for / user / 3 will display the new name.
Conclusion : In this case, there are several entities of the same meaning with different data sets in the system.
Question: Why is it bad?
Answer: In the best case, the user will see different names of one person in different sections of the site, in the worst case, he will transfer money to the old bank details.
Solution options
Currently, there are the following solutions to this problem:
graphql (apollo relay)
. . , ? , ?
mobx:
class Store {
users = new Map();
async getUsers() {
const users = await fetch(`/users`);
users.forEach((user) => this.users.set(user.id, user));
}
async getUser(id) {
const user = await fetch(`/user/${id}`);
this.users.set(user.id, user);
}
}
mobx , redux .
graphql (apollo relay)
Apollo relay , . graphql apollo, , , .
graphql ? apollo! apollo :
...normalizes query response objects before it saves them to its internal data store.
normalize?
Normalization involves the following steps:
1. The cache generates a unique ID for every identifiable object included in the response.
2. The cache stores the objects by ID in a flat lookup table.
apollo , . Apollo . :
const store = new Map();
const user = {
id: '0',
type: 'user',
name: 'alex',
age: 24,
};
const id = `${user.type}:${user.id}`;
store.set(id, user);
id - . , id, .
Apollo , __typename, graphql?
, . :
id
:
const store = new Map();
const user = {
id: '0',
};
const comment = {
id: '1',
};
store.set(user.id, user);
store.set(comment.id, comment);
// ...
store.get('0'); // user
store.get('1'); // comment
, id . , id / ( - ). , id , .
:
const store = new Map();
const user = {
id: '0',
type: 'user', // <-- new field
};
const comment = {
id: '1',
type: 'comment', // <-- new field
};
function getStoreId(entity) {
return `${entity.type}:${entity.id}`;
}
store.set(getStoreId(user), user);
store.set(getStoreId(comment), comment);
// ...
store.get('user:0'); // user
store.get('comment:1'); // comment
- , . . .
?
. - . .
, :
app.get('/users', (req, res) => {
const users = db.get('users');
const typedUsers = users.map((user) => ({
...user,
type: 'user',
}));
res.json(typedUsers);
});
, :
function getUsers() {
const users = fetch('/users');
const typedUsers = users.map((user) => ({
...user,
type: 'user',
}));
return typedUsers;
}
. Api, , . , .
.
iresine
iresine .
iresine :
iresine react-query:
@iresine/core
const iresine = new Iresine();
const oldRequest = {
users: [oldUser],
comments: {
0: oldComment,
},
};
// new request data have new structure, but it is OK to iresine
const newRequest = {
users: {
0: newUser,
},
comments: [newComment],
};
iresine.parse(oldRequest);
iresine.parse(newRequest);
iresine.get('user:0' /*identifier for old and new user*/) === newRequest.users['0']; // true
iresine.get('comment:0' /*identifier for old and new comment*/) === newRequest.comments['0']; // true
, , @iresine/core :
entityType + ':' + entityId;
@iresine/core type
, id id
. , . apollo:
const iresine = new Iresine({
getId: (entity) => {
if (!entity) {
return null;
}
if (!entity.id) {
return null;
}
if (!entity.__typename) {
return null;
}
return `${entity.__typename}:${entity.id}`;
},
});
id:
const iresine = new Iresine({
getId: (entity) => {
if (!entity) {
return null;
}
if (!entity.id) {
return null;
}
return entity.id;
},
});
@iresine/core , ? :
const user = {
id: '0',
type: 'user',
jobs: [
{
name: 'milkman',
salary: '1$',
},
{
name: 'woodcutter',
salary: '2$',
},
],
};
user , jobs? type id! @iresine/core : , .
@iresine/core , . ! .
@iresine/react-query
react-query , . , iresine.
@iresine/react-query react-query. @iresine/core react-query. react-query , iresine.
import Iresine from '@iresine/core';
import IresineReactQuery from '@iresone/react-query';
import {QueryClient} from 'react-query';
const iresineStore = new IresineStore();
const queryClient = new QueryClient();
new IresineReactQueryWrapper(iresineStore, queryClient);
// now any updates in react-query store will be consumbed by @iresine/core
( ):
. . . , , iresine