Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

useOptimistic - optymistyczne aktualizacje UI

Hook

useOptimistic
pozwala na natychmiastowe aktualizowanie interfejsu użytkownika przed otrzymaniem odpowiedzi z serwera. To kluczowe narzędzie do tworzenia responsywnych aplikacji, które dają użytkownikowi wrażenie błyskawicznej reakcji.

Podstawy useOptimistic

1. Składnia i podstawowe użycie

1import { useOptimistic } from 'react';
2
3function TodoList() {
4  const [todos, setTodos] = useState([
5    { id: 1, text: 'Nauczyć się React', completed: false },
6    { id: 2, text: 'Zbudować aplikację', completed: false }
7  ]);
8  
9  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
10    todos,
11    // Funkcja aktualizująca stan optymistyczny
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 // Oznaczenie jako pending
21    };
22    
23    // Optymistyczna aktualizacja
24    addOptimisticTodo(newTodo);
25    
26    try {
27      // Rzeczywiste wywołanie API
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      // Aktualizacja rzeczywistego stanu
36      setTodos(current => [...current, savedTodo]);
37    } catch (error) {
38      // W przypadku błędu stan optymistyczny zostanie automatycznie cofnięty
39      console.error('Błąd dodawania todo:', error);
40    }
41  }
42  
43  return (
44    <div style={{ maxWidth: '500px', margin: '0 auto', padding: '20px' }}>
45      <h2>Lista zadań</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          Dodaj
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                (zapisywanie...)
115              </span>
116            )}
117          </li>
118        ))}
119      </ul>
120    </div>
121  );
122}

2. Optymistyczne toggle z obsługą błędów

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    // Optymistyczna aktualizacja
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('Błąd aktualizacji');
29      }
30      
31      const data = await response.json();
32      
33      // Aktualizacja rzeczywistego stanu
34      setLikes(data.likes);
35      setIsLiked(data.isLiked);
36    } catch (error) {
37      // Stan optymistyczny zostanie automatycznie cofnięty
38      alert('Nie udało się zaktualizować polubienia');
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// Przykład użycia w komponencie posta
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('pl-PL')}
92        </span>
93      </div>
94    </article>
95  );
96}

3. Optymistyczne sortowanie i filtrowanie

1import { useOptimistic, useState } from 'react';
2
3function OptimisticTaskList() {
4  const [tasks, setTasks] = useState([
5    { id: 1, title: 'Zadanie 1', priority: 'low', completed: false },
6    { id: 2, title: 'Zadanie 2', priority: 'high', completed: false },
7    { id: 3, title: 'Zadanie 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    // Optymistyczna aktualizacja
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      // Aktualizacja rzeczywistego stanu
32      setTasks(current =>
33        current.map(task => task.id === taskId ? updatedTask : task)
34      );
35    } catch (error) {
36      console.error('Błąd aktualizacji zadania:', 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>Zarządzanie zadaniami</h2>
49      
50      <div style={{ marginBottom: '20px' }}>
51        <h3>Statystyki:</h3>
52        <div style={{ display: 'flex', gap: '20px', fontSize: '14px' }}>
53          <span>
54            Ukończone: {optimisticTasks.filter(t => t.completed).length}
55          </span>
56          <span>
57            W toku: {optimisticTasks.filter(t => !t.completed).length}
58          </span>
59          <span>
60            Wysokie priorytety: {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">Niski</option>
115                <option value="medium">Średni</option>
116                <option value="high">Wysoki</option>
117              </select>
118              
119              {task.updating && (
120                <span style={{ 
121                  marginLeft: '10px',
122                  color: '#007bff',
123                  fontSize: '12px'
124                }}>
125                  Zapisywanie...
126                </span>
127              )}
128            </div>
129          </div>
130        ))}
131      </div>
132    </div>
133  );
134}

Zaawansowane wzorce z useOptimistic

1. Optymistyczne usuwanie z animacją

1import { useOptimistic, useState } from 'react';
2
3function OptimisticDeleteList() {
4  const [items, setItems] = useState([
5    { id: 1, name: 'Element 1', createdAt: new Date() },
6    { id: 2, name: 'Element 2', createdAt: new Date() },
7    { id: 3, name: 'Element 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    // Optymistyczne oznaczenie jako usuwane
20    removeOptimisticItem(itemId);
21    
22    // Animacja przed usunięciem
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('Błąd usuwania');
31        }
32        
33        // Rzeczywiste usunięcie ze stanu
34        setItems(current => current.filter(item => item.id !== itemId));
35      } catch (error) {
36        // Cofnięcie optymistycznej zmiany
37        alert('Nie udało się usunąć elementu');
38      }
39    }, 300);
40  }
41  
42  return (
43    <div style={{ maxWidth: '500px', margin: '0 auto', padding: '20px' }}>
44      <h2>Lista z optymistycznym usuwaniem</h2>
45      
46      <div style={{ display: 'grid', gap: '10px' }}>
47        {optimisticItems
48          .filter(item => !item.deleting || true) // Pokazuj też usuwane
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                  Utworzono: {item.createdAt.toLocaleString('pl-PL')}
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 ? 'Usuwanie...' : 'Usuń'}
87              </button>
88            </div>
89          ))}
90      </div>
91    </div>
92  );
93}

