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.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}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}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}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}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}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}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}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 }}>
80 ☰
81 </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}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};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}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.