Skip to main content

ReactJS Storage – From Basic LocalStorage to Advanced Offline Apps with RxDB

Modern ReactJS applications often need to store data on the client side. Whether you’re preserving simple user preferences or building offline-ready features, choosing the right storage mechanism can make or break your development experience. In this guide, we’ll start with a basic localStorage approach for minimal data. Then, we’ll explore more powerful, reactive solutions via RxDB—including offline functionality, indexing, preact signals, and even encryption.


Part 1: Storing Data in ReactJS with LocalStorage​

localStorage is a built-in browser API for storing key-value pairs in the user’s browser. It’s straightforward to set and get items—ideal for trivial preferences or small usage data.

import React, { useState, useEffect } from 'react';

function LocalStorageExample() {
const [username, setUsername] = useState(() => {
const saved = localStorage.getItem('username');
return saved ? JSON.parse(saved) : '';
});

useEffect(() => {
localStorage.setItem('username', JSON.stringify(username));
}, [username]);

return (
<div>
<h2>ReactJS LocalStorage Demo</h2>
<input
type="text"
value={username}
onChange={e => setUsername(e.target.value)}
placeholder="Enter your username"
/>
<p>Stored: {username}</p>
</div>
);
}

export default LocalStorageExample;

Pros of localStorage in ReactJS:

  • Easy to implement quickly for minimal data
  • Built-in to the browser—no extra libs
  • Persistent across sessions

Downsides of localStorage While localStorage is convenient for small amounts of data, it has certain limitations:

  • Synchronous: Reading or writing localStorage can block the main thread if data is large.
  • No advanced queries: You only store stringified objects by a single key. Searching or filtering requires manually scanning everything.
  • No concurrency or offline logic: If multiple tabs or users need to manipulate the same data, localStorage doesn’t handle concurrency or sync with a server.
  • No indexing: You can’t perform partial lookups or advanced matching.

For “remember user preference” use cases, localStorage is excellent. But if your app grows complex—needing structured data, large data sets, or offline-first features—you might quickly surpass localStorage’s utility.

Part 2: LocalStorage vs. IndexedDB​

While localStorage is simple, it’s limited to string-based key-value lookups and can be synchronous for all reads/writes. For more robust ReactJS storage needs, browsers also provide IndexedDB—a low-level, asynchronous API that can store larger amounts of JSON data with indexing.

LocalStorage:

  • Good for small amounts of data (like user settings or flags)
  • String-only storage
  • Single key-value access, no searching by subfields

IndexedDB:

  • Stores large JSON objects, able to index by multiple fields
  • Asynchronous and usually more scalable
  • More complicated to use directly (i.e., not as simple as .getItem()) RxDB, as you’ll see, simplifies IndexedDB usage in ReactJS by adding a more intuitive layer for queries, reactivity, and advanced capabilities like encryption.
RxDB

Part 3: Moving Beyond Basic Storage: RxDB for ReactJS​

When data shapes get complex—large sets of nested documents, or you want offline sync to a server—RxDB can transform your approach to ReactJS storage. It stores documents in (usually) IndexedDB or alternative backends but offers a reactive, NoSQL-based interface.

RxDB Quick Example (Observables)​

