Is it good to update caches inside reducers? I mean, can a cache be updated in every part of my front-end (components, screens, hooks, helpers...) or should this behavior be delegated to a specific part of the system?
In my app, every user have a field "totalFollowers".
I am storing the data of every user in the state of a React Context. To perform complex and pure updates based on previous state, I am using a reducer.
Also, I have implemented an in-memory LRU cache, just to avoid unnecessary DB requests and improve the user experience/performance. In this cache, I am storing some data which is also included in the React Context I have talked about... so, in both sides, the data has to be equal.
As I am updating the totalFollowers field of a specific user when the current user follows him, in my reducer I have the following:
export default (otherUsers, action) => {
switch (action.type) {
case "follow-user": {
const { userId, isFollowing } = action;
const prevUserData = otherUsers.get(userId);
return new Map([
...otherUsers,
[
userId,
{
...prevUserData,
totalFollowers: prevUserData.totalFollowers + (isFollowing ? 1 : -1)
]
]
);
}
...
}
}
As I said before... my cache have some data which is also included in the context (which uses the reducer I described), and "totalFollowers" is one of that data.
If my reducer gives me the possibility to access the state in a native/atomic way, I think that updating my cache inside it is a good idea. But, I have read that the only purpose of a reducer is to update state... I am afraid of that, as my idea of updating the cache inside the reducer could be an anti-pattern (avoid side effects inside pure functions)
What do you think? Is it "correct" to perform cache updates inside reducers?
import { usersCache } from "../../services/firebase/api/users"
export default (otherUsers, action) => {
switch (action.type) {
case "follow-user": {
const { userId, isFollowing } = action;
const prevUserData = otherUsers.get(userId);
const newTotalFollowers = prevUserData.totalFollowers + (isFollowing ? 1 : -1);
usersCache.updateUser(userId, { totalFollowers: newTotalFollowers }); // merge update
return new Map([
...otherUsers,
[
userId,
{
...prevUserData,
totalFollowers: newTotalFollowers
]
]
);
}
...
}
}
Cache and reducer are two different things.
only when you want to build a mini state machine, you would choose to use reducer. But even doing that has its own problem, normally every update to any property of the state should trigger an update. Therefore reducer or not is mostly determined on the decision if the logic you are building are really condensed (or twisted) within a group of related properties.
I have a guts feeling you know cache more than I do. so I'll skip this part. You use cache to make sure things can be returned quickly if the same condition met, also you want to make sure you have at least more than two conditions. Otherwise it'll be a waste.
You also mentioned context, but context is also different. It makes sure you can share a value. But the tricky part is that whenever a context value changes due to state change, it'll render all children underneath it.
Normally you would think cache is very good supplement to reducer (or context). However the gain here is the cache, for instance, it saves time to skip calculation.
But don't bet on the render. Even when you fetch from the memory, the value wired to the reducer or context is still changed! Which means you might still get 100% re-render with the cache.
What I'm trying to say is, cache is a concept that has nothing to do with React. If you apply it, you only get its own benefit, but don't expect other benefits.