Figbird
Effortless realtime data management for React + Feathers applications. A library used in and extracted from Humaans.
Idiomatic React Hooks
Fetch some data with const { data } = useFind('notes')
and your components will rerender in realtime as the data changes upstream. Modify the data using the const { patch } = useMutation('notes')
and the upates will be instantly propagated to all components referencing the same objects.
useGet
useFind
useMutation
Live Queries
Works with Feathers realtime events and with local data mutations. Once a record is created/modified/removed all queries referencing this record get updated. For example, if your data is fetched using useFind('notes', { query: { tag: 'ideas' } })
and you then patch some note with patch({ tag: 'ideas' })
- the query will updated immediately and rerender all components referencing that query. Adjust behaviour per query:
merge
- merge realtime events into cached queries as they come (default)refetch
- refetch data for this query from the server when a realtime event is receiveddisabled
- ignore realtime events for this query
Fetch policies
Fetch policies allow you to fine tune Figbird to your requirements. With the default swr
(stale-while-revalidate) Figbir’s uses cached data when possible for maximum responsiveness, but refetches in the background on mount to make sure data is up to date. The other policies are cache-first
which will use cache and not refresh from the server (compatible with realtime)
swr
- show cached data if possible and refetch in the background (default)cache-first
- show cached data if possible and avoid fetching if data is therenetwork-only
- always refetch data on mount
Cache eviction
The usage of useGet
and useFind
hooks gets reference counted so that Figbird knows exactly if any of the queries are still being referenced by the UI. By default, Figbird will keep all the data and cache without ever evicting, but if your application demands it, you can strategically implement cache eviction hooks (e.g. on page navigation) to clear out all unused cache items or specific items based on service name or data attributes. (Note: yet to be implemnted)
manual
- cache all queries in memory forever, evict manually (default)unmount
- remove cached query data on unmount (if no component is referencing that particular cached data anymore)delayed
- remove unused cached data after some time
Install
$ npm install figbird
Example
import React, { useState } from 'react'
import io from 'socket.io-client'
import feathers from '@feathersjs/client'
import { Provider, useFind } from 'figbird'
const socket = io('http://localhost:3030')
const client = feathers()
client.configure(feathers.socketio(socket))
client.configure(
feathers.authentication({
storage: window.localStorage,
}),
)
function App() {
return (
<Provider feathers={client}>
<Notes />
</Provider>
)
}
function Notes({ tag }) {
const { status, data, total } = useFind('notes', { query: { tag } })
if (status === 'loading') {
return 'Loading...'
} else if (status === 'error') {
return notes.error.message
}
return (
<div>
Showing {data.length} notes of {total}
</div>
)
}
API Reference
useGet
const { data, status, isFetching, error, refetch } = useGet(serviceName, id, params)
Arguments
serviceName
- the name of Feathers serviceid
- the id of the resourceparams
- any params you’d pass to a Feathers service call, plus any Figbird params
Figbird params
skip
- setting to true will not fetch the datarealtime
- one ofmerge
(default),refetch
ordisabled
fetchPolicy
- one ofswr
(default),cache-first
ornetwork-only
Returns
data
- starts of asnull
and is set to the fetch result, usually an objectstatus
- one ofloading
,success
orerror
isFetching
- true if fetching data for the first time or in the backgrounderror
- error object if request failedrefetch
- function to refetch data
useFind
const { data, status, isFetching, error, refetch } = useFind(serviceName, params)
Arguments
serviceName
- the name of Feathers serviceparams
- any params you’d pass to Feathers, plus any Figbird params
Figbird params
skip
- setting true will not fetch the datarealtime
- one ofmerge
(default),refetch
ordisabled
fetchPolicy
- one ofswr
(default),cache-first
ornetwork-only
allPages
- fetch all pagesparallel
- when used in combination withallPages
will fetch all pages in parallelmatcher
- custom matcher function of signature(defaultMatcher) => (query) => (item): bool
, used when merging realtime events into local query cache
Returns
data
- starts of asnull
and is set to the fetch result, usually an arraystatus
- one ofloading
,success
orerror
isFetching
- true if fetching data for the first time or in the backgrounderror
- error object if request failedrefetch
- function to refetch data
The return object also has the rest of the Feathers response mixed, typically:
total
- total number of recordslimit
- max number of items per pageskip
- number of skipped items (offset)
useMutation
const { data, status, error, create, update, patch, remove } = useMutation(serviceName, params)
Arguments
serviceName
- the name of Feathers service
Returns
create(data, params)
- createupdate(id, data, params)
- updatepatch(id, data, params)
- patchremove(id, params)
- removestatus
- one ofidle
,loading
,success
orerror
data
- starts of asnull
and is set to the latest mutation resulterror
- error object of the last failed mutation
useFeathers
const { feathers } = useFeathers()
Get the feathers instance passed to Provider
.
Provider
<Provider feathers={feathers}>{children}</Provider>
feathers
- feathers instanceidField
- string or function, defaults toitem => item.id || item._id
updatedAtField
- string or function, defaults toitem => item.updatedAt || item.updated_at
, used to avoid overwriting newer data in cache with older data whenget
or realtimepatched
requests are racingstore
- custom store instance - allows inspecting cache contents
createStore
const store = createStore()
Create a custom store - helpful for inspecting contents of the cache.
cache
import { createStore, cache } from 'figbird'
const store = createStore()
store.get(cache)
store.set(cache, val => { ...val, a: 1 })
Get the cache atom for manipulating via the store API.
Realtime
Figbird is compatible with the Feathers realtime model out of the box. The moment you mount a component with a useFind
or useGet
hook, Figbird will start listening to realtime events for the services in use. It will only at most subscribe once per service. All realtime events will get processed in the following manner:
created
- check if the created object matches any of the cachedfind
queries, if so, push it at the end of the array, discard otherwise, note: the created object is only pushed ifdata.length === total
, that is if the query has full data set, if the query is paginated and has only a slice of data, the created object will not be pushed, consider usingrealtime: 'refetch'
mode for such casesupdated
andpatched
- check if this object is in cache, if so, updateremoved
- remove this object from cache and anyfind
queries referencing it
This behaviour can be configured on a per hook basis by passing a realtime
param with one of the following values.
merge
This is the default mode that merges all realtime events into cached queries as described above.
refetch
Sometimes, the client does not have the full information to make a decision about how to merge an individual realtime event into the local query result set. For example, if you have a server side query that picks the latest record of each kind and some record is removed - the client might not want to remove it from the query set, but instead show the correct record in it’s place. In these cases, setting realtime
param to refetch
might be useful.
In refetch
mode, the useGet
and useFind
results are not shared with other components that are in realtime mode, instead the objects are cached locally to those queries and those components. And once a realtime event is received, instead of merging that event as described above, the find
or get
is refetched in full. That is, the server told us that something in this service changed, and we use that as a signal to update our local result set.
disabled
Setting realtime
to disabled
will store the useGet
and useFind
results locally and will not share them with components that are in realtime
or refetch
mode. This way, the results will stay as they are even as realtime events are received. You can still manually trigger a refetch using the refetch
function which is returned by the useGet
and useFind
hooks.
Fetch Policies
Fetch policy controls when Figbird uses data from cache or network and can be configured by passing the fetchPolicy
param to useGet
and useFind
hooks.
swr
This is the default and stands for stale-while-revalidate
. With this policy, Figbird will show cached data if possible upon mounting the component and will refetch it in the background.
cache-first
With this policy, Figbird will show cached data if possible upon mounting the component and will only fetch data from the server if data was not found in cache.
network-only
With this policy, Figbird will never show cached data on mount and will always fetch on component mount.
Architecture
The idea behind Figbird is rather simple. Fetch all the data requested by the hooks, index all items by id in the cache, listen to realtime events and update all items/queries as neccessary.
Let’s take a look at an example. Say, component X uses useFind('comments')
hook, and component Y uses useGet('comments/5')
hook. Figbird fetches this data and caches in a structure similar to:
{
entities: {
comments: {
2: { id: 2, content: 'a' },
4: { id: 4, content: 'b' },
5: { id: 5, content: 'c' },
7: { id: 7, content: 'd' },
}
}
queries: {
comments: {
'f223315': {
method: 'find',
data: [2, 4, 5, 7],
params: {},
meta: { total: 4, limit: 100, skip: 0 }
},
'g742218': {
method: 'get',
id: 5,
data: [5]
}
}
}
}
Now if some other user/client/server makes a modification to the resource already referenced, the cache will be updated. For example, if we receive the following realtime event:
["comments patched",{"id":4,"content":"b2"}]
.
The entities get updated:
{
entities: {
comments: {
2: { id: 2, content: 'a' },
4: { id: 4, content: 'b2' }, // updated
5: { id: 5, content: 'c' },
7: { id: 7, content: 'd' },
}
}
queries: {
// ...same as before
}
}
And now the component that was using useFind
gets rerendered since it’s data has been updated, but component using useGet
does not, since it is not referencing the changed comment.
Hopefully this small example gives you more clarity in how to fit Figbird into your application.
Advanced usage
Inspect cache contents
If you want to have a looke at the cache contents for debugging reasons, you can do so as shown above. You can also update the store using this direct access. See kinfolk
docs for full API docs.
import React, { useState } from 'react'
import createFeathersClient from '@feathersjs/feathers'
import { Provider, createStore, cache } from 'figbird'
const store = (window.store = createStore())
const feathers = createFeathersClient()
export function App({ children }) {
return (
<Provider feathers={feathers} store={store}>
{children}
</Provider>
)
}
// inspect contents of the entire store, the figbird
// cache atom will be namespaced under the "figbird" key
console.log(window.store.debug())
// inspect the contents of the figbird cache directly
console.log(window.store.get(cache)))
// update contents
window.store.set(cache, val => { ...val, a: 1 })
Use with existing kinfolk store
Figbird is using kinfolk for it’s cache. This allows for a succint implementation and efficient bindings from cached data to components. It is possible to pass in a shared kinfolk
store to figbird
if you’re already using kinfolk
in your app you’d like to create a selector that depends on figbird cache and your application state. Note - that is advanced usage and is entirely optional.
Use without Feathers.js
In principle, you could use Figbird with any REST API as long as several conventions are followed or are mapped to. Feathers is a collection of patterns as much as it is a library. In fact, Figbird does not have any code dependencies on Feathers. It’s only the Feathers patterns and conventions that the library is designed for. In short, those conventions are:
- Structure your API around resources
- Where the resources support operations:
find
,get
,create
,update
,patch
,remove
- The server should emit a websocket event after each operation (see Service Events)
For example, if you have a comments
resource in your application, you would have some or all of the following endpoints:
GET /comments
GET /comments/:id
POST /comments
PUT /comments/:id
PATCH /comments/:id
DELETE /comments/id
The result of the find
operation or GET /comments
would be an object of shape { data, total, limit, skip }
(Note: the pagination envolope will be customizable in Figbird in the future, but it’s current fixed to this format).