We use cookies to enhance your experience on the site
CodeWorlds

useOptimistic - optimistic UI updates

The

useOptimistic
hook allows you to instantly update the user interface before receiving a response from the server. This is a key tool for creating responsive applications that give the user the impression of instantaneous reaction.

useOptimistic Basics

1. Syntax and basic usage

1import { useOptimistic } from 'react';
2
3function TodoList() {
4  const [todos, setTodos] = useState([
5    { id: 1, text: 'Learn React', completed: false },
6    { id: 2, text: 'Build an app', completed: false }
7  ]);
8
9  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
10    todos,
11    // Function updating optimistic state
12    (currentTodos, newTodo) => [...currentTodos, newTodo]
13  );
14
15  async function handleAddTodo(text) {
16    const newTodo = {
17      id: Date.now(),
18      text,
19      completed: false,
20      pending: true // Mark as pending
21    };
22
23    // Optimistic update
24    addOptimisticTodo(newTodo);
25
26    try {
27      // Actual API call
28      const response = await fetch('/api/todos', {
29        method: 'POST',
30        body: JSON.stringify({ text })
31      });
32
33      const savedTodo = await response.json();
34
35      // Update actual state
36      setTodos(current => [...current, savedTodo]);
37    } catch (error) {
38      // On error, optimistic state will be automatically rolled back
39      console.error('Error adding todo:', error);
40    }
41  }
42
43  return (
44    <div style={{ maxWidth: '500px', margin: '0 auto', padding: '20px' }}>
45      <h2>Task List</h2>
46
47      <form
48        onSubmit={(e) => {
49          e.preventDefault();
50          const text = e.target.todo.value;
51          if (text) {
52            handleAddTodo(text);
53            e.target.reset();
54          }
55        }}
56        style={{ marginBottom: '20px' }}
57      >
58        <input
59          name="todo"
60            style={{
61            width: '70%',
62            padding: '8px',
63            marginRight: '10px',
64            border: '1px solid #ddd',
65            borderRadius: '4px'
66          }}
67        />
68        <button
69          type="submit"
70          style={{
71            padding: '8px 16px',
72            backgroundColor: '#007bff',
73            color: 'white',
74            border: 'none',
75            borderRadius: '4px',
76            cursor: 'pointer'
77          }}
78        >
79          Add
80        </button>
81      </form>
82
83      <ul style={{ listStyle: 'none', padding: 0 }}>
84        {optimisticTodos.map(todo => (
85          <li
86            key={todo.id}
87            style={{
88              padding: '10px',
89              marginBottom: '5px',
90              backgroundColor: todo.pending ? '#f0f8ff' : '#f8f9fa',
91              border: '1px solid #ddd',
92              borderRadius: '4px',
93              opacity: todo.pending ? 0.7 : 1,
94              transition: 'all 0.3s'
95            }}
96          >
97            <input
98              type="checkbox"
99              checked={todo.completed}
100              readOnly
101              style={{ marginRight: '10px' }}
102            />
103            <span style={{
104              textDecoration: todo.completed ? 'line-through' : 'none'
105            }}>
106              {todo.text}
107            </span>
108            {todo.pending && (
109              <span style={{
110                marginLeft: '10px',
111                color: '#007bff',
112                fontSize: '12px'
113              }}>
114                (saving...)
115              </span>
116            )}
117          </li>
118        ))}
119      </ul>
120    </div>
121  );
122}

2. Optimistic toggle with error handling