import { createRxDatabase } from 'rxdb';
import { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';

(async function setUpRxDB() {
const db = await createRxDatabase({
name: 'heroDB',
storage: getRxStorageDexie(),
multiInstance: false
});

const heroSchema = {
title: 'hero schema',
version: 0,
type: 'object',
primaryKey: 'id',
properties: {
id: { type: 'string' },
name: { type: 'string' },
power: { type: 'string' }
},
required: ['id', 'name']
};

await db.addCollections({ heroes: { schema: heroSchema } });

// Insert a doc
await db.heroes.insert({ id: '1', name: 'AlphaHero', power: 'Lightning' });

// Query docs once
const allHeroes = await db.heroes.find().exec();
console.log('Heroes: ', allHeroes);
})();

Reactive Queries: In a React component, you can subscribe to a query via RxDB’s $ property, letting your UI automatically update when data changes. React components can subscribe to updates from .find() queries, letting the UI automatically reflect changes—perfect for dynamic offline-first apps.

import React, { useEffect, useState } from 'react';

function HeroList({ collection }) {
const [heroes, setHeroes] = useState([]);

useEffect(() => {
const query = collection.find();
// query.$ is an RxJS Observable that emits whenever data changes
const sub = query.$.subscribe(newHeroes => {
setHeroes(newHeroes);
});

return () => sub.unsubscribe(); // clean up subscription
}, [collection]);

return (
<ul>
{heroes.map(hero => (
<li key={hero.id}>
{hero.name} - Power: {hero.power}
</li>
))}
</ul>
);
}

export default HeroList;

realtime ui updates

By using these reactive queries, your React app knows exactly when data changes locally (or from another browser tab) or from remote sync, keeping your UI in sync effortlessly.

Part 4: Using Preact Signals Instead of Observables​

RxDB typically exposes reactivity via RxJS observables. However, some developers prefer newer reactivity approaches like Preact Signals. RxDB supports them via a special plugin or advanced usage:

import { createRxDatabase } from 'rxdb/plugins/core';
import { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';
import { PreactSignalsRxReactivityFactory } from 'rxdb-premium/plugins/reactivity-preact-signals';

(async function setUpRxDBWithSignals() {
const db = await createRxDatabase({
name: 'heroDB_signals',
storage: getRxStorageDexie(),
reactivity: PreactSignalsRxReactivityFactory
});

// Create a signal-based query instead of using Observables:
const collection = db.heroes;
const heroesSignal = collection.find().$$; // signals version
// Now you can reference heroesSignal() in Preact or React with adapter usage
})();

Preact Signals rely on “signals” instead of Observables—some developers find them more straightforward to adopt, especially for fine-grained reactivity. In ReactJS, you might still prefer RxJS-based subscriptions unless you add bridging code for signals.

Part 5: Encrypting the Storage with RxDB​

For more advanced ReactJS storage needs—especially when sensitive user data is involved—you might want to encrypt stored documents at rest. RxDB provides a robust encryption plugin:

import { createRxDatabase } from 'rxdb';
import { wrappedKeyEncryptionCryptoJsStorage } from 'rxdb/plugins/encryption-crypto-js';
import { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';

(async function secureSetup() {
const encryptedDexieStorage = wrappedKeyEncryptionCryptoJsStorage({
storage: getRxStorageDexie()
});

// Provide a password for encryption
const db = await createRxDatabase({
name: 'secureReactStorage',
storage: encryptedDexieStorage,
password: 'MyStrongPassword123'
});

await db.addCollections({
secrets: {
schema: {
title: 'secret schema',
version: 0,
type: 'object',
primaryKey: 'id',
properties: {
id: { type: 'string' },
secretInfo: { type: 'string' }
},
required: ['id'],
encrypted: ['secretInfo'] // field to encrypt
}
}
});
})();

All data in the marked encrypted fields is automatically encrypted at rest. This is crucial if you store user credentials, private messages, or other personal data in your ReactJS application storage.

Offline Sync​

If you need multi-device or multi-user data synchronization, RxDB provides replication plugins for various endpoints (HTTP, GraphQL, CouchDB, Firestore, etc.). Your local offline changes can then merge automatically with a remote database whenever internet connectivity is restored.

Overview: localStorage vs IndexedDB vs RxDB​

CharacteristiclocalStorageIndexedDBRxDB
Data ModelKey-value store (only strings)Low-level, JSON-like storage engine with object stores and indexesNoSQL JSON documents with optional JSON-Schema
Query CapabilitiesBasic get/set by key; manual parse for more complex searchesIndex-based queries, but API is fairly verbose; lacks a high-level query languageJSON-based queries, optional indexes, real-time reactivity
ObservabilityNone. Must re-fetch data yourself.None natively. Must implement eventing or manual re-check.Built-in reactivity. UI auto-updates via Observables or Preact signals
Large Data UsageNot recommended for large data (blocking, synchronous calls)Better for large amounts of data, asynchronous reads/writesScales for medium to large data. Uses IndexedDB or other storages under the hood
ConcurrencyMinimal. Overwrites if multiple tabs write simultaneouslyMultiple tabs can open the same DB, but must handle concurrency logic carefullyMulti-instance concurrency with built-in conflict resolution plugins if needed
Offline SyncNone. Purely local.None out of the box. Must be implemented manuallyBuilt-in replication to remote endpoints (HTTP, GraphQL, CouchDB, etc.) for offline-first usage
EncryptionNot supported nativelyNot supported natively; must encrypt data manually before storingEncryption plugins available. Supports field-level encryption at rest
UsageGreat for small flags or settings

Follow Up​

If you’re looking to dive deeper into ReactJS storage topics and take full advantage of RxDB’s offline-first, real-time capabilities, here are some recommended resources:

With these follow-up steps, you can refine your reactjs storage strategy to meet your app’s unique needs, whether it’s simple user preferences or robust offline data syncing.

âś•