JavaScript and React course Β· Module 9: Modern React Hooks

Project: Advanced React 18+ Application

13 min read
In this lesson4

Every crew eventually goes through a full mission simulation, with all systems running at once. You already know transitions, deferred values, useId, use and useOptimistic, but separately. Now they have to cooperate in one codebase.

In this project, you will create a modern React application using all the React 18+ features you have learned. You will build an Advanced Task Management System - an advanced task management system utilizing Concurrent Features, Server Components, and modern hooks.

Project Specification

Main Features

  1. Dashboard with real-time updates

    • Using useTransition and useDeferredValue
    • Optimistic updates with useOptimistic
    • Content streaming with Suspense
  2. Advanced forms

    • useFormStatus and useActionState
    • Server Actions
    • Real-time validation
  3. Notification system

    • useId for accessibility
    • Concurrent rendering
    • Progressive enhancement
  4. Performance and optimization

    • React Compiler (optional)
    • Automatic Batching
    • Selective Hydration

React Compiler has had a stable 1.0 release since 2025 and adds memoization by itself, so it is worth considering.

Application Structure

The project uses the Next.js App Router, a framework that supports Server Components and Server Actions. Here is the directory map:

1src/
2β”œβ”€β”€ app/
3β”‚   β”œβ”€β”€ layout.tsx          # Root layout (Server Component)
4β”‚   β”œβ”€β”€ page.tsx            # Home page
5β”‚   β”œβ”€β”€ dashboard/
6β”‚   β”‚   β”œβ”€β”€ page.tsx        # Dashboard (Server Component)
7β”‚   β”‚   └── loading.tsx     # Loading UI
8β”‚   β”œβ”€β”€ tasks/
9β”‚   β”‚   β”œβ”€β”€ page.tsx        # Tasks list
10β”‚   β”‚   β”œβ”€β”€ [id]/page.tsx   # Task details
11β”‚   β”‚   └── new/page.tsx    # Create task
12β”‚   └── api/
13β”‚       β”œβ”€β”€ tasks/route.ts  # Tasks API
14β”‚       └── notifications/route.ts
15β”œβ”€β”€ components/
16β”‚   β”œβ”€β”€ ui/                 # Basic UI components
17β”‚   β”œβ”€β”€ forms/              # Form components
18β”‚   β”œβ”€β”€ dashboard/          # Dashboard components
19β”‚   └── tasks/              # Task components
20β”œβ”€β”€ hooks/                  # Custom hooks
21β”œβ”€β”€ lib/                    # Utilities
22└── types/                  # TypeScript types

The app/ folder holds the routes, components/ the UI building blocks, and lib/ the server logic.

Step-by-step Implementation

Step 1: Basic structure and types

We start with the data. A TypeScript interface is a contract describing the fields of a task, a user and a notification:

1// types/index.ts
2export interface Task {
3  id: string;
4  title: string;
5  description: string;
6  status: 'pending' | 'in_progress' | 'completed';
7  priority: 'low' | 'medium' | 'high';
8  assignee?: string;
9  dueDate?: Date;
10  createdAt: Date;
11  updatedAt: Date;
12}
13
14export interface User {
15  id: string;
16  name: string;
17  email: string;
18  avatar?: string;
19}
20
21export interface Notification {
22  id: string;
23  type: 'task_created' | 'task_updated' | 'task_completed';
24  message: string;
25  timestamp: Date;
26  read: boolean;
27}

Fields with a question mark, such as assignee?, are optional, and unions like 'low' | 'medium' | 'high' allow only the listed values.

Step 2: Root Layout with Server Components

The layout is an async Server Component, so it can fetch the user's data while still on the server:

1// app/layout.tsx (Server Component)
2import { Suspense } from 'react';
3import { Navigation } from '@/components/Navigation';
4import { NotificationProvider } from '@/components/NotificationProvider';
5
6async function RootLayout({ children }: { children: React.ReactNode }) {
7  // Fetch user data on the server
8  const user = await getCurrentUser();
9  const notifications = await getUnreadNotifications(user.id);
10
11  return (
12    <html lang="en">
13      <head>
14        <title>Advanced Task Manager</title>
15        <meta name="description" content="Advanced task management system" />
16      </head>
17      <body>
18        <NotificationProvider initialNotifications={notifications}>
19          <div className="app-layout">
20            <Suspense fallback={<NavigationSkeleton />}>
21              <Navigation user={user} />
22            </Suspense>
23
24            <main className="main-content">
25              <Suspense fallback={<PageSkeleton />}>
26                {children}
27              </Suspense>
28            </main>
29          </div>
30        </NotificationProvider>
31      </body>
32    </html>
33  );
34}
35
36export default RootLayout;