1import { useOptimistic } from 'react';
2
3function LikeButton({ postId, initialLikes, initialIsLiked }) {
4  const [likes, setLikes] = useState(initialLikes);
5  const [isLiked, setIsLiked] = useState(initialIsLiked);
6
7  const [optimisticLikes, updateOptimisticLikes] = useOptimistic(
8    { count: likes, isLiked },
9    (current, optimisticValue) => optimisticValue
10  );
11
12  async function handleLikeToggle() {
13    const newIsLiked = !optimisticLikes.isLiked;
14    const newCount = optimisticLikes.count + (newIsLiked ? 1 : -1);
15
16    // Optimistic update
17    updateOptimisticLikes({
18      count: newCount,
19      isLiked: newIsLiked
20    });
21
22    try {
23      const response = await fetch(`/api/posts/${postId}/like`, {
24        method: newIsLiked ? 'POST' : 'DELETE'
25      });
26
27      if (!response.ok) {
28        throw new Error('Update error');
29      }
30
31      const data = await response.json();
32
33      // Update actual state
34      setLikes(data.likes);
35      setIsLiked(data.isLiked);
36    } catch (error) {
37      // Optimistic state will be automatically rolled back
38      alert('Failed to update like');
39    }
40  }
41
42  return (
43    <button
44      onClick={handleLikeToggle}
45      style={{
46        display: 'flex',
47        alignItems: 'center',
48        gap: '8px',
49        padding: '8px 16px',
50        backgroundColor: optimisticLikes.isLiked ? '#dc3545' : '#fff',
51        color: optimisticLikes.isLiked ? '#fff' : '#dc3545',
52        border: '1px solid #dc3545',
53        borderRadius: '20px',
54        cursor: 'pointer',
55        transition: 'all 0.3s',
56        fontSize: '16px'
57      }}
58    >
59      <span>{optimisticLikes.isLiked ? '❤️' : '🤍'}</span>
60      <span>{optimisticLikes.count}</span>
61    </button>
62  );
63}
64
65// Example usage in a post component
66function Post({ post }) {
67  return (
68    <article style={{
69      padding: '20px',
70      marginBottom: '20px',
71      backgroundColor: '#fff',
72      borderRadius: '8px',
73      boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
74    }}>
75      <h3>{post.title}</h3>
76      <p>{post.content}</p>
77
78      <div style={{
79        display: 'flex',
80        justifyContent: 'space-between',
81        alignItems: 'center',
82        marginTop: '15px'
83      }}>
84        <LikeButton
85          postId={post.id}
86          initialLikes={post.likes}
87          initialIsLiked={post.isLiked}
88        />
89
90        <span style={{ color: '#6c757d', fontSize: '14px' }}>
91          {new Date(post.createdAt).toLocaleDateString('en-US')}
92        </span>
93      </div>
94    </article>
95  );
96}

3. Optimistic sorting and filtering

