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 notation
  • useDb: IndexedDB database via Dexie
  • useCachedQuery: data cache with strategies (NetworkFirst, CacheFirst, SWR)

For complete offline synchronization, see Offline Synchronization (useSyncClient).

Documentation IndexedDB Documentation Dexie Documentation Redux Toolkit

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,
});

useDb (IndexedDB)

For storing large amounts of data or complex structured data, use useDb based on Dexie.

import { useDb } from '@cap-rel/smartcommon';

// Define the database
const db = useDb({
  name: 'myApp',
  version: 1,
  stores: {
    items: 'id++, name, category, createdAt',
    categories: 'id++, name'
  },
  debug: true
});

export const ItemsManager = () => {
  const [items, setItems] = useState([]);

  // Load items
  useEffect(() => {
    db.items.toArray().then(setItems);
  }, []);

  // Add an item
  const addItem = async (item) => {
    const id = await db.items.add(item);
    console.log('Item added with id:', id);
  };

  // Update an item
  const updateItem = async (id, changes) => {
    await db.items.update(id, changes);
  };

  // Delete an item
  const deleteItem = async (id) => {
    await db.items.delete(id);
  };

  // Query with filter
  const getByCategory = async (category) => {
    return db.items.where('category').equals(category).toArray();
  };

  return (/* ... */);
};

Automatic Features

useDb automatically adds:

  • createdAt: creation timestamp
  • updatedAt: last modification timestamp
  • logs table: 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 simple IndexedDB storage, use useDb. For API data caching, use useCachedQuery. For complete offline mode with synchronization, use useSyncClient.

See Also