Two Suspense boundaries let the navigation and the content appear independently. In Next.js, the page title and description are usually defined through a metadata export rather than a hand-written <head>.

Step 3: Dashboard with Concurrent Features

The dashboard page fetches statistics and recent tasks in parallel and streams the slower sections:

1// app/dashboard/page.tsx (Server Component)
2import { Suspense } from 'react';
3import { TasksOverview } from '@/components/dashboard/TasksOverview';
4import { RecentActivity } from '@/components/dashboard/RecentActivity';
5import { PerformanceMetrics } from '@/components/dashboard/PerformanceMetrics';
6
7async function DashboardPage() {
8  // Parallel data fetching
9  const [tasksStats, recentTasks] = await Promise.all([
10    getTasksStatistics(),
11    getRecentTasks(10)
12  ]);
13
14  return (
15    <div className="dashboard">
16      <h1>Dashboard</h1>
17
18      {/* Fast-loading section */}
19      <TasksOverview stats={tasksStats} />
20
21      <div className="dashboard-grid">
22        {/* Streams independently */}
23        <Suspense fallback={<RecentActivitySkeleton />}>
24          <RecentActivity initialTasks={recentTasks} />
25        </Suspense>
26
27        {/* Can load later */}
28        <Suspense fallback={<MetricsSkeleton />}>
29          <PerformanceMetrics />
30        </Suspense>
31      </div>
32    </div>
33  );
34}
35
36export default DashboardPage;

TasksOverview appears immediately, while PerformanceMetrics loads later.

Step 4: Interactive Components with new hooks

The 'use client' directive marks the boundary beyond which code is sent to the browser and can use state:

1// components/dashboard/RecentActivity.tsx (Client Component)
2'use client';
3
4import { useState, useTransition, useDeferredValue } from 'react';
5import { Task } from '@/types';
6
7interface RecentActivityProps {
8  initialTasks: Task[];
9}
10
11export function RecentActivity({ initialTasks }: RecentActivityProps) {
12  const [tasks, setTasks] = useState(initialTasks);
13  const [searchQuery, setSearchQuery] = useState('');
14  const [isPending, startTransition] = useTransition();
15
16  // Deferred value for search
17  const deferredSearchQuery = useDeferredValue(searchQuery);
18
19  // Filter tasks using deferredValue
20  const filteredTasks = tasks.filter(task =>
21    task.title.toLowerCase().includes(deferredSearchQuery.toLowerCase()) ||
22    task.description.toLowerCase().includes(deferredSearchQuery.toLowerCase())
23  );
24
25  const handleSearch = (query: string) => {
26    setSearchQuery(query);
27
28    // Use transition for less priority updates
29    startTransition(() => {
30      // Additional operations that can be interrupted
31      updateSearchAnalytics(query);
32    });
33  };
34
35  const refreshTasks = async () => {
36    startTransition(async () => {
37      const newTasks = await fetchRecentTasks();
38      setTasks(newTasks);
39    });
40  };
41
42  return (
43    <div className="recent-activity">
44      <div className="activity-header">
45        <h2>Recent Activity</h2>
46        <button
47          onClick={refreshTasks}
48          disabled={isPending}
49          className={isPending ? 'loading' : ''}
50        >
51          {isPending ? 'Refreshing...' : 'Refresh'}
52        </button>
53      </div>
54
55      <SearchInput
56        value={searchQuery}
57        onChange={handleSearch}
58        isPending={isPending}
59      />
60
61      <div className="tasks-list">
62        {filteredTasks.map(task => (
63          <TaskCard
64            key={task.id}
65            task={task}
66            onUpdate={setTasks}
67          />
68        ))}
69
70        {deferredSearchQuery && filteredTasks.length === 0 && (
71          <div className="no-results">
72            No results for "{deferredSearchQuery}"
73          </div>
74        )}
75      </div>
76    </div>
77  );
78}

The list filters by deferredSearchQuery, so the search field does not slow down. Two notes: startTransition in handleSearch does not interrupt the updateSearchAnalytics call itself, because only renders caused by state changes can be interrupted. And setTasks after await should, according to react.dev, go into another startTransition.

