perf(media): memoize toast context and improve loading state UX

- wrap ToastContext value in useMemo to prevent unnecessary re-renders
- show skeleton only on initial load, use opacity transition for subsequent fetches
- destructure toastError from toast context to stabilize fetchItems dependency
This commit is contained in:
2026-04-26 19:22:55 -04:00
parent 87e5889b76
commit 66ced30d8f
3 changed files with 8 additions and 7 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { createContext, useContext, useState, useCallback } from 'react'; import React, { createContext, useContext, useState, useCallback, useMemo } from 'react';
const ToastContext = createContext(); const ToastContext = createContext();
@@ -88,7 +88,7 @@ export const ToastProvider = ({ children }) => {
}); });
}, [addToast]); }, [addToast]);
const value = { const value = useMemo(() => ({
toasts, toasts,
addToast, addToast,
removeToast, removeToast,
@@ -97,7 +97,7 @@ export const ToastProvider = ({ children }) => {
error, error,
warning, warning,
info, info,
}; }), [toasts, addToast, removeToast, clearAllToasts, success, error, warning, info]);
return ( return (
<ToastContext.Provider value={value}> <ToastContext.Provider value={value}>
@@ -40,7 +40,7 @@ export default function MediaGrid({
selectedIds = new Set(), selectedIds = new Set(),
emptyMessage = 'Aucun média', emptyMessage = 'Aucun média',
}) { }) {
if (loading) { if (loading && items.length === 0) {
return ( return (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3"> <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
{Array.from({ length: 12 }).map((_, i) => ( {Array.from({ length: 12 }).map((_, i) => (
@@ -59,7 +59,7 @@ export default function MediaGrid({
} }
return ( return (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3"> <div className={`grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3 transition-opacity ${loading ? 'opacity-60' : ''}`}>
{items.map(media => { {items.map(media => {
const Icon = iconForMime(media.mime_type); const Icon = iconForMime(media.mime_type);
const isSelected = selectedIds.has(media.id); const isSelected = selectedIds.has(media.id);
+3 -2
View File
@@ -204,6 +204,7 @@ const MediaPage = ({ user }) => {
const [filters, setFilters] = useState({ search: '', kind: '', visibility: '' }); const [filters, setFilters] = useState({ search: '', kind: '', visibility: '' });
const [selected, setSelected] = useState(null); const [selected, setSelected] = useState(null);
const { error: toastError } = toast;
const fetchItems = useCallback(async () => { const fetchItems = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
@@ -217,11 +218,11 @@ const MediaPage = ({ user }) => {
const data = await response.json(); const data = await response.json();
setItems(data.media || []); setItems(data.media || []);
} catch (err) { } catch (err) {
toast.error(err.message || 'Échec du chargement'); toastError(err.message || 'Échec du chargement');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [filters, toast]); }, [filters, toastError]);
useEffect(() => { useEffect(() => {
fetchItems(); fetchItems();