Nhost - Backend-as-a-Service with GraphQL for modern app development - Interview with Johan Eliasson
Currently our application cannot retain its state if refreshed. One neat way to get around this problem is to store the application state to localStorage and then restore it when we run the application again.
If you were working against a back-end, this wouldn't be a problem. Even then having a temporary cache in localStorage
could be handy. Just make sure you don't store anything sensitive there as it is easy to access.
localStorage
#localStorage
is a part of the Web Storage API. The other half, sessionStorage
, exists as long as the browser is open while localStorage
persists even in this case. They both share the same API as discussed below:
storage.getItem(k)
- Returns the stored string value for the given key.storage.removeItem(k)
- Removes the data matching the key.storage.setItem(k, v)
- Stores the given value using the given key.storage.clear()
- Empties the storage contents.It is convenient to operate on the API using your browser developer tools. In Chrome especially the Resources tab is useful as it allows you to inspect the data and perform direct operations on it. You can even use storage.key
and storage.key = 'value'
shorthands in the console for quick tweaks.
localStorage
and sessionStorage
can use up to 10 MB of data combined. Even though they are well supported, there are certain corner cases that can fail. These include running out of memory in Internet Explorer (fails silently) and failing altogether in Safari's private mode. It is possible to work around these glitches, though.
You can support Safari in private mode by trying to write into localStorage
first. If that fails, you can use Safari's in-memory store instead, or just let the user know about the situation. See Stack Overflow for details.
localStorage
#To keep things simple and manageable, we will implement a little wrapper for storage
to wrap the complexity. The API will consist of get(k)
to fetch items from the storage and set(k, v)
to set them. Given the underlying API is string based, we'll use JSON.parse
and JSON.stringify
for serialization. Since JSON.parse
can fail, that's something we need to take into account. Consider the implementation below:
app/libs/storage.js
export default storage => ({
get(k) {
try {
return JSON.parse(storage.getItem(k));
}
catch(e) {
return null;
}
},
set(k, v) {
storage.setItem(k, JSON.stringify(v));
}
})
The implementation is enough for our purposes. It's not foolproof and it will fail if we put too much data into a storage. To overcome these problems without having to solve them yourself, it would be possible to use a wrapper such as localForage to hide the complexity.
FinalStore
#Just having means to write and read from the localStorage
won't do. We still need to connect out application to it somehow. State management solutions provide hooks for this purpose. Often you'll find a way to intercept them somehow. In Alt's case that happens through a built-in store known as FinalStore
.
We have already set it up at our Alt instance. What remains is writing the application state to the localStorage
when it changes. We also need to load the state when we start running it. In Alt terms these processes are known as snapshotting and bootstrapping.
An alternative way to handle storing the data would be to take a snapshot only when the window gets closed. There's a Window level beforeunload
hook that could be used. This approach is brittle, though. What if something unexpected happens and the hook doesn't get triggered for some reason? You'll lose data.
We can handle the persistency logic at a separate module dedicated to it. We will hook it up at the application setup and off we go.
Given it can be useful to be able to disable snapshotting temporarily, it can be a good idea to implement a debug
flag. The idea is that if the flag is set, we'll skip storing the data.
This is particularly useful if we manage to break the application state dramatically during development somehow as it allows us to restore it to a blank slate easily through localStorage.setItem('debug', 'true')
(localStorage.debug = true
), localStorage.clear()
, and finally a refresh.
Given bootstrapping could fail for an unknown reason, we catch a possible error. It can still be a good idea to proceed with starting the application even if something horrible happens at this point. The snapshot portion is easier as we just need to check for the debug
flag there and then set data if the flag is not active.
The implementation below illustrates the ideas:
app/libs/persist.js
export default function(alt, storage, storageName) {
try {
alt.bootstrap(storage.get(storageName));
}
catch(e) {
console.error('Failed to bootstrap data', e);
}
alt.FinalStore.listen(() => {
if(!storage.get('debug')) {
storage.set(storageName, alt.takeSnapshot());
}
});
}
You would end up with something similar in other state management systems. You'll need to find equivalent hooks to initialize the system with data loaded from the localStorage
and write the state there when it happens to change.
We are still missing one part to make this work. We'll need to connect the logic with our application. Fortunately there's a suitable place for this, the setup. Tweak as follows:
app/components/Provider/setup.js
import storage from '../../libs/storage';
import persist from '../../libs/persist';
import NoteStore from '../../stores/NoteStore';
export default alt => {
alt.addStore('NoteStore', NoteStore);
persist(alt, storage(localStorage), 'app');
}
If you try refreshing the browser now, the application should retain its state. Given the solution is generic, adding more state to the system shouldn't be a problem. We could also integrate a proper back-end through the same hooks if we wanted.
If we had a real back-end, we could pass the initial payload as a part of the HTML and load it from there. This would avoid a round trip. If we rendered the initial markup of the application as well, we would end up implementing basic universal rendering approach. Universal rendering is a powerful technique that allows you to use React to improve the performance of your application while gaining SEO benefits.
Ourpersist
implementation isn't without its flaws. It is easy to end up in a situation wherelocalStorage
contains invalid data due to changes made to the data model. This brings you to the world of database schemas and migrations. The lesson here is that the more you inject state and logic to your application, the more complicated it gets to handle.
NoteStore
#Before moving on, it would be a good idea to clean up NoteStore
. There's still some code hanging around from our earlier experiments. Given persistency works now, we might as well start from a blank slate. Even if we wanted some initial data, it would be better to handle that at a higher level, such as application initialization. Adjust NoteStore
as follows:
app/stores/NoteStore.js
import uuid from 'uuid';
import NoteActions from '../actions/NoteActions';
export default class NoteStore {
constructor() {
this.bindActions(NoteActions);
this.notes = [
{
id: uuid.v4(),
task: 'Learn React'
},
{
id: uuid.v4(),
task: 'Do laundry'
}
];
this.notes = [];
}
...
}
This is enough for now. Now our application should start from a blank slate.
Even though we ended up using Alt in this initial implementation, it's not the only option. In order to benchmark various architectures, I've implemented the same application using different techniques. I've compared them briefly below:
Compared to Flux, Facebook's Relay improves on the data fetching department. It allows you to push data requirements to the view level. It can be used standalone or with Flux depending on your needs.
Given it's still largely untested technology, we won't be covering it in this book yet. Relay comes with special requirements of its own (GraphQL compatible API). Only time will tell how it gets adopted by the community.
In this chapter, you saw how to set up localStorage
for persisting the application state. It is a useful little technique to know. Now that we have persistency sorted out, we are ready to start generalizing towards a full blown Kanban board.
This book is available through Leanpub. By purchasing the book you support the development of further content.