2. Optymistyczna współpraca w czasie rzeczywistym

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 lub inne połączenie real-time
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    // Optymistyczna aktualizacja
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('Błąd zapisywania:', error);
55    }
56  }
57  
58  function handleCursorMove(position) {
59    updateOptimisticCollaborators({
60      type: 'cursor',
61      userId,
62      position
63    });
64    
65    // Wyślij aktualizację kursora przez 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>Edytor współdzielony</h2>
72      
73      <div style={{ marginBottom: '10px' }}>
74        <h4>Aktywni użytkownicy:</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                  (linia {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('
104').length,
105            column: e.target.selectionStart
106          };
107          handleCursorMove(position);
108        }}
109        style={{
110          width: '100%',
111          height: '400px',
112          padding: '15px',
113          border: '1px solid #ddd',
114          borderRadius: '8px',
115          fontSize: '16px',
116          fontFamily: 'monospace'
117        }}
118      />
119      
120      <div style={{ 
121        marginTop: '10px',
122        fontSize: '12px',
123        color: '#6c757d'
124      }}>
125        Ostatnia synchronizacja: {new Date().toLocaleTimeString('pl-PL')}
126      </div>
127    </div>
128  );
129}

3. Batch updates z kolejkowaniem

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 na raz
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('Błąd wysyłania batch:', error);
34      // Przywróć do kolejki
35      updateQueue.current.unshift(...batch);
36    } finally {
37      isProcessing.current = false;
38      // Przetwórz następną partię jeśli są
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    // Optymistyczna aktualizacja
55    addOptimisticMessage(newMessage);
56    
57    // Dodaj do kolejki
58    updateQueue.current.push(newMessage);
59    
60    // Rozpocznij przetwarzanie jeśli nie jest aktywne
61    processBatch();
62  }
63  
64  return (
65    <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
66      <h2>Chat z 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('pl-PL')}</span>
97              <span>
98                {message.pending ? '⏳ Wysyłanie...' : '✓ Wysłane'}
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          Wyślij
137        </button>
138      </form>
139      
140      <div style={{ 
141        marginTop: '10px',
142        fontSize: '12px',
143        color: '#6c757d'
144      }}>
145        W kolejce: {updateQueue.current.length} | 
146        Przetwarzanie: {isProcessing.current ? 'Tak' : 'Nie'}
147      </div>
148    </div>
149  );
150}

Praktyczne zastosowania useOptimistic

1. System komentarzy z reakcjami

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: 'Ty',
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('Błąd dodawania komentarza:', 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      // Zaktualizuj rzeczywisty stan
82      const response = await fetch(`/api/posts/${postId}/comments`);
83      const updatedComments = await response.json();
84      setComments(updatedComments);
85    } catch (error) {
86      console.error('Błąd reakcji:', 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>Komentarze</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          Dodaj komentarz
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('pl-PL')}
159              </span>
160              {comment.pending && (
161                <span style={{ 
162                  marginLeft: '10px',
163                  color: '#007bff',
164                  fontSize: '12px'
165                }}>
166                  (wysyłanie...)
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 z optymistycznym sortowaniem

1import { useOptimistic, useState } from 'react';
2
3function DragDropList() {
4  const [items, setItems] = useState([
5    { id: 1, text: 'Element 1', order: 0 },
6    { id: 2, text: 'Element 2', order: 1 },
7    { id: 3, text: 'Element 3', order: 2 },
8    { id: 4, text: 'Element 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    // Optymistyczna aktualizacja
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('Błąd zmiany kolejności:', error);
40    }
41  }
42  
43  return (
44    <div style={{ maxWidth: '400px', margin: '0 auto', padding: '20px' }}>
45      <h2>Lista Drag & Drop</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        Przeciągnij elementy, aby zmienić kolejność
100      </p>
101    </div>
102  );
103}

Najlepsze praktyki z useOptimistic

1. Struktura danych

1// ✅ DOBRZE - oznaczanie stanu optymistycznego
2const optimisticItem = {
3  ...originalItem,
4  optimistic: true, // lub pending: true
5  optimisticId: Date.now() // do identyfikacji
6};
7
8// ✅ DOBRZE - zachowanie oryginalnych danych
9const [optimistic, update] = useOptimistic(
10  realData,
11  (current, change) => ({
12    ...current,
13    ...change,
14    _original: current // zachowaj oryginał
15  })
16);
17
18// ❌ ŹLE - brak oznaczenia stanu
19const badOptimistic = {
20  ...originalItem
21  // Brak sposobu na odróżnienie od rzeczywistych danych
22};

2. Obsługa błędów

1// Strategia retry z 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  // Pokaż użytkownikowi błąd
22  showErrorNotification('Operacja nie powiodła się. Spróbuj ponownie.');
23  throw lastError;
24}

3. Wydajność

1// ✅ DOBRZE - batch updates
2const [optimistic, batchUpdate] = useOptimistic(
3  items,
4  (current, updates) => {
5    // Przetwórz wiele aktualizacji na raz
6    return updates.reduce((acc, update) => {
7      // Zastosuj każdą aktualizację
8      return applyUpdate(acc, update);
9    }, current);
10  }
11);
12
13// Użycie
14function handleMultipleUpdates(updates) {
15  batchUpdate(updates); // Jedna aktualizacja zamiast wielu
16}
17
18// ✅ DOBRZE - memoizacja ciężkich obliczeń
19const processedOptimisticData = useMemo(() => {
20  return expensiveProcessing(optimisticData);
21}, [optimisticData]);

Hook

useOptimistic
to klucz do tworzenia responsywnych aplikacji, które dają użytkownikowi natychmiastową informację zwrotną. Prawidłowe używanie tego hooka może znacząco poprawić postrzeganą wydajność aplikacji.

Vai a CodeWorlds