Step 5: Advanced forms with Server Actions

The form combines three hooks. useActionState holds the action's result, useOptimistic shows the save immediately, and useFormStatus tells the button about the submission. Important: useFormStatus is imported from react-dom, not from react. First, the logic:

1// components/forms/TaskForm.tsx (Client Component)
2'use client';
3
4import { useActionState, useEffect, useId, useOptimistic } from 'react';
5import { useFormStatus } from 'react-dom';
6import { Task } from '@/types';
7import { createTask, updateTask } from '@/lib/actions';
8
9interface TaskFormProps {
10  task?: Task;
11  onSuccess?: () => void;
12}
13
14export function TaskForm({ task, onSuccess }: TaskFormProps) {
15  const formId = useId();
16  const titleId = useId();
17  const descriptionId = useId();
18
19  // useActionState for action state management
20  const [state, formAction] = useActionState(
21    task ? updateTask.bind(null, task.id) : createTask,
22    {
23      success: false,
24      errors: {},
25      data: task || null
26    }
27  );
28
29  // useOptimistic for instant feedback
30  const [optimisticState, addOptimistic] = useOptimistic(
31    state,
32    (currentState, optimisticUpdate) => ({
33      ...currentState,
34      ...optimisticUpdate,
35      optimistic: true
36    })
37  );
38
39  const handleSubmit = async (formData: FormData) => {
40    // Optimistic update
41    addOptimistic({
42      success: true,
43      data: {
44        id: task?.id || 'temp-' + Date.now(),
45        title: formData.get('title') as string,
46        description: formData.get('description') as string,
47        status: formData.get('status') as Task['status'],
48        priority: formData.get('priority') as Task['priority'],
49        createdAt: new Date(),
50        updatedAt: new Date()
51      }
52    });
53
54    // Execute actual action
55    formAction(formData);
56  };
57
58  // The new state arrives in the next render, so we react to it in an effect
59  useEffect(() => {
60    if (state.success) {
61      onSuccess?.();
62    }
63  }, [state, onSuccess]);
64

The formAction function does not return a Promise, and state in this closure is still the old state. That is why we react to success in an effect, once the new state arrives in the next render. Now the form view:

1  return (
2    <form
3      id={formId}
4      action={handleSubmit}
5      className="task-form"
6    >
7      <div className="form-group">
8        <label htmlFor={titleId}>Task title</label>
9        <input
10          id={titleId}
11          name="title"
12          type="text"
13          required
14          defaultValue={optimisticState.data?.title || ''}
15          className={optimisticState.errors?.title ? 'error' : ''}
16        />
17        {optimisticState.errors?.title && (
18          <span className="error-message" role="alert">
19            {optimisticState.errors.title}
20          </span>
21        )}
22      </div>
23
24      <div className="form-group">
25        <label htmlFor={descriptionId}>Description</label>
26        <textarea
27          id={descriptionId}
28          name="description"
29          rows={4}
30          defaultValue={optimisticState.data?.description || ''}
31        />
32      </div>
33
34      <div className="form-group">
35        <label>Status</label>
36        <select name="status" defaultValue={optimisticState.data?.status || 'pending'}>
37          <option value="pending">Pending</option>
38          <option value="in_progress">In Progress</option>
39          <option value="completed">Completed</option>
40        </select>
41      </div>
42
43      <div className="form-group">
44        <label>Priority</label>
45        <select name="priority" defaultValue={optimisticState.data?.priority || 'medium'}>
46          <option value="low">Low</option>
47          <option value="medium">Medium</option>
48          <option value="high">High</option>
49        </select>
50      </div>
51
52      <FormSubmitButton />
53
54      {optimisticState.optimistic && (
55        <div className="optimistic-indicator">
56          Saving...
57        </div>
58      )}
59
60      {optimisticState.success && !optimisticState.optimistic && (
61        <div className="success-message">
62          Task has been {task ? 'updated' : 'created'}!
63        </div>
64      )}
65    </form>
66  );
67}
68
69// Component using useFormStatus
70function FormSubmitButton() {
71  const { pending } = useFormStatus();
72
73  return (
74    <button
75      type="submit"
76      disabled={pending}
77      className={pending ? 'loading' : ''}
78    >
79      {pending ? 'Saving...' : 'Save task'}
80    </button>
81  );
82}

