@wq/store is a wq.app module providing a persistent storage API for retrieving and querying JSON data from a web service via AJAX. @wq/store is used internally by @wq/app to store model data (via @wq/model) and application configuration. As of wq.app 1.2, @wq/store relies extensively on Redux to manage state, with Redux Persist and localForage to handle the work of storing data offline in IndexedDB.
@wq/store is almost always used in conjunction with @wq/outbox to sync local changes (i.e., form submissions) back to the server.
python3 -m venv venv # create virtual env (if needed)
. venv/bin/activate # activate virtual env
python3 -m pip install wq # install wq framework (wq.app, wq.db, etc.)
# pip install wq.app # install wq.app only
npm install @wq/app # install all @wq/app deps including @wq/store
# npm install @wq/store # install only @wq/store and deps
@wq/store
is typically imported as ds
(i.e. "datastore"), though any local variable name can be used. ds
is a singleton instance of a Store
class, and can also be used to retrieve and/or create other stores.
Note: When working with @wq/app, the store is initialized automatically and exported as
app.store
.
// myapp.js
define(['wq/store', ...], function(ds, ...) {
ds.init(config); // Main store
var secondStore = ds.getStore('store2');
secondStore.init(config2);
});
// myapp.js
import ds, { getStore, Store } from '@wq/store';
ds.init(config); // Main store
// secondStore = new Store('store2') // This will fail if store2 already exists
const secondStore = getStore('store2'); // This will return store2 if it exists, or create it
secondStore.init(config2);
The query
argument, used by ds.get()
, ds.prefetch()
, and other functions, specifies a query to retrieve from the datastore (and potentially from the web service). A query can be:
{"url": string}
.For example, a query
value of "settings"
is treated as corresponding to a local variable, while a query
of {"param1": "value"}
would be converted to the query "?param1=value" and appended to the datastore's base service
URL to make a request. The following attributes have special meaning for web query objects:
name | purpose |
---|---|
url |
If the web service is a full REST API (like wq.db), the url argument can be used to define URL paths relative to the base service URL. |
format |
If set, the format value will be appended to the end of the base URL rather than included as a parameter (unless formatKeyword is set) |
// query:
ds.get({'url': 'items', 'format': 'json'});
// resulting URL (assuming web service at root URL):
fetch("/items.json");
A web query is usually stored locally after being loaded for the first time.
Note: When working with a collection of items retreived from a remote URL, it is generally best to use the @wq/model API rather than
ds.get()
.
ds.init(config)
ds.init()
configures the datastore with the necessary information to communicate with a web service. ds.init()
takes a configuration object specifying the web service URL and other options, as described below. Note that @wq/app automatically calls ds.init()
during startup, passing the "store" property of the app config.
name | purpose |
---|---|
service |
URL to the web service to access when retrieving data that isn't already stored locally. This should be specified without a trailing slash. |
defaults |
A set of default query arguments to apply to every web query. |
debug |
Sets the debug level for console.log() information. Level 0 (or false) disables debugging. Level 1 logs network requests, 2 logs all data lookups, and 3 logs actual data values. |
formatKeyword |
If true , disables special handling of the "format" query argument (see above). |
ajax(url, data, method, headers) |
Override how requests are sent to the server and how the response is interpreted. (See plugin hook below) |
storageFail(value, error) |
Defines a callback to use when localForage.setItem() fails for any reason (e.g. when offline storage is full or disabled). The callback will be provided with the value being saved as well as the error object. |
fetchFail(query, error) |
Defines a callback to use when a network request fails or the result is unparseable. The callback will be passed the original query and a description of the error. |
As of wq.app 1.2, the
jsonp
andparseData
configuration options no longer exist. To customize how data is retrieved and parsed, use theajax()
plugin hook instead.
@wq/store provides support for the following @wq/app plugin hooks.
ajax(url, data, method, headers)
New in wq.app 1.1.1.
The ajax()
hook allows customization of how requests are sent and processed. The default implementation is a fetch()
wrapper designed to work with wq.db and should be sufficient for most cases.
When using with @wq/app, it is recommended to use the plugin syntax. It is also possible to set config.store.ajax
(as noted above), but this may be removed in a later version.
// myapp/ajax.js
define({
"ajax": function(url, data, method, headers) {
if (method == "POST") {
return somePostMethod(url, data, headers)
} else {
return someGetMethod(url, data, headers);
}
}
});
// myapp/main.js
define(["wq/app", "./ajax", "./config"], function(app, customAjax, config) {
app.use(customAjax);
app.init(config).then(...);
});
// src/ajax.js
export default {
ajax(url, data, method, headers) {
if (method == "POST") {
return somePostMethod(url, data, headers)
} else {
return someGetMethod(url, data, headers);
}
}
};
// src/index.js
import app from '@wq/app';
import customAjax from './ajax';
import config from './config';
app.use(customAjax);
app.init(config).then(...);
Here are a few things to keep in mind:
ajax()
method should return a Promise
that will resolve to a JSON object.{"list": [], "count": 0, "per_page": 50}
URL
object, and data is a FormData
object.Error
with a json
attribute if the error is an object or a text
attribute otherwise. Note that unlike $.ajax()
, fetch()
does not automatically throw in the case of 400 and 500 errors.reducer(state, action)
& actions
New in wq.app 1.2.
The reducer()
plugin hook makes it possible to define a reducer that subscribes to Redux actions and updates a plugin-specific state. Reducer plugins and are generally defined with a name
and an action
object containing action creator functions. The action creators are bound to the dispatch method and re-attached to the plugin, as shown in the example below.
// myapp/timer.js
define({
"name": "timer",
"actions": {
"startTimer": function() {
return {
"type": "START_TIMER"
};
},
"stopTimer": function() {
return {
"type": "STOP_TIMER"
};
}
},
"reducer": function(timerState, action) {
if (!timerState) {
timerState = {};
}
switch (action.type) {
case "START_TIMER":
return {"active": true};
case "STOP_TIMER":
return {"active": false};
default:
return timerState;
}
},
"render": function(state) {
if (state.timer.active) {
someShowMethod();
} else {
someHideMethod();
}
}
});
// myapp/main.js
define(["wq/app", "./timer", "./config"], function(app, timer, config) {
app.use(timer);
app.init(config).then(...);
// Auto-bound methods
timer.start(); // Equivalent to app.store.dispatch(timer.actions.start())
timer.stop();
});
// src/timer.js
export default {
name: "timer",
actions: {
startTimer() {
return {
"type": "START_TIMER"
};
},
stopTimer() {
return {
"type": "STOP_TIMER"
};
}
},
reducer(timerState={}, action) {
switch (action.type) {
case "START_TIMER":
return {"active": true};
case "STOP_TIMER":
return {"active": false};
default:
return timerState;
}
}
render(state) {
if (state.timer.active) {
someShowMethod();
} else {
someHideMethod();
}
}
};
// src/index.js
import app from '@wq/app';
import timer from './timer';
import config from './config';
app.use(timer);
app.init(config).then(...);
timer.start(); // Equivalent to app.store.dispatch(timer.actions.start())
timer.stop();
Note: The
render()
plugin hook is technically managed by @wq/router, but is included here for completeness.
ds.dispatch(action)
New in wq.app 1.2.
Dispatch an arbitrary action to the Redux store. Generally, it is better to use a reducer
plugin with actions
action creators, but calling dispatch()
directly can be useful in some cases.
ds.dispatch({
'type': 'START_TIMER',
});
ds.getState()
New in wq.app 1.2.
ds.getState()
returns the full Redux state tree (including the latest updates from all registered reducers). Generally, it is better to use a render()
plugin to subscribe to state updates, but getState()
can be useful in some cases.
const state = ds.getState();
console.log(state.timer);
ds.subscribe(fn)
New in wq.app 1.2.
ds.subscribe()
registeres a callback function that will be executed whenever the state changes. Generally, it is better to use a render() plugin to subscribe to state updates, but subscribe()
can be useful in some cases.
function onUpdate() {
const state = ds.getState();
console.log(state.timer);
}
ds.subscribe(onUpdate);
ds.get(query)
Note:
ds.get()
is a legacy API and may be removed in a future major release of wq.app. If you are working extensively with arrays or collections of similarly-structured objects, it is generally best to use the @wq/model API rather thands.get()
. For other cases, use a reducer plugin and/ords.getState()
.
ds.get()
retrieves values from the datastore. It accepts a query
value (see above) and returns a Promise that is resolved when the value is loaded. If the value is not already stored locally, ds.get()
can automatically generate an AJAX request to load the data from the web service.
ds.get("name").then(function(name) {
// ...
});
ds.get({'url': 'items'}).then(function(items) {
// ...
});
Note that
ds.get()
is an asynchronous-only API, even if the data is already stored locally and no AJAX request is needed. This is to ensure the usage remains the same whether or not an AJAX call is needed. SeegetState()
above for a synchronous API.
ds.get()
can be passed an array of queries, which will be individually resolved and passed back through the promise in a corresponding array.
ds.get(['/items', '/types']).then(function(result) {
var items = result[0];
var types = result[1];
});
ds.set(query, value)
Note:
ds.set()
is a legacy API and may be removed in a future major release of wq.app. If you are working extensively with arrays or collections of similarly-structured objects, it is generally best to use the @wq/model API rather thands.set()
. For other cases, use a reducer plugin and/ords.dispatch()
.
ds.set()
is used to assign a value for the specified query to the local datastore.
ds.set('name', "Example");
ds.set('name', "Example").then(function() {
ds.get('name').then(function(name) {
// name == "Example";
});
});
Changed in wq.app 1.2: The promise returned by
ds.set()
no longer waits until the data has been fully persisted to offline storage before resolving.
ds.exists(query)
Note:
ds.exists()
is a legacy API and may be removed in a future major release of wq.app.
ds.exists()
is used to check whether a value for the query has been previously stored (with ds.set()
).
Changed in wq.app 1.2: The entire state is now stored in memory, so there is no longer a performance benefit to running ds.exists() before ds.get(). Also, note that ds.exists() only works for values defined with ds.set(), and ignores other parts of the Redux state.
ds.keys()
ds.keys()
lists all of the keys created by ds.set()
.
ds.storageUsage()
Changed in wq.app 1.2: ds.storageUsage() has been removed.
ds.reset()
ds.reset()
clears out all values in the ds
, including those defined by custom reducers.
Changed in wq.app 1.2: ds.reset() no longer has the capability of clearing out other stores. It does, however, reset the entire Redux state for the current store (i.e. not only the keys defined by
ds.set()
).
While ds.get()
and @wq/model can automatically generate AJAX requests as needed, it is sometimes necessary to access those functions directly. The available methods are listed here.
ds.fetch(query, [cache])
ds.fetch()
submits a web query to the datastore's web service (by calling ds.ajax()
). If cache
is set to true, ds.fetch()
stores the result in the local store (via ds.set()
). ds.fetch()
returns a promise that is resolved when the data is loaded from the server. If ds.fetch()
is called more than once with the same query while the AJAX request is still processing, the same server response will be used to fulfill all requests.
ds.prefetch()
is equivalent to calling ds.fetch()
with cache
set to true.
ds.prefetch(query, callback)
ds.prefetch()
provides a simple API for ensuring the latest data is present before continuing.
ds.prefetch('/url').then(function(result) {
console.log(result)
});
Note that @wq/model also provides a prefetch()
function, and @wq/app provides the function app.prefetchAll()
which can automatically prefetch JSON data for all registered models.
ds.prefetch(query)
is equivalent to ds.fetch(query, true)
.
When called with only a web query argument, the three retrieval functions are effectively identical APIs with the following distinctions:
function | loads from | saves to storage |
---|---|---|
ds.get(query).then(callback) |
storage, then web | if not already present |
ds.fetch(query).then(callback) |
web | no |
ds.prefetch(query).then(callback) |
web | yes |
To persist storage across user sessions, @wq/store requires some kind of offline storage to function as designed. Nearly all browsers in use today (including IE 11) have IndexedDB
available. localForage handles most the heavy lifting on automatically determining browser capabilities. However, note that a significant fraction of web users prefer to disable offline storage. Most notably, the "Block Cookies" setting for iOS Safari will also disable other offline storage options. If @wq/store is unable to leverage localForage, it will still work but values will not be persisted. This will work fine for most users, though any unsynced items in the outbox (see @wq/outbox) will be lost if the browser window is closed.
Last modified on 2019-07-10 10:42 AM
Edit this Page |
Suggest Improvement
© 2013-2019 by S. Andrew Sheppard