How to Implement the Memento Design Pattern for ReactJS
In this article, I discuss how the Object-Oriented Memento Design Pattern can be implemented in ReactJS. The base code could be downloaded at the end of the article, but I highly recommend reading the article first to understand how to use it!
Memento Design Pattern
Firstly, it’s important to understand what the Memento Design Pattern is for and how it can help you in making bullet-proof code. This means that a small investment in upfront complexity will pay off as code you don’t have to touch again down the road.
The Memento Design Pattern abstracts the interface for storing the state of objects without breaking encapsulation. This means that only the same class will know how to save and load its state, thus preventing any bad cross-class dependencies. The pattern also creates Memento objects, which can be loaded and reloaded back at any time, allowing the client object to revert its state.
As a developer, this also means that once this is implemented, you no longer have to worry about how the states are saved, and instead can focus on what parts of the state you wish to save in any new component.
Memento uses the following components:
- Virtual Memento Client
- Concrete Memento Client(s)
- Memento
The Concrete Client inherits the Virtual Client’s interface and calls the load() and save() functions at desired points in its React.Component lifecycle.
The load() function will typically be called in the React.Component constructor(props) function, or in the componentDidMount() function.
export class ConcreteMementoClient extends VirtualMementoClient {
constructor(props){
super(props);
this.load();
}
//...
}
Meanwhile, the save() function will be called in any user function that requires saving the state information after changes have been made. For example, the function could be called in a userDidAction(), as a callback:
export class ConcreteMementoClient extends VirtualMementoClient{
// constructor(props)...
userDidAction(){
// Do stuff...
const state = this.state; // get the state
this.setState(state,this.save);
}
//...
}
Note that this.setState(state,callback) takes two arguments, the new state object and the callback. In this case, the save() function is used as the callback.
Below is an example for ConcreteMementoClient.js
/* ConcreteMementoClient.js */
import {VirtualMementoClient} from './VirtualMementoClient';
export class ConcreteMementoClient extends VirtualMementoClient{
constructor(props){
super(props);
this.load();
}
userDidAction(){
// Do stuff...
const state = this.state; // get the state
this.setState(state,this.save);
}
}.
Here is the implementation of the VirtualMementoClient.js for you to see how the getKey(), getSaveData(), and loadSaveData() are called:
/* VirtualMementoClient.js */import React from 'react';
import {Memento} from './Memento';export class VirtualMementoClient extends React.Component {
getKey(){return "";}
save(callback=()=>{}){
const saveData = this.getSaveData();
// Call an external storage implementation
const key = this.getKey();.
this.memento = new Memento();
this.memento.setState(key,saveData);
}
load(callback=null){
//Set the callback to this.onLoadComplete for default
callback = callback ? callback : this.onLoadComplete; if (!this.memento){this.memento = new Memento();}
// Call an external load implementation
const key = this.getKey();
this.memento.getState(key,(data)=>{
data = data ? data : {}; // Set default data to {}
this.loadSaveData(data, callback);
});
}
getSaveData(){return this.state;}
loadSaveData(data,callback=()=>{}){
if (data){
this.setState(data,callback);
}
else {
callback();
}
} onLoadComplete(){} // Virtual function
}
An example of the Memento.js (this varies based on where you want to store your data!).
In the example below, the Memento uses the chrome.storage API to store and load the data. This is intended for use in a Chrome Extension.
/* Memento.js */
export class Memento {
setState(key,data,callback=()=>{}){
if (!key) return;
const saveData = {};
saveData[key] = data;
chrome.storage && chrome.storage.local.set(
saveData,
callback
);
}
getState(key,callback=()=>{}){
if (!key) return;
chrome.storage && chrome.storage.local.get(
key,
function(result){
callback(result[key])
}
);
}
}
The Memento’s interface is then called through the Virtual Client’s implementation.
The following sequence diagram shows how this works:
Two processes are shown in the diagram above: (1) Saving, and (2) Loading.
(1) Saving
The Concrete Memento Component calls this.save(), which triggers the implementation in the Virtual Memento Client.
The Virtual Memento Client then calls getSaveData(), which is implemented in Concrete Memento Client.
The Concrete Memento Client returns the necessary data.
The Virtual Memento Client then calls setState() on the Memento to store the data.
(2) Loading
The Concrete Memento Component calls this.load(), which triggers the implementation in the Virtual Memento Client.
The Virtual Memento Client then calls the getState() function of the Memento to load the data.
Once the data is loaded from the memento via a callback, the Virtual Memento Client calls the loadSaveData() function in the Concrete Memento Client.
The Concrete Memento Client then processes the data as needed and triggers its onLoadComplete() callback at the end.
Code
Download the code at: https://github.com/JSANJ/Memento
Conclusion
Using the Memento code above, you can flexibly store and load state in any component by just declaring the implementation of the virtual functions, thus minimizing the amount of code needed to be written for new components and reusing the Memento framework.
You can now rest easy knowing the state of your objects will be saved and loaded as expected.