Caching
You will learn
- What is a browser cache and how to use it
- When it is reasonable to use custom caching
- Where to store cached data
- How to invalidate cache
- How skip requests if data is already cached
Browser cache
Browsers have a built-in cache for HTTP requests — HTTP cache. It is a great tool to improve performance of your application that allows you to avoid unnecessary requests to the server and reduce the load on it.
Example
Real-world showcase with SolidJS around JSON API in the Farfetched repository is using HTTP cache. Try to walk through the application and observe — after the initial load it is blazing fast because of proper HTTP cache headers in API.
Browser cache is build on top of HTTP headers, so this mechanism cannot be controlled directly by frontend applications.
- If you are using Backend-for-frontend architecture, you can set proper HTTP cache headers on your BFF.
- If your frontend application calls a separated backend, you can communicate with your backend team to about proper HTTP cache headers.
Further reading
Read more about HTTP caching on MDN.
Custom cache
Probably you should not use custom cache
Browsers are very powerful systems, they are developed by large teams with a great experience. They have a lot of optimizations and tricks to make your application work faster. If you are using custom cache, you are bypassing all of them. This can lead to unexpected behavior and performance issues.
Consider using browser cache first, talk to your backend team to make sure that they are using proper cache headers. If you still need custom cache, make sure that you are aware of the consequences and that it is worth it.
If browser cache is not enough for your use case, you can use custom cache. Farfetched provides cache
operator that allows you to do it with a simple declarative API.
TL;DR
Call cache
operator with a Query that you want to cache.
import { cache } from '@farfetched/core';
cache(characterQuery);
Below, let's take a bit more detailed review of this feature.
Prerequisites
Every Query has to have a unique key that is used to identify it. You can set it with name
field while creating a Query:
const characterQuery = createQuery({
name: 'character',
// ...
});
However, it could be annoying to control uniqueness of the names manually, so you can set up code transformation tool to do it for you.
Babel plugin
If your project already uses Babel, you do not have to install any additional packages, just modify your Babel config with the following plugin:
{
"plugins": ["effector/babel-plugin"]
}
INFO
Read more about effector/babel-plugin
configuration in the Effector's documentation.
SWC plugin
WARNING
Note that plugins for SWC are experimental and may not work as expected. We recommend to stick with Babel for now.
SWC is a blazing fast alternative to Babel. If you are using it, you can install effector-swc-plugin
to get the same DX as with Babel.
pnpm add --save-dev effector-swc-plugin @swc/core
yarn add --dev effector-swc-plugin @swc/core
npm install --dev effector-swc-plugin @swc/core
Now just modify your .swcrc
config to enable installed plugin:
{
"$schema": "https://json.schemastore.org/swcrc",
"jsc": {
"experimental": {
"plugins": ["effector-swc-plugin"]
}
}
}
INFO
Read more about effector-swc-plugin
configuration in the plugin documentation.
Vite
If you are using Vite, please read the recipe about it.
Cache adapters
cache
does not specify where to store cached data. It is up to you to choose a cache adapter. Farfetched provides a few adapters out of the box:
inMemoryCache
(default adapter) is the simplest adapter that stores cached data in memory, so it is not persistent, and you can cache any data without serialization. Cache wipes out when the page is reloaded and could not be shared between tabs.localStorageCache
is an adapter that stores cached data in thelocalStorage
of the browser. It is persistent, so you can store only serializable data with this adapter. Cache is shared between tabs and persists after page reloads.sessionStorageCache
is an adapter that stores cached data in thesessionStorage
of the browser. It is persistent, so you can store only serializable data with this adapter. Cache is not shared between tabs, but stores after page reloads.
TIP
Use localStorageCache
only with auto-deletion options, like maxAge
or maxEntries
. Otherwise, you can easily run out of space in the localStorage
.
Auto-delete cache entries
You can use maxAge
and maxEntries
options to automatically delete cache entries. This is useful to avoid running out of space in the localStorage
or sessionStorage
or out of memory errors in the inMemoryCache
. Any adapter can be configured with these options.
maxEntries
It is the maximum number of entries that can be stored in the cache. When the limit is reached, the oldest entry is deleted.
import { cache, localStorageCache } from '@farfetched/core';
cache(characterQuery, {
adapter: localStorageCache({ maxEntries: 100 }),
});
maxAge
It is the maximum age of the entry in milliseconds or human-readable format. When the limit is reached, the entry is deleted.
import { cache, localStorageCache } from '@farfetched/core';
cache(characterQuery, {
adapter: localStorageCache({ maxAge: '1h30min' }),
});
TIP
Due to browser internal limitations, the maxAge
option is not precise. It is not guaranteed that the entry will be deleted exactly after the specified time. It can be deleted earlier or later. However, your application will never see outdated entries because cache
operator always checks the age of the entry before returning it.
Combination
You can combine these options to achieve the desired behavior. For example, you can store data for 1 hour or 100 entries, whichever comes first.
import { cache, localStorageCache } from '@farfetched/core';
cache(characterQuery, {
adapter: localStorageCache({ maxAge: '1h30min', maxEntries: 100 }),
});
Purge all data from cache
Sometimes you need to purge all data from cache. For example, when you want to force user to reload all data after logout. Farfetched provides purge
option of cache
operator for this purpose.
import { createEvent } from 'effector';
import { cache } from '@farfetched/core';
const logout = createEvent();
cache(characterQuery, { purge: logout });
document.getElementById('logout').addEventListener('click', () => {
logout();
});
TIP
createEvent
is a part of Effector API that creates Event that could be called and could be watched. In this example, we are calling logout
event when user clicks on the logout button.
After calling purge
Event, all data from cache will be deleted immediately.
Do not fetch fresh data
By default, Farfetched will fetch fresh data from the server immediately after Query is started even if data is already found in cache. So, default behavior is to fetch fresh data every time and provide stale data as soon as possible.
You can alter this behavior with staleAfter
option of cache
operator. It accepts a number of milliseconds or human-readable format and considers data as fresh if it is not older than the specified time.
import { cache } from '@farfetched/core';
cache(characterQuery, { staleAfter: '10min' });
👆 if data is not older than 10 minutes, it will be considered as fresh and will not be fetched from the server.
Deep-dive
If you want to learn more about internal implementation of cache
operator, consider reading the deep-dive article about it.