Module 11: Best Practices
This final module sums up the best practices for developing SmartMaker applications.
Code organization
Component structure
components/pages/private/TasksPage/
├── index.jsx # Main component (export)
├── TasksPage.jsx # Logic and rendering
├── components/ # Local sub-components
│ ├── TaskList.jsx
│ └── TaskFilters.jsx
└── hooks/ # Hooks specific to this page
└── useTasks.js
Naming convention
- Components: PascalCase (
TaskList,UserProfile) - Hooks: camelCase with the use prefix (
useTasks,useAuth) - Files: same name as the component (
TaskList.jsx) - Folders: PascalCase for components, camelCase for hooks
Performance
Avoiding useless re-renders
// Avoid: object recreated on every render
<Component style={{ color: 'red' }} />
// Prefer: stable object
const style = useMemo(() => ({ color: 'red' }), []);
<Component style={style} />
Memoizing callbacks
// For memoized components
const handleClick = useCallback((id) => {
setSelected(id);
}, []);
Lazy loading pages
import { lazy, Suspense } from 'react';
import { Spinner } from '@cap-rel/smartcommon';
const TasksPage = lazy(() => import('./pages/private/TasksPage'));
<Suspense fallback={<Spinner />}>
<TasksPage />
</Suspense>
State management
Choosing the right tool
| Need | Solution |
|---|---|
| Form state | useStates or useForm |
| Page state (loading, error) | useStates |
| User session | useGlobalStates() -> gst.get('session') |
| Preferences | useGlobalStates() -> gst.get('settings') |
| Shared data | useGlobalStates() -> gst.get/set |
| Large volumes, offline | useDb |
Structuring the global state
globalState: {
reducers: {
// Auth
session: null,
// Preferences
settings: { lng: 'fr', theme: 'light' },
// Data cache
cache: {
categories: [],
lastFetch: null
}
}
}
API calls
Always handle errors
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 for debugging
console.error('Load error:', err);
} finally {
st.set('loading', false);
}
};
Cancel requests on unmount
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();
}, []);
Forms
Manual validation with setField
// useForm does not validate automatically.
// Validate the fields manually:
form.setField({
name: 'email',
value: inputValue,
errors: {
required: { condition: !inputValue },
format: { condition: inputValue && !inputValue.includes('@') }
}
});
Displaying errors
// Check the errors
const hasError = form.get('errors.email.required');
const hasFormatError = form.get('errors.email.format');
Security
Never store secrets on the client side
// NEVER
const API_KEY = 'secret123';
// OK: server environment variables only
// The client only has access to the VITE_ prefixed ones
const API_URL = import.meta.env.VITE_API_URL;
Validate on the server side
Never trust client data:
// PHP Controller
public function create($payload)
{
// Always validate
if (empty($payload['label'])) {
return ['Label required', 400];
}
// Always check the permissions
if (!$user->hasRight('mymodule', 'create')) {
return ['Forbidden', 403];
}
// Always escape
$label = $db->escape($payload['label']);
}
Protecting against XSS
React escapes automatically, but beware of dangerouslySetInnerHTML:
// Dangerous
<div dangerouslySetInnerHTML={{ __html: userContent }} />
// If needed, sanitize first
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userContent) }} />
Tests
Test the critical components
// TasksPage.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import { TasksPage } from './TasksPage';
test('displays the task list', async () => {
render(<TasksPage />);
await waitFor(() => {
expect(screen.getByText('My tasks')).toBeInTheDocument();
});
});
Debug
Use the tools
- React DevTools: inspect the components and the state
- Redux DevTools: see the actions and the global state
- Console: st.set with debug: true
const st = useStates({
initialStates: { ... },
debug: import.meta.env.DEV // Logs in dev only
});
Pre-deployment checklist
- [ ] All errors are handled
- [ ] Loading states show a spinner
- [ ] Validation works on the client AND the server
- [ ] Permissions are checked on the server side
- [ ] Offline mode works (if applicable)
- [ ] Translations are complete
- [ ] Performance is acceptable
- [ ] HTTPS is enabled
- [ ] Production environment variables are configured
Resources
- React documentation: https://react.dev
- SmartCommon documentation: https://inligit.fr/cap-rel/dolibarr/smartmaker/smartcommon
- Dolibarr documentation: https://wiki.dolibarr.org