Lifecycle management for asynchronous data in javascript single page applications. Framework agnostic.
This library helps with managing the lifecycle of data fetched from a resource. For example when there is a HTTP resource that you need data from, this library will tell you when to to perform the actual request to fetch the data and when to clear/cleanup the data.
Originally this library was created to make it easier to manage asynchronous actions in redux. The “standard” way of performing async actions in redux (described in its documentation) has many advantages, however it also has a few disadvantages:
This library was created to help with these disadvantages. However it does not actually depend on redux and is generic enough to be used in many other situations.
npm install meridvia
And then you can include it in your project using import
or require()
, assuming you are using something like webpack or browserify:
const {createManager} = require('meridvia');
or
import {createManager} from 'meridvia';
There are a few important concepts: The Resource Definition which defines the behaviour of each resource. And the Session
by which you can begin a transaction.
For each asynchronous source of data that you have, a Resource Definition should be created. At the very least it contains a unique resource name, a fetch
callback and a clear
callback. It is also possible to configure if and when the data is cached and refreshed. A resource is requested using its resource name and an a key/value map of params. These params can be represented using a plain javascript object, or as an Immutable Map and are passed to the fetch
callback and the clear
callback.
Each unique combination of resource name and params is considered to be one specific resource. For example if you call the request
function multiple times with the same resource name and params, the fetch
callback will only be called once. If a plain javascript object is passed as the params, it is compared to the params of other resources using a shallow equality check.
A Session
is used to manage which resources are in use. For each place in your codebase where you would normally perform an API call, a Session
should be created instead. A Session
object lets you start and end a transaction.
The only time that a resource can be requested is between the start and end of such a transaction. When requesting data the Session
will figure out if any existing data can be re-used, if not the fetch
callback is called.
When a transaction ends, the session will compare all the resources that have been requested to the requested resources of the previous transaction. For all resources that are no longer in use, the clear
callback is called. This callback will not occur immediately if caching has been configured.
An example to demonstrate these concepts:
const {createManager} = require('meridvia');
// Create a new instance of this library by calling
// createManager(), this is usually only done once
// in your codebase
const manager = createManager();
// Register a new Resource Definition by calling
// manager.resource(...)
manager.resource({
name: 'example',
fetch: async (params) => {
console.log('Fetch the example resource with params', params);
// This is where you would normally fetch data using an
// actual API call and store the result in your state store
},
clear: (params) => {
console.log('Clear the example resource with params', params);
// This is where you would normally remove the previously
// fetched data from your state store
},
});
// Create a new session
const session = manager.createSession();
console.log('\nLets begin the first transaction!');
// The session transaction begins now:
session(request => {
// Fetch two resources for the first time:
request('example', {exampleId: 1});
request('example', {exampleId: 2});
});
// The session transaction has ended
console.log('\nThe second transaction!');
// The session transaction begins now:
session(request => {
// This resource we had already fetched before,
// so it is reused:
request('example', {exampleId: 1});
// This resource is fetched for the first time:
request('example', {exampleId: 9001});
});
// The session transaction has ended,
// The resource with {exampleId: 2} was no longer
// used, so it is cleared
console.log('\nAll done, lets destroy the session');
// All resources will be cleared now:
session.destroy();
console.log('End of example');
Output:
Lets begin the first transaction!
Fetch the example resource with params { exampleId: 1 }
Fetch the example resource with params { exampleId: 2 }
The second transaction!
Fetch the example resource with params { exampleId: 9001 }
Clear the example resource with params { exampleId: 2 }
All done, lets destroy the session
Clear the example resource with params { exampleId: 1 }
Clear the example resource with params { exampleId: 9001 }
End of example
createManager([dispatcher], [options]) ⇒ Manager
Argument | Type | Default | |
---|---|---|---|
dispatcher | function | x => x |
The dispatcher callback. This function will be called with the return value of any fetch callback and any clear callback |
options.allowTransactionAbort | boolean | false |
If false , overlapping transactions are not allowed. If true an overlapping transaction for a Session will cause the previous transaction to be aborted. This option can also be set per session, see manager.createSession([options]) ⇒ Session . |
Return value: A new Manager
instance
Create a new Manager
instance. See Manager API
const {createManager} = require('meridvia');
const manager = createManager();
manager.resource(options)
Argument | Type | Default | |
---|---|---|---|
options.name | string | required | A unique resource name for this resource. The same name can later be used to request this resource. |
options.fetch | function(params, options) | required | The fetch callback for this resource. Called whenever the asynchronous data should be retrieved. |
options.clear | function(params, options) | null |
The clear callback for this resource. Called whenever asynchronous data that has been previously retrieved, is no longer in use. |
options.initStorage | function(params) | () => ({}) | Called the first time a resource is fetched, the return value is available to the other actions of the same resource. |
options.maximumStaleness | Time interval | 0 | The maximum amount of time that the data of a fetched resource will be reused in a future transaction. A value of 0 means forever/infinite. |
options.maximumRejectedStaleness | Time interval | maximumStaleness |
The maximum amount of time that the rejected or thrown error of a fetched resource will be reused in a future transaction. A value of 0 means forever/infinite. |
options.cacheMaxAge | Time interval | 0 | The maximum amount of time that the data of a fetched resource may be cached if no Session is using the resource. A value of 0 disables caching. |
options.refreshInterval | Time interval | 0 | How often to fetch the resource again, as long as there is a Session using this resource. A value of 0 disables refreshing. |
Return value: undefined
\
Throws: IllegalStateError
if the Manager
has been destroyed \
Throws: TypeError
if the any of the options has an invalid type \
Throws: ValueError
if the given resource name is already in use
Register a Resource Definition with the given options
. Each Resource Definition is identified by its unique resource name (options.name
), the resource can be requested using this resource name during a transaction using the request
function. The other options define the behaviour of the resource.
The options.maximumStaleness
and options.cacheMaxAge
values have very similar effects. They both define for how long the asynchronous data retrieved by the fetch
callback may be reused by a future transaction. The difference is that if options.cacheMaxAge
is not set, the resource is always cleared if it is no longer in use by any Session
. If options.cacheMaxAge
is set, the data may be reused even if there was a moment where the resource was not in use by any Session
.
If options.refreshInterval
is set, the fetch
callback is called again periodically to refresh the data, but only if the resource is currently in use by a Session
.
Argument | Type | ||
---|---|---|---|
params | object \ | Immutable.Map | The params that were passed to the request function during the transaction. |
options.storage | any | The value that was previously returned by the “initStorage” callback. | |
options.invalidate | function() | May be called at any time to indicate that the data from this specific fetch should no longer be cached in any way. | |
options.onCancel | function(callback) | May be called to register a cancellation callback. |
Return value: object | Promise
The fetch
callback function is called whenever the asynchronous data should be retrieved, either for the first time or to refresh existing data. For example, this is where you would perform an HTTP request. As its first argument the callback receives the params that were given during the transaction. The second argument is an options
object containing optional utilities.
The options.invalidate()
function may be called at any time to indicate that the data from this specific fetch should no longer be cached in any way. If a transaction requests this resource again it will always result in the fetch
callback being called again. This can be used to implement more advanced caching strategies
The options.onCancel(callback)
function maybe called to register a cancellation callback. When a resource is no longer in use, or if a fetch
callback is superseded by a more recent fetch
callback, all cancellation callbacks will be called. This can be used for example to cancel a http request.
The return value of the fetch
callback is passed to the dispatcher
callback of the Manager
. This allows for easy integration with state store frameworks such as redux.
Argument | Type | ||
---|---|---|---|
params | object \ | Immutable.Map | The params that were passed to the request function during the transaction. |
options.storage | any | The value that was previously returned by the “initStorage” callback |
Return value: object | Promise
The clear
callback callback function is called whenever asynchronous data that has been previously retrieved, is no longer in use.
When integration with a state store framework such as redux, this is where an action should be dispatched that causes the asynchronous data to be removed from the store.
The return value of the clear
callback is passed to the dispatcher
callback of the Manager
.
Argument | Type | ||
---|---|---|---|
params | object \ | Immutable.Map | The params that were given during the transaction. |
Return value: any
This callback function is called the first time a resource is fetched (for the specific combination of resource name and params). The return value is passed to any subsequent fetch
callback and clear
callback. This feature is useful if you need to keep track of some sort of state between (re-)fetching and clearing the same resource.
Time intervals, such as “cacheMaxAge”, can be expressed in two ways. If the value is a javascript number, it specifies the amount of milliseconds. If the value is a string, it must consist of a (floating point / rational) number and a suffix to indicate if the number indicates milliseconds (“ms”), seconds (“s”), minutes (“m”), hours (“h”) or days (“d”). Here are some examples:
Input Value | Milliseconds | Description |
---|---|---|
10 |
10 | 10 Milliseconds |
"10ms" |
10 | 10 Milliseconds |
"10s" |
10000 | 10 Seconds |
"10m" |
600000 | 10 Minutes |
"10h" |
36000000 | 10 Hours |
"10d" |
864000000 | 10 Days (10 * 24 hours) |
A minimal example
manager.resource({
name: 'thing',
fetch: async (params) => {
const payload = await doApiCall('/thing', params);
return {type: 'FETCH_THING', params, payload};
},
clear: (params) => {
return {type: 'CLEAR_THING', params};
},
});
A more exhaustive example:
manager.resource({
name: 'thing',
initStorage: (params) => {
return {
fetchCount: 0,
};
},
fetch: async (params, {storage, invalidate, onCancel}) => {
++storage.fetchCount;
const controller = new AbortController();
onCancel(() => controller.abort());
const url = '/thing/' + encodeURIComponent(params.thingId);
const response = await fetch(url, {
signal: controller.signal,
});
const payload = await response.json();
if (payload.maximumCacheDurationMs) {
setTimeout(() => invalidate(), payload.maximumCacheDurationMs);
}
return {type: 'FETCH_THING', params, payload};
},
clear: (params, {storage}) => {
console.log('This resource was fetched', storage.fetchCount, 'times!');
return {type: 'CLEAR_THING', params};
},
maximumStaleness: '10m',
cacheMaxAge: '5m',
refreshInterval: '30s',
});
manager.resources(options)
Return value: undefined
\
Throws: See manager.resource(options)
This function is a simple shorthand that lets you register multiple resources in a single call. It accepts an array for which every item is registered as a resource in exactly the same way as manager.resource(options)
.
Example:
manager.resources([
{
name: 'thing',
fetch: async (params) => {
const payload = await doApiCall('/thing', params);
return {type: 'FETCH_THING', params, payload};
},
clear: (params) => {
return {type: 'CLEAR_THING', params};
},
},
{
name: 'otherThing',
fetch: async (params) => {
const payload = await doApiCall('/otherThing', params);
return {type: 'FETCH_OTHER_THING', params, payload};
},
clear: (params) => {
return {type: 'CLEAR_OTHER_THING', params};
},
},
]);
manager.createSession([options])
⇒ Session
Argument | Type | Default | |
---|---|---|---|
options.allowTransactionAbort | boolean | Value of the allowTransactionAbort option passed to the createManager function |
If false , overlapping transactions are not allowed for this session. If true an overlapping transaction for this Session will cause the previous transaction to be aborted. |
Return value: Session
object \
Throws: IllegalStateError
if the Manager
has been destroyed
Creates a new Session
object, which is used to manage which resources are actually in use. See Session API.
manager.invalidate([resourceName], [params]) ⇒ number
Argument | Type | ||
---|---|---|---|
resourceName | string | The resource name that was previously given to the request function. |
|
params | object \ | Immutable.Map | The params that was previously given to the request function. |
Return value: number
: The number of resources that have actually been invalidated \
Throws: No
Invalidate all matching resources. If a matching resource is currently in-use by a Session
, the next time the resource is requested the fetch
callback will be called again. If a matching resource is not currently in-use by any Session
the clear
callback will be called immediately.
If 0 arguments are passed to this function, all resources will be invalidated. If 1 argument is passed, all resources with the given resource name are invalidated. If 2 arguments are passed, only one specific resource is invalidated.
manager.refresh([resourceName], [params]) ⇒ number
Argument | Type | ||
---|---|---|---|
resourceName | string | The resource name that was previously given to the request function. |
|
params | object \ | Immutable.Map | The params that was previously given to the request function. |
Return value: number
\
Throws: IllegalStateError
if the Manager
has been destroyed \
Throws: CompositeError
containing further errors in the “errors” property, if the dispatcher
callback has thrown for any resource.
Refresh all matching resources. If a matching resource is currently in-use by a Session
, the fetch
callback is immediately called again. If a matching resource is not currently in-use by any Session
the clear
callback will be called immediately.
If 0 arguments are passed to this function, all resources will be refreshed. If 1 argument is passed, all resources with the given resource name are refreshed. If 2 arguments are passed, only one specific resource is refreshed.
manager.destroy()
Return value: undefined
\
Throws: No
Destroy the Manager
instance. All resources are cleared, all sessions are destroyed and the Manager
is no longer allowed to be used.
A Session
object is used to request resources from the Manager
. If multiple Session
objects request the same resources, the Manager
will make sure that the same resource is only fetched once. The Session
object will remember which resources you are currently using. A resource that is in-use will never be cleared.
A transaction is used to change which resources are in-use by a Session
. Such a transaction has an explicit beginning and end. A transaction from the same Session
object is not allowed to overlap with a different transaction that is still active. While the transaction is active a request
function is available which should be called to request a specific resource. Doing so marks a specific resource as being in-use in the Session
. When the transaction ends, all of the requested resources are compared to those requested in the previous transaction, the resources that have not been requested again are then no longer marked as in-use.
Argument | Type | |
---|---|---|
callback | function(request) | The callback function that determines the lifetime of the transaction. The request function is passed as the first argument to this callback. |
Return value: Same as the return value of the called “callback” \
Throws: TypeError
if callback is not a function \
Throws: IllegalStateError
if the Session
has been destroyed \
Throws: IllegalStateError
if another transaction is still in progress (can only occur if allowTransactionAbort
is false
) \
Throws: Any thrown value from the called callback function
By calling the Session
object as a function, a new transaction begins. The given “callback” argument is then immediately called, with the request
function as an argument. This request
function is used to request resources with. When the “callback” function returns, the transaction ends. If a Promise
is returned the transaction will end after the promise has settled.
If the allowTransactionAbort
option passed to createManager
was set to false
(the default), an overlapping transaction will result in an error to be thrown by this function. If the option was set to true
, the previous transaction will be aborted if they overlap. If an transaction is aborted, the request
function will throw an error any time it is used.
Argument | Type | ||
---|---|---|---|
resourceName | string | A resource name belonging to a previously registered Resource Definition | |
params | object \ | Immutable.Map | The params to pass on to the fetch callback |
Return value: The value returned by the dispatcher
callback \
Throws: MeridviaTransactionAborted
if the transaction has been aborted (can only occur if allowTransactionAbort
is true
) \
Throws: MeridviaTransactionAborted
if the session has been destroyed \
Throws: IllegalStateError
if the transaction has ended \
Throws: ValueError
if the given resource name has not been registered \
Throws: Any thrown value from the dispatcher
callback
Request a specific resource and mark it as in-use for the Session
. The resource name must be belong to a registered Resource Definition. The Manager
will determine if the combination of resource name and params (a resource) has been requested previously and is allowed to be cached.
If the resource is not cached: the fetch
callback of the Resource Definition will be called, and the return value of this callback is passed to the dispatcher
callback of the Manager
. The return value of the dispatcher
callback is then returned from the request
function. If the resource is cached then the request
function will simply return the same value as it did the first time the resource was requested.
Conceptually, the implementation of the request
function looks a bit like this:
function request(resourceName, params) {
const resourceDefinition = getResourceDefinition(resourceName);
const resource = getResource(resourceName, params);
if (resource.isCached()) {
return resource.cachedValue;
}
else {
return resource.cachedValue = dispatcher(resourceDefinition.fetch(params));
}
}
Example of a transaction
session(request => {
request('post', {postId: 1});
request('post', {postId: 2});
request('comments', {postId: 2});
});
Example of a transaction with promises
async function example() {
await session(async request => {
const post = await request('post', {postId: 1});
request('user', {userId: post.authorId});
});
}
example().then(() => console.log('End of example'));
Return value: undefined
\
Throws: No
Destroy the session. All resources that were marked as in-use for this Session
are unmarked as such. Attempting to use the Session
again will result in an error.
/*
This example demonstrates how this library could be used to fetch and
display the details of a "user account" using the react
lifecycle methods componentDidMount, componentWillUnmount,
componentDidUpdate and then store the result in the react component
state.
This example includes:
* A fake API which pretends to fetch details of a user
account using a http request
* A meridvia resource manager on which we register a resource for
the user account details
* A react component which lets the resource manager know which
resources it needs using a meridvia session and stores the result
* Some logging to demonstrate what is going on
This is a trivial example to demonstrate one way to integrate this
library with react. It has been kept simple on purpose, however the
strength of this library becomes most apparent in more complex code
bases, for example: When the same resource is used in multiple
places in the code base; When resources should be cached; When data
has to be refreshed periodically; Et cetera.
*/
const {Component, createElement} = require('react');
const ReactDOM = require('react-dom');
const {createManager} = require('meridvia');
const myApi = {
// Perform a http request to fetch the user details for the
// given userId
userDetails: async (userId) => {
// This is where we would normally perform a real http
// request. For example:
// const response = await fetch(`/user/${encodeURIComponent(userId)}`);
// if (!response.ok) {
// throw Error(`Request failed: ${response.status}`);
// }
// return await response.json();
// however to keep this example simple, we only pretend.
await new Promise(resolve => setTimeout(resolve, 10));
if (userId === 4) {
return {name: 'Jack O\'Neill', email: 'jack@example.com'};
}
else if (userId === 5) {
return {name: 'Isaac Clarke', email: 'iclarke@unitology.gov'};
}
throw Error(`Unknown userId ${userId}`);
},
};
const setupResourceManager = () => {
// Set the `dispatch` callback so that the return value of
// the "fetch" callback (a promise that will resolve to the
// api result) is returned as-is from the request function during
// the session.
// (The library will cache this value as appropriate).
const dispatcher = value => value;
const resourceManager = createManager(dispatcher, {
// Because we are using promises during the transaction, it is
// possible that the transactions might overlap. Normally this
// is not allowed. By setting this option to true, the
// older transaction will be aborted instead.
allowTransactionAbort: true,
});
resourceManager.resource({
name: 'userDetails',
fetch: async (params) => {
console.log('Resource userDetails: fetch', params);
const {userId} = params;
const result = await myApi.userDetails(userId);
return result;
},
});
return resourceManager;
};
class Hello extends Component {
constructor(props) {
super(props);
this.state = {user: null};
}
componentDidMount() {
console.log('<Hello></Hello> componentDidMount');
// Component is now present in the DOM. Create a new
// meridvia session which will represent the resources
// in use by this component. The resource manager will
// combine the state of all active sessions to make
// its decisions.
this.session = this.props.resourceManager.createSession();
this.updateResources();
}
componentWillUnmount() {
console.log('<Hello></Hello> componentWillUnmount');
// The component is going to be removed from the DOM.
// Destroy the meridvia session to indicate that we
// no longer need any resources. Attempting to use
// the session again will result in an error.
this.session.destroy();
}
componentDidUpdate() {
console.log('<Hello></Hello> componentDidUpdate');
// The props have changed.
// In this example the specific resource that we need is based
// on the "userId" prop, so we have to update our meridvia
// session
this.updateResources();
}
updateResources() {
this.session(async request => {
const user = await request('userDetails', {
userId: this.props.userId,
});
if (user !== this.state.user) {
this.setState({user});
}
});
}
render() {
const {user} = this.state;
return createElement('div', {className: 'Hello'},
user ? `Hello ${user.name}` : 'Loading...'
);
/* If you prefer JSX, this is what it would look like:
return <div className="Hello">
{user ? `Hello ${user.name}` : 'Loading...'}
</div>
*/
}
}
const example = () => {
const resourceManager = setupResourceManager();
// Create the container element used by react:
const container = document.createElement('div');
document.body.appendChild(container);
// create a DOM MutationObserver so that we can log
// what the effects of the rendering are during this example
const observer = new MutationObserver(() => {
console.log('Render result:', container.innerHTML);
});
observer.observe(container, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
});
const renderMyApp = userId => {
const element = createElement(Hello, {resourceManager, userId}, null);
/* If you prefer JSX, this is what it would look like:
const element = (
<Hello resourceManager={resourceManager} userId={userId} ></Hello>
);
*/
ReactDOM.render(element, container);
};
console.log('First render...');
renderMyApp(4);
setTimeout(() => {
console.log('Second render...');
renderMyApp(5);
}, 100);
};
example();
Output:
First render...
<Hello></Hello> componentDidMount
Resource userDetails: fetch { userId: 4 }
Render result: <div class="Hello">Loading...</div>
<Hello></Hello> componentDidUpdate
Render result: <div class="Hello">Hello Jack O'Neill</div>
Second render...
<Hello></Hello> componentDidUpdate
Resource userDetails: fetch { userId: 5 }
<Hello></Hello> componentDidUpdate
Render result: <div class="Hello">Hello Isaac Clarke</div>
/*
This example demonstrates how this library could be used to fetch and
display the details of a "user account" using redux and the react
lifecycle methods componentDidMount, componentWillUnmount and
componentDidUpdate.
This example includes:
* A fake API which pretends to fetch details of a user
account using a http request
* A redux store which stores the user account details
* A reducer for the redux store that handles fetch and clear
actions for the details of a specific user account
* A meridvia resource manager on which we register a resource for
the user account details
* A react component which lets the resource manager know which
resources it needs using a meridvia session.
* A react-redux container which passes the user details from the
state store to the component.
* Some logging to demonstrate what is going on
This is a trivial example to demonstrate one way to integrate this
library with react and redux. It has been kept simple on purpose,
however the strength of this library becomes most apparent in more
complex code bases, for example: When the same resource is used in
multiple places in the code base; When resources should be cached;
When data has to be refreshed periodically; Et cetera.
*/
const {Component, createElement} = require('react');
const ReactDOM = require('react-dom');
const {createStore, combineReducers, applyMiddleware} = require('redux');
const {Provider: ReduxProvider, connect} = require('react-redux');
const {default: promiseMiddleware} = require('redux-promise');
const {createManager} = require('meridvia');
const myApi = {
// Perform a http request to fetch the user details for the
// given userId
userDetails: async (userId) => {
// This is where we would normally perform a real http
// request. For example:
// const response = await fetch(`/user/${encodeURIComponent(userId)}`);
// if (!response.ok) {
// throw Error(`Request failed: ${response.status}`);
// }
// return await response.json();
// however to keep this example simple, we only pretend.
await new Promise(resolve => setTimeout(resolve, 10));
if (userId === 4) {
return {name: 'Jack O\'Neill', email: 'jack@example.com'};
}
else if (userId === 5) {
return {name: 'Isaac Clarke', email: 'iclarke@unitology.gov'};
}
throw Error(`Unknown userId ${userId}`);
},
};
// In the state store, userDetailsById contains the
// details of a user, indexed by the userId:
// userDetailsById[userId] = {name: ..., email: ...}
const userDetailsByIdReducer = (state = {}, action) => {
if (action.type === 'FETCH_USER_DETAILS') {
// In this example we only store the resolved
// value of the api call. However you could also
// store an error message if the api call fails,
// or an explicit flag to indicate an api call is
// in progress.
const newState = Object.assign({}, state);
newState[action.userId] = action.result;
return newState;
}
else if (action.type === 'CLEAR_USER_DETAILS') {
// Completely remove the data from the state store.
// `delete` must be used to avoid memory leaks.
const newState = Object.assign({}, state);
delete newState[action.userId];
return newState;
}
return state;
};
// The reducer used by our redux store
const rootReducer = combineReducers({
userDetailsById: userDetailsByIdReducer,
});
const setupResourceManager = (dispatch) => {
// The resource manager will pass on the return value of `fetch`
// and `clear` to the `dispatch` callback here
const resourceManager = createManager(dispatch);
resourceManager.resource({
name: 'userDetails',
fetch: async (params) => {
// This function returns a promise. In this example
// we are using the redux-promise middleware. Which
// will resolve the promise before passing the action
// on to our reducers.
console.log('Resource userDetails: fetch', params);
const {userId} = params;
const result = await myApi.userDetails(userId);
return {
type: 'FETCH_USER_DETAILS',
userId,
result,
};
},
clear: (params) => {
console.log('Resource userDetails: clear', params);
const {userId} = params;
return {
type: 'CLEAR_USER_DETAILS',
userId,
};
},
});
return resourceManager;
};
class Hello extends Component {
componentDidMount() {
console.log('<Hello></Hello> componentDidMount');
// Component is now present in the DOM. Create a new
// meridvia session which will represent the resources
// in use by this component. The resource manager will
// combine the state of all active sessions to make
// its decisions.
this.session = this.props.resourceManager.createSession();
this.updateResources();
}
componentWillUnmount() {
console.log('<Hello></Hello> componentWillUnmount');
// The component is going to be removed from the DOM.
// Destroy the meridvia session to indicate that we
// no longer need any resources. Attempting to use
// the session again will result in an error.
this.session.destroy();
}
componentDidUpdate() {
console.log('<Hello></Hello> componentDidUpdate');
// The props have changed.
// In this example the specific resource that we need is based
// on the "userId" prop, so we have to update our meridvia
// session
this.updateResources();
}
updateResources() {
this.session(request => {
request('userDetails', {userId: this.props.userId});
});
}
render() {
const {user} = this.props;
return createElement('div', {className: 'Hello'},
user ? `Hello ${user.name}` : 'Loading...'
);
/* If you prefer JSX, this is what it would look like:
return <div className="Hello">
{user ? `Hello ${user.name}` : 'Loading...'}
</div>
*/
}
}
// A react-redux container component
const HelloContainer = connect((state, props) => ({
user: state.userDetailsById[props.userId],
}))(Hello);
const example = () => {
const store = createStore(rootReducer, applyMiddleware(promiseMiddleware));
const resourceManager = setupResourceManager(store.dispatch);
// Create the container element used by react:
const container = document.createElement('div');
document.body.appendChild(container);
// create a DOM MutationObserver so that we can log
// what the effects of the rendering are during this example
const observer = new MutationObserver(() => {
console.log('Render result:', container.innerHTML);
});
observer.observe(container, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
});
const renderMyApp = userId => {
const element = createElement(ReduxProvider, {store},
createElement(HelloContainer, {resourceManager, userId}, null)
);
/* If you prefer JSX, this is what it would look like:
const element = <ReduxProvider store={store}>
<HelloContainer resourceManager={resourceManager} userId={userId} ></HelloContainer>
</ReduxProvider>
*/
ReactDOM.render(element, container);
};
console.log('First render...');
renderMyApp(4);
setTimeout(() => {
console.log('Second render...');
renderMyApp(5);
}, 100);
};
example();
Output:
First render...
<Hello></Hello> componentDidMount
Resource userDetails: fetch { userId: 4 }
Render result: <div class="Hello">Loading...</div>
<Hello></Hello> componentDidUpdate
Render result: <div class="Hello">Hello Jack O'Neill</div>
Second render...
<Hello></Hello> componentDidUpdate
Resource userDetails: fetch { userId: 5 }
Resource userDetails: clear { userId: 4 }
<Hello></Hello> componentDidUpdate
Render result: <div class="Hello">Loading...</div>
<Hello></Hello> componentDidUpdate
Render result: <div class="Hello">Hello Isaac Clarke</div>