1import { useOptimistic, useState } from 'react';
2
3function OptimisticTaskList() {
4  const [tasks, setTasks] = useState([
5    { id: 1, title: 'Task 1', priority: 'low', completed: false },
6    { id: 2, title: 'Task 2', priority: 'high', completed: false },
7    { id: 3, title: 'Task 3', priority: 'medium', completed: true }
8  ]);
9
10  const [optimisticTasks, updateOptimisticTask] = useOptimistic(
11    tasks,
12    (currentTasks, { id, updates }) =>
13      currentTasks.map(task =>
14        task.id === id ? { ...task, ...updates, updating: true } : task
15      )
16  );
17
18  async function handleTaskUpdate(taskId, updates) {
19    // Optimistic update
20    updateOptimisticTask({ id: taskId, updates });
21
22    try {
23      const response = await fetch(`/api/tasks/${taskId}`, {
24        method: 'PATCH',
25        headers: { 'Content-Type': 'application/json' },
26        body: JSON.stringify(updates)
27      });
28
29      const updatedTask = await response.json();
30
31      // Update actual state
32      setTasks(current =>
33        current.map(task => task.id === taskId ? updatedTask : task)
34      );
35    } catch (error) {
36      console.error('Error updating task:', error);
37    }
38  }
39
40  const priorityColors = {
41    low: '#28a745',
42    medium: '#ffc107',
43    high: '#dc3545'
44  };
45
46  return (
47    <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
48      <h2>Task Management</h2>
49
50      <div style={{ marginBottom: '20px' }}>
51        <h3>Statistics:</h3>
52        <div style={{ display: 'flex', gap: '20px', fontSize: '14px' }}>
53          <span>
54            Completed: {optimisticTasks.filter(t => t.completed).length}
55          </span>
56          <span>
57            In progress: {optimisticTasks.filter(t => !t.completed).length}
58          </span>
59          <span>
60            High priority: {optimisticTasks.filter(t => t.priority === 'high').length}
61          </span>
62        </div>
63      </div>
64
65      <div style={{ display: 'grid', gap: '10px' }}>
66        {optimisticTasks.map(task => (
67          <div
68            key={task.id}
69            style={{
70              padding: '15px',
71              backgroundColor: task.updating ? '#f0f8ff' : '#fff',
72              border: '1px solid #ddd',
73              borderRadius: '8px',
74              opacity: task.updating ? 0.8 : 1,
75              transition: 'all 0.3s'
76            }}
77          >
78            <div style={{
79              display: 'flex',
80              justifyContent: 'space-between',
81              alignItems: 'center'
82            }}>
83              <div style={{ flex: 1 }}>
84                <input
85                  type="checkbox"
86                  checked={task.completed}
87                  onChange={(e) =>
88                    handleTaskUpdate(task.id, { completed: e.target.checked })
89                  }
90                  style={{ marginRight: '10px' }}
91                />
92                <span style={{
93                  textDecoration: task.completed ? 'line-through' : 'none',
94                  color: task.completed ? '#6c757d' : '#000'
95                }}>
96                  {task.title}
97                </span>
98              </div>
99
100              <select
101                value={task.priority}
102                onChange={(e) =>
103                  handleTaskUpdate(task.id, { priority: e.target.value })
104                }
105                style={{
106                  padding: '4px 8px',
107                  border: `1px solid ${priorityColors[task.priority]}`,
108                  borderRadius: '4px',
109                  color: priorityColors[task.priority],
110                  backgroundColor: 'transparent',
111                  cursor: 'pointer'
112                }}
113              >
114                <option value="low">Low</option>
115                <option value="medium">Medium</option>
116                <option value="high">High</option>
117              </select>
118
119              {task.updating && (
120                <span style={{
121                  marginLeft: '10px',
122                  color: '#007bff',
123                  fontSize: '12px'
124                }}>
125                  Saving...
126                </span>
127              )}
128            </div>
129          </div>
130        ))}
131      </div>
132    </div>
133  );
134}

Advanced patterns with useOptimistic

1. Optimistic deletion with animation

1import { useOptimistic, useState } from 'react';
2
3function OptimisticDeleteList() {
4  const [items, setItems] = useState([
5    { id: 1, name: 'Item 1', createdAt: new Date() },
6    { id: 2, name: 'Item 2', createdAt: new Date() },
7    { id: 3, name: 'Item 3', createdAt: new Date() }
8  ]);
9
10  const [optimisticItems, removeOptimisticItem] = useOptimistic(
11    items,
12    (currentItems, removedId) =>
13      currentItems.map(item =>
14        item.id === removedId ? { ...item, deleting: true } : item
15      )
16  );
17
18  async function handleDelete(itemId) {
19    // Optimistically mark as deleting
20    removeOptimisticItem(itemId);
21
22    // Animation before deletion
23    setTimeout(async () => {
24      try {
25        const response = await fetch(`/api/items/${itemId}`, {
26          method: 'DELETE'
27        });
28
29        if (!response.ok) {
30          throw new Error('Deletion error');
31        }
32
33        // Actually remove from state
34        setItems(current => current.filter(item => item.id !== itemId));
35      } catch (error) {
36        // Rollback optimistic change
37        alert('Failed to delete item');
38      }
39    }, 300);
40  }
41
42  return (
43    <div style={{ maxWidth: '500px', margin: '0 auto', padding: '20px' }}>
44      <h2>List with optimistic deletion</h2>
45
46      <div style={{ display: 'grid', gap: '10px' }}>
47        {optimisticItems
48          .filter(item => !item.deleting || true) // Show deleting items too
49          .map(item => (
50            <div
51              key={item.id}
52              style={{
53                padding: '15px',
54                backgroundColor: '#fff',
55                border: '1px solid #ddd',
56                borderRadius: '8px',
57                display: 'flex',
58                justifyContent: 'space-between',
59                alignItems: 'center',
60                opacity: item.deleting ? 0 : 1,
61                transform: item.deleting ? 'translateX(-100%)' : 'translateX(0)',
62                transition: 'all 0.3s ease-out',
63                overflow: 'hidden'
64              }}
65            >
66              <div>
67                <h4 style={{ margin: '0 0 5px 0' }}>{item.name}</h4>
68                <small style={{ color: '#6c757d' }}>
69                  Created: {item.createdAt.toLocaleString('en-US')}
70                </small>
71              </div>
72
73              <button
74                onClick={() => handleDelete(item.id)}
75                disabled={item.deleting}
76                style={{
77                  padding: '5px 10px',
78                  backgroundColor: '#dc3545',
79                  color: 'white',
80                  border: 'none',
81                  borderRadius: '4px',
82                  cursor: item.deleting ? 'not-allowed' : 'pointer',
83                  opacity: item.deleting ? 0.5 : 1
84                }}
85              >
86                {item.deleting ? 'Deleting...' : 'Delete'}
87              </button>
88            </div>
89          ))}
90      </div>
91    </div>
92  );
93}

