Ce module final résume les bonnes pratiques pour le développement d'applications SmartMaker.
components/pages/private/TasksPage/
├── index.jsx # Composant principal (export)
├── TasksPage.jsx # Logique et rendu
├── components/ # Sous-composants locaux
│ ├── TaskList.jsx
│ └── TaskFilters.jsx
└── hooks/ # Hooks spécifiques à cette page
└── useTasks.js
TaskList, UserProfile)useTasks, useAuth)TaskList.jsx)// Éviter : objet recréé à chaque rendu <Component style={{ color: 'red' }} /> // Préférer : objet stable const style = useMemo(() => ({ color: 'red' }), []); <Component style={style} />
// Pour les composants mémorisés const handleClick = useCallback((id) => { setSelected(id); }, []);
import { lazy, Suspense } from 'react'; import { Spinner } from '@cap-rel/smartcommon'; const TasksPage = lazy(() => import('./pages/private/TasksPage')); <Suspense fallback={<Spinner />}> <TasksPage /> </Suspense>
| Besoin | Solution |
|---|---|
| État d'un formulaire | useStates ou useForm |
| État d'une page (loading, error) | useStates |
| Session utilisateur | useGlobalStates('session') |
| Préférences | useGlobalStates('settings') |
| Données partagées | useGlobalStates |
| Gros volumes, offline | useDb |
globalState: { reducers: { // Auth session: null, // Préférences settings: { lng: 'fr', theme: 'light' }, // Cache de données cache: { categories: [], lastFetch: null } } }
const loadData = async () => { st.set('loading', true); st.set('error', null); try { const data = await api.private.get('items').json(); st.set('items', data.items); } catch (err) { st.set('error', err.message); // Log pour debug console.error('Load error:', err); } finally { st.set('loading', false); } };
useEffect(() => { const controller = new AbortController(); const load = async () => { try { const data = await api.private.get('items', { signal: controller.signal }).json(); setItems(data); } catch (err) { if (err.name !== 'AbortError') { setError(err.message); } } }; load(); return () => controller.abort(); }, []);
import { z } from 'zod'; const schema = z.object({ email: z.string().email('Email invalide'), password: z.string().min(8, 'Minimum 8 caractères'), age: z.number().min(18, 'Doit être majeur').optional() });
<Input name="email" label="Email" error={errors.email?.message} required />
// JAMAIS const API_KEY = 'secret123'; // OK : variables d'environnement serveur uniquement // Le client n'a accès qu'aux VITE_ prefixés const API_URL = import.meta.env.VITE_API_URL;
Ne pas faire confiance aux données du client :
// Controller PHP public function create($payload) { // Toujours valider if (empty($payload['label'])) { return ['Label required', 400]; } // Toujours vérifier les droits if (!$user->hasRight('mymodule', 'create')) { return ['Forbidden', 403]; } // Toujours échapper $label = $db->escape($payload['label']); }
React échappe automatiquement, mais attention à dangerouslySetInnerHTML :
// Dangereux <div dangerouslySetInnerHTML={{ __html: userContent }} /> // Si nécessaire, sanitiser d'abord import DOMPurify from 'dompurify'; <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userContent) }} />
// TasksPage.test.jsx import { render, screen, waitFor } from '@testing-library/react'; import { TasksPage } from './TasksPage'; test('affiche la liste des tâches', async () => { render(<TasksPage />); await waitFor(() => { expect(screen.getByText('Mes tâches')).toBeInTheDocument(); }); });
const st = useStates({ initialStates: { ... }, debug: import.meta.env.DEV // Logs uniquement en dev });