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

Previous Chapter | Back to the training index