2. Optimistic real-time collaboration

1import { useOptimistic, useState, useEffect } from 'react';
2
3function CollaborativeEditor({ documentId, userId }) {
4  const [content, setContent] = useState('');
5  const [collaborators, setCollaborators] = useState([]);
6
7  const [optimisticContent, updateOptimisticContent] = useOptimistic(
8    content,
9    (_, newContent) => newContent
10  );
11
12  const [optimisticCollaborators, updateOptimisticCollaborators] = useOptimistic(
13    collaborators,
14    (currentCollaborators, update) => {
15      if (update.type === 'cursor') {
16        return currentCollaborators.map(c =>
17          c.id === update.userId ? { ...c, cursor: update.position } : c
18        );
19      }
20      return currentCollaborators;
21    }
22  );
23
24  // WebSocket or other real-time connection
25  useEffect(() => {
26    const ws = new WebSocket(`ws://localhost:8080/doc/${documentId}`);
27
28    ws.onmessage = (event) => {
29      const data = JSON.parse(event.data);
30
31      if (data.type === 'content-update' && data.userId !== userId) {
32        setContent(data.content);
33      } else if (data.type === 'cursor-update') {
34        setCollaborators(data.collaborators);
35      }
36    };
37
38    return () => ws.close();
39  }, [documentId, userId]);
40
41  async function handleContentChange(newContent) {
42    // Optimistic update
43    updateOptimisticContent(newContent);
44
45    try {
46      await fetch(`/api/documents/${documentId}`, {
47        method: 'PATCH',
48        headers: { 'Content-Type': 'application/json' },
49        body: JSON.stringify({ content: newContent, userId })
50      });
51
52      setContent(newContent);
53    } catch (error) {
54      console.error('Error saving:', error);
55    }
56  }
57
58  function handleCursorMove(position) {
59    updateOptimisticCollaborators({
60      type: 'cursor',
61      userId,
62      position
63    });
64
65    // Send cursor update via WebSocket
66    // ws.send(JSON.stringify({ type: 'cursor', position }));
67  }
68
69  return (
70    <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
71      <h2>Shared Editor</h2>
72
73      <div style={{ marginBottom: '10px' }}>
74        <h4>Active users:</h4>
75        <div style={{ display: 'flex', gap: '10px' }}>
76          {optimisticCollaborators.map(collaborator => (
77            <div
78              key={collaborator.id}
79              style={{
80                padding: '5px 10px',
81                backgroundColor: collaborator.color,
82                color: 'white',
83                borderRadius: '15px',
84                fontSize: '12px'
85              }}
86            >
87              {collaborator.name}
88              {collaborator.cursor && (
89                <span style={{ marginLeft: '5px' }}>
90                  (line {collaborator.cursor.line})
91                </span>
92              )}
93            </div>
94          ))}
95        </div>
96      </div>
97
98      <textarea
99        value={optimisticContent}
100        onChange={(e) => handleContentChange(e.target.value)}
101        onSelectionChange={(e) => {
102          const position = {
103            line: e.target.value.substring(0, e.target.selectionStart).split('\n').length,
104            column: e.target.selectionStart
105          };
106          handleCursorMove(position);
107        }}
108        style={{
109          width: '100%',
110          height: '400px',
111          padding: '15px',
112          border: '1px solid #ddd',
113          borderRadius: '8px',
114          fontSize: '16px',
115          fontFamily: 'monospace'
116        }}
117      />
118
119      <div style={{
120        marginTop: '10px',
121        fontSize: '12px',
122        color: '#6c757d'
123      }}>
124        Last sync: {new Date().toLocaleTimeString('en-US')}
125      </div>
126    </div>
127  );
128}

3. Batch updates with queuing

1import { useOptimistic, useState, useRef } from 'react';
2
3function BatchedUpdatesDemo() {
4  const [messages, setMessages] = useState([]);
5  const updateQueue = useRef([]);
6  const isProcessing = useRef(false);
7
8  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
9    messages,
10    (currentMessages, newMessage) => [...currentMessages, newMessage]
11  );
12
13  async function processBatch() {
14    if (isProcessing.current || updateQueue.current.length === 0) return;
15
16    isProcessing.current = true;
17    const batch = updateQueue.current.splice(0, 5); // Max 5 at a time
18
19    try {
20      const response = await fetch('/api/messages/batch', {
21        method: 'POST',
22        headers: { 'Content-Type': 'application/json' },
23        body: JSON.stringify({ messages: batch })
24      });
25
26      const savedMessages = await response.json();
27
28      setMessages(current => [
29        ...current,
30        ...savedMessages.map(msg => ({ ...msg, sent: true }))
31      ]);
32    } catch (error) {
33      console.error('Error sending batch:', error);
34      // Return to queue
35      updateQueue.current.unshift(...batch);
36    } finally {
37      isProcessing.current = false;
38      // Process next batch if there are items
39      if (updateQueue.current.length > 0) {
40        setTimeout(processBatch, 100);
41      }
42    }
43  }
44
45  function sendMessage(text) {
46    const newMessage = {
47      id: Date.now(),
48      text,
49      timestamp: new Date(),
50      sent: false,
51      pending: true
52    };
53
54    // Optimistic update
55    addOptimisticMessage(newMessage);
56
57    // Add to queue
58    updateQueue.current.push(newMessage);
59
60    // Start processing if not active
61    processBatch();
62  }
63
64  return (
65    <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
66      <h2>Chat with batch updates</h2>
67
68      <div style={{
69        height: '400px',
70        overflowY: 'auto',
71        border: '1px solid #ddd',
72        borderRadius: '8px',
73        padding: '15px',
74        marginBottom: '15px',
75        backgroundColor: '#f8f9fa'
76      }}>
77        {optimisticMessages.map(message => (
78          <div
79            key={message.id}
80            style={{
81              marginBottom: '10px',
82              padding: '10px',
83              backgroundColor: message.pending ? '#e3f2fd' : '#fff',
84              borderRadius: '8px',
85              opacity: message.pending ? 0.8 : 1,
86              transition: 'all 0.3s'
87            }}
88          >
89            <div style={{ marginBottom: '5px' }}>{message.text}</div>
90            <div style={{
91              fontSize: '12px',
92              color: '#6c757d',
93              display: 'flex',
94              justifyContent: 'space-between'
95            }}>
96              <span>{message.timestamp.toLocaleTimeString('en-US')}</span>
97              <span>
98                {message.pending ? 'Sending...' : 'Sent'}
99              </span>
100            </div>
101          </div>
102        ))}
103      </div>
104
105      <form
106        onSubmit={(e) => {
107          e.preventDefault();
108          const input = e.target.message;
109          if (input.value) {
110            sendMessage(input.value);
111            input.value = '';
112          }
113        }}
114        style={{ display: 'flex', gap: '10px' }}
115      >
116        <input
117          name="message"
118            style={{
119            flex: 1,
120            padding: '10px',
121            border: '1px solid #ddd',
122            borderRadius: '4px'
123          }}
124        />
125        <button
126          type="submit"
127          style={{
128            padding: '10px 20px',
129            backgroundColor: '#007bff',
130            color: 'white',
131            border: 'none',
132            borderRadius: '4px',
133            cursor: 'pointer'
134          }}
135        >
136          Send
137        </button>
138      </form>
139
140      <div style={{
141        marginTop: '10px',
142        fontSize: '12px',
143        color: '#6c757d'
144      }}>
145        In queue: {updateQueue.current.length} |
146        Processing: {isProcessing.current ? 'Yes' : 'No'}
147      </div>
148    </div>
149  );
150}

