Chapter 3: Data Flow
Overview
Understanding how data flows in SmartMaker is essential. This chapter traces the complete path of data, from user click to final rendering.
The 3 Types of Data
| Type | Storage | Persistence | Hook |
|---|---|---|---|
| Local State | Component memory | No | useState, useStates |
| Global State | Redux store | Optional (localStorage) | useGlobalStates |
| Server Data | API -> Dolibarr | Database | useApi |
API Request Flow
┌──────────────────────────────────────────────────────────────┐
│ FRONTEND │
├──────────────────────────────────────────────────────────────┤
│ 1. User clicks "Load" │
│ ↓ │
│ 2. Component calls useApi() │
│ ↓ │
│ 3. api.private.get('items') │
│ ↓ │
│ 4. ky automatically adds: │
│ - Authorization: Bearer <accessToken> │
│ - X-DEVICEID: <deviceId> │
└──────────────────────────────────────────────────────────────┘
↓ HTTPS
┌──────────────────────────────────────────────────────────────┐
│ BACKEND │
├──────────────────────────────────────────────────────────────┤
│ 5. pwa/api.php receives the request │
│ ↓ │
│ 6. SmartAuth validates JWT │
│ ↓ │
│ 7. Router calls ItemController::index() │
│ ↓ │
│ 8. Controller loads Dolibarr objects │
│ ↓ │
│ 9. Mapper converts to DTO │
│ ↓ │
│ 10. Returns JSON │
└──────────────────────────────────────────────────────────────┘
↓ HTTPS
┌──────────────────────────────────────────────────────────────┐
│ FRONTEND │
├──────────────────────────────────────────────────────────────┤
│ 11. ky parses JSON response │
│ ↓ │
│ 12. Component updates state │
│ ↓ │
│ 13. React re-renders with new data │
└──────────────────────────────────────────────────────────────┘
Concrete Example: Product List
1. The React Component
// components/pages/private/ProductsPage/index.jsx
import { useEffect } from 'react';
import { Page, Block, List, ListItem, Spinner } from '@cap-rel/smartcommon';
import { useApi, useStates } from '@cap-rel/smartcommon';
export const ProductsPage = () => {
const api = useApi();
const st = useStates({
initialStates: {
products: [],
loading: true,
error: null
}
});
// Load on mount
useEffect(() => {
loadProducts();
}, []);
const loadProducts = async () => {
st.set('loading', true);
st.set('error', null);
try {
// API request with automatic JWT
const data = await api.private.get('products').json();
st.set('products', data.products);
} catch (err) {
st.set('error', err.message);
} finally {
st.set('loading', false);
}
};
// Conditional display
if (st.get('loading')) {
return <Page><Spinner /></Page>;
}
if (st.get('error')) {
return (
<Page>
<Block>Error: {st.get('error')}</Block>
</Page>
);
}
return (
<Page title="Products">
<List>
{st.get('products').map(product => (
<ListItem
key={product.id}
title={product.label}
subtitle={`${product.price} €`}
/>
))}
</List>
</Page>
);
};
2. The API Router (PHP)
<?php
// pwa/api.php
require_once '../smartmaker-api-prepend.php';
use SmartAuth\Api\AuthController;
use SmartAuth\Api\RouteController as Route;
use MonModule\Api\ProductController;
// Authentication routes
Route::get('login', AuthController::class, 'index');
Route::post('login', AuthController::class, 'login');
Route::get('refresh', AuthController::class, 'refresh');
Route::post('logout', AuthController::class, 'logout', true);
// Product routes (protected)
Route::get('products', ProductController::class, 'index', true);
Route::get('products/{id}', ProductController::class, 'show', true);
Route::post('products', ProductController::class, 'create', true);
Route::put('products/{id}', ProductController::class, 'update', true);
Route::delete('products/{id}', ProductController::class, 'delete', true);
// Fallback
json_reply('Access denied', 403);
3. The Controller (PHP)
<?php
// smartmaker-api/Controllers/ProductController.php
namespace MonModule\Api;
use MonModule\Api\Mappers\dmProduct;
class ProductController
{
public function index($payload = null)
{
global $db, $user;
// Load Dolibarr products
require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';
$sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "product";
$sql .= " WHERE entity IN (" . getEntity('product') . ")";
$sql .= " ORDER BY label ASC";
$result = $db->query($sql);
$products = [];
if ($result) {
$mapper = new dmProduct();
while ($obj = $db->fetch_object($result)) {
$product = new \Product($db);
$product->fetch($obj->rowid);
// Map to React DTO
$products[] = $mapper->exportMappedData($product);
}
}
return [['products' => $products], 200];
}
public function show($payload = null)
{
global $db;
$id = $payload['id'];
require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';
$product = new \Product($db);
if ($product->fetch($id) <= 0) {
return [['error' => 'Product not found'], 404];
}
$mapper = new dmProduct();
return [$mapper->exportMappedData($product), 200];
}
}
4. The Mapper (PHP)
<?php
// smartmaker-api/Mappers/dmProduct.php
namespace MonModule\Api\Mappers;
class dmProduct
{
public function exportMappedData($product)
{
return [
'id' => (int) $product->id,
'ref' => $product->ref,
'label' => $product->label,
'description' => $product->description,
'price' => (float) $product->price,
'price_ttc' => (float) $product->price_ttc,
'stock' => (float) $product->stock_reel,
'status' => (int) $product->status,
'created_at' => $product->datec,
'updated_at' => $product->tms
];
}
public function importMappedData($data)
{
// DTO -> Dolibarr conversion for creation/modification
return [
'ref' => $data['ref'] ?? '',
'label' => $data['label'] ?? '',
'description' => $data['description'] ?? '',
'price' => $data['price'] ?? 0
];
}
}
JWT Authentication
Login Flow
1. User enters email/password
↓
2. api.login({ login, password })
↓
3. SmartAuth checks credentials
↓
4. Returns { accessToken, refreshToken, user }
↓
5. SmartCommon stores in session (localStorage)
↓
6. All future requests have the token
Login Example
// components/pages/public/LoginPage/index.jsx
import { Page, Block, Form, Input, Button } from '@cap-rel/smartcommon';
import { useApi, useNavigation } from '@cap-rel/smartcommon';
export const LoginPage = () => {
const api = useApi();
const nav = useNavigation();
const handleSubmit = async (values) => {
try {
// api.login handles everything automatically
await api.login({
login: values.email,
password: values.password
});
// Redirect to home
nav.navigate('/');
} catch (err) {
alert('Incorrect credentials');
}
};
return (
<Page title="Login">
<Block>
<Form onSubmit={handleSubmit}>
<Input name="email" label="Email" type="email" required />
<Input name="password" label="Password" type="password" required />
<Button type="submit">Login</Button>
</Form>
</Block>
</Page>
);
};
Automatic Refresh
The access token expires after 15 minutes. SmartCommon automatically handles:
- Detects 401 error (expired token)
- Calls
/refreshwith the refreshToken - Gets a new accessToken
- Retries the original request
Everything is transparent to the developer.
Global State vs Local State
When to Use useStates (local)
- Data specific to a component
- Form state
- Loading state
- Temporary data
const st = useStates({
initialStates: {
loading: false,
formData: { name: '', email: '' }
}
});
When to Use useGlobalStates
- Data shared between components
- User session
- Preferences
- Main business data
const gst = useGlobalStates();
const session = gst.get('session');
const cart = gst.get('cart');
// Write
gst.local.set('session', userData);
gst.set('cart', { items: [], total: 0 });
Data Creation
Creation Flow
1. Form filled
↓
2. api.private.post('products', { json: data })
↓
3. Controller::create() validates and creates
↓
4. Returns the created product
↓
5. Update local/global state
Example
const handleCreate = async (formData) => {
try {
const newProduct = await api.private
.post('products', { json: formData })
.json();
// Add to local list
st.set('products', [...st.get('products'), newProduct]);
// Or update global state
// const gst = useGlobalStates();
// gst.set('products', [...gst.get('products'), newProduct]);
nav.navigate('/products');
} catch (err) {
st.set('error', err.message);
}
};
Full Flow Summary
| Step | Layer | Action |
|---|---|---|
| 1 | React | User interacts |
| 2 | Hook | useApi() prepares the request |
| 3 | ky | Adds JWT + sends |
| 4 | PHP | api.php routes the request |
| 5 | SmartAuth | Validates token |
| 6 | Controller | Executes business logic |
| 7 | Dolibarr | Database access |
| 8 | Mapper | Converts to DTO |
| 9 | PHP | Returns JSON |
| 10 | ky | Parses the response |
| 11 | Hook | Updates state |
| 12 | React | Re-renders the interface |
Key Points to Remember
- useApi automatically handles JWT authentication
- Controller loads Dolibarr objects
- Mapper converts Dolibarr -> React DTO
- useStates for local state
- useGlobalStates for shared state
- Refresh token is handled automatically
Previous Chapter | Back to Module | Next Module: SmartCommon Components ->