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 true or false
  • 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

  1. useGlobalStates for shared and persisted data
  2. useStates for local state with path notation
  3. useForm for forms with manual validation
  4. useConfirm for confirmation and alert dialogs
  5. usePWAUpdate to detect PWA updates
  6. UpdatePrompt to display a ready-to-use update UI
  7. Path notation: user.address.city, items[0], items[]
  8. Configure storage.local for persistence

Previous Chapter | Back to Module | Next Chapter: Utilities ->