FormSubmitButton is a separate component because useFormStatus reads the status of the parent form, so it must be called inside a <form>.

Step 6: Server Actions

The 'use server' directive makes these functions run only on the server. We validate with the Zod library:

1// lib/actions.ts
2'use server';
3
4import { revalidatePath } from 'next/cache';
5import { redirect } from 'next/navigation';
6import { z } from 'zod';
7
8const TaskSchema = z.object({
9  title: z.string().min(1, 'Title is required').max(100, 'Title is too long'),
10  description: z.string().optional(),
11  status: z.enum(['pending', 'in_progress', 'completed']),
12  priority: z.enum(['low', 'medium', 'high']),
13  dueDate: z.string().optional().transform(str => str ? new Date(str) : undefined)
14});
15
16export async function createTask(prevState: any, formData: FormData) {
17  try {
18    const validatedFields = TaskSchema.safeParse({
19      title: formData.get('title'),
20      description: formData.get('description'),
21      status: formData.get('status'),
22      priority: formData.get('priority'),
23      dueDate: formData.get('dueDate')
24    });
25
26    if (!validatedFields.success) {
27      return {
28        success: false,
29        errors: z.flattenError(validatedFields.error).fieldErrors,
30        data: null
31      };
32    }
33
34    const task = await db.tasks.create({
35      data: {
36        ...validatedFields.data,
37        id: generateId(),
38        createdAt: new Date(),
39        updatedAt: new Date()
40      }
41    });
42
43    // Revalidate cache
44    revalidatePath('/dashboard');
45    revalidatePath('/tasks');
46
47    return {
48      success: true,
49      errors: {},
50      data: task
51    };
52
53  } catch (error) {
54    return {
55      success: false,
56      errors: { _form: ['An error occurred while creating the task'] },
57      data: null
58    };
59  }
60}
61

safeParse does not throw, it returns an object with a success field. We flatten errors with z.flattenError, because in Zod 4 the error.flatten() method is marked as deprecated. The update action looks similar:

1export async function updateTask(taskId: string, prevState: any, formData: FormData) {
2  try {
3    const validatedFields = TaskSchema.safeParse({
4      title: formData.get('title'),
5      description: formData.get('description'),
6      status: formData.get('status'),
7      priority: formData.get('priority'),
8      dueDate: formData.get('dueDate')
9    });
10
11    if (!validatedFields.success) {
12      return {
13        success: false,
14        errors: z.flattenError(validatedFields.error).fieldErrors,
15        data: null
16      };
17    }
18
19    const task = await db.tasks.update({
20      where: { id: taskId },
21      data: {
22        ...validatedFields.data,
23        updatedAt: new Date()
24      }
25    });
26
27    revalidatePath(`/tasks/${taskId}`);
28    revalidatePath('/dashboard');
29
30    return {
31      success: true,
32      errors: {},
33      data: task
34    };
35
36  } catch (error) {
37    return {
38      success: false,
39      errors: { _form: ['An error occurred while updating the task'] },
40      data: null
41    };
42  }
43}

updateTask receives taskId as its first argument thanks to bind in the form, and revalidatePath refreshes the pages' data.

Step 7: Advanced Suspense Patterns

Suspense boundaries can be nested, all the way down to a single card:

1// components/tasks/TasksList.tsx (Server Component)
2import { Suspense } from 'react';
3import { TaskCard } from './TaskCard';
4import { TasksFilter } from './TasksFilter';
5
6async function TasksList({ searchParams }: { searchParams: any }) {
7  return (
8    <div className="tasks-page">
9      <div className="tasks-header">
10        <h1>Tasks</h1>
11
12        {/* Filter doesn't require Suspense - loads quickly */}
13        <TasksFilter />
14      </div>
15
16      <div className="tasks-content">
17        {/* Main task list */}
18        <Suspense fallback={<TasksListSkeleton />}>
19          <TasksGrid searchParams={searchParams} />
20        </Suspense>
21
22        {/* Sidebar with additional info */}
23        <aside className="tasks-sidebar">
24          <Suspense fallback={<StatsSkeleton />}>
25            <TasksStatistics />
26          </Suspense>
27
28          <Suspense fallback={<ActivitySkeleton />}>
29            <RecentActivity />
30          </Suspense>
31        </aside>
32      </div>
33    </div>
34  );
35}
36
37// Component with long load time
38async function TasksGrid({ searchParams }: { searchParams: any }) {
39  // Simulate a long query
40  const tasks = await getFilteredTasks(searchParams);
41
42  return (
43    <div className="tasks-grid">
44      {tasks.map(task => (
45        <Suspense
46          key={task.id}
47          fallback={<TaskCardSkeleton />}
48        >
49          <TaskCard task={task} />
50        </Suspense>
51      ))}
52    </div>
53  );
54}

