Data Storage
SmartCommon offers several solutions for client-side data storage:
useGlobalStates: global state with automatic persistence (localStorage/sessionStorage)useStates: reactive local state with path notationuseDb: IndexedDB database via DexieuseCachedQuery: data cache with strategies (NetworkFirst, CacheFirst, SWR)
For complete offline synchronization, see Offline Synchronization (useSyncClient).
Documentation IndexedDB Documentation Dexie Documentation Redux Toolkit
useGlobalStates (recommended)
The useGlobalStates hook provides a global state with automatic persistence. This is the recommended method for user data (session, preferences, etc.).
Basic Usage
import { useGlobalStates } from '@cap-rel/smartcommon';
export const MyComponent = () => {
const gst = useGlobalStates();
// Read a value
const user = gst.get('user');
// Write to localStorage (persistent)
const login = (userData) => {
gst.local.set('user', userData);
};
// Write to sessionStorage (session only)
const setTempData = (data) => {
gst.session.set('tempData', data);
};
// Delete a value
const logout = () => {
gst.unset('user');
};
return (
<div>
{user ? `Hello ${user.name}` : 'Not connected'}
</div>
);
};
Available Methods
| Method | Description |
|---|---|
gst.get(path) |
Retrieve a value by its path |
gst.local.set(path, value) |
Store in localStorage (persistent) |
gst.session.set(path, value) |
Store in sessionStorage (session) |
gst.unset(path) |
Remove a value |
gst.values |
Object containing all values |
Path Notation
You can access nested data using dot notation:
// Set nested data
gst.local.set('user.preferences.theme', 'dark');
gst.local.set('user.preferences.language', 'en');
// Read nested data
const theme = gst.get('user.preferences.theme'); // 'dark'
const prefs = gst.get('user.preferences'); // { theme: 'dark', language: 'en' }
useStates (local state)
The useStates hook provides a reactive local state with the same API as useGlobalStates.
import { useStates } from '@cap-rel/smartcommon';
export const MyForm = () => {
const st = useStates({
initialStates: {
name: '',
email: '',
errors: {}
},
debug: true // displays changes in the console
});
const handleChange = (field, value) => {
st.set(field, value);
};
const validate = () => {
if (!st.get('name')) {
st.set('errors.name', 'Name is required');
}
};
return (
<form>
<Input
value={st.get('name')}
onChange={(e) => handleChange('name', e.target.value)}
error={st.get('errors.name')}
/>
</form>
);
};
Available Methods
| Method | Description |
|---|---|
st.get(path) |
Retrieve a value |
st.set(path, value) |
Set a value |
st.unset(path) |
Remove a value |
st.values |
Object containing all values |
st.states |
Alias of values |
Array Manipulation
const st = useStates({ initialStates: { items: [] } });
// Add an element (push)
st.set('items[]', { id: 1, name: 'Item 1' });
// Modify an element by index
st.set('items[0].name', 'Modified item');
// Remove an element by index
st.unset('items[0]');
useCachedQuery (cache with strategies)
The useCachedQuery hook allows caching API data in IndexedDB with configurable strategies.
import { useCachedQuery } from '@cap-rel/smartcommon';
const ItemsList = () => {
const db = useMemo(() => new Dexie('myApp'), []);
const {
data, // data (from cache or network)
isLoading, // loading in progress
isFromCache, // true if data comes from cache
isStale, // true if data is stale
error, // possible error
lastFetch, // timestamp of last fetch
refetch, // force a new fetch
invalidate, // invalidate cache
} = useCachedQuery({
db,
store: 'cache', // IndexedDB store name (schema: 'key')
key: 'items-list', // unique cache key
fetchFn: () => api.get('items'), // fetch function
strategy: 'networkFirst', // cache strategy
ttl: 3600000, // cache TTL (1h default)
staleTime: 60000, // time before cache is "stale" (1min default)
enabled: true, // enable/disable fetch
});
if (isLoading) return <Spinner />;
return (
<List>
{data?.map(item => (
<ListItem key={item.id}>{item.label}</ListItem>
))}
</List>
);
};
Available Strategies
| Strategy | Constant | Behavior |
|---|---|---|
networkFirst |
NETWORK_FIRST |
Network first, cache as fallback (default) |
cacheFirst |
CACHE_FIRST |
Cache first, network if absent or expired |
swr |
STALE_WHILE_REVALIDATE |
Returns cache immediately, revalidates in background |
import { CACHE_STRATEGIES } from '@cap-rel/smartcommon';
// Usage with constant
useCachedQuery({
// ...
strategy: CACHE_STRATEGIES.STALE_WHILE_REVALIDATE,
});
IndexedDB database (Dexie)
To store large amounts of data or complex structured data, SmartCommon
provides the Db class (and the useDb hook), based on Dexie.
One database, defined in src/db/
An application has a single local database, declared once in
src/db/index.js and imported wherever it is needed: the business hooks
useDb<Feature>, useReferenceSync, useCachedQuery... See
Architecture for the full layout.
// src/db/index.js
import { Db } from '@cap-rel/smartcommon';
export const db = new Db({
name: 'myApp',
version: 1,
stores: {
items: '++id, name, category, createdAt',
categories: '++id, name',
// useReferenceSync metadata (key / value)
syncMeta: 'key'
}
}).db;
Components do not open a database: they import db and go through a
business hook.
// src/db/stores/items/useDbItems.jsx
import { db } from 'src/db';
export const useDbItems = () => ({
list: () => db.items.toArray(),
add: (item) => db.items.add(item),
update: (id, changes) => db.items.update(id, changes),
remove: (id) => db.items.delete(id),
byCategory: (category) => db.items.where('category').equals(category).toArray()
});
import { useEffect, useState } from 'react';
import { useDbItems } from 'src/db';
export const ItemsManager = () => {
const dbItems = useDbItems();
const [items, setItems] = useState([]);
useEffect(() => {
dbItems.list().then(setItems);
}, []);
return (/* ... */);
};
The useDb hook
useDb({ name, version, stores }) builds the same database from a component
(instance memoized as long as the schema does not change). It suits a
prototype or an isolated component. As soon as two parts of the application
share data, use the src/db/ database: two useDb calls with different
schemas on the same name open two competing versions of the same database.
useDb only handles one schema version, see
Schema migrations.
Schema migrations
useDb and the new Db({ version, stores }) form only describe one schema
version. As soon as a released version of the application has created the
database on users' devices, a schema change goes through a new version:
never modify an existing version, or Dexie refuses to open the database
(VersionError) on devices already installed.
To chain several versions, pass versions to Db in src/db/index.js. Each
entry gives the full schema of that version and, if needed, an upgrade
function that transforms the existing data:
// src/db/index.js
import { Db } from '@cap-rel/smartcommon';
export const db = new Db({
name: 'myApp',
versions: [
{
version: 1,
stores: {
items: '++id, name, category',
drafts: '++id'
}
},
{
// v2: index on status, "name" renamed to "label", drafts removed
version: 2,
stores: {
items: '++id, label, category, status',
drafts: null
},
upgrade: (tx) => tx.items.toCollection().modify((item) => {
item.label = item.name;
delete item.name;
item.status = item.status ?? 'active';
})
}
]
}).db;
Rules:
- Version numbers are strictly increasing. A device still on v1 goes through
every step up to the last one,
upgradeincluded. - Repeat the full schema in each version:
createdAtandupdatedAtare only added automatically on the stores listed in the last one. - A store set to
nullis deleted with its data. upgradereceives the Dexie transaction of the migration: make no network call in it, it ends as soon as it waits for anything other than IndexedDB.versionsreplacesversionandstores: do not mix both forms.
Automatic Features
Db and useDb automatically add:
createdAt: creation timestampupdatedAt: last modification timestamplogstable: journal of all operations (create, update, delete)
Dexie Queries
// All items
const all = await db.items.toArray();
// By primary key
const item = await db.items.get(1);
// Filter
const filtered = await db.items
.where('category')
.equals('electronics')
.toArray();
// Sort
const sorted = await db.items
.orderBy('createdAt')
.reverse()
.toArray();
// Limit
const first10 = await db.items.limit(10).toArray();
// Count
const count = await db.items.count();
Tip
For API cache with automatic strategies, prefer useCachedQuery. For complete offline synchronization with conflict management, use useSyncClient.
Redux (classic method)
If you prefer to use Redux directly, SmartCommon provides ReduxProvider.
Create a Slice
// src/redux/reducers/sessionSlice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
data: JSON.parse(sessionStorage.getItem("session")) ?? null
};
const sessionSlice = createSlice({
name: "session",
initialState,
reducers: {
setSession(state, action) {
state.data = action.payload;
sessionStorage.setItem("session", JSON.stringify(action.payload));
},
unsetSession(state) {
state.data = null;
sessionStorage.removeItem("session");
}
},
});
export default sessionSlice.reducer;
export const { setSession, unsetSession } = sessionSlice.actions;
Configure the Store
// src/redux/index.js
import sessionReducer from "./reducers/sessionSlice";
import { combineReducers } from "redux";
import { configureStore } from "@reduxjs/toolkit";
const rootReducer = combineReducers({
session: sessionReducer
});
export const reduxStore = configureStore({ reducer: rootReducer });
export * from "./reducers/sessionSlice";
Use in Components
import { useSelector, useDispatch } from "react-redux";
import { setSession, unsetSession } from "../../../redux";
export const MyComponent = () => {
const session = useSelector(state => state.session.data);
const dispatch = useDispatch();
const login = (data) => dispatch(setSession(data));
const logout = () => dispatch(unsetSession());
return (/* ... */);
};
Method Comparison
| Method | Use Case | Persistence |
|---|---|---|
useGlobalStates |
User session, preferences | localStorage / sessionStorage |
useStates |
Form state, local UI | Memory (non-persistent) |
useDb |
Structured data, local storage | IndexedDB |
useCachedQuery |
API cache with strategies | IndexedDB |
useSyncClient |
Complete offline synchronization | IndexedDB |
| Redux | Complex applications, middleware | Configurable |
Tip
For most cases, useGlobalStates and useStates are sufficient. For IndexedDB storage, declare the application database in src/db/ with Db. For API data caching, use useCachedQuery. For complete offline mode with synchronization, use useSyncClient.
See Also
- Hooks - Complete hooks documentation
- API Requests - Combine with API calls