- Typescript supported
Suppose we have a web application in which the user can search for pets based on some filter. We want to:
- fetch the pets that match this filter from the server whenever the filter changes,
- display a loader while fetching
In applications we were used to building this looks like this:
(start)->(Change filter)
(Change filter)->(Dispatch update filter action)
(Change filter)->(Fetch pets)
(Fetch pets)->(Dispatch start loading action)
(Fetch pets)->[...]
[...]->(Dispatch set pets action)
[...]->(Dispatch done loading action)
This libary changes this pattern by asking: "How would we solve this if we had all pets already available in the Redux store?" Easy! We would use selectors:
const petsThatMatchFilterSelector = createSelector(
filterSelector,
petsSelector,
(filter, pets) => {
return pets.filter(pet => pet.type === filter.petType)
}
)
Of course, we don't have all pets available in the Redux store, but the solution doesn't need to be much different if we don't:
const asyncPetsThatMatchFilterSelector = createSelector(
filterSelector,
petsCacheSelector,
(filter, petsCache) => {
return petsCache
.getFor(filter)
.orElse(() => fetch(`/api/pets?type=${filter.petType}`))
}
)
Here, fetch
is a function that returns a Promise
.
Instead of filtering the pets locally, we let the server take care of that.
The body of the selector may be executed many times. To prevent making the same request multiple times we use a cache of pets. How the cache gets its values is something we will discuss shortly. For now, let's focus on what asyncPetsThatMatchFilterSelector
returns.
Instead of getting a list of pets right now, we have to wait for the server to respond with the pets. We can get:
- an object with property
type
equal to'AWAITING_RESULT'
, which indicates that the request was made but no response was received yet, - an object with property
type
equal to'RESULT_ARRIVED'
, which indicates that the response was received (this object contains the response under the propertyresult
), - an object with property
type
equal to'ERROR_OCURRED'
, which indicates that the request failed, - an object with property
type
equal to'ADVICE'
which indicates that no request was made yet.
It's important to note that the library doesn't make any requests on its own! It will only advise you to do so (hence the type 'ADVICE'
). It's up to you to follow the advice (and make the request), or not.
You follow a request by dispatching:
const asyncPets = asyncPetsThatMatchFilterSelector(appState)
if (asyncPets.type === 'ADVICE') {
dispatch(asyncPets.followAdvice)
}
Now is the time to discuss how the cache gets its values. The answer will probably not surprise you.
By following advices, actions are dispatched that update the pets cache so that next time you apply the asynchronous selector, you will (hopefully) get an arrived result.
So far, we have neglected to specify a couple of things:
-
We need to make the cache part of the store:
const reducer = combineReducers({ ... petsCacheItems: PetsCache.itemsReducer, ... })
-
We didn't define
petsCacheSelector
:const petsCacheSelector = PetsCache.selector
Both cases require a cache definition with the name PetsCache
, which is given below. Fortunately, creating a cache definition doesn't require a whole lot of configuration.
const PetsCache = Cache({
cacheId: 'pets',
inputsAreEqual: (filter1, filter2) => filter1.petType === filter2.petType,
cacheItemsSelector: appState => appState.petsCacheItems
})
We basically need three things:
- a unique
cacheId
to identify the pets cache among all the other possible caches in the store, - a way to check if two filters are the same (to prevent making the same request multiple times),
- the location of the cache in the store.
If we want, we can further specify:
- The maximum number of responses cached,
- The time that a response remains valid,
- The key under which a response is stored in the cache (to make searching the cache faster).
- Tests for lazy list
- Split
AsyncResult
intoRequestState
and full-featuredAsyncResult
- Add error state.
- Tests for
map
andflatMap
- Tests for the reducer.
- Rename
resourceId
tocacheId
everywhere. - Create an add or update function for CacheItems.