Since Next.js 15, searchParams on a page is a Promise, so you have to read it with await before using it.

Step 8: Accessibility with useId

A universal form field injects identifiers into its child:

1// components/ui/FormField.tsx
2'use client';
3
4import { useId, forwardRef, cloneElement } from 'react';
5
6interface FormFieldProps {
7  label: string;
8  error?: string;
9  help?: string;
10  required?: boolean;
11  children: React.ReactNode;
12}
13
14export const FormField = forwardRef<HTMLDivElement, FormFieldProps>(
15  ({ label, error, help, required, children }, ref) => {
16    const fieldId = useId();
17    const errorId = useId();
18    const helpId = useId();
19
20    return (
21      <div ref={ref} className="form-field">
22        <label
23          htmlFor={fieldId}
24          className={required ? 'required' : ''}
25        >
26          {label}
27          {required && <span aria-hidden="true">*</span>}
28        </label>
29
30        <div className="field-input">
31          {cloneElement(children as React.ReactElement, {
32            id: fieldId,
33            'aria-describedby': [
34              help ? helpId : null,
35              error ? errorId : null
36            ].filter(Boolean).join(' ') || undefined,
37            'aria-invalid': error ? 'true' : undefined,
38            required
39          })}
40        </div>
41
42        {help && (
43          <div id={helpId} className="field-help">
44            {help}
45          </div>
46        )}
47
48        {error && (
49          <div
50            id={errorId}
51            className="field-error"
52            role="alert"
53            aria-live="polite"
54          >
55            {error}
56          </div>
57        )}
58      </div>
59    );
60  }
61);
62
63FormField.displayName = 'FormField';

In React 19, ref is a regular prop of a function component, and forwardRef is planned to be deprecated in the future, so in new code you can accept ref directly.

Step 9: Performance Monitoring

Finally, a context that collects performance metrics:

1// components/providers/PerformanceProvider.tsx
2'use client';
3
4import { createContext, useContext, useEffect, useState } from 'react';
5
6interface PerformanceMetrics {
7  renderTime: number;
8  interactionTime: number;
9  suspenseTime: number;
10}
11
12const PerformanceContext = createContext<{
13  metrics: PerformanceMetrics;
14  recordMetric: (type: keyof PerformanceMetrics, time: number) => void;
15}>({
16  metrics: { renderTime: 0, interactionTime: 0, suspenseTime: 0 },
17  recordMetric: () => {}
18});
19
20export function PerformanceProvider({ children }: { children: React.ReactNode }) {
21  const [metrics, setMetrics] = useState<PerformanceMetrics>({
22    renderTime: 0,
23    interactionTime: 0,
24    suspenseTime: 0
25  });
26
27  const recordMetric = (type: keyof PerformanceMetrics, time: number) => {
28    setMetrics(prev => ({
29      ...prev,
30      [type]: time
31    }));
32
33    // Send metrics to analytics system
34    if (time > getThreshold(type)) {
35      analytics.track('performance_issue', {
36        type,
37        time,
38        url: window.location.pathname
39      });
40    }
41  };
42
43  useEffect(() => {
44    // Observe Web Vitals
45    const observer = new PerformanceObserver((list) => {
46      for (const entry of list.getEntries()) {
47        if (entry.entryType === 'measure') {
48          recordMetric('renderTime', entry.duration);
49        }
50      }
51    });
52
53    observer.observe({ entryTypes: ['measure'] });
54
55    return () => observer.disconnect();
56  }, []);
57
58  return (
59    <PerformanceContext.Provider value={{ metrics, recordMetric }}>
60      {children}
61    </PerformanceContext.Provider>
62  );
63}
64
65export const usePerformance = () => useContext(PerformanceContext);
66
67function getThreshold(type: keyof PerformanceMetrics): number {
68  const thresholds = {
69    renderTime: 100,      // 100ms
70    interactionTime: 50,  // 50ms
71    suspenseTime: 1000    // 1s
72  };
73  return thresholds[type];
74}

