mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-06-22 21:08:12 +08:00
feat(curriculum): add react data fetching and memoization transcripts (#59638)
Co-authored-by: Zaira <33151350+zairahira@users.noreply.github.com> Co-authored-by: Jessica Wilkins <67210629+jdwilkin4@users.noreply.github.com> Co-authored-by: Dario-DC <105294544+Dario-DC@users.noreply.github.com>
This commit is contained in:
parent
d1e78aa987
commit
08c12b6620
@ -2,13 +2,300 @@
|
||||
id: 67d1a99d10fd509c88faf3bf
|
||||
title: How Does Data Fetching Work in React?
|
||||
challengeType: 11
|
||||
videoId: nVAaxZ34khk
|
||||
videoId: _gwWcrZ1WJo
|
||||
dashedName: how-does-data-fetching-work-in-react
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
Watch the video lecture and answer the questions below.
|
||||
Watch the video or read the transcript and answer the questions below.
|
||||
|
||||
# --transcript--
|
||||
|
||||
How does data fetching work in React?
|
||||
|
||||
React apps often rely on external APIs and databases for dynamic content. To access the data from those APIs and databases, you need to use some data fetching techniques.
|
||||
|
||||
Let's take a look at how data fetching works in React and the different options available to you for fetching data.
|
||||
|
||||
React is not opinionated about how you fetch your data, this means on a basic level, you can use the built-in Fetch API, which all modern browsers support.
|
||||
|
||||
You can also use Axios and SWR. Axios is promise-based HTTP request library built on top of the XMLHttpRequest object, and SWR is a React hook for data fetching created by the Vercel team.
|
||||
|
||||
Let's start with an example. You first need to import the `useState` and `useEffect` hooks:
|
||||
|
||||
```js
|
||||
import { useState, useEffect } from "react";
|
||||
```
|
||||
|
||||
Then you will need to create three state variables called `loading`, `data`, and `error`:
|
||||
|
||||
```js
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
```
|
||||
|
||||
The `loading` variable will track whether the data is still being fetched. The `data` variable represents the data itself, and the `error` variable will capture any errors that might occur during the data fetching process.
|
||||
|
||||
Since data fetching is a side effect, it's best to use the Fetch API inside of a `useEffect` hook.
|
||||
|
||||
Here's an example of that:
|
||||
|
||||
```js
|
||||
useEffect(() => {
|
||||
fetch("https://jsonplaceholder.typicode.com/posts")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setData(data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setError(err);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
```
|
||||
|
||||
This `useEffect` fetches the data with the Fetch API and sets all the states.
|
||||
|
||||
You can make things better by using `async`/`await` instead of the `.then()` syntax. That means you have to have a separate function inside the `useEffect` because you cannot prefix `useEffect` with the `async` keyword:
|
||||
|
||||
```js
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
setData(data);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
```
|
||||
|
||||
You can then go ahead and use all of those states to render the data from the API.
|
||||
|
||||
Here's the full code:
|
||||
|
||||
```js
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const FetchPosts = () => {
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
setData(data);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <p>Loading...</p>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <p>{error.message}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{data.map((post) => (
|
||||
<li key={post.id}>{post.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export default FetchPosts;
|
||||
```
|
||||
|
||||
In the UI, you would see `Loading...` on the screen when the data is being fetched, and then the data or error would show depending on if the data fetch was successful.
|
||||
|
||||
Remember we talked about data fetching with Axios and SWR too. Let's take a look at an example using Axios.
|
||||
|
||||
You will first need to install Axios from the command line like this:
|
||||
|
||||
```sh
|
||||
npm i axios
|
||||
```
|
||||
|
||||
Then you will need to import Axios like this:
|
||||
|
||||
```js
|
||||
import axios from "axios";
|
||||
```
|
||||
|
||||
Then you can use the same state variables from earlier and fetch data from the API using `axios.get`:
|
||||
|
||||
```js
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await axios.get(
|
||||
"https://jsonplaceholder.typicode.com/users"
|
||||
);
|
||||
setData(res.data);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
```
|
||||
|
||||
You might have noticed that there is no `await res.json()` line in this example. That's because Axios automatically parses JSON, so there's no need for that.
|
||||
|
||||
The last example we will look at is to use the `useSWR` hook to fetch data.
|
||||
|
||||
Just like with Axios, you will need to install SWR like this:
|
||||
|
||||
```sh
|
||||
npm install swr
|
||||
```
|
||||
|
||||
Then you will need to import the `useSWR` hook into the file like this:
|
||||
|
||||
```js
|
||||
import useSWR from "swr";
|
||||
```
|
||||
|
||||
In comparison to the previous examples, the SWR syntax is way shorter. What you need to do is to create a fetcher function and pass it into the `useSWR` hook as its second parameter (the endpoint is the first parameter).
|
||||
|
||||
You also get to destructure both the data and error states from the `useSWR` hook, so you don't need the `useState` hook.
|
||||
|
||||
Here is the syntax:
|
||||
|
||||
```js
|
||||
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||
const { data, error } = useSWR(endpoint, fetcher);
|
||||
```
|
||||
|
||||
Note that the "fetcher" name here is only a convention, so you're free to name the variable whatever you want.
|
||||
|
||||
Here's a component fetching todos from the JSON Placeholder API:
|
||||
|
||||
```js
|
||||
import useSWR from "swr";
|
||||
|
||||
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||
|
||||
const FetchTodos = () => {
|
||||
const { data, error } = useSWR(
|
||||
"https://jsonplaceholder.typicode.com/todos",
|
||||
fetcher
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return <h2>Loading...</h2>;
|
||||
}
|
||||
if (error) {
|
||||
return <h2>Error: {error.message}</h2>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Todos</h2>
|
||||
<div>
|
||||
{data.map((todo) => (
|
||||
<h3 key={todo.id}>{todo.title}</h3>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FetchTodos;
|
||||
```
|
||||
|
||||
As you learned in a previous lecture on custom hooks, data fetching is a logic you can extract into a custom hook. So, if you're fetching data in multiple components and pages, it is best to create a `useFetch` hook.
|
||||
|
||||
Here's a `useFetch` hook that uses SWR for data fetching:
|
||||
|
||||
```js
|
||||
import useSWR from "swr";
|
||||
|
||||
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||
|
||||
const useFetch = (url) => {
|
||||
const { data, error } = useSWR(url, fetcher);
|
||||
|
||||
return {
|
||||
data,
|
||||
loading: !data && !error,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFetch;
|
||||
```
|
||||
|
||||
And here's how to use the `useFetch` hook to rewrite the first example that fetches posts from the JSON Placeholder API:
|
||||
|
||||
```js
|
||||
import useFetch from "./useFetch";
|
||||
|
||||
const FetchPosts = () => {
|
||||
const { data, loading, error } = useFetch(
|
||||
"https://jsonplaceholder.typicode.com/posts"
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <h2>Loading...</h2>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <h2>{error.message}</h2>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Posts</h2>
|
||||
<ul>
|
||||
{data.map((post) => (
|
||||
<li key={post.id}>{post.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FetchPosts;
|
||||
```
|
||||
|
||||
# --questions--
|
||||
|
||||
|
||||
@ -2,13 +2,138 @@
|
||||
id: 67d2f4ddb4a4306fdf5bbaee
|
||||
title: What Is Memoization, and How Does the useMemo Hook Work?
|
||||
challengeType: 11
|
||||
videoId: nVAaxZ34khk
|
||||
videoId: 2X7LD_6P4eI
|
||||
dashedName: what-is-memoization-and-how-does-the-usememo-hook-work
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
Watch the lecture video and answer the questions below.
|
||||
Watch the video or read the transcript and answer the questions below.
|
||||
|
||||
# --transcript--
|
||||
|
||||
What is memoization and how does the `useMemo` hook work?
|
||||
|
||||
As your React app gets larger, unnecessary re-renders and expensive calculations can slow down performance, leading to slow UI updates and increased resource usage.
|
||||
|
||||
This can be especially problematic in apps with complex state management, large lists, functions that require heavy computations, and many components with a single parent.
|
||||
|
||||
This gives rise to the need to optimize your React app for better performance by minimizing redundant computations and ensuring smoother interactions.
|
||||
|
||||
React solves this problem with a process called memoization, a technique which caches values and functions to prevent unnecessary recalculations, so your app can be faster and more responsive.
|
||||
|
||||
By definition, memoization is an optimization technique in which the result of expensive function calls are cached (remembered) based on specific arguments. When the same arguments are provided again, the cached result is returned instead of re-computing the function.
|
||||
|
||||
The memoization process happens this way:
|
||||
|
||||
- Store the results of function calls along with their input arguments.
|
||||
|
||||
- Before executing the function, check if the result for the current arguments already exists in the cache.
|
||||
|
||||
- If it exists, return the cached result instead of running the computation again.
|
||||
|
||||
- If it doesn't exist, compute the result, store it in the cache, and then return it.
|
||||
|
||||
To improve developer experience with memoization, React provides three tools – `React.memo` (or `memo`), `useMemo` and `useCallback`.
|
||||
|
||||
As you might guess, both `useMemo` and `useCallback` are hooks, but `React.memo` is a component wrapper, a higher-order function (HOC).
|
||||
|
||||
In the next lecture, we will take a look at how the `useCallback` hook and `React.memo` work.
|
||||
|
||||
`useMemo` lets you memoize computed values while `useCallback` does the same for function references.
|
||||
|
||||
If you're wondering what computed values and function references are, computed values refer to the result of executing a function, while function references are the pointers to functions – the function object in memory.
|
||||
|
||||
Let's see how to use the `useMemo` hook first. Here's the basic syntax of the `useMemo` hook:
|
||||
|
||||
```js
|
||||
const memoizedValue = useMemo(
|
||||
function () {
|
||||
return computeExpensiveValue(a, b);
|
||||
},
|
||||
[a, b]
|
||||
);
|
||||
```
|
||||
|
||||
You can see all that's needed is to wrap the `useMemo` hook around the function.
|
||||
|
||||
This `ExpensiveSquare` component will receive a `num` prop which it will use to calculate the square:
|
||||
|
||||
```js
|
||||
function ExpensiveSquare({ num }) {
|
||||
function calculateSquare(n) {
|
||||
console.log("Calculating square...");
|
||||
return n * n;
|
||||
}
|
||||
|
||||
const squared = calculateSquare(num);
|
||||
return (
|
||||
<p>
|
||||
Square of {num}: {squared}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
export default ExpensiveSquare;
|
||||
```
|
||||
|
||||
Here's the `App` component where the `ExpensiveSquare` is being used:
|
||||
|
||||
```js
|
||||
import { useState, useEffect } from "react";
|
||||
import ExpensiveSquare from "./components/ExpensiveSquare";
|
||||
|
||||
function App() {
|
||||
const [timer, setTimer] = useState(0);
|
||||
const [num, setNum] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => setTimer((c) => c + 1), 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Timer: {timer} seconds gone</h1>
|
||||
<ExpensiveSquare num={num} />
|
||||
<button onClick={() => setNum((n) => n + 1)}>Increase Number</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
The `timer` in the `useEffect`, running every second, will make the `calculateSquare` function runs any time it runs, even when you don't increase the `num` state variable.
|
||||
|
||||
To solve this problem, we can use the `useMemo` hook by wrapping the function call in it and specifying the `num` variable as the dependency:
|
||||
|
||||
```js
|
||||
// import the useMemo hook
|
||||
import { useMemo } from "react";
|
||||
|
||||
function ExpensiveSquare({ num }) {
|
||||
function calculateSquare(n) {
|
||||
console.log("Calculating square...");
|
||||
return n * n;
|
||||
}
|
||||
|
||||
// const squared = calculateSquare(num);
|
||||
// Wrap the function call in useMemo instead
|
||||
const squared = useMemo(() => calculateSquare(num), [num]);
|
||||
|
||||
return (
|
||||
<p>
|
||||
Square of {num}: {squared}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExpensiveSquare;
|
||||
```
|
||||
|
||||
This will make sure the function is memoized by caching the result, so calculation happens only when the `num` variable changes, not when anything changes in the component it's being used in.
|
||||
|
||||
The `calculateSquare` function call is not running any time `timer` changes anymore but on the initial render and when `num` changes.
|
||||
|
||||
# --questions--
|
||||
|
||||
|
||||
@ -2,13 +2,185 @@
|
||||
id: 67d2f51ff2c927713caa24fa
|
||||
title: How Do the useCallback Hook and React.memo Work?
|
||||
challengeType: 11
|
||||
videoId: nVAaxZ34khk
|
||||
videoId: BGK2SLMOthI
|
||||
dashedName: how-do-the-usecallback-hook-and-react-memo-work
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
Watch the lecture video and answer the questions below.
|
||||
Watch the video or read the transcript and answer the questions below.
|
||||
|
||||
# --transcript--
|
||||
|
||||
How do the `useCallback` hook and `React.memo` work?
|
||||
|
||||
In the last lecture, you learned about memoization and how the `useMemo` hook works.
|
||||
|
||||
In this lecture, you'll learn how the `useCallback` hook and `React.memo` work.
|
||||
|
||||
In the last lecture, we also mentioned that `useCallback` is for memoizing function references.
|
||||
|
||||
For `React.memo`, it lets you memoize a component to prevent it from unnecessary re-renders when its prop has not changed.
|
||||
|
||||
Here's the basic syntax of the `useCallback` hook:
|
||||
|
||||
```js
|
||||
const handleClick = useCallback(() => {
|
||||
// code goes here
|
||||
}, [dependency]);
|
||||
```
|
||||
|
||||
And here's the basic syntax of `React.memo`:
|
||||
|
||||
```js
|
||||
const MemoizedComponent = React.memo(({ prop }) => {
|
||||
return (
|
||||
<>
|
||||
{/* Presentation */}
|
||||
</>
|
||||
)
|
||||
});
|
||||
```
|
||||
|
||||
Let's look at an example of the `useCallback` hook:
|
||||
|
||||
```js
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
const handleClick = () => {
|
||||
setCount((prevCount) => prevCount + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log("useEffect runs");
|
||||
}, [handleClick]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Count: {count}</p>
|
||||
<button onClick={handleClick}>Increment</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Counter;
|
||||
```
|
||||
|
||||
In the component, the effect runs any time `handleClick` changes because the `handleClick` function is being recreated on every render.
|
||||
|
||||
To fix this, you need to tell React to treat the `handleClick` function as the same thing across renders by memoizing it with the `useCallback` hook, so it doesn't get recreated:
|
||||
|
||||
```js
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
// Memoize the handleClick function with useCallback
|
||||
const handleClick = useCallback(() => {
|
||||
setCount((prevCount) => prevCount + 1);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("useEffect runs");
|
||||
}, [handleClick]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Count: {count}</p>
|
||||
<button onClick={handleClick}>Increment</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Counter;
|
||||
```
|
||||
|
||||
Now the `handleClick` function is not being recreated on every render.
|
||||
|
||||
To show you how the `React.memo` (or `memo`) higher-order function works and the `useCallback` hook work in tandem, here's a `Counter` component with a `handleClick` function that needs `useCallback` but is currently not using it:
|
||||
|
||||
```js
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import CounterChild from "./CounterChild";
|
||||
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
const [timer, setTimer] = useState(new Date().toLocaleTimeString());
|
||||
|
||||
const handleClick = () => {
|
||||
setCount(count + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setTimer(new Date().toLocaleTimeString());
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Time: {timer}</h1>
|
||||
<p>Count: {count}</p>
|
||||
<button onClick={handleClick}>Increment</button>
|
||||
<CounterChild onClick={handleClick} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Counter;
|
||||
```
|
||||
|
||||
This function also has a timer in state that updates every second. This makes the component re-render every time the `timer` changes, making the `handleClick` function get recreated on every render.
|
||||
|
||||
That's why the `handleClick` needs to be memoized with `useCallback`.
|
||||
|
||||
Here's the `CounterChild` component:
|
||||
|
||||
```js
|
||||
const CounterChild = ({ onClick }) => {
|
||||
console.log("CounterChild component rendered");
|
||||
return <button onClick={onClick}>Increment from Child</button>;
|
||||
};
|
||||
|
||||
export default CounterChild;
|
||||
```
|
||||
|
||||
This `CounterChild` component takes an `onClick` prop, giving you the ability to also increment the counter from it.
|
||||
|
||||
Since the `CounterChild` component is a child of the `Counter` component, it will also render any time the `Counter` re-renders due to the changing timer. So, the `CounterChild` also needs to be memoized.
|
||||
|
||||
Without memoization, because as the component re-renders due to the timer updating every second, the `CounterChild` component is also re-rendered.
|
||||
|
||||
To prevent this, you need to memoize the `CounterChild` component with `React.memo`:
|
||||
|
||||
```js
|
||||
import React from "react";
|
||||
|
||||
const CounterChild = React.memo(({ onClick }) => {
|
||||
console.log("CounterChild component rendered");
|
||||
return <button onClick={onClick}>Increment from Child</button>;
|
||||
});
|
||||
|
||||
export default CounterChild;
|
||||
```
|
||||
|
||||
Things do not work optimally yet even after memoizing the `CounterChild` with `React.memo`.
|
||||
|
||||
This happens because the `handleClick` function is being recreated on every render, so it also needs to be memoized with `useCallback`, in order to tell React that you need the function to stay the same across renders:
|
||||
|
||||
```js
|
||||
const handleClick = useCallback(() => {
|
||||
setCount((prevCount) => prevCount + 1);
|
||||
}, [count]);
|
||||
```
|
||||
|
||||
Now, the component only re-renders when the `count` state changes.
|
||||
|
||||
# --questions--
|
||||
|
||||
|
||||
@ -2,31 +2,327 @@
|
||||
id: 67e2a513dbffdc8dcf1700af
|
||||
title: What Is the useOptimistic Hook, and How Does It Work?
|
||||
challengeType: 11
|
||||
videoId: nVAaxZ34khk
|
||||
videoId: ZmjYqlrU4g0
|
||||
dashedName: what-is-the-useoptimistic-hook-and-how-does-it-work
|
||||
---
|
||||
|
||||
# --description--
|
||||
|
||||
Watch the lecture video and answer the questions below.
|
||||
Watch the video or read the transcript and answer the questions below.
|
||||
|
||||
# --transcript--
|
||||
|
||||
What is the `useOptimistic` hook and how does it work?
|
||||
|
||||
Recent versions of React introduced server components and server actions to shift some of the rendering and logic responsibilities to the server.
|
||||
|
||||
Along with those updates, React added a new hook called `useOptimistic` to keep UIs responsive while waiting for an async action to complete in the background.
|
||||
|
||||
While this is often used for fetching data from a server, it's not limited to that. The hook is generally useful for handling async operations, ensuring the UI remains smooth and interactive while the action runs.
|
||||
|
||||
Let's take a look at what the `useOptimistic` hook is and how it contributes to making snappy and responsive UIs.
|
||||
|
||||
The `useOptimistic` hook helps manage "optimistic updates" in the UI, a strategy in which you provide immediate updates to the UI based on the expected outcome of an action, like waiting for a server response.
|
||||
|
||||
Here's the basic syntax of the `useOptimistic` hook:
|
||||
|
||||
```js
|
||||
const [optimisticState, addOptimistic] = useOptimistic(actualState, updateFunction);
|
||||
```
|
||||
|
||||
- `optimisticState` is the temporary state that updates right away for a better user experience.
|
||||
|
||||
- `addOptimistic` is the function that applies the optimistic update before the actual state changes.
|
||||
|
||||
- `actualState` is the real state value that comes from the result of an action, like fetching data from a server.
|
||||
|
||||
- `updateFunction` is the function that determines how the optimistic state should update when called.
|
||||
|
||||
At first glance, it might seem like the `useOptimistic` hook is just another way to handle loading states in React. But it's more than that.
|
||||
|
||||
A loading state controls whether you see a spinner, message, or some other indicator in the UI while something happens in the background.
|
||||
|
||||
However, the `useOptimistic` hook updates the UI instantaneously based on an expected outcome, even before you, say, make a call to an API. This hook gives you a chance to show a loading indicator or message, handle potential errors gracefully, and show instant feedback to make the UI feel snappy.
|
||||
|
||||
This will become clearer as we go through some examples showing how the `useOptimistic` hook works.
|
||||
|
||||
Here's an action that simulates saving a task to a server. It returns the task after a 1 second delay, as it could happen with a real-world API request:
|
||||
|
||||
```js
|
||||
export async function saveTask(task) {
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
|
||||
return task;
|
||||
}
|
||||
```
|
||||
|
||||
Here's the code that sets up the `useOptimistic` hook by importing and initializing it, with an `handleSubmit` function that sends an input to the action:
|
||||
|
||||
```js
|
||||
"use client";
|
||||
|
||||
import { useOptimistic } from "react";
|
||||
|
||||
export default function TaskList({ tasks, addTask }) {
|
||||
const [optimisticTasks, addOptimisticTask] = useOptimistic(
|
||||
tasks,
|
||||
(state, newTask) => [...state, { text: newTask, pending: true }]
|
||||
);
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
|
||||
addOptimisticTask(formData.get("task"));
|
||||
|
||||
addTask(formData);
|
||||
e.target.reset();
|
||||
}
|
||||
|
||||
return <>{/* UI */}</>;
|
||||
}
|
||||
```
|
||||
|
||||
In the code, the `useOptimistic` hook keeps a temporary list of tasks that updates immediately when you add a new task.
|
||||
|
||||
The line, `(state, newTask) => [...state, { text: newTask, pending: true }]` ensures that a new task appears with a pending status even before the server confirms something is coming from the form.
|
||||
|
||||
When the form is submitted, the `handleSubmit` function extracts the task and adds it "optimistically" with the `addOptimisticTask` parameter. Then `addTask` is passed as a prop which sends the task to the server. Finally, the form is reset by calling `e.target.reset()`.
|
||||
|
||||
Here's the `TaskList` component:
|
||||
|
||||
```js
|
||||
"use client";
|
||||
import { useOptimistic, startTransition } from "react";
|
||||
|
||||
export default function TaskList({ tasks, addTask }) {
|
||||
const [optimisticTasks, addOptimisticTask] = useOptimistic(
|
||||
tasks,
|
||||
(state, newTask) => [...state, { text: newTask, pending: true }]
|
||||
);
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
|
||||
startTransition(() => {
|
||||
addOptimisticTask(formData.get("task"));
|
||||
});
|
||||
|
||||
addTask(formData);
|
||||
e.target.reset();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto p-4">
|
||||
<h3 className="text-xl font-medium mb-3">Tasks</h3>
|
||||
|
||||
<ul className="space-y-2 mb-4">
|
||||
{optimisticTasks.map((task, index) => (
|
||||
<li key={index} className="p-2 border-b">
|
||||
{task.text}
|
||||
{task.pending && (
|
||||
<small className="ml-2 text-gray-500 text-sm">
|
||||
Adding Task...
|
||||
</small>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
name="task"
|
||||
placeholder="Type in a task..."
|
||||
className="flex-1 p-2 border"
|
||||
/>
|
||||
<button type="submit" className="bg-gray-200 px-3 py-2 cursor-pointer">
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Here, we are looping through the `optimisticTask` parameter to display the task. When `task.pending` is `true`, the text `Adding Task...` is displayed next to the task, confirming that the task has been added optimistically before the server confirms it.
|
||||
|
||||
Here's the `Task` component that manages the state for the form. It calls the `saveTask` function from the action so it can add the task, and appends the new task once it is received by the server:
|
||||
|
||||
```js
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import TaskList from "./TaskList";
|
||||
import { saveTask } from "./actions";
|
||||
|
||||
export default function Tasks() {
|
||||
const [tasks, setTasks] = useState([]);
|
||||
|
||||
async function addTask(formData) {
|
||||
const newTaskText = formData.get("task");
|
||||
|
||||
const savedTask = await saveTask(newTaskText);
|
||||
setTasks((prev) => [...prev, { text: savedTask, pending: false }]);
|
||||
}
|
||||
|
||||
return <TaskList tasks={tasks} addTask={addTask} />;
|
||||
}
|
||||
```
|
||||
|
||||
This ensures snappy UI updates by showing instant feedback instead of waiting for a response. Once the task is saved, the `pending` property is removed, and the final task list updates accordingly.
|
||||
|
||||
In the UI, there are two things happening that are not supposed to happen. First, you can't see the `Adding Task...` text since it appears and disappears too quickly. Next, there's an error occurring after adding the task.
|
||||
|
||||
There are two things we need to do to address those issues.
|
||||
|
||||
First, we need to import `startTransition` from React and use it to wrap the line `addOptimisticTask(formData.get('task'))`:
|
||||
|
||||
```js
|
||||
startTransition(() => {
|
||||
addOptimisticTask(formData.get("task"));
|
||||
});
|
||||
```
|
||||
|
||||
Second, we need to make the `Adding Task...` text visible for some time before it goes away.
|
||||
|
||||
To do this, we can modify the `addTask` function with a pending state and simulate a delay of a few seconds before marking the task as completed. `setTimeout()` is ideal for this:
|
||||
|
||||
```js
|
||||
async function addTask(formData) {
|
||||
const newTaskText = formData.get("task");
|
||||
|
||||
// Add an optimistic task with a pending state
|
||||
const tempTask = { id: Date.now(), text: newTaskText, pending: true };
|
||||
setTasks((prev) => [...prev, tempTask]);
|
||||
|
||||
// Simulate a 3 seconds delay before marking the task as completed
|
||||
setTimeout(async () => {
|
||||
const savedTask = await saveTask(newTaskText);
|
||||
|
||||
setTasks((prev) =>
|
||||
prev.map((task) =>
|
||||
task.id === tempTask.id
|
||||
? { ...task, text: savedTask, pending: false }
|
||||
: task
|
||||
)
|
||||
);
|
||||
}, 3000);
|
||||
}
|
||||
```
|
||||
|
||||
And once you do that, everything works fine.
|
||||
|
||||
# --questions--
|
||||
|
||||
## --text--
|
||||
|
||||
What Is the useOptimistic Hook, and How Does It Work? question?
|
||||
What is the purpose of the `useOptimistic` hook?
|
||||
|
||||
## --answers--
|
||||
|
||||
Answer 1
|
||||
It allows components to fetch data from the server before rendering the UI.
|
||||
|
||||
### --feedback--
|
||||
|
||||
This hook ensures the UI reflects expected changes before an async operation completes.
|
||||
|
||||
---
|
||||
|
||||
Answer 2
|
||||
It helps manage optimistic updates by updating the UI immediately while waiting for an async operation, like a server response.
|
||||
|
||||
---
|
||||
|
||||
Answer 3
|
||||
It enables automatic error handling and rollback for failed API requests in React applications.
|
||||
|
||||
### --feedback--
|
||||
|
||||
This hook ensures the UI reflects expected changes before an async operation completes.
|
||||
|
||||
---
|
||||
|
||||
It optimizes state updates by batching them together to improve performance.
|
||||
|
||||
### --feedback--
|
||||
|
||||
This hook ensures the UI reflects expected changes before an async operation completes.
|
||||
|
||||
## --video-solution--
|
||||
|
||||
2
|
||||
|
||||
## --text--
|
||||
|
||||
How is the `useOptimistic` hook different from a loading state?
|
||||
|
||||
## --answers--
|
||||
|
||||
A loading state shows UI feedback while waiting for a response, whereas `useOptimistic` updates the UI immediately based on an expected outcome.
|
||||
|
||||
---
|
||||
|
||||
A loading state modifies server data instantly while `useOptimistic` only updates the client UI.
|
||||
|
||||
### --feedback--
|
||||
|
||||
One updates the UI before the server even knows about the request.
|
||||
|
||||
---
|
||||
|
||||
The `useOptimistic` hook is used for handling errors, whereas a loading state is only for showing spinners.
|
||||
|
||||
### --feedback--
|
||||
|
||||
One updates the UI before the server even knows about the request.
|
||||
|
||||
---
|
||||
|
||||
Both are the same, but `useOptimistic` provides automatic retries for failed requests.
|
||||
|
||||
### --feedback--
|
||||
|
||||
One updates the UI before the server even knows about the request.
|
||||
|
||||
## --video-solution--
|
||||
|
||||
1
|
||||
|
||||
## --text--
|
||||
|
||||
What does `addOptimistic` do in the `useOptimistic` hook syntax below?
|
||||
|
||||
```js
|
||||
const [optimisticState, addOptimistic] = useOptimistic(actualState, updateFunction);
|
||||
```
|
||||
|
||||
## --answers--
|
||||
|
||||
It applies the optimistic update before the actual state changes, providing a smoother user experience.
|
||||
|
||||
---
|
||||
|
||||
It fetches the real state from the server and updates the UI accordingly.
|
||||
|
||||
### --feedback--
|
||||
|
||||
This function updates the UI before the actual state changes.
|
||||
|
||||
---
|
||||
|
||||
It replaces the actual state with a temporary state after receiving a server response.
|
||||
|
||||
### --feedback--
|
||||
|
||||
This function updates the UI before the actual state changes.
|
||||
|
||||
---
|
||||
|
||||
It validates server data before applying the optimistic update to the UI.
|
||||
|
||||
### --feedback--
|
||||
|
||||
This function updates the UI before the actual state changes.
|
||||
|
||||
## --video-solution--
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user