Stores
Stores are used to request and cache data. Each store contains a collection of items of the same type, where each item is referred to by a particular set of args
.
Some examples:
A
userStore
may contain severalUser
objects, each referred to by theirid
stringA
postOfficeStore
may contain severalPostOffice
class instances, each referred to by theirpostcode
stringA
petListStore
may contain severalPet[]
arrays, each referred to by their owner'sid
stringA
userListStore
may contain severalUser[]
arrays, each referred to by a search parameters object{keyword: string, sort: string}
import {Store} from 'mobx-fog-of-war';
const userStore = new Store<string,User,Error>({
name: 'User Store',
staleTime: 30 // after 30 seconds, the item is eligible to be requested again
request: // ...fetchUser
});
const postOfficeStore = new Store<string,PostOffice,Error>({
name: 'Post Office Store',
request: // ...fetchPostOffice
});
const petListStore = new Store<string,Pet[],Error>({
name: 'Pet List Store',
request: // ...fetchPetList
});
const userListStore = new Store<SearchParams,User[],Error>({
name: 'User List Store',
request: // ...fetchUserList
});
Getting data
You use a store by calling methods to access its contents, and the store will handle whether the data needs to be requested from somewhere like a server, or simply returned from the cache. Items cached in the store can be configured to become stale after a period of time, and if an stale item is retrieved from the store, then it should be requested from the server again.
Methods to use include get(), read(), request(), and the React hooks useGet(), useBatchGet() and useGetMany().
Requests
When a store needs to return data that it doesn't have yet, it makes a request. You can pass request functions into each store when you instantiate them. It's common for these requests to make XHR requests to a server.
asyncRequest
The asyncRequest
helper lets you return a promise with data.
import {Store, asyncRequest} from 'mobx-fog-of-war';
type ID = string;
const userStore = new Store<ID,User,Error>({
name: 'User Store',
request: asyncRequest(async (id: ID): User => {
const response = await fetch(`http://example.com/user/${id}`)
return new User(await response.json());
})
});
When a request is fired, the corresponding StoreItem
has the following attributes set:
loading: true
If the promise is resolved, the corresponding StoreItem
has the following attributes set:
loading: false
data: value in the promise
hasData: true
error: undefined
hasError: false
If the promise is rejected, the corresponding StoreItem
has the following attributes set:
loading: false
error: value in the promise
hasError: true
Please keep in mind that asyncRequest has limitations. It fires once for each piece of data that you need. This is fine if you don't need to make many requests, but mobx-fog-of-war
is designed to let your components ask for data whenever they need it. If you happen to render a large number of components then you can easily end up with a large number of asyncRequest
calls, all in a short burst. To combat this we need buffering and batching.
rxRequest
To be used with RxJS, the rxRequest
function lets you turn the series of requests fired by a store into an Rx observable, after which buffering and batching is easy to do with rxBatch
. With this setup you can the most out of mobx-fog-of-war
's design - when items can be requested in batches, then your data will fall into place incrementally as each batch of items is returned.
Read more about rxRequest and rxBatch.
If you would like buffering and batching but don't want to use RxJS, drop a comment in this issue.
Setting data
Usually most setting will be done by your request functions, but you can also set items directly if you need to.
Methods to use include receive(), setLoading(), setData() and setError().
Sending mutations to the server (saving)
So far we've only spoken about loading data from server to client. While that may be a more common action to want to perform on a client, sending data to the server is obviously important too.
Here you can choose if you want to use a mobx-fog-of-war
store, or if you just call your request function directly. The data returned from saving is often of much less long-term importance compared to loading so you may find it unnecessary to want to store that for later. However there are a few advantages to using a mobx-fog-of-war
store for sending mutations:
- Using a store means you can subscribe to the changes in the request state of the returned
StoreItem
, which is especially useful if you're want your UI to react to those changes using Mobx, React etc. - Your app's code has a consistent pattern for interacting with requests, regardless of whether it is loading or saving.
When using a store, there is nothing special about sending mutations or "saving" data. On your new store, args
would be the data you want to send, and the data
in the store can be whatever you want to keep from the response of the mutation. The data
does not have to be the same type as args
, and it could simply be null
if the mutation doesn't return anything of use to the client.
For example you may have a store for creating users.
const userCreateStore = new Store<User,null,Error>({
name: 'User Create Store',
request: asyncRequest(async (user: User): null => {
await fetch(`http://example.com/user/create`, {
method: 'POST',
body: JSON.stringify(user));
});
return null;
}
});
You could then create a user by calling request
on the store.
let user = new User('new guy');
const userCreateFromStore = await userCreateStore.request(user).promise();
if(userCreateFromStore.hasError) {
console.error(`Error:`, userCreateFromStore.error.message);
} else {
console.log(`Success`);
}
Also see how to send mutations with React.
API
new Store()
const store = new Store<A,D,E,AA>({
name?: string;
request?: (store: Store<A,D,E,AA>) => void;
staleTime?: number;
log?: (...args: any[]) => void;
});
name
A name for the store. Mainly for developer reference, but also used by log(). Defaults to 'unnamed'
.
request
A request function, such as asyncRequest. Defaults to a no-op.
staleTime
Determines the default duration for how long an item should be held in cache before it's eligible to be requested again.
staleTime: 30
- after 30 seconds, the item is eligible to be requested againstaleTime: 0
- the item should always be requested freshstaleTime: -1
- the item should never be requested again
log
A logging function for debugging.
const userStore = new Store({
name: 'User Store',
request: // ...
log: console.log // example logger
});
Store Types
const store = new Store<A,D,E,AA>();
A: Args
Args can be of any JSON.stringify
-able data type. It cannot be undefined
.
Args are JSON.stringify
-ed and used as keys to refer to each item in the store.
D: Data
Data can be any data type other than undefined
.
When choosing your data type, choose a type that you'll want to use throughout your app. Sometimes the data returned from the server needs some processing or needs to be turned into class instances, such as the User
class in some of the examples on this page; in these cases your request
function should prepare your data so that User
instances are collected in the store, as seen in the asyncRequest example.
E: Error
Error can be of any type other than undefined
.
This is the data shape you want your errors to be.
AA: Alias
Aliases let you easily refer to specific items by an alternative identifier other than args
. A single alias may refer to different items over time, but only ever one at a time. Defaults to string
.
store.get()
// signature
store.get(args: A?, options?: {}) => StoreItem
// usage
const userFromStore = userStore.get('a');
const user = userFromStore.data;
The get()
method returns a StoreItem
. If there is no item corresponding to args
or if the item is stale, the get()
method will request the data.
All attributes on a StoreItem
are mobx observables, so mobx can trigger downstream updates once new data arrives.
const userFromStore = await userStore.get('a');
autorun(() => {
console.log('User A name changed:', userFromStore.name);
});
You can also turn the StoreItem
into a Promise
and await the result. Note that the promise never rejects - even if the request encounters an error, the promise always resolves.
const userFromStore = await userStore.get('a').promise();
If you just need the data, use .await()
. Unlike .promise()
this will throw an error if the request encounters an error.
const user = await userStore.get('a').await();
You can also turn the StoreItem
into a tuple to access and name StoreItem
's data with a one-liner.
const [user, userFromStore] = await userStore.get('a').tuple();
- A
StoreItem
is always returned, even if no request has been made and no data exists yet. If no item matches the alias, then a blankStoreItem
is returned. - If
undefined
is passed as the first argument, no request will take place. - The optional
options
object can containstaleTime: number
to use a different stale time than the Store's default time. For example{staleTime: 0}
can be used to always force a new request. - The optional
options
object can containalias: AA
. This creates an alias for the currentargs
that can be looked up via readAlias()
If you are using React, you should consider using the store.useGet() hook.
store.read()
// signature
store.read(args: A?) => StoreItem
// usage
const userFromStore = userStore.read('a');
The read()
method returns a StoreItem
. It is similar to get(), except it will only return data from cache and never fire a request.
store.readAlias()
// signature
store.readAlias(alias: AA) => StoreItem
// usage
const userFromStore = userStore.readAlias('alias');
Aliases let you easily refer to specific items by an alternative identifier other than args
. A single alias may refer to different items over time, but only ever one at a time. The readAlias()
method returns the StoreItem
associated with the alias provided. If no item matches the alias, then a blank StoreItem
is returned. It is similar to read().
store.request()
// signature
store.request(args: A?, options?: {}) => StoreItem
// usage
const userFromStore = store.request('a');
The request()
method always immediately requests the data.
- It immediately sets the corresponding
StoreItem
toloading: true
. - The optional
options
object can containalias: AA
. This creates an alias for the currentargs
that can be looked up via readAlias()
store.useGet()
// signature
store.useGet(args: A?, options?: {}) => StoreItem
// usage
const MyComponent = (props) => {
const userFromStore = userStore.useGet(props.id);
};
The useGet()
method is a React hook very similar to get(), except it uses a useEffect
hook internally to make sure that side-effects are not fired during React's render phase.
- If
undefined
is passed as the first argument, no request will take place. - The optional
options
object can containstaleTime: number
to use a different stale time than the Store's default time. For example{staleTime: 0}
can be used to always force a new request. - The optional
options
object can containalias: AA
. This creates an alias for the currentargs
that can be looked up via readAlias() - The optional
options
object can containdependencies: any[]
which are passed to the internaluseEffect
hook. If any dependencies change,get()
will be called again with the currentargs
.
store.useBatchGet()
// signature
store.useBatchGet(argsArray: A[]?, options?: {}) => StoreItem[]
// usage
const MyComponent = (props) => {
const usersFromStore = userStore.useBatchGet(props.idArray);
};
The useBatchGet()
method is a React hook very similar to useGet(), except it allows you to get an arbitrary and variable amount of items.
- If
undefined
is passed as the first argument, no request will take place. - The optional
options
object can containstaleTime: number
to use a different stale time than the Store's default time. For example{staleTime: 0}
can be used to always force a new request. - The optional
options
object can containalias: AA
. This creates an alias for the currentargs
that can be looked up via readAlias() - The optional
options
object can containdependencies: any[]
which are passed to the internaluseEffect
hook. If any dependencies change,get()
will be called again with the currentargs
.
store.useGetMany()
// signature
store.useGetMany(argsArray: A[]?, options?: {}) => StoreItem
// usage
const MyComponent = (props) => {
const usersFromStore = userStore.useGetMany(props.idArray);
};
The useGetMany()
method is a React hook very similar to useBatchGet(), allowing you to get an arbitrary and variable amount of items, but returning the results in a single merged StoreItem.
- If
undefined
is passed as the first argument, no request will take place. - If an empty array is passed as the first argument, no request will take place but the resulting StoreItem will contain
.hasData = true
, as though a request succeeded. - The optional
options
object can containpriorities: string
which are used to determine how loading states are merged; see Merging StoreItems for details. - The optional
options
object can containstaleTime: number
to use a different stale time than the Store's default time. For example{staleTime: 0}
can be used to always force a new request. - The optional
options
object can containalias: AA
. This creates an alias for the currentargs
that can be looked up via readAlias() - The optional
options
object can containdependencies: any[]
which are passed to the internaluseEffect
hook. If any dependencies change,get()
will be called again with the currentargs
.
store.setLoading()
// signature
store.setLoading(args: A, loading: boolean) => void
// usage
store.setLoading('a', true);
Sets the loading
status of the StoreItem
corresponding to args
.
You'll very rarely need to call this directly if you are using premade request functions like asyncRequest().
store.setData()
// signature
store.setData(args: A, data?: D) => void
// usage
store.setData('a', new User());
Sets the data
of the StoreItem
corresponding to args
.
When called, the corresponding StoreItem
has the following attributes set:
loading: false
data: data
hasData: true
error: undefined
hasError: false
You'll very rarely need to call this directly if you are using premade request functions like asyncRequest().
- If
data
isundefined
, theStoreItem
is removed.
store.setError()
// signature
store.setError(args: A, error: E) => void
// usage
store.setError('a', new Error());
Sets the error
of the StoreItem
corresponding to args
.
If the promise is rejected, the corresponding StoreItem
has the following attributes set:
loading: false
error: error
hasError: true
You'll very rarely need to call this directly if you are using premade request functions like asyncRequest().
store.receive()
// signature
store.receive(receive: {args: A, data: D?}|{args: A, error: E}) => void
// usage
store.receive({args: 'a', data: new User()});
store.receive({args: 'a', error: new Error()});
A short way of calling setData or setError.
You'll very rarely need to call this directly if you are using premade request functions like asyncRequest().
store.remove()
// signature
store.remove(args: A) => void
// usage
store.remove('a');
Removes the StoreItem
corresponding to args
if it exists.
store.removeByAlias()
// signature
store.removeByAlias(alias: AA) => void
// usage
store.removeByAlias('alias');
Removes the StoreItem
corresponding to alias
if it exists.