JavaScript and React course Β· Module 9: Modern React Hooks
use - universal hook for handling Promise and Context
In this lesson6
The orbital station is waiting for a report from a probe. Until now, in React that meant three states (loading, error, data), an effect with fetch and quite a few conditions in JSX. On top of that, regular hooks cannot sit inside an if, so you read context even when you did not need it.
use is a React API that allows you to "use" resources such as Promises or Context. In React 19 it is stable (earlier it was available only in experimental releases). It is the first hook that can be called conditionally and in loops. It still has to be called inside a component or a hook, though, and according to react.dev it must not be placed in a try/catch block.
use Basics
1. Reading Promise with use
When you pass a Promise to use, the component "suspends", and the nearest Suspense boundary shows a fallback until the promise resolves:
1import { use, Suspense } from 'react';
2
3// Function returning a Promise
4function fetchUser(id) {
5 return fetch(`/api/users/${id}`).then(res => res.json());
6}
7
8// Component using use with Promise
9function UserProfile({ userPromise }) {
10 // use automatically "unwraps" the Promise
11 const user = use(userPromise);
12
13 return (
14 <div style={{
15 padding: '20px',
16 border: '1px solid #ddd',
17 borderRadius: '8px'
18 }}>
19 <h2>{user.name}</h2>
20 <p>Email: {user.email}</p>
21 <p>Joined: {new Date(user.joinedDate).toLocaleDateString('en-US')}</p>
22 </div>
23 );
24}
25
26// Parent component
27function UserDashboard({ userId }) {
28 // Note: a new Promise is created on EVERY parent render (solution: a cache, below)
29 const userPromise = fetchUser(userId);
30
31 return (
32 <div>
33 <h1>User Dashboard</h1>
34 <Suspense fallback={<div>Loading user data...</div>}>
35 <UserProfile userPromise={userPromise} />
36 </Suspense>
37 </div>
38 );
39}UserProfile reads the data like a plain value, with no loading state. The problem is in the parent: fetchUser(userId) creates a new Promise on every render of UserDashboard, so every redraw sends a new request. React recommends Promises that come from a Server Component, from a framework or from a cache. We will build such a cache in the advanced section.
2. Using use with Context
use also reads context, just like useContext, but it can sit inside a condition. Here are three components using two contexts:
1import { use, createContext, useState } from 'react';
2
3// Creating Context
4const ThemeContext = createContext(null);
5const UserContext = createContext(null);
6
7// Component using use with Context
8function Header() {
9 // use can be used conditionally!
10 const theme = use(ThemeContext);
11 const user = use(UserContext);
12
13 return (
14 <header style={{
15 backgroundColor: theme === 'dark' ? '#333' : '#f8f9fa',
16 color: theme === 'dark' ? '#fff' : '#333',
17 padding: '20px'
18 }}>
19 <h1>Welcome, {user.name}!</h1>
20 <p>Current theme: {theme}</p>
21 </header>
22 );
23}
24
25// Ability to use conditionally
26function ConditionalComponent({ showUserInfo }) {
27 const theme = use(ThemeContext);
28
29 // use can be used conditionally - this is impossible with other hooks!
30 if (showUserInfo) {
31 const user = use(UserContext);
32 return <div>User: {user.name}</div>;
33 }
34
35 return <div>Theme: {theme}</div>;
36}
37
38// App with providers
39function App() {
40 const [theme, setTheme] = useState('light');
41 const [user] = useState({ name: 'John Smith', id: 1 });
42
43 return (
44 <ThemeContext.Provider value={theme}>
45 <UserContext.Provider value={user}>
46 <div>
47 <Header />
48 <ConditionalComponent showUserInfo={true} />
49 <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
50 Toggle theme
51 </button>
52 </div>
53 </UserContext.Provider>
54 </ThemeContext.Provider>
55 );
56}ConditionalComponent reaches for UserContext only when it has to show the user. We wrote the providers as ThemeContext.Provider, which still works, but since React 19 the shorter <ThemeContext value={theme}> is enough, and Provider is planned to be deprecated in the future.
3. use in loops
The hook can also appear in a loop, which is unthinkable with useState. The key is that every Promise comes from a cache:
1import { use, Suspense } from 'react';
2
3// Function creating a Promise for each post
4const postCache = new Map();
5
6function fetchPost(id) {
7 if (!postCache.has(id)) {
8 postCache.set(id, fetch(`/api/posts/${id}`).then(res => res.json()));
9 }
10 return postCache.get(id);
11}
12
13// Component displaying multiple posts
14function PostList({ postIds }) {
15 const posts = [];
16 for (const id of postIds) {
17 // use can be called in a loop!
18 posts.push(use(fetchPost(id)));
19 }
20
21 return (
22 <div>
23 {posts.map(post => (
24 <article
25 key={post.id}
26 style={{
27 marginBottom: '20px',
28 padding: '15px',
29 backgroundColor: '#f8f9fa',
30 borderRadius: '8px'
31 }}
32 >
33 <h3>{post.title}</h3>
34 <p>{post.content}</p>
35 <small>Author: {post.author}</small>
36 </article>
37 ))}
38 </div>
39 );
40}
41
42// Wrapper with Suspense
43function BlogSection() {
44 const postIds = [1, 2, 3, 4, 5];
45
46 return (
47 <section>
48 <h2>Latest Posts</h2>
49 <Suspense fallback={<PostListSkeleton count={5} />}>
50 <PostList postIds={postIds} />
51 </Suspense>
52 </section>
53 );
54}
55
56function PostListSkeleton({ count }) {
57 return (
58 <div>
59 {Array.from({ length: count }).map((_, i) => (
60 <div
61 key={i}
62 style={{
63 marginBottom: '20px',
64 padding: '15px',
65 backgroundColor: '#e9ecef',
66 borderRadius: '8px',
67 height: '100px'
68 }}
69 />
70 ))}
71 </div>
72 );
73}The for...of loop sits directly in the component body. If use sat in a callback passed to map, a plain function would call it instead of the component, and the React linter would report an error. The fetchPost function keeps promises in postCache. Without it, every retry of the render after suspending would create new promises and the list would never load.
Advanced patterns with use
1. Cache for Promises
Let's generalize the idea. getCachedPromise takes a key and a factory, and the Promise is created only on the first request for a given key:
1import { use, Suspense, useState } from 'react';
2
3// Simple cache for Promises
4const promiseCache = new Map();
5
6function getCachedPromise(key, promiseFactory) {
7 if (!promiseCache.has(key)) {
8 promiseCache.set(key, promiseFactory());
9 }
10 return promiseCache.get(key);
11}
12
13// API functions
14function fetchUserData(userId) {
15 const cached = getCachedPromise(
16 `user-${userId}`,
17 () => fetch(`/api/users/${userId}`).then(res => res.json())
18 );
19 return cached;
20}
21
22function fetchUserPosts(userId) {
23 const cached = getCachedPromise(
24 `posts-${userId}`,
25 () => fetch(`/api/users/${userId}/posts`).then(res => res.json())
26 );
27 return cached;
28}
29
30// Components
31function UserInfo({ userId }) {
32 const userPromise = fetchUserData(userId);
33 const user = use(userPromise);
34
35 return (
36 <div style={{
37 padding: '20px',
38 backgroundColor: '#f8f9fa',
39 borderRadius: '8px',
40 marginBottom: '20px'
41 }}>
42 <h2>{user.name}</h2>
43 <p>{user.email}</p>
44 <p>{user.location}</p>
45 <p>Joined: {new Date(user.joinedDate).toLocaleDateString('en-US')}</p>
46 </div>
47 );
48}
49
50function UserPosts({ userId }) {
51 const postsPromise = fetchUserPosts(userId);
52 const posts = use(postsPromise);
53
54 return (
55 <div>
56 <h3>User posts ({posts.length})</h3>
57 {posts.map(post => (
58 <article
59 key={post.id}
60 style={{
61 marginBottom: '15px',
62 padding: '15px',
63 border: '1px solid #e9ecef',
64 borderRadius: '6px'
65 }}
66 >
67 <h4>{post.title}</h4>
68 <p>{post.excerpt}</p>
69 <small>{new Date(post.date).toLocaleDateString('en-US')}</small>
70 </article>
71 ))}
72 </div>
73 );
74}
75
76function UserProfilePage() {
77 const [userId, setUserId] = useState(1);
78
79 return (
80 <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
81 <div style={{ marginBottom: '20px' }}>
82 <button onClick={() => setUserId(1)} disabled={userId === 1}>
83 User 1
84 </button>
85 <button onClick={() => setUserId(2)} disabled={userId === 2}>
86 User 2
87 </button>
88 <button onClick={() => promiseCache.clear()}>
89 Clear cache
90 </button>
91 </div>
92
93 <Suspense fallback={<div>Loading profile...</div>}>
94 <UserInfo userId={userId} />
95 </Suspense>
96
97 <Suspense fallback={<div>Loading posts...</div>}>
98 <UserPosts userId={userId} />
99 </Suspense>
100 </div>
101 );
102}Two separate Suspense boundaries let the profile and the posts appear independently. The "Clear cache" button removes the promises but does not force a new render, so fresh data will be fetched on the next user change.
2. Parallel data loading
Promise.all fetches several resources at once, and use waits for all of them together. First, the API function and the dashboard component:
1import { use, Suspense } from 'react';
2
3// API functions
4async function fetchDashboardData() {
5 const [stats, activities, notifications] = await Promise.all([
6 fetch('/api/stats').then(res => res.json()),
7 fetch('/api/activities').then(res => res.json()),
8 fetch('/api/notifications').then(res => res.json())
9 ]);
10
11 return { stats, activities, notifications };
12}
13
14// Dashboard component
15function Dashboard({ dataPromise }) {
16 const { stats, activities, notifications } = use(dataPromise);
17
18 return (
19 <div style={{
20 display: 'grid',
21 gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
22 gap: '20px',
23 padding: '20px'
24 }}>
25 <StatsCard stats={stats} />
26 <ActivitiesCard activities={activities} />
27 <NotificationsCard notifications={notifications} />
28 </div>
29 );
30}
31The data reaches the three cards through props, so the cards are ordinary components. Here is their implementation:
1function StatsCard({ stats }) {
2 return (
3 <div style={{
4 padding: '20px',
5 backgroundColor: '#f8f9fa',
6 borderRadius: '8px',
7 boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
8 }}>
9 <h3>Statistics</h3>
10 <div style={{ display: 'grid', gap: '10px' }}>
11 <div>
12 <strong>Users:</strong> {stats.users.toLocaleString('en-US')}
13 </div>
14 <div>
15 <strong>Revenue:</strong> {stats.revenue.toLocaleString('en-US', {
16 style: 'currency',
17 currency: 'USD'
18 })}
19 </div>
20 <div>
21 <strong>Growth:</strong>
22 <span style={{ color: stats.growth > 0 ? '#28a745' : '#dc3545' }}>
23 {stats.growth > 0 ? '+' : ''}{stats.growth}%
24 </span>
25 </div>
26 </div>
27 </div>
28 );
29}
30
31function ActivitiesCard({ activities }) {
32 return (
33 <div style={{
34 padding: '20px',
35 backgroundColor: '#f8f9fa',
36 borderRadius: '8px',
37 boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
38 }}>
39 <h3>Recent Activities</h3>
40 <ul style={{ listStyle: 'none', padding: 0 }}>
41 {activities.slice(0, 5).map(activity => (
42 <li
43 key={activity.id}
44 style={{
45 padding: '8px 0',
46 borderBottom: '1px solid #e9ecef'
47 }}
48 >
49 <div style={{ fontWeight: '500' }}>{activity.user}</div>
50 <div style={{ fontSize: '14px', color: '#6c757d' }}>
51 {activity.action}
52 </div>
53 <small style={{ color: '#6c757d' }}>
54 {new Date(activity.timestamp).toLocaleString('en-US')}
55 </small>
56 </li>
57 ))}
58 </ul>
59 </div>
60 );
61}
62
63function NotificationsCard({ notifications }) {
64 const unreadCount = notifications.filter(n => !n.read).length;
65
66 return (
67 <div style={{
68 padding: '20px',
69 backgroundColor: '#f8f9fa',
70 borderRadius: '8px',
71 boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
72 }}>
73 <h3>
74 Notifications
75 {unreadCount > 0 && (
76 <span style={{
77 marginLeft: '10px',
78 padding: '2px 8px',
79 backgroundColor: '#dc3545',
80 color: 'white',
81 borderRadius: '12px',
82 fontSize: '14px'
83 }}>
84 {unreadCount}
85 </span>
86 )}
87 </h3>
88 <div style={{ maxHeight: '200px', overflowY: 'auto' }}>
89 {notifications.map(notification => (
90 <div
91 key={notification.id}
92 style={{
93 padding: '10px',
94 marginBottom: '8px',
95 backgroundColor: notification.read ? 'transparent' : '#e3f2fd',
96 borderRadius: '4px',
97 fontSize: '14px'
98 }}
99 >
100 <strong>{notification.title}</strong>
101 <p style={{ margin: '4px 0' }}>{notification.message}</p>
102 </div>
103 ))}
104 </div>
105 </div>
106 );
107}
108No card knows anything about Promises or Suspense. Finally, the main component and the skeleton:
1// Main component
2function DashboardApp() {
3 const dataPromise = fetchDashboardData();
4
5 return (
6 <div>
7 <h1 style={{ padding: '20px' }}>Admin Panel</h1>
8 <Suspense fallback={<DashboardSkeleton />}>
9 <Dashboard dataPromise={dataPromise} />
10 </Suspense>
11 </div>
12 );
13}
14
15function DashboardSkeleton() {
16 return (
17 <div style={{
18 display: 'grid',
19 gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
20 gap: '20px',
21 padding: '20px'
22 }}>
23 {[1, 2, 3].map(i => (
24 <div
25 key={i}
26 style={{
27 height: '250px',
28 backgroundColor: '#e9ecef',
29 borderRadius: '8px',
30 animation: 'pulse 1.5s ease-in-out infinite'
31 }}
32 />
33 ))}
34 </div>
35 );
36}Here again the Promise is created in the render of DashboardApp, so in a real application move it to a cache or a Server Component.
3. Error boundaries with use
A rejected Promise does not end up in a try/catch, but in the nearest Error Boundary. First the boundary itself, which still requires a class component:
1import { use, Suspense, Component, useState } from 'react';
2
3// Error Boundary
4class ErrorBoundary extends Component {
5 constructor(props) {
6 super(props);
7 this.state = { hasError: false, error: null };
8 }
9
10 static getDerivedStateFromError(error) {
11 return { hasError: true, error };
12 }
13
14 componentDidCatch(error, errorInfo) {
15 console.error('Error caught by boundary:', error, errorInfo);
16 }
17
18 render() {
19 if (this.state.hasError) {
20 return (
21 <div style={{
22 padding: '20px',
23 backgroundColor: '#f8d7da',
24 border: '1px solid #f5c6cb',
25 borderRadius: '8px',
26 color: '#721c24'
27 }}>
28 <h3>Something went wrong</h3>
29 <p>{this.state.error?.message || 'Unknown error'}</p>
30 <button
31 onClick={() => this.setState({ hasError: false, error: null })}
32 style={{
33 marginTop: '10px',
34 padding: '8px 16px',
35 backgroundColor: '#721c24',
36 color: 'white',
37 border: 'none',
38 borderRadius: '4px',
39 cursor: 'pointer'
40 }}
41 >
42 Try again
43 </button>
44 </div>
45 );
46 }
47
48 return this.props.children;
49 }
50}
51getDerivedStateFromError switches the view to a message, and componentDidCatch logs the error. Now a demo that can simulate a failure:
1// API function that may return an error
2function fetchDataWithError(shouldFail = false) {
3 return new Promise((resolve, reject) => {
4 setTimeout(() => {
5 if (shouldFail) {
6 reject(new Error('Error fetching data from server'));
7 } else {
8 resolve({
9 message: 'Data loaded successfully!',
10 timestamp: new Date().toISOString()
11 });
12 }
13 }, 1000);
14 });
15}
16
17// Component using use
18function DataDisplay({ dataPromise }) {
19 const data = use(dataPromise);
20
21 return (
22 <div style={{
23 padding: '20px',
24 backgroundColor: '#d4edda',
25 border: '1px solid #c3e6cb',
26 borderRadius: '8px',
27 color: '#155724'
28 }}>
29 <h3>Success!</h3>
30 <p>{data.message}</p>
31 <small>Loaded: {new Date(data.timestamp).toLocaleString('en-US')}</small>
32 </div>
33 );
34}
35
36// Main component
37function ErrorHandlingDemo() {
38 const [shouldFail, setShouldFail] = useState(false);
39 const [key, setKey] = useState(0);
40
41 const dataPromise = fetchDataWithError(shouldFail);
42
43 return (
44 <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
45 <h2>Error handling with use</h2>
46
47 <div style={{ marginBottom: '20px' }}>
48 <label>
49 <input
50 type="checkbox"
51 checked={shouldFail}
52 onChange={(e) => setShouldFail(e.target.checked)}
53 />
54 Simulate error
55 </label>
56 <button
57 onClick={() => setKey(k => k + 1)}
58 style={{ marginLeft: '20px' }}
59 >
60 Refresh
61 </button>
62 </div>
63
64 <ErrorBoundary key={key}>
65 <Suspense fallback={
66 <div style={{
67 padding: '20px',
68 backgroundColor: '#cfe2ff',
69 border: '1px solid #b6d4fe',
70 borderRadius: '8px',
71 color: '#084298'
72 }}>
73 Loading data...
74 </div>
75 }>
76 <DataDisplay dataPromise={dataPromise} />
77 </Suspense>
78 </ErrorBoundary>
79 </div>
80 );
81}Changing the key on ErrorBoundary recreates the boundary, which resets the error. Instead of a boundary you can also add .catch() to the promise and return a fallback value.
Practical applications of use
1. Routing system with lazy loading
A simple router combines lazy, Suspense and a context read with use:
1import { use, Suspense, lazy, useState, createContext } from 'react';
2
3// Simulation of a routing system
4const routes = {
5 '/': lazy(() => import('./pages/Home')),
6 '/about': lazy(() => import('./pages/About')),
7 '/contact': lazy(() => import('./pages/Contact')),
8 '/profile': lazy(() => import('./pages/Profile'))
9};
10
11// Context for routing
12const RouteContext = createContext(null);
13
14// Navigation hook
15function useNavigation() {
16 const { navigate } = use(RouteContext);
17 return navigate;
18}
19
20// Page component
21function Page({ path }) {
22 const Component = routes[path] || routes['/'];
23
24 return (
25 <Suspense fallback={<PageLoader />}>
26 <Component />
27 </Suspense>
28 );
29}
30
31function PageLoader() {
32 return (
33 <div style={{
34 display: 'flex',
35 justifyContent: 'center',
36 alignItems: 'center',
37 minHeight: '400px'
38 }}>
39 <div style={{ textAlign: 'center' }}>
40 <div style={{
41 width: '50px',
42 height: '50px',
43 border: '3px solid #f3f3f3',
44 borderTop: '3px solid #007bff',
45 borderRadius: '50%',
46 animation: 'spin 1s linear infinite',
47 margin: '0 auto 20px'
48 }} />
49 <p>Loading page...</p>
50 </div>
51 </div>
52 );
53}
54
55// Main application
56function RouterApp() {
57 const [currentPath, setCurrentPath] = useState('/');
58
59 const navigate = (path) => {
60 setCurrentPath(path);
61 window.history.pushState({}, '', path);
62 };
63
64 return (
65 <RouteContext.Provider value={{ navigate, currentPath }}>
66 <div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
67 <Navigation />
68 <main style={{ flex: 1, padding: '20px' }}>
69 <Page path={currentPath} />
70 </main>
71 </div>
72 </RouteContext.Provider>
73 );
74}
75
76function Navigation() {
77 const navigate = useNavigation();
78 const { currentPath } = use(RouteContext);
79
80 const links = [
81 { path: '/', label: 'Home' },
82 { path: '/about', label: 'About' },
83 { path: '/contact', label: 'Contact' },
84 { path: '/profile', label: 'Profile' }
85 ];
86
87 return (
88 <nav style={{
89 backgroundColor: '#333',
90 padding: '10px 20px'
91 }}>
92 {links.map(link => (
93 <button
94 key={link.path}
95 onClick={() => navigate(link.path)}
96 style={{
97 marginRight: '10px',
98 padding: '8px 16px',
99 backgroundColor: currentPath === link.path ? '#007bff' : 'transparent',
100 color: 'white',
101 border: 'none',
102 borderRadius: '4px',
103 cursor: 'pointer'
104 }}
105 >
106 {link.label}
107 </button>
108 ))}
109 </nav>
110 );
111}lazy loads a page's code only on the first visit, and Suspense shows PageLoader meanwhile. This is only a model, because in practice you will use React Router or your framework's router.
2. Dynamic forms with validation
The form keeps its state in context, and the fields read it through use. First the context, the field hook and the FormField component:
1import { use, createContext, useState } from 'react';
2
3// Context for the form
4const FormContext = createContext(null);
5
6// Hook for form management
7function useFormField(name) {
8 const form = use(FormContext);
9
10 return {
11 value: form.values[name] || '',
12 error: form.errors[name],
13 onChange: (e) => form.setValue(name, e.target.value),
14 onBlur: () => form.validateField(name)
15 };
16}
17
18// Form field component
19function FormField({ name, label, type = 'text', validate }) {
20 const field = useFormField(name);
21 const form = use(FormContext);
22
23 // Register validator
24 if (validate && !form.validators[name]) {
25 form.registerValidator(name, validate);
26 }
27
28 return (
29 <div style={{ marginBottom: '15px' }}>
30 <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
31 {label}
32 </label>
33 <input
34 type={type}
35 value={field.value}
36 onChange={field.onChange}
37 onBlur={field.onBlur}
38 style={{
39 width: '100%',
40 padding: '8px',
41 border: `1px solid ${field.error ? '#dc3545' : '#ddd'}`,
42 borderRadius: '4px'
43 }}
44 />
45 {field.error && (
46 <div style={{ color: '#dc3545', fontSize: '14px', marginTop: '5px' }}>
47 {field.error}
48 </div>
49 )}
50 </div>
51 );
52}
53Every field registers its validator and shows its own error. Now the provider that holds the values and runs validation:
1// Form provider
2function FormProvider({ children, onSubmit }) {
3 const [values, setValues] = useState({});
4 const [errors, setErrors] = useState({});
5 const [validators] = useState({});
6
7 const setValue = (name, value) => {
8 setValues(prev => ({ ...prev, [name]: value }));
9 // Clear error on value change
10 setErrors(prev => ({ ...prev, [name]: '' }));
11 };
12
13 const registerValidator = (name, validate) => {
14 validators[name] = validate;
15 };
16
17 const validateField = (name) => {
18 if (validators[name]) {
19 const error = validators[name](values[name], values);
20 setErrors(prev => ({ ...prev, [name]: error }));
21 return !error;
22 }
23 return true;
24 };
25
26 const validateAll = () => {
27 const newErrors = {};
28 let isValid = true;
29
30 Object.keys(validators).forEach(name => {
31 const error = validators[name](values[name], values);
32 if (error) {
33 newErrors[name] = error;
34 isValid = false;
35 }
36 });
37
38 setErrors(newErrors);
39 return isValid;
40 };
41
42 const handleSubmit = (e) => {
43 e.preventDefault();
44 if (validateAll()) {
45 onSubmit(values);
46 }
47 };
48
49 const formValue = {
50 values,
51 errors,
52 validators,
53 setValue,
54 registerValidator,
55 validateField,
56 handleSubmit
57 };
58
59 return (
60 <FormContext.Provider value={formValue}>
61 <form onSubmit={handleSubmit}>
62 {children}
63 </form>
64 </FormContext.Provider>
65 );
66}
67Validators receive a second argument: all form values. That matters in the usage example:
1// Usage example
2function RegistrationFormExample() {
3 const handleSubmit = (values) => {
4 console.log('Form submitted:', values);
5 alert('Registration completed successfully!');
6 };
7
8 return (
9 <div style={{ maxWidth: '400px', margin: '0 auto', padding: '20px' }}>
10 <h2>Registration Form</h2>
11
12 <FormProvider onSubmit={handleSubmit}>
13 <FormField
14 name="email"
15 label="Email"
16 type="email"
17 validate={(value) => {
18 if (!value) return 'Email is required';
19 if (!/\S+@\S+\.\S+/.test(value)) return 'Invalid email';
20 return '';
21 }}
22 />
23
24 <FormField
25 name="password"
26 label="Password"
27 type="password"
28 validate={(value) => {
29 if (!value) return 'Password is required';
30 if (value.length < 8) return 'Password must be at least 8 characters';
31 return '';
32 }}
33 />
34
35 <FormField
36 name="confirmPassword"
37 label="Confirm password"
38 type="password"
39 validate={(value, allValues) => {
40 if (value !== allValues.password) return 'Passwords do not match';
41 return '';
42 }}
43 />
44
45 <button
46 type="submit"
47 style={{
48 width: '100%',
49 padding: '10px',
50 backgroundColor: '#007bff',
51 color: 'white',
52 border: 'none',
53 borderRadius: '4px',
54 fontSize: '16px',
55 cursor: 'pointer'
56 }}
57 >
58 Register
59 </button>
60 </FormProvider>
61 </div>
62 );
63}The "Confirm password" field compares its value with allValues.password. It cannot call use inside the validator, because use only works while a component or hook is rendering, and the validator runs in an event handler.
Best practices with use
1. When to use use
Here are good and bad uses in a nutshell:
1// GOOD - conditional usage
2function ConditionalData({ showDetails }) {
3 if (showDetails) {
4 const details = use(detailsPromise);
5 return <DetailView details={details} />;
6 }
7 return <Summary />;
8}
9
10// GOOD - in loops
11function MultipleResources({ resourceIds }) {
12 const resources = [];
13 for (const id of resourceIds) {
14 resources.push(use(fetchResource(id)));
15 }
16 return <ResourceList resources={resources} />;
17}
18
19// BAD - unnecessary use for simple values
20function BadExample() {
21 const value = use(Promise.resolve(42)); // Unnecessary!
22 return <div>{value}</div>;
23}In MultipleResources, the fetchResource function must return a cached Promise, just like fetchPost above.
2. Cache and optimization
The cache can be wrapped in a custom hook:
1// Simple cache implementation
2const cache = new Map();
3
4function useCachedResource(key, fetcher) {
5 if (!cache.has(key)) {
6 cache.set(key, fetcher());
7 }
8
9 const promise = cache.get(key);
10 const resource = use(promise);
11
12 return resource;
13}
14
15// Usage
16function CachedDataComponent({ userId }) {
17 const userData = useCachedResource(
18 `user-${userId}`,
19 () => fetchUser(userId)
20 );
21
22 return <UserProfile user={userData} />;
23}useCachedResource is an ordinary custom hook, and a module-level Map survives between renders.
3. Error handling
Retrying with exponential backoff can be wrapped in a promise factory:
1// Wrapper with retry logic
2function createRetryablePromise(fetcher, maxRetries = 3) {
3 return async () => {
4 let lastError;
5
6 for (let i = 0; i < maxRetries; i++) {
7 try {
8 return await fetcher();
9 } catch (error) {
10 lastError = error;
11 if (i < maxRetries - 1) {
12 // Exponential backoff
13 await new Promise(resolve =>
14 setTimeout(resolve, Math.pow(2, i) * 1000)
15 );
16 }
17 }
18 }
19
20 throw lastError;
21 };
22}
23
24// Usage
25function RobustDataFetching() {
26 const dataPromise = createRetryablePromise(
27 () => fetch('/api/critical-data').then(res => res.json())
28 )();
29
30 return (
31 <ErrorBoundary>
32 <Suspense fallback={<div>Loading...</div>}>
33 <DataComponent dataPromise={dataPromise} />
34 </Suspense>
35 </ErrorBoundary>
36 );
37}Here, too, the Promise is created during render, so in production combine it with a cache.
Recap before the exercises: memo with a custom comparison and the optimization process
The exercises after this lesson come back to memo. By default memo compares every prop shallowly, with Object.is. As the second argument you can pass your own function arePropsEqual(prevProps, nextProps): when it returns true, React treats the props as equal and skips the render, and when it returns false, it renders the component:
1import { memo } from 'react';
2
3const TelemetryPanel = memo(function TelemetryPanel({ readings, title }) {
4 return <Chart data={readings} label={title} />;
5}, (prevProps, nextProps) =>
6 // true = we treat the props as equal, so React skips the render
7 prevProps.readings.length === nextProps.readings.length && prevProps.title === nextProps.title
8);The function does nothing by itself, for example it does not switch on a deep comparison of objects: only what you write in it counts. A mistake in the comparison means a stale screen, so use it rarely.
Each of these techniques is a tool, not a goal. Always optimize in the same order: first a baseline measurement, then profiling, which shows where the render time goes, then identifying the bottleneck and implementing one fix. Finally you measure the impact of the change and run regression tests to check that the app still works as before. You can do these last two steps in either order, as long as you skip neither.
Summary
The use hook opens new possibilities in React, allowing for more flexible management of asynchronous data and context. Its unique ability to be used conditionally and in loops makes it a powerful tool for advanced applications.
My advice: never create a Promise in a client component without a cache. Remember: use is the station's receiver waiting for the probe's report, but the report itself needs one fixed channel.
Code for this lesson: App.jsx
1import React, { use, createContext, useState, Suspense } from 'react';
2
3// Space application contexts
4const ThemeContext = createContext('dark');
5const CrewContext = createContext(null);
6
7// Simulating data fetching from API (Promise)
8function fetchMissionData() {
9 return new Promise((resolve) => {
10 setTimeout(() => {
11 resolve({
12 name: 'Andromeda Mission',
13 status: 'In progress',
14 crew: 7,
15 distance: '2.537 million ly',
16 fuelLevel: 78,
17 });
18 }, 1500);
19 });
20}
21
22// Component reading data from Context using use()
23function CrewPanel() {
24 // use() with Context - works like useContext, but can be conditional!
25 const crew = use(CrewContext);
26 const theme = use(ThemeContext);
27
28 const isDark = theme === 'dark';
29 return (
30 <div style={{
31 padding: '16px', marginBottom: '16px',
32 background: isDark ? 'rgba(0, 212, 255, 0.08)' : '#f0f8ff',
33 borderRadius: '10px', border: `1px solid ${isDark ? '#1a3a5c' : '#b0d4f1'}`,
34 }}>
35 <h3 style={{ color: isDark ? '#00d4ff' : '#0066cc', margin: '0 0 8px' }}>
36 Ship crew
37 </h3>
38 {crew.map((member) => (
39 <div key={member.id} style={{
40 display: 'flex', justifyContent: 'space-between',
41 padding: '6px 0', borderBottom: `1px solid ${isDark ? '#1a2a3c' : '#dde8f0'}`,
42 color: isDark ? '#c0c8d0' : '#333', fontSize: '14px',
43 }}>
44 <span>{member.name}</span>
45 <span style={{ color: isDark ? '#778da9' : '#666' }}>{member.role}</span>
46 </div>
47 ))}
48 </div>
49 );
50}
51
52// Component reading a Promise using use()
53function MissionDetails({ missionPromise }) {
54 // use() automatically "unwraps" the Promise
55 const mission = use(missionPromise);
56
57 return (
58 <div style={{
59 padding: '20px', background: 'rgba(255, 215, 0, 0.08)',
60 borderRadius: '10px', border: '1px solid #3a3a1c',
61 }}>
62 <h3 style={{ color: '#ffd700', margin: '0 0 12px' }}>
63 {mission.name}
64 </h3>
65 <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
66 {[
67 ['Status', mission.status],
68 ['Crew', `${mission.crew} people`],
69 ['Distance', mission.distance],
70 ['Fuel', `${mission.fuelLevel}%`],
71 ].map(([label, value]) => (
72 <div key={label} style={{
73 padding: '10px', background: 'rgba(255, 255, 255, 0.03)',
74 borderRadius: '6px', fontSize: '13px',
75 }}>
76 <div style={{ color: '#778da9', marginBottom: '4px' }}>{label}</div>
77 <div style={{ color: '#e0e1dd', fontWeight: 'bold' }}>{value}</div>
78 </div>
79 ))}
80 </div>
81 </div>
82 );
83}
84
85// use() can sit in a condition: here we read the context only when the details are visible.
86// With useContext such a call after a return statement would not be allowed.
87function ConditionalInfo({ showDetails }) {
88 if (!showDetails) {
89 return (
90 <div style={{
91 padding: '12px', marginTop: '16px',
92 background: 'rgba(255, 255, 255, 0.03)',
93 borderRadius: '8px', color: '#778da9', fontSize: '13px',
94 }}>
95 Details hidden
96 </div>
97 );
98 }
99 const crew = use(CrewContext);
100 return (
101 <div style={{
102 padding: '12px', marginTop: '16px',
103 background: 'rgba(0, 212, 255, 0.06)',
104 borderRadius: '8px', color: '#c0c8d0', fontSize: '13px',
105 }}>
106 On board: {crew.length} crew members ({crew.map((member) => member.role).join(', ')})
107 </div>
108 );
109}
110
111function LoadingFallback() {
112 return (
113 <div style={{
114 padding: '40px', textAlign: 'center', color: '#778da9',
115 }}>
116 <div style={{ fontSize: '24px', marginBottom: '10px' }}>Loading mission data...</div>
117 <div style={{
118 width: '50px', height: '50px', margin: '0 auto',
119 border: '3px solid #1a3a5c', borderTop: '3px solid #00d4ff',
120 borderRadius: '50%', animation: 'spin 1s linear infinite',
121 }} />
122 </div>
123 );
124}
125
126export default function App() {
127 const [showDetails, setShowDetails] = useState(true);
128 const missionPromise = React.useMemo(() => fetchMissionData(), []);
129
130 const crewData = [
131 { id: 1, name: 'Captain Nova', role: 'Commander' },
132 { id: 2, name: 'Dr. Stella', role: 'Navigator' },
133 { id: 3, name: 'Eng. Orion', role: 'Mechanic' },
134 { id: 4, name: 'Lt. Vega', role: 'Pilot' },
135 ];
136
137 return (
138 <ThemeContext.Provider value="dark">
139 <CrewContext.Provider value={crewData}>
140 <div style={{
141 minHeight: '100vh',
142 background: 'linear-gradient(135deg, #0a0e27 0%, #1a1a3e 100%)',
143 padding: '24px', fontFamily: 'monospace',
144 }}>
145 <h1 style={{ color: '#00d4ff', textAlign: 'center', marginBottom: '8px' }}>
146 Hook use() - Promise and Context
147 </h1>
148 <p style={{ color: '#778da9', textAlign: 'center', marginBottom: '24px', fontSize: '14px' }}>
149 A new way to read data in React
150 </p>
151
152 <div style={{ maxWidth: '600px', margin: '0 auto' }}>
153 <CrewPanel />
154
155 <Suspense fallback={<LoadingFallback />}>
156 <MissionDetails missionPromise={missionPromise} />
157 </Suspense>
158
159 <div style={{ marginTop: '16px', textAlign: 'center' }}>
160 <button
161 onClick={() => setShowDetails(d => !d)}
162 style={{
163 padding: '10px 20px', background: '#1a3a5c',
164 border: 'none', borderRadius: '6px', color: '#00d4ff',
165 cursor: 'pointer', fontSize: '13px',
166 }}
167 >
168 {showDetails ? 'Hide details' : 'Show details'}
169 </button>
170 </div>
171
172 <ConditionalInfo showDetails={showDetails} />
173 </div>
174 </div>
175 </CrewContext.Provider>
176 </ThemeContext.Provider>
177 );
178}Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is the purpose of the second argument (custom comparator) in React.memo(Component, compareFn)?
Hands-on tasks in the game
- Vertical ordering
Arrange the performance optimization workflow in order: