Unit 8: Storage

In this unit you will complete your knowledge to begin your journey as a oneJS developer. Throughout this unit we will discuss the different types of storage. Storage is in fact what it seems, the process of storing and persisting data even if the app is closed. Depending on the topology (the location of the data) we can divide storage in local, when data is stored in the user’s device, or online, when data is stored in an external database.

Local Storage

Let’s begin by exploring local storage. The typical use-case for this type of storage is possibly saving user specific configuration. Suppose our app lets the user choose the default language. You can leverage this type of storage to retrieve this value every time the user launches the app. To persist data, first choose the variable you would like to persist and make it part of the state, if it is not already. Then, specify the local type of storage and give it an id. This id will be used to store the variable. After this step, your variable will be saved every time its value is modified. Now, if you want to retrieve its value when the app is loaded, you need to define the source. Just define local as the source providing the same id as the one used in the storage.

const state = {
language: {
source: {local: 'myAppLanguage'},
storage: {local: 'myAppLanguage'}
}
};

Try to give a unique name to your ids, since local storage is shared by other apps running in your device. See the example below putting this in practice:

import {app, read, update, readFlavor} from '@onejs-dev/core';
import {View, Input, Text} from '@onejs-dev/components';

const state = {
language: {
default: 'English',
source: {local: 'myAppLanguage'},
storage: {local: 'myAppLanguage'}
}
}

const languages = ['Spanish', 'English', 'French', 'German'];

const App = () => {
return View({content: {direction: 'column', gap: 10}})([
Input({type: 'list', options: languages, value: read('language'), onChange: update('language')}),
View()(Text()('Selected Language: ' + read('language')))
]);
};

app({component: App, state: state, theme: {preset: 'oneJS'}});

You can refresh the page and see how the selection in the example above persists. Internally, oneJS leverages the Storage API for the web and Async Storage package for iOS and Android. Using this type of storage you can save simple key-value pairs, but it is not adequate for complex data models. Keep reading to learn how to manage complex scenarios.

Online Storage

For some apps local storage may be enough, but as your app grows in complexity you may find it necessary to use an external database to store your data. oneJS support Firebase's Cloud Firestore database out of the box. It follows a document-oriented model as described on their website:

Documents Each document has its own unique id Selected document’s path: ‘products/product1’ Collection path: ‘products’ Subcollection path: ‘products/product1/ingredients’ Data id: product 1name: pizzaprice: $10ingredients:

To use Cloud Firestore in your app, you first need to initialize your database. First, import Cloud Firestore in your project and call the initializeApp function with the configuration object corresponding to your project. Then, call the getFirestore function using the output of the previous function. This function returns the configured Firestore database object. It should look something like the example below:

import {initializeApp} from 'firebase/app';
import {getFirestore} from 'firebase/firestore';

// TODO: Replace the following with your app's Firebase project configuration
// See: https://firebase.google.com/docs/web/learn-more#config-object
const firebaseConfig = {
    // ...
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);

// Initialize Cloud Firestore and get a reference to the service
const db = getFirestore(app);

Once you have the Cloud Firestore database object (db in the example), feed it to the app function’s firestore property. This enables oneJS to perform read and write operations. Finally, create a state variable that uses firestore as its source, storage or both. Indicate the path to the document or collection that you want to retrieve, create or modify. For documents, you can include state variables in your path by wrapping their name in < >. See the example below:

import {app} from '@onejs-dev/core';
import {initializeApp} from 'firebase/app';
import {getFirestore} from 'firebase/firestore';

/* Cloud Firestore Initialization */
const firebaseConfig = {
    // ...
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

const state = {
    products:  {source: {firestore: 'products'}, storage: {firestore: 'products'}},
    selectedProductId: {default: 'pizza'},
    product: {
        source: {firestore: 'products/<selectedProductId>'}, 
        storage: {firestore: 'products/<selectedProductId>'}
    }
}

const App = () => {
    // ...
};

app({component: App, firestore: db, state: state, theme: {preset: 'oneJS'}});

oneJS syncs the state with the database data automatically. This way, you can focus on modifying the state of your app, and oneJS will perform the dirty work for you. We are working to support more popular serverless databases. We would love to have feedback from you!

IndexedDB (web-only)

IndexedDB is an in-browser database that allows you to store significant amounts of structured data, including files/blobs. It is supported by most modern browsers and uses indexes to enable high-performance searches. While the Local Storage discussed in the previous section is useful for storing smaller amounts of data, it is less adequate for storing larger amounts of structured data. IndexedDB uses a data model based on object stores and transactions. You can read all about it in the MDM Web Docs. For simplicity, you can think of it as Cloud Firestore's document-oriented model; the root level is a collection that can contain any number of documents. Documents are JavaScript objects that store key-value pairs. Unlike Cloud Firestore, IndexedDB does not allow collection nesting or subcollections. Using IndexedDB in oneJS requires no set up, just create a state variable using IndexedDB as your source and storage indicating the path to the data. You can then modify the state variable and oneJS will sync updates to the IndexedDB database for you.

import {app} from '@onejs-dev/core';

const state = {
    products:  {source: {indexedDB: 'products'}, storage: {indexedDB: 'products'}},
    selectedProductId: {default: 'pizza'},
    product: {
        source: {indexedDB: 'products/<selectedProductId>'}, 
        storage: {indexedDB: 'products/<selectedProductId>'}
    }
}

const App = () => {
    // ...
};

app({component: App, state: state, theme: {preset: 'oneJS'}});

Modifying Data

Working with more complex data also requires additional tools to update your data. In the previous units, we have displayed how to use the update function; it takes the state variable's id and replaces its value when an event is triggered:

import {app, read, update} from '@onejs-dev/core';
import {Input} from '@onejs-dev/components';

const state = {
    name: ''
}

const App = () => [
    Input({value: read('name'), onChange: update('name'), placeholder: 'Name'}),
];

app({component: App, state: state, theme: {preset: 'oneJS'}});

When working with databases, you may just want to update a certain document within a collection and not the entire collection. You can achieve this with the update function, just provide a second argument, in addition to the state id, which represents the document id to be updated:

import {app, read, update} from '@onejs-dev/core';
import {Input, Text} from '@onejs-dev/components';

const state = {
    stateId:  {default: [
        {id: 'documentId', name: 'Default value'},
        {id: 'otherDocumentId', name: 'Other default value'}
    ]},
    name: ''
}

const App = () => [
    Input({value: read('name'), onChange: update('name'), placeholder: 'Name'}),
    Input({type: 'button', title: 'Save', onClick: () => {
        update('stateId', 'documentId')({...read('stateId', 'documentId'), name: read('name')});
    }})
];

app({component: App, state: state, theme: {preset: 'oneJS'}});

You may also want to add a new document into a collection without needing to overwrite the entire collection. In this case, you can use the add function. Just provide the state id of the collection and a new document will be created when the add function is triggered:

import {app, read, update} from '@onejs-dev/core';
import {Input} from '@onejs-dev/components';

const state = {
    stateId:  {default: [
        {id: 'documentId', name: 'Default value'},
        {id: 'otherDocumentId', name: 'Other default value'}
    ]},
    name: ''
}

const App = () => [
    Input({value: read('name'), onChange: update('name'), placeholder: 'Name'}),
    Input({type: 'button', title: 'Save', onClick: () => {
        add('stateId')({name: read('name')});
    }})
];

app({component: App, state: state, theme: {preset: 'oneJS'}});

Finally, to complete the CRUD operations, oneJS gives you the power to remove data from your database. Beware that with great power comes great responsibility 🕷. To delete an item from a collection, call the remove function with the desired document id:

import {app, read, update} from '@onejs-dev/core';
import {Input} from '@onejs-dev/components';

const state = {
    stateId:  {default: [
        {id: 'documentId', name: 'Default value'},
        {id: 'otherDocumentId', name: 'Other default value'}
    ]},
    name: ''
}

const App = () => [
    Input({value: read('name'), onChange: update('name'), placeholder: 'Name'}),
    Input({type: 'button', title: 'Remove', onClick: () => {
        remove('stateId', 'documentId');
    }})
];

app({component: App, state: state, theme: {preset: 'oneJS'}});

Let's see all this concepts in a real example; a TODO app. Do you know any other frameworks where you can write this in less than 30 lines of code?. Please let us know!

import {app, read, add, update, remove, readFlavor} from '@onejs-dev/core';
import {View, Input, Text} from '@onejs-dev/components';

const state = {
    todo: '',
    todos: {source: {indexedDB: 'todos'}, storage: {indexedDB: 'todos'}}    
};
const saveTodo = () => {add('todos')({name: read('todo'), done: false});update('todo')('');}
const updateTodo = todo => () => {update('todos', todo?.id)({...todo, done: !todo.done});}  
const removeTodo = todo => () => {remove('todos', todo?.id);}  

const App = () => {
    return  View({content: {direction: 'column', gap: 10}})([
        Text({flavor: readFlavor('section')})('Write your TODOs:'),
        View()([
            Input({value: read('todo'), onChange: update('todo')}),
            Input({type: 'button', title: 'Save', onClick: saveTodo, flavor: readFlavor('light','primaryBackground')})
        ]),
        read('todos')?.length > 0 && read('todos').map(todo =>  View({content: {gap: 10}})([
            Input({type: 'button', title: 'Delete', onClick: saveTodo, onPress: removeTodo(todo), flavor: readFlavor('light','rejectBackground')}),
            Input({type: 'checkbox', title: todo.name, value: todo.done, onChange: updateTodo(todo)})          
        ]))
    ]);
};

app({component: App, state: state, theme: {preset: 'oneJS'}});

Well done! You have completed all the learning units, use this knowledge to build something amazing. You can head to the Playground section to test your ideas or check out the Docs for more in-depth content. To be continued… More units with more incredible features are coming.