Practical applications of useOptimistic

1. Comment system with reactions

1import { useOptimistic, useState } from 'react';
2
3function CommentSystem({ postId }) {
4  const [comments, setComments] = useState([]);
5
6  const [optimisticComments, updateOptimisticComment] = useOptimistic(
7    comments,
8    (currentComments, action) => {
9      switch (action.type) {
10        case 'add':
11          return [...currentComments, action.comment];
12        case 'react':
13          return currentComments.map(comment =>
14            comment.id === action.commentId
15              ? {
16                  ...comment,
17                  reactions: {
18                    ...comment.reactions,
19                    [action.reaction]: (comment.reactions[action.reaction] || 0) + 1
20                  },
21                  userReacted: action.reaction
22                }
23              : comment
24          );
25        case 'reply':
26          return currentComments.map(comment =>
27            comment.id === action.parentId
28              ? {
29                  ...comment,
30                  replies: [...(comment.replies || []), action.reply]
31                }
32              : comment
33          );
34        default:
35          return currentComments;
36      }
37    }
38  );
39
40  async function addComment(text) {
41    const newComment = {
42      id: Date.now(),
43      text,
44      author: 'You',
45      timestamp: new Date(),
46      reactions: {},
47      replies: [],
48      pending: true
49    };
50
51    updateOptimisticComment({ type: 'add', comment: newComment });
52
53    try {
54      const response = await fetch(`/api/posts/${postId}/comments`, {
55        method: 'POST',
56        headers: { 'Content-Type': 'application/json' },
57        body: JSON.stringify({ text })
58      });
59
60      const savedComment = await response.json();
61      setComments(current => [...current, savedComment]);
62    } catch (error) {
63      console.error('Error adding comment:', error);
64    }
65  }
66
67  async function reactToComment(commentId, reaction) {
68    updateOptimisticComment({
69      type: 'react',
70      commentId,
71      reaction
72    });
73
74    try {
75      await fetch(`/api/comments/${commentId}/react`, {
76        method: 'POST',
77        headers: { 'Content-Type': 'application/json' },
78        body: JSON.stringify({ reaction })
79      });
80
81      // Update actual state
82      const response = await fetch(`/api/posts/${postId}/comments`);
83      const updatedComments = await response.json();
84      setComments(updatedComments);
85    } catch (error) {
86      console.error('Error reacting:', error);
87    }
88  }
89
90  const reactionEmojis = {
91    like: '👍',
92    love: '❤️',
93    laugh: '😄',
94    wow: '😮',
95    sad: '😢'
96  };
97
98  return (
99    <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
100      <h3>Comments</h3>
101
102      <form
103        onSubmit={(e) => {
104          e.preventDefault();
105          const text = e.target.comment.value;
106          if (text) {
107            addComment(text);
108            e.target.reset();
109          }
110        }}
111        style={{ marginBottom: '20px' }}
112      >
113        <textarea
114          name="comment"
115            rows="3"
116          style={{
117            width: '100%',
118            padding: '10px',
119            border: '1px solid #ddd',
120            borderRadius: '4px',
121            marginBottom: '10px'
122          }}
123        />
124        <button
125          type="submit"
126          style={{
127            padding: '8px 16px',
128            backgroundColor: '#007bff',
129            color: 'white',
130            border: 'none',
131            borderRadius: '4px',
132            cursor: 'pointer'
133          }}
134        >
135          Add comment
136        </button>
137      </form>
138
139      <div style={{ display: 'grid', gap: '15px' }}>
140        {optimisticComments.map(comment => (
141          <div
142            key={comment.id}
143            style={{
144              padding: '15px',
145              backgroundColor: comment.pending ? '#f0f8ff' : '#fff',
146              border: '1px solid #ddd',
147              borderRadius: '8px',
148              opacity: comment.pending ? 0.8 : 1
149            }}
150          >
151            <div style={{ marginBottom: '10px' }}>
152              <strong>{comment.author}</strong>
153              <span style={{
154                marginLeft: '10px',
155                color: '#6c757d',
156                fontSize: '14px'
157              }}>
158                {comment.timestamp.toLocaleString('en-US')}
159              </span>
160              {comment.pending && (
161                <span style={{
162                  marginLeft: '10px',
163                  color: '#007bff',
164                  fontSize: '12px'
165                }}>
166                  (sending...)
167                </span>
168              )}
169            </div>
170
171            <p style={{ margin: '10px 0' }}>{comment.text}</p>
172
173            <div style={{ display: 'flex', gap: '10px' }}>
174              {Object.entries(reactionEmojis).map(([reaction, emoji]) => (
175                <button
176                  key={reaction}
177                  onClick={() => reactToComment(comment.id, reaction)}
178                  style={{
179                    padding: '4px 8px',
180                    backgroundColor: comment.userReacted === reaction ? '#e3f2fd' : '#f8f9fa',
181                    border: '1px solid #ddd',
182                    borderRadius: '15px',
183                    cursor: 'pointer',
184                    fontSize: '14px',
185                    display: 'flex',
186                    alignItems: 'center',
187                    gap: '4px'
188                  }}
189                >
190                  <span>{emoji}</span>
191                  {comment.reactions[reaction] > 0 && (
192                    <span>{comment.reactions[reaction]}</span>
193                  )}
194                </button>
195              ))}
196            </div>
197
198            {comment.replies && comment.replies.length > 0 && (
199              <div style={{
200                marginTop: '15px',
201                marginLeft: '20px',
202                paddingLeft: '15px',
203                borderLeft: '2px solid #e9ecef'
204              }}>
205                {comment.replies.map(reply => (
206                  <div key={reply.id} style={{ marginBottom: '10px' }}>
207                    <strong>{reply.author}:</strong> {reply.text}
208                  </div>
209                ))}
210              </div>
211            )}
212          </div>
213        ))}
214      </div>
215    </div>
216  );
217}

