Chapter 2: State Management
SmartCommon provides several hooks for state management according to needs:
- useGlobalStates: shared state between components with persistence
- useStates: local component state
- useForm: specialized state for forms
useGlobalStates
Global state accessible everywhere, with automatic persistence.
Basic Syntax
import { useGlobalStates } from '@cap-rel/smartcommon';
function MyComponent() {
const gst = useGlobalStates();
return (
<div>
<p>User: {gst.get('session')?.user?.name}</p>
<p>Language: {gst.get('settings')?.lng}</p>
</div>
);
}
Persistence Configuration
In appConfig.js:
export const config = {
storage: {
local: ["session", "settings"] // Persisted in localStorage
},
globalState: {
reducers: {
session: null,
settings: { lng: "fr", theme: "light" },
cart: { items: [], total: 0 }
}
}
};
Read and Write
function SettingsPage() {
const gst = useGlobalStates();
const settings = gst.get('settings');
const changeLanguage = (lng) => {
gst.local.set('settings', { ...settings, lng });
};
const changeTheme = (theme) => {
gst.local.set('settings', { ...settings, theme });
};
return (
<div>
<select
value={settings?.lng}
onChange={(e) => changeLanguage(e.target.value)}
>
<option value="fr">French</option>
<option value="en">English</option>
</select>
<button onClick={() => changeTheme('light')}>Light</button>
<button onClick={() => changeTheme('dark')}>Dark</button>
</div>
);
}
Example: Shopping Cart
function useCart() {
const gst = useGlobalStates();
const cart = gst.get('cart');
const addItem = (product) => {
const existing = cart.items.find(i => i.id === product.id);
let newItems;
if (existing) {
newItems = cart.items.map(i =>
i.id === product.id
? { ...i, quantity: i.quantity + 1 }
: i
);
} else {
newItems = [...cart.items, { ...product, quantity: 1 }];
}
const total = newItems.reduce(
(sum, i) => sum + i.price * i.quantity,
0
);
gst.set('cart', { items: newItems, total });
};
const removeItem = (productId) => {
const newItems = cart.items.filter(i => i.id !== productId);
const total = newItems.reduce(
(sum, i) => sum + i.price * i.quantity,
0
);
gst.set('cart', { items: newItems, total });
};
const clearCart = () => {
gst.set('cart', { items: [], total: 0 });
};
return { cart, addItem, removeItem, clearCart };
}
// Usage
function ProductCard({ product }) {
const { addItem } = useCart();
return (
<div>
<h3>{product.label}</h3>
<p>{product.price} €</p>
<button onClick={() => addItem(product)}>
Add to cart
</button>
</div>
);
}
function CartIcon() {
const { cart } = useCart();
const itemCount = cart.items.reduce((sum, i) => sum + i.quantity, 0);
return <span>🛒 {itemCount}</span>;
}
useStates
Local state with path notation.
Basic Syntax
import { useStates } from '@cap-rel/smartcommon';
function MyComponent() {
const st = useStates({
initialStates: {
count: 0,
user: { name: '', email: '' },
items: [],
loading: false
},
debug: true // Display changes in console
});
return (
<div>
<p>Count: {st.get('count')}</p>
<button onClick={() => st.set('count', st.get('count') + 1)}>
+1
</button>
</div>
);
}
Available Methods
| Method | Description |
|---|---|
| st.get(path) | Read a value |
| st.set(path, value) | Write a value |
| st.unset(path) | Delete a value |
| st.values | Object containing all values |
Path notation
const st = useStates({
initialStates: {
user: { name: '', address: { city: '' } },
items: []
}
});
// Read
st.get('user'); // { name: '', address: { city: '' } }
st.get('user.name'); // ''
st.get('user.address.city'); // ''
st.get('items'); // []
st.get('items[0]'); // undefined
// Write
st.set('user.name', 'John');
st.set('user.address.city', 'Paris');
// Write with function
st.set('count', prev => prev + 1);
// Array manipulation
st.set('items[]', { id: 1 }); // Push
st.set('items[0].name', 'test'); // Modify index
st.unset('items[0]'); // Delete index
Example: Detail Page
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { Page, Block, Spinner } from '@cap-rel/smartcommon';
import { useApi, useStates } from '@cap-rel/smartcommon';
export const ProductDetailPage = () => {
const { id } = useParams();
const api = useApi();
const st = useStates({
initialStates: {
product: null,
loading: true,
error: null,
isEditing: false
}
});
useEffect(() => {
loadProduct();
}, [id]);
const loadProduct = async () => {
st.set('loading', true);
st.set('error', null);
try {
const data = await api.private.get(`products/${id}`).json();
st.set('product', data);
} catch (err) {
st.set('error', err.message);
} finally {
st.set('loading', false);
}
};
if (st.get('loading')) {
return <Page><Spinner /></Page>;
}
if (st.get('error')) {
return <Page><Block>Error: {st.get('error')}</Block></Page>;
}
const product = st.get('product');
return (
<Page title={product.label}>
<Block>
<p>Price: {product.price} €</p>
<p>Stock: {product.stock}</p>
</Block>
</Page>
);
};
useForm
Specialized hook for forms with validation.
Basic Syntax
import { useForm } from '@cap-rel/smartcommon';
import { Form, Input, Button } from '@cap-rel/smartcommon';
function LoginForm() {
const form = useForm({ defaultValues: { email: '', password: '' } });
const handleSubmit = async (data) => {
console.log('Validated data:', data);
};
return (
<Form form={form} onSubmit={handleSubmit}>
<Input name="email" label="Email" type="email" />
<Input name="password" label="Password" type="password" />
<Button type="submit" loading={form.isFormSubmitting}>
Login
</Button>
</Form>
);
}
With Initial Values
function EditProductForm({ product }) {
const form = useForm({
defaultValues: {
label: product.label,
price: product.price,
description: product.description
}
});
const handleSubmit = async (data) => {
await api.private.put(`products/${product.id}`, { json: data });
};
return (
<Form form={form} onSubmit={handleSubmit}>
<Input name="label" label="Name" />
<Input name="price" label="Price" type="number" />
<Textarea name="description" label="Description" />
<Button type="submit">Save</Button>
</Form>
);
}
Manual Validation with setField
// useForm does not perform automatic validation.
// Use setField to manage errors manually:
form.setField({
name: 'email',
value: inputValue,
errors: {
required: { condition: !inputValue },
format: { condition: inputValue && !isValidEmail(inputValue) }
}
});
// Check errors
const hasError = form.get('errors.email.required'); // true | false
State Hooks Comparison
| Hook | Scope | Persistence | Use Case |
|---|---|---|---|
| useGlobalStates | Application | localStorage | Session, preferences, cart |
| useStates | Component | No | Page state, loading |
| useForm | Component | No | Forms with validation |
| useState (React) | Component | No | Simple state |
Best Practices
1. Choose the Right Hook
// User session -> useGlobalStates
const gst = useGlobalStates();
const session = gst.get('session');
// Page loading state -> useStates
const st = useStates({ initialStates: { loading: true, data: null } });
// Form -> useForm
const form = useForm({ defaultValues: { name: '', email: '' } });
// Simple toggle -> useState
const [isOpen, setIsOpen] = useState(false);
2. Organize Global State
// appConfig.js
globalState: {
reducers: {
// Auth
session: null,
// User preferences
settings: { lng: 'fr', theme: 'light' },
// Global business data
cart: { items: [], total: 0 },
// Cache
categories: []
}
}
useConfirm
Hook to display confirmation and alert modal dialogs.
Import
import { useConfirm } from '@cap-rel/smartcommon';
Returned Functions
The hook returns an object with two functions:
const { confirm, alert } = useConfirm();
- confirm: displays a dialog with Confirm/Cancel buttons, returns
trueorfalse - alert: displays a dialog with a single OK button, always returns
true
Using confirm
function DeleteButton({ item, onDelete }) {
const { confirm } = useConfirm();
const handleDelete = async () => {
const confirmed = await confirm({
type: 'delete',
title: 'Delete this item?',
message: `Are you sure you want to delete "${item.name}"?`,
detail: item.ref,
confirmText: 'Delete',
cancelText: 'Cancel'
});
if (confirmed) {
onDelete(item.id);
}
};
return <button onClick={handleDelete}>Delete</button>;
}
Using alert
function SaveButton({ onSave }) {
const { alert } = useConfirm();
const handleSave = async () => {
try {
await onSave();
await alert({
type: 'info',
title: 'Success',
message: 'Data has been saved.'
});
} catch (err) {
await alert({
type: 'warning',
title: 'Error',
message: err.message
});
}
};
return <button onClick={handleSave}>Save</button>;
}
Options
| Option | Type | Description |
|---|---|---|
| type | string | Dialog type: 'danger', 'delete', 'warning', 'info' |
| title | string | Dialog title |
| message | string | Confirmation message |
| detail | string | Additional text displayed in a gray box |
| confirmText | string | Confirm button text |
| cancelText | string | Cancel button text |
Types and Icons
| Type | Icon | Button Color |
|---|---|---|
| danger | Trash can (red) | Red |
| delete | Trash can (red) | Red |
| warning | Exclamation triangle (orange) | Orange |
| info | Info (blue) | Blue |
| (other) | Question mark (gray) | Blue (default) |
Prerequisites
The ConfirmProvider component must wrap the application:
import { ConfirmProvider } from '@cap-rel/smartcommon';
function App() {
return (
<ConfirmProvider labels={{ cancel: 'Cancel', confirm: 'OK' }}>
<MyApp />
</ConfirmProvider>
);
}
The labels prop allows defining default button texts. If not provided, values are "Cancel" and "OK".
usePWAUpdate
Hook to detect and manage PWA application updates.
Import
import { usePWAUpdate } from '@cap-rel/smartcommon';
Usage
function UpdateBanner() {
const {
updateAvailable,
updateActivated,
checkForUpdates,
applyUpdate
} = usePWAUpdate({
checkInterval: 300000 // Check every 5 min
});
if (!updateAvailable) return null;
return (
<div className="bg-blue-500 text-white p-4">
<p>An update is available</p>
<button onClick={applyUpdate}>
Update now
</button>
</div>
);
}
With Automatic Reload
function App() {
usePWAUpdate({
autoReload: true,
onUpdateAvailable: () => {
console.log('Update available');
},
onUpdateActivated: () => {
console.log('Update activated');
}
});
return <MyApp />;
}
Options
| Option | Type | Default | Description |
|---|---|---|---|
| autoReload | boolean | false | Automatically reload after update |
| checkInterval | number | 0 | Check interval in ms (0 = disabled) |
| onUpdateAvailable | function | - | Callback when update is available |
| onUpdateActivated | function | - | Callback when update is activated |
Returned Values
| Property | Type | Description |
|---|---|---|
| updateAvailable | boolean | Update pending |
| updateActivated | boolean | Update activated |
| registration | object | ServiceWorkerRegistration |
| checkForUpdates | function | Manually check |
| applyUpdate | function | Apply update |
| reloadPage | function | Reload page |
UpdatePrompt
Ready-to-use UI component that encapsulates usePWAUpdate and displays a notification when an update is available.
Import
import { UpdatePrompt } from '@cap-rel/smartcommon';
Display Variants
Three variants are available:
- toast (default): discrete notification via react-hot-toast
- banner: fixed banner at top or bottom of screen
- modal: centered modal dialog
Direct Usage
function App() {
return (
<div>
<MyApp />
<UpdatePrompt
variant="banner"
position="bottom"
checkInterval={300000}
labels={{
title: 'New version',
message: 'An update is available.',
reloadButton: 'Refresh',
dismissButton: 'Later'
}}
/>
</div>
);
}
Via Provider
The SmartCommon Provider accepts a pwaUpdate prop that automatically integrates UpdatePrompt:
import { Provider } from '@cap-rel/smartcommon';
import { config } from './appConfig';
export const App = () => (
<Provider
config={config}
pwaUpdate={{
variant: 'toast',
checkInterval: 300000
}}
>
<Router />
</Provider>
);
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | string | "toast" |
"toast", "banner" or "modal" |
| position | string | "bottom" |
Banner position: "top" or "bottom" |
| autoReload | boolean | false |
Automatically reload after activation |
| checkInterval | number | 0 |
Check interval in ms (0 = disabled) |
| labels | object | - | Custom texts (see below) |
| onUpdateAvailable | function | - | Callback when update is detected |
| onUpdateActivated | function | - | Callback when update is activated |
Default Labels
| Key | Default Value |
|---|---|
| title | "Update available" |
| message | "A new version is available." |
| reloadButton | "Refresh" |
| dismissButton | "Later" |
Key Points to Remember
- useGlobalStates for shared and persisted data
- useStates for local state with path notation
- useForm for forms with manual validation
- useConfirm for confirmation and alert dialogs
- usePWAUpdate to detect PWA updates
- UpdatePrompt to display a ready-to-use update UI
- Path notation:
user.address.city,items[0],items[] - Configure storage.local for persistence
Previous Chapter | Back to Module | Next Chapter: Utilities ->