Hook
useTransition jest jedną z najważniejszych funkcji React 18, która pozwala na oznaczanie aktualizacji stanu jako "przejściowych" (transition). Dzięki temu React może przerwać renderowanie tych aktualizacji, jeśli pojawią się bardziej pilne zmiany, co sprawia, że interfejs użytkownika pozostaje responsywny.1import { useTransition, useState } from 'react';
2
3function SearchComponent() {
4 const [query, setQuery] = useState('');
5 const [results, setResults] = useState([]);
6 const [isPending, startTransition] = useTransition();
7
8 const handleSearch = (searchTerm) => {
9 // Pilna aktualizacja - natychmiastowa
10 setQuery(searchTerm);
11
12 // Przejściowa aktualizacja - może być przerwana
13 startTransition(() => {
14 // Ciężkie obliczenia lub duże listy
15 const searchResults = performHeavySearch(searchTerm);
16 setResults(searchResults);
17 });
18 };
19
20 return (
21 <div>
22 <input
23 value={query}
24 onChange={(e) => handleSearch(e.target.value)}
25 />
26
27 {isPending && <div>Wyszukiwanie...</div>}
28
29 <div>
30 {results.map(result => (
31 <div key={result.id}>{result.title}</div>
32 ))}
33 </div>
34 </div>
35 );
36}
37
38function performHeavySearch(term) {
39 // Symulacja ciężkich obliczeń
40 const items = [];
41 for (let i = 0; i < 10000; i++) {
42 if (`Item ${i}`.toLowerCase().includes(term.toLowerCase())) {
43 items.push({ id: i, title: `Item ${i}` });
44 }
45 }
46 return items;
47}1import { useTransition, useState } from 'react';
2
3function ResponsiveList() {
4 const [filter, setFilter] = useState('');
5 const [items, setItems] = useState(generateItems(10000));
6 const [isPending, startTransition] = useTransition();
7
8 const handleFilterChange = (newFilter) => {
9 // PILNA aktualizacja - input musi być responsywny
10 setFilter(newFilter);
11
12 // PRZEJŚCIOWA aktualizacja - może poczekać
13 startTransition(() => {
14 const filtered = items.filter(item =>
15 item.name.toLowerCase().includes(newFilter.toLowerCase())
16 );
17 setItems(filtered);
18 });
19 };
20
21 return (
22 <div>
23 <input
24 value={filter}
25 onChange={(e) => handleFilterChange(e.target.value)}
26 style={{
27 padding: '8px',
28 border: '1px solid #ccc',
29 borderRadius: '4px'
30 }}
31 />
32
33 <div style={{ marginTop: '10px' }}>
34 {isPending ? (
35 <div style={{ color: '#666' }}>
36 Aktualizowanie listy...
37 </div>
38 ) : (
39 <div>Znaleziono {items.length} elementów</div>
40 )}
41 </div>
42
43 <div style={{ height: '400px', overflow: 'auto', marginTop: '10px' }}>
44 {items.map(item => (
45 <div
46 key={item.id}
47 style={{
48 padding: '8px',
49 borderBottom: '1px solid #eee',
50 backgroundColor: item.id % 2 === 0 ? '#f9f9f9' : '#fff'
51 }}
52 >
53 <strong>{item.name}</strong> - {item.description}
54 </div>
55 ))}
56 </div>
57 </div>
58 );
59}
60
61function generateItems(count) {
62 return Array.from({ length: count }, (_, i) => ({
63 id: i,
64 name: `Element ${i + 1}`,
65 description: `Opis elementu numer ${i + 1}`
66 }));
67}1import { Suspense, useTransition, useState } from 'react';
2
3// Symulacja asynchronicznego zasobu
4function createResource(promise) {
5 let status = 'pending';
6 let result;
7
8 const suspender = promise.then(
9 (response) => {
10 status = 'success';
11 result = response;
12 },
13 (error) => {
14 status = 'error';
15 result = error;
16 }
17 );
18
19 return {
20 read() {
21 if (status === 'pending') {
22 throw suspender;
23 } else if (status === 'error') {
24 throw result;
25 } else if (status === 'success') {
26 return result;
27 }
28 }
29 };
30}
31
32// Komponent wykorzystujący zasób
33function UserProfile({ userId }) {
34 const userResource = createResource(
35 fetch(`/api/users/${userId}`).then(res => res.json())
36 );
37
38 const user = userResource.read();
39
40 return (
41 <div style={{ padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
42 <h2>{user.name}</h2>
43 <p>Email: {user.email}</p>
44 <p>ID: {user.id}</p>
45 </div>
46 );
47}
48
49// Główny komponent z useTransition
50function UsersList() {
51 const [selectedUserId, setSelectedUserId] = useState(1);
52 const [isPending, startTransition] = useTransition();
53
54 const users = [
55 { id: 1, name: 'Anna Kowalska' },
56 { id: 2, name: 'Jan Nowak' },
57 { id: 3, name: 'Maria Wiśniewska' }
58 ];
59
60 const handleUserSelect = (userId) => {
61 startTransition(() => {
62 setSelectedUserId(userId);
63 });
64 };
65
66 return (
67 <div style={{ display: 'flex', gap: '20px' }}>
68 <div style={{ width: '200px' }}>
69 <h3>Wybierz użytkownika:</h3>
70 {users.map(user => (
71 <button
72 key={user.id}
73 onClick={() => handleUserSelect(user.id)}
74 style={{
75 display: 'block',
76 width: '100%',
77 margin: '5px 0',
78 padding: '10px',
79 backgroundColor: selectedUserId === user.id ? '#007bff' : '#f8f9fa',
80 color: selectedUserId === user.id ? 'white' : 'black',
81 border: '1px solid #ddd',
82 borderRadius: '4px',
83 cursor: 'pointer'
84 }}
85 >
86 {user.name}
87 </button>
88 ))}
89 </div>
90
91 <div style={{ flex: 1 }}>
92 <Suspense
93 fallback={
94 <div style={{
95 padding: '20px',
96 textAlign: 'center',
97 border: '1px dashed #ddd',
98 borderRadius: '8px'
99 }}>
100 {isPending ? 'Ładowanie nowego użytkownika...' : 'Ładowanie...'}
101 </div>
102 }
103 >
104 <UserProfile userId={selectedUserId} />
105 </Suspense>
106 </div>
107 </div>
108 );
109}1import { useTransition, useState, useOptimistic } from 'react';
2
3function TodoApp() {
4 const [todos, setTodos] = useState([
5 { id: 1, text: 'Kupić mleko', completed: false },
6 { id: 2, text: 'Napisać raport', completed: true }
7 ]);
8 const [isPending, startTransition] = useTransition();
9
10 const addTodo = async (text) => {
11 const optimisticTodo = {
12 id: Date.now(),
13 text,
14 completed: false,
15 isPending: true
16 };
17
18 // Optymistyczna aktualizacja
19 setTodos(prev => [...prev, optimisticTodo]);
20
21 startTransition(async () => {
22 try {
23 // Symulacja API call
24 await new Promise(resolve => setTimeout(resolve, 1000));
25
26 // Aktualizacja po pomyślnym wykonaniu
27 setTodos(prev =>
28 prev.map(todo =>
29 todo.id === optimisticTodo.id
30 ? { ...todo, isPending: false }
31 : todo
32 )
33 );
34 } catch (error) {
35 // Cofnięcie optymistycznej aktualizacji
36 setTodos(prev => prev.filter(todo => todo.id !== optimisticTodo.id));
37 alert('Błąd przy dodawaniu zadania');
38 }
39 });
40 };
41
42 const toggleTodo = async (id) => {
43 // Optymistyczna aktualizacja
44 setTodos(prev =>
45 prev.map(todo =>
46 todo.id === id ? { ...todo, completed: !todo.completed, isPending: true } : todo
47 )
48 );
49
50 startTransition(async () => {
51 try {
52 await new Promise(resolve => setTimeout(resolve, 500));
53
54 setTodos(prev =>
55 prev.map(todo =>
56 todo.id === id ? { ...todo, isPending: false } : todo
57 )
58 );
59 } catch (error) {
60 // Cofnięcie zmiany
61 setTodos(prev =>
62 prev.map(todo =>
63 todo.id === id ? { ...todo, completed: !todo.completed, isPending: false } : todo
64 )
65 );
66 }
67 });
68 };
69
70 return (
71 <div style={{ maxWidth: '500px', margin: '0 auto', padding: '20px' }}>
72 <h2>Lista zadań</h2>
73
74 <form
75 onSubmit={(e) => {
76 e.preventDefault();
77 const formData = new FormData(e.target);
78 const text = formData.get('text');
79 if (text) {
80 addTodo(text);
81 e.target.reset();
82 }
83 }}
84 style={{ marginBottom: '20px' }}
85 >
86 <input
87 name="text"
88 style={{ padding: '8px', marginRight: '8px', flex: 1 }}
89 />
90 <button
91 type="submit"
92 disabled={isPending}
93 style={{
94 padding: '8px 16px',
95 backgroundColor: '#007bff',
96 color: 'white',
97 border: 'none',
98 borderRadius: '4px',
99 cursor: isPending ? 'not-allowed' : 'pointer'
100 }}
101 >
102 {isPending ? 'Dodawanie...' : 'Dodaj'}
103 </button>
104 </form>
105
106 <ul style={{ listStyle: 'none', padding: 0 }}>
107 {todos.map(todo => (
108 <li
109 key={todo.id}
110 style={{
111 display: 'flex',
112 alignItems: 'center',
113 padding: '10px',
114 marginBottom: '5px',
115 backgroundColor: '#f8f9fa',
116 borderRadius: '4px',
117 opacity: todo.isPending ? 0.6 : 1
118 }}
119 >
120 <input
121 type="checkbox"
122 checked={todo.completed}
123 onChange={() => toggleTodo(todo.id)}
124 disabled={todo.isPending}
125 style={{ marginRight: '10px' }}
126 />
127 <span
128 style={{
129 textDecoration: todo.completed ? 'line-through' : 'none',
130 flex: 1
131 }}
132 >
133 {todo.text}
134 </span>
135 {todo.isPending && (
136 <span style={{ color: '#666', fontSize: '12px' }}>
137 Aktualizowanie...
138 </span>
139 )}
140 </li>
141 ))}
142 </ul>
143 </div>
144 );
145}1import { useTransition, useState, useEffect } from 'react';
2
3function SearchWithDebouncing() {
4 const [query, setQuery] = useState('');
5 const [debouncedQuery, setDebouncedQuery] = useState('');
6 const [results, setResults] = useState([]);
7 const [isPending, startTransition] = useTransition();
8
9 // Debouncing input
10 useEffect(() => {
11 const timer = setTimeout(() => {
12 setDebouncedQuery(query);
13 }, 300);
14
15 return () => clearTimeout(timer);
16 }, [query]);
17
18 // Wyszukiwanie po zmianie debounced query
19 useEffect(() => {
20 if (debouncedQuery) {
21 startTransition(() => {
22 const searchResults = performSearch(debouncedQuery);
23 setResults(searchResults);
24 });
25 } else {
26 setResults([]);
27 }
28 }, [debouncedQuery]);
29
30 return (
31 <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
32 <h2>Wyszukiwarka z debouncing</h2>
33
34 <div style={{ position: 'relative', marginBottom: '20px' }}>
35 <input
36 value={query}
37 onChange={(e) => setQuery(e.target.value)}
38 style={{
39 width: '100%',
40 padding: '12px',
41 fontSize: '16px',
42 border: '2px solid #ddd',
43 borderRadius: '8px',
44 outline: 'none'
45 }}
46 />
47 {isPending && (
48 <div style={{
49 position: 'absolute',
50 right: '10px',
51 top: '50%',
52 transform: 'translateY(-50%)',
53 color: '#666'
54 }}>
55 🔍 Wyszukiwanie...
56 </div>
57 )}
58 </div>
59
60 <div>
61 <p style={{ color: '#666', margin: '10px 0' }}>
62 Znaleziono {results.length} wyników
63 {debouncedQuery && ` dla "${debouncedQuery}"`}
64 </p>
65
66 <div style={{ maxHeight: '400px', overflowY: 'auto' }}>
67 {results.map(result => (
68 <div
69 key={result.id}
70 style={{
71 padding: '12px',
72 marginBottom: '8px',
73 backgroundColor: '#f8f9fa',
74 border: '1px solid #e9ecef',
75 borderRadius: '6px'
76 }}
77 >
78 <h4 style={{ margin: '0 0 5px 0', color: '#007bff' }}>
79 {highlightMatch(result.title, debouncedQuery)}
80 </h4>
81 <p style={{ margin: 0, color: '#666', fontSize: '14px' }}>
82 {highlightMatch(result.description, debouncedQuery)}
83 </p>
84 </div>
85 ))}
86 </div>
87 </div>
88 </div>
89 );
90}
91
92function performSearch(query) {
93 const items = [
94 { id: 1, title: 'React Hooks', description: 'Nowoczesny sposób na zarządzanie stanem w React' },
95 { id: 2, title: 'JavaScript ES6+', description: 'Najnowsze funkcje JavaScript' },
96 { id: 3, title: 'TypeScript Guide', description: 'Kompletny przewodnik po TypeScript' },
97 { id: 4, title: 'Node.js Backend', description: 'Tworzenie serwerów z Node.js' },
98 { id: 5, title: 'CSS Grid Layout', description: 'Zaawansowane układy CSS' }
99 ];
100
101 return items.filter(item =>
102 item.title.toLowerCase().includes(query.toLowerCase()) ||
103 item.description.toLowerCase().includes(query.toLowerCase())
104 );
105}
106
107function highlightMatch(text, query) {
108 if (!query) return text;
109
110 const regex = new RegExp(`(${query})`, 'gi');
111 const parts = text.split(regex);
112
113 return parts.map((part, index) =>
114 part.toLowerCase() === query.toLowerCase() ? (
115 <mark key={index} style={{ backgroundColor: '#ffeb3b', padding: '2px' }}>
116 {part}
117 </mark>
118 ) : part
119 );
120}1import { useTransition, useState } from 'react';
2
3function MultipleTransitionsDemo() {
4 const [activeTab, setActiveTab] = useState('users');
5 const [filter, setFilter] = useState('');
6 const [sortBy, setSortBy] = useState('name');
7
8 const [tabPending, startTabTransition] = useTransition();
9 const [filterPending, startFilterTransition] = useTransition();
10 const [sortPending, startSortTransition] = useTransition();
11
12 const data = {
13 users: [
14 { id: 1, name: 'Anna Kowalska', email: 'anna@example.com', role: 'Admin' },
15 { id: 2, name: 'Jan Nowak', email: 'jan@example.com', role: 'User' },
16 { id: 3, name: 'Maria Wiśniewska', email: 'maria@example.com', role: 'Editor' }
17 ],
18 products: [
19 { id: 1, name: 'Laptop', price: 2500, category: 'Electronics' },
20 { id: 2, name: 'Książka', price: 45, category: 'Books' },
21 { id: 3, name: 'Słuchawki', price: 150, category: 'Electronics' }
22 ]
23 };
24
25 const handleTabChange = (tab) => {
26 startTabTransition(() => {
27 setActiveTab(tab);
28 setFilter(''); // Reset filter when changing tabs
29 });
30 };
31
32 const handleFilterChange = (newFilter) => {
33 setFilter(newFilter); // Immediate update for input
34 startFilterTransition(() => {
35 // Any heavy filtering logic would go here
36 });
37 };
38
39 const handleSortChange = (newSort) => {
40 startSortTransition(() => {
41 setSortBy(newSort);
42 });
43 };
44
45 const filteredAndSortedData = data[activeTab]
46 .filter(item =>
47 Object.values(item).some(value =>
48 value.toString().toLowerCase().includes(filter.toLowerCase())
49 )
50 )
51 .sort((a, b) => {
52 if (sortBy === 'name') return a.name.localeCompare(b.name);
53 if (sortBy === 'price') return (a.price || 0) - (b.price || 0);
54 return 0;
55 });
56
57 const isPending = tabPending || filterPending || sortPending;
58
59 return (
60 <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
61 <h2>Multiple Transitions Demo</h2>
62
63 {/* Tab Navigation */}
64 <div style={{ marginBottom: '20px', borderBottom: '1px solid #ddd' }}>
65 {['users', 'products'].map(tab => (
66 <button
67 key={tab}
68 onClick={() => handleTabChange(tab)}
69 disabled={tabPending}
70 style={{
71 padding: '10px 20px',
72 marginRight: '10px',
73 backgroundColor: activeTab === tab ? '#007bff' : 'transparent',
74 color: activeTab === tab ? 'white' : '#007bff',
75 border: `2px solid ${activeTab === tab ? '#007bff' : '#ddd'}`,
76 borderBottom: 'none',
77 borderRadius: '8px 8px 0 0',
78 cursor: tabPending ? 'not-allowed' : 'pointer',
79 opacity: tabPending ? 0.6 : 1
80 }}
81 >
82 {tab.charAt(0).toUpperCase() + tab.slice(1)}
83 {tabPending && ' ⏳'}
84 </button>
85 ))}
86 </div>
87
88 {/* Controls */}
89 <div style={{
90 display: 'flex',
91 gap: '15px',
92 marginBottom: '20px',
93 alignItems: 'center'
94 }}>
95 <input
96 value={filter}
97 onChange={(e) => handleFilterChange(e.target.value)}
98 style={{
99 padding: '8px',
100 border: '1px solid #ddd',
101 borderRadius: '4px',
102 flex: 1
103 }}
104 />
105
106 <select
107 value={sortBy}
108 onChange={(e) => handleSortChange(e.target.value)}
109 style={{
110 padding: '8px',
111 border: '1px solid #ddd',
112 borderRadius: '4px'
113 }}
114 >
115 <option value="name">Sortuj po nazwie</option>
116 {activeTab === 'products' && <option value="price">Sortuj po cenie</option>}
117 </select>
118
119 {isPending && (
120 <div style={{ color: '#666', fontSize: '14px' }}>
121 Aktualizowanie...
122 </div>
123 )}
124 </div>
125
126 {/* Data Display */}
127 <div style={{
128 minHeight: '200px',
129 opacity: isPending ? 0.7 : 1,
130 transition: 'opacity 0.2s'
131 }}>
132 {filteredAndSortedData.length === 0 ? (
133 <div style={{
134 textAlign: 'center',
135 color: '#666',
136 padding: '40px'
137 }}>
138 Brak wyników
139 </div>
140 ) : (
141 <div style={{ display: 'grid', gap: '10px' }}>
142 {filteredAndSortedData.map(item => (
143 <div
144 key={item.id}
145 style={{
146 padding: '15px',
147 backgroundColor: '#f8f9fa',
148 border: '1px solid #e9ecef',
149 borderRadius: '6px'
150 }}
151 >
152 <h4 style={{ margin: '0 0 5px 0' }}>{item.name}</h4>
153 {activeTab === 'users' ? (
154 <div>
155 <p style={{ margin: '2px 0', color: '#666' }}>
156 📧 {item.email}
157 </p>
158 <p style={{ margin: '2px 0', color: '#666' }}>
159 👤 {item.role}
160 </p>
161 </div>
162 ) : (
163 <div>
164 <p style={{ margin: '2px 0', color: '#666' }}>
165 💰 {item.price} zł
166 </p>
167 <p style={{ margin: '2px 0', color: '#666' }}>
168 📂 {item.category}
169 </p>
170 </div>
171 )}
172 </div>
173 ))}
174 </div>
175 )}
176 </div>
177 </div>
178 );
179}useTransition to potężne narzędzie do utrzymania responsywności aplikacji React. Pozwala na inteligentne zarządzanie priorytetami aktualizacji, dzięki czemu użytkownik zawsze ma wrażenie płynnego działania interfejsu.