2. Drag & Drop with optimistic sorting

1import { useOptimistic, useState } from 'react';
2
3function DragDropList() {
4  const [items, setItems] = useState([
5    { id: 1, text: 'Item 1', order: 0 },
6    { id: 2, text: 'Item 2', order: 1 },
7    { id: 3, text: 'Item 3', order: 2 },
8    { id: 4, text: 'Item 4', order: 3 }
9  ]);
10
11  const [draggedItem, setDraggedItem] = useState(null);
12
13  const [optimisticItems, reorderOptimistic] = useOptimistic(
14    items,
15    (currentItems, { fromIndex, toIndex }) => {
16      const newItems = [...currentItems];
17      const [removed] = newItems.splice(fromIndex, 1);
18      newItems.splice(toIndex, 0, removed);
19      return newItems.map((item, index) => ({ ...item, order: index }));
20    }
21  );
22
23  async function handleDrop(fromIndex, toIndex) {
24    if (fromIndex === toIndex) return;
25
26    // Optimistic update
27    reorderOptimistic({ fromIndex, toIndex });
28
29    try {
30      const response = await fetch('/api/items/reorder', {
31        method: 'POST',
32        headers: { 'Content-Type': 'application/json' },
33        body: JSON.stringify({ fromIndex, toIndex })
34      });
35
36      const reorderedItems = await response.json();
37      setItems(reorderedItems);
38    } catch (error) {
39      console.error('Error reordering:', error);
40    }
41  }
42
43  return (
44    <div style={{ maxWidth: '400px', margin: '0 auto', padding: '20px' }}>
45      <h2>Drag & Drop List</h2>
46
47      <div style={{
48        border: '1px solid #ddd',
49        borderRadius: '8px',
50        overflow: 'hidden'
51      }}>
52        {optimisticItems.map((item, index) => (
53          <div
54            key={item.id}
55            draggable
56            onDragStart={() => setDraggedItem({ item, index })}
57            onDragOver={(e) => e.preventDefault()}
58            onDrop={() => {
59              if (draggedItem) {
60                handleDrop(draggedItem.index, index);
61                setDraggedItem(null);
62              }
63            }}
64            style={{
65              padding: '15px',
66              backgroundColor: '#fff',
67              borderBottom: '1px solid #e9ecef',
68              cursor: 'move',
69              display: 'flex',
70              alignItems: 'center',
71              transition: 'all 0.3s',
72              opacity: draggedItem?.item.id === item.id ? 0.5 : 1
73            }}
74          >
75            <span style={{
76              marginRight: '15px',
77              fontSize: '20px',
78              color: '#6c757d'
79            }}>
8081            </span>
82            <span style={{ flex: 1 }}>{item.text}</span>
83            <span style={{
84              color: '#6c757d',
85              fontSize: '14px'
86            }}>
87              #{item.order + 1}
88            </span>
89          </div>
90        ))}
91      </div>
92
93      <p style={{
94        marginTop: '15px',
95        fontSize: '14px',
96        color: '#6c757d',
97        textAlign: 'center'
98      }}>
99        Drag items to change the order
100      </p>
101    </div>
102  );
103}