PerformanceObserver listens here for measure entries, meaning your own measurements, not Web Vitals. For Core Web Vitals use the web-vitals library.

Project Summary

This project demonstrates:

  1. Concurrent Features: useTransition, useDeferredValue for responsive UI
  2. Form Handling: useActionState, useFormStatus with Server Actions
  3. Accessibility: useId for unique identifiers
  4. Optimistic Updates: useOptimistic for instant feedback
  5. Server Components: Server-side rendering with parallel data fetching
  6. Suspense Patterns: Hierarchical loading with streaming
  7. Performance: Monitoring and performance optimization
  8. Error Handling: Granular Error Boundaries
  9. Type Safety: Full TypeScript utilization

Next steps for extension:

  1. Add real-time synchronization with WebSockets
  2. Implement offline support with Service Workers
  3. Add advanced filters and sorting
  4. Create a user permissions system
  5. Integrate with push notification system
  6. Add data export to various formats
  7. Implement dark mode with CSS-in-JS
  8. Create a mobile app with React Native

This project is a solid foundation for building modern React applications utilizing the latest framework features.

My advice: build step by step and run the application after every step. Remember: a good mission is one where every system on the ship knows its job, and mission control sees all of them at once.

Code for this lesson: App.jsx
1import React, {
2  useState, useMemo, useCallback, memo,
3  useTransition, useDeferredValue, Suspense, lazy, createContext, use,
4} from 'react';
5
6// ========================
7// APPLICATION CONTEXTS
8// ========================
9const ThemeContext = createContext('dark');
10
11// ========================
12// SIMULATED DATA
13// ========================
14function generateCrewMembers(count) {
15  const roles = ['Pilot', 'Navigator', 'Engineer', 'Medic', 'Scientist', 'Technician'];
16  const statuses = ['Active', 'On mission', 'Resting', 'Training'];
17  const members = [];
18  for (let i = 0; i < count; i++) {
19    members.push({
20      id: i + 1,
21      name: `Astronaut-${(i + 1).toString().padStart(3, '0')}`,
22      role: roles[i % roles.length],
23      status: statuses[i % statuses.length],
24      missions: Math.floor(Math.random() * 50),
25      rating: (3 + Math.random() * 2).toFixed(1),
26    });
27  }
28  return members;
29}
30
31const ALL_CREW = generateCrewMembers(200);
32
33// ========================
34// OPTIMIZED COMPONENTS
35// ========================
36
37// React.memo - prevents unnecessary re-renders
38const StatCard = memo(function StatCard({ label, value, color, icon }) {
39  return (
40    <div style={{
41      padding: '16px', borderRadius: '10px',
42      background: `${color}11`, border: `1px solid ${color}33`,
43      textAlign: 'center', minWidth: '120px',
44    }}>
45      <div style={{ fontSize: '24px', marginBottom: '4px' }}>{icon}</div>
46      <div style={{ fontSize: '22px', fontWeight: 'bold', color }}>{value}</div>
47      <div style={{ fontSize: '12px', color: '#778da9', marginTop: '4px' }}>{label}</div>
48    </div>
49  );
50});
51
52// Memoized table row
53const CrewRow = memo(function CrewRow({ member, onSelect }) {
54  const statusColor = {
55    'Active': '#4ecdc4',
56    'On mission': '#ffd700',
57    'Resting': '#778da9',
58    'Training': '#00d4ff',
59  }[member.status] || '#778da9';
60
61  return (
62    <div
63      onClick={() => onSelect(member)}
64      style={{
65        display: 'grid', gridTemplateColumns: '1fr 100px 90px 60px 50px',
66        padding: '10px 14px', fontSize: '13px',
67        borderBottom: '1px solid #1a2a3c', cursor: 'pointer',
68        transition: 'background 0.15s',
69      }}
70      onMouseEnter={e => e.currentTarget.style.background = 'rgba(0,212,255,0.05)'}
71      onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
72    >
73      <span style={{ color: '#e0e1dd' }}>{member.name}</span>
74      <span style={{ color: '#778da9' }}>{member.role}</span>
75      <span style={{ color: statusColor }}>{member.status}</span>
76      <span style={{ color: '#c0c8d0', textAlign: 'center' }}>{member.missions}</span>
77      <span style={{ color: '#ffd700', textAlign: 'center' }}>{member.rating}</span>
78    </div>
79  );
80});
81
82// TODO 1: Wrap DetailPanel component in React.memo
83// It should render only when selectedMember changes
84function DetailPanel({ selectedMember }) {
85  if (!selectedMember) {
86    return (
87      <div style={{
88        padding: '40px', textAlign: 'center', color: '#556677',
89        background: 'rgba(255,255,255,0.02)', borderRadius: '10px',
90      }}>
91        Select a crew member to see details
92      </div>
93    );
94  }
95  return (
96    <div style={{
97      padding: '20px', background: 'rgba(0, 212, 255, 0.04)',
98      borderRadius: '10px', border: '1px solid #1a3a5c',
99    }}>
100      <h3 style={{ color: '#00d4ff', margin: '0 0 12px' }}>{selectedMember.name}</h3>
101      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px', fontSize: '13px' }}>
102        <div><span style={{ color: '#778da9' }}>Role:</span> <span style={{ color: '#e0e1dd' }}>{selectedMember.role}</span></div>
103        <div><span style={{ color: '#778da9' }}>Status:</span> <span style={{ color: '#4ecdc4' }}>{selectedMember.status}</span></div>
104        <div><span style={{ color: '#778da9' }}>Missions:</span> <span style={{ color: '#e0e1dd' }}>{selectedMember.missions}</span></div>
105        <div><span style={{ color: '#778da9' }}>Rating:</span> <span style={{ color: '#ffd700' }}>{selectedMember.rating}/5.0</span></div>
106      </div>
107    </div>
108  );
109}
110
111// ========================
112// MAIN DASHBOARD
113// ========================
114export default function App() {
115  const [searchQuery, setSearchQuery] = useState('');
116  const [statusFilter, setStatusFilter] = useState('all');
117  const [selectedMember, setSelectedMember] = useState(null);
118  const [isPending, startTransition] = useTransition();
119
120  // useDeferredValue - deferred search
121  const deferredSearch = useDeferredValue(searchQuery);
122
123  // useMemo - memoizing the filtered list
124  const filteredCrew = useMemo(() => {
125    return ALL_CREW.filter(member => {
126      const matchesSearch = member.name.toLowerCase().includes(deferredSearch.toLowerCase()) ||
127                            member.role.toLowerCase().includes(deferredSearch.toLowerCase());
128      const matchesStatus = statusFilter === 'all' || member.status === statusFilter;
129      return matchesSearch && matchesStatus;
130    });
131  }, [deferredSearch, statusFilter]);
132
133  // useMemo - calculating statistics
134  const stats = useMemo(() => ({
135    total: ALL_CREW.length,
136    active: ALL_CREW.filter(m => m.status === 'Active').length,
137    onMission: ALL_CREW.filter(m => m.status === 'On mission').length,
138    avgRating: (ALL_CREW.reduce((sum, m) => sum + parseFloat(m.rating), 0) / ALL_CREW.length).toFixed(1),
139  }), []);
140
141  // useCallback - stable function reference for memoized components
142  const handleSelect = useCallback((member) => {
143    setSelectedMember(member);
144  }, []);
145
146  // TODO 2: Use useCallback for handleStatusChange
147  // to prevent re-renders of the filtering components
148  const handleStatusChange = (status) => {
149    startTransition(() => {
150      setStatusFilter(status);
151    });
152  };
153
154  // TODO 3: Use useMemo to compute topPerformers
155  // (top 5 members by rating from the filtered list)
156  // sort changes the array in place, so we sort a copy: otherwise the table would also be ordered by rating
157  const topPerformers = [...filteredCrew]
158    .sort((a, b) => parseFloat(b.rating) - parseFloat(a.rating))
159    .slice(0, 5);
160
161  const STATUSES = ['all', 'Active', 'On mission', 'Resting', 'Training'];
162
163  return (
164    <div style={{
165      minHeight: '100vh',
166      background: 'linear-gradient(135deg, #0a0e27 0%, #1a1a3e 100%)',
167      padding: '20px', fontFamily: 'monospace', color: '#e0e1dd',
168    }}>
169      <h1 style={{ color: '#00d4ff', textAlign: 'center', margin: '0 0 4px' }}>
170        Fleet Command Center
171      </h1>
172      <p style={{ color: '#778da9', textAlign: 'center', marginBottom: '20px', fontSize: '13px' }}>
173        Dashboard with React.memo | useMemo | useCallback | useTransition | useDeferredValue
174      </p>
175
176      {/* Statistics */}
177      <div style={{
178        display: 'flex', gap: '12px', justifyContent: 'center',
179        marginBottom: '20px', flexWrap: 'wrap',
180      }}>
181        <StatCard icon="β—†" label="Total crew" value={stats.total} color="#00d4ff" />
182        <StatCard icon="OK" label="Active" value={stats.active} color="#4ecdc4" />
183        <StatCard icon="β–²" label="On mission" value={stats.onMission} color="#ffd700" />
184        <StatCard icon="β—†" label="Average rating" value={stats.avgRating} color="#ff6b4a" />
185      </div>
186
187      <div style={{ maxWidth: '800px', margin: '0 auto' }}>
188        {/* Search and filters */}
189        <div style={{ display: 'flex', gap: '8px', marginBottom: '12px', flexWrap: 'wrap' }}>
190          <input
191            value={searchQuery}
192            onChange={e => setSearchQuery(e.target.value)}
193            placeholder="Search crew member..."
194            style={{
195              flex: 1, minWidth: '200px', padding: '10px 14px',
196              background: '#0d1b2a', border: '1px solid #1a3a5c',
197              borderRadius: '8px', color: '#e0e1dd', fontSize: '14px',
198            }}
199          />
200          {STATUSES.map(status => (
201            <button
202              key={status}
203              onClick={() => handleStatusChange(status)}
204              style={{
205                padding: '8px 14px',
206                background: statusFilter === status ? '#1a3a5c' : 'transparent',
207                border: `1px solid ${statusFilter === status ? '#00d4ff' : '#1a3a5c'}`,
208                borderRadius: '6px', fontSize: '12px',
209                color: statusFilter === status ? '#00d4ff' : '#778da9',
210                cursor: 'pointer',
211              }}
212            >
213              {status === 'all' ? 'All' : status}
214            </button>
215          ))}
216        </div>
217
218        {isPending && (
219          <div style={{ color: '#00d4ff', fontSize: '12px', marginBottom: '8px' }}>
220            Filtering...
221          </div>
222        )}
223
224        <div style={{ display: 'grid', gridTemplateColumns: '1fr 280px', gap: '16px' }}>
225          {/* Crew table */}
226          <div>
227            <div style={{
228              display: 'grid', gridTemplateColumns: '1fr 100px 90px 60px 50px',
229              padding: '8px 14px', fontSize: '11px', color: '#556677',
230              borderBottom: '2px solid #1a3a5c', textTransform: 'uppercase',
231            }}>
232              <span>Name</span><span>Role</span><span>Status</span>
233              <span style={{ textAlign: 'center' }}>Missions</span>
234              <span style={{ textAlign: 'center' }}>Rating</span>
235            </div>
236            <div style={{ maxHeight: '350px', overflow: 'auto' }}>
237              {filteredCrew.slice(0, 50).map(member => (
238                <CrewRow
239                  key={member.id}
240                  member={member}
241                  onSelect={handleSelect}
242                />
243              ))}
244            </div>
245            <div style={{ padding: '8px 14px', fontSize: '12px', color: '#556677' }}>
246              Showing {Math.min(50, filteredCrew.length)} of {filteredCrew.length} results
247            </div>
248          </div>
249
250          {/* Side panel */}
251          <div>
252            <DetailPanel selectedMember={selectedMember} />
253
254            <div style={{ marginTop: '16px' }}>
255              <h4 style={{ color: '#ffd700', margin: '0 0 8px', fontSize: '13px' }}>
256                Top 5 - highest rating
257              </h4>
258              {topPerformers.map((m, i) => (
259                <div key={m.id} style={{
260                  display: 'flex', justifyContent: 'space-between',
261                  padding: '6px 10px', fontSize: '12px',
262                  background: i === 0 ? 'rgba(255, 215, 0, 0.06)' : 'transparent',
263                  borderRadius: '4px',
264                }}>
265                  <span style={{ color: '#c0c8d0' }}>
266                    {i + 1}. {m.name}
267                  </span>
268                  <span style={{ color: '#ffd700' }}>{m.rating}</span>
269                </div>
270              ))}
271            </div>
272          </div>
273        </div>
274      </div>
275    </div>
276  );
277}

Spotted a mistake in this lesson?

Useful articles