Best practices with useOptimistic

1. Data structure

1// GOOD - marking optimistic state
2const optimisticItem = {
3  ...originalItem,
4  optimistic: true, // or pending: true
5  optimisticId: Date.now() // for identification
6};
7
8// GOOD - preserving original data
9const [optimistic, update] = useOptimistic(
10  realData,
11  (current, change) => ({
12    ...current,
13    ...change,
14    _original: current // keep the original
15  })
16);
17
18// BAD - no state marking
19const badOptimistic = {
20  ...originalItem
21  // No way to distinguish from actual data
22};

2. Error handling

1// Retry strategy with exponential backoff
2async function optimisticUpdateWithRetry(action, maxRetries = 3) {
3  let lastError;
4
5  for (let i = 0; i < maxRetries; i++) {
6    try {
7      const result = await action();
8      return result;
9    } catch (error) {
10      lastError = error;
11
12      if (i < maxRetries - 1) {
13        // Exponential backoff
14        await new Promise(resolve =>
15          setTimeout(resolve, Math.pow(2, i) * 1000)
16        );
17      }
18    }
19  }
20
21  // Show error to user
22  showErrorNotification('Operation failed. Please try again.');
23  throw lastError;
24}

3. Performance

1// GOOD - batch updates
2const [optimistic, batchUpdate] = useOptimistic(
3  items,
4  (current, updates) => {
5    // Process multiple updates at once
6    return updates.reduce((acc, update) => {
7      // Apply each update
8      return applyUpdate(acc, update);
9    }, current);
10  }
11);
12
13// Usage
14function handleMultipleUpdates(updates) {
15  batchUpdate(updates); // One update instead of many
16}
17
18// GOOD - memoization of heavy computations
19const processedOptimisticData = useMemo(() => {
20  return expensiveProcessing(optimisticData);
21}, [optimisticData]);

The

useOptimistic
hook is the key to creating responsive applications that give the user instant feedback. Proper use of this hook can significantly improve the perceived performance of an application.

Go to CodeWorlds