Browse Source

[Beta] Lifecycle of Reactive Effects (#4875)

* [Beta] Lifecycle of Reactive Effects

* rm future pages

* more
main
dan 2 years ago
committed by GitHub
parent
commit
5cea9c4f56
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2103
      beta/src/pages/learn/lifecycle-of-reactive-effects.md
  2. 22
      beta/src/pages/learn/synchronizing-with-effects.md
  3. 4
      beta/src/pages/learn/updating-objects-in-state.md
  4. 100
      beta/src/pages/learn/you-might-not-need-an-effect.md
  5. 4
      beta/src/sidebarLearn.json

2103
beta/src/pages/learn/lifecycle-of-reactive-effects.md

File diff suppressed because it is too large

22
beta/src/pages/learn/synchronizing-with-effects.md

@ -399,7 +399,7 @@ video { width: 250px; }
The dependency array can contain multiple dependencies. React will only skip re-running the Effect if *all* of the dependencies you specify have exactly the same values as they had during the previous render. React compares the dependency values using the [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) comparison. See the [`useEffect` API reference](/apis/useeffect#reference) for more details.
**Notice that you can't "choose" your dependencies.** You will get a lint error if the dependencies you specified don't match what React expects based on the code inside your Effect. This helps catch many bugs in your code. If your Effect uses some value but you *don't* want to re-run the Effect when it changes, you'll need to *edit the Effect code itself* to not "need" that dependency. Learn more about this in [Specifying the Effect Dependencies](/learn/specifying-effect-dependencies).
**Notice that you can't "choose" your dependencies.** You will get a lint error if the dependencies you specified don't match what React expects based on the code inside your Effect. This helps catch many bugs in your code. If your Effect uses some value but you *don't* want to re-run the Effect when it changes, you'll need to [*edit the Effect code itself* to not "need" that dependency.](/learn/lifecycle-of-reactive-effects#what-to-do-when-you-dont-want-to-re-synchronize)
<Gotcha>
@ -505,10 +505,10 @@ export function createConnection() {
// A real implementation would actually connect to the server
return {
connect() {
console.log('Connecting...');
console.log('Connecting...');
},
disconnect() {
console.log('Disconnected.');
console.log('Disconnected.');
}
};
}
@ -520,11 +520,11 @@ input { display: block; margin-bottom: 20px; }
</Sandpack>
This Effect only runs on mount, so you might expect `"Connecting..."` to be printed once in the console. **However, if you check the console, `"Connecting..."` gets printed twice. Why does it happen?**
This Effect only runs on mount, so you might expect `"Connecting..."` to be printed once in the console. **However, if you check the console, `"Connecting..."` gets printed twice. Why does it happen?**
Imagine the `ChatRoom` component is a part of a larger app with many different screens. The user starts their journey on the `ChatRoom` page. The component mounts and calls `connection.connect()`. Then imagine the user navigates to another screen--for example, to the Settings page. The `ChatRoom` component unmounts. Finally, the user clicks Back and `ChatRoom` mounts again. This would set up a second connection--but the first connection was never destroyed! As the user navigates across the app, the connections would keep piling up.
Bugs like this are easy to miss without extensive manual testing. To help you spot them quickly, in development React remounts every component once immediately after its initial mount. **Seeing the `"Connecting..."` log twice helps you notice the real issue: your code doesn't close the connection when the component unmounts.**
Bugs like this are easy to miss without extensive manual testing. To help you spot them quickly, in development React remounts every component once immediately after its initial mount. **Seeing the `"Connecting..."` log twice helps you notice the real issue: your code doesn't close the connection when the component unmounts.**
To fix the issue, return a *cleanup function* from your Effect:
@ -561,10 +561,10 @@ export function createConnection() {
// A real implementation would actually connect to the server
return {
connect() {
console.log('Connecting...');
console.log('Connecting...');
},
disconnect() {
console.log('Disconnected.');
console.log('Disconnected.');
}
};
}
@ -578,13 +578,13 @@ input { display: block; margin-bottom: 20px; }
Now you get three console logs in development:
1. `"Connecting..."`
2. `"Disconnected."`
3. `"Connecting..."`
1. `"Connecting..."`
2. `"Disconnected."`
3. `"Connecting..."`
**This is the correct behavior in development.** By remounting your component, React verifies that navigating away and back would not break your code. Disconnecting and then connecting again is exactly what should happen! When you implement the cleanup well, there should be no user-visible difference between running the Effect once vs running it, cleaning it up, and running it again. There's an extra connect/disconnect call pair because React is probing your code for bugs in development. This is normal and you shouldn't try to make it go away.
**In production, you would only see `"Connecting..."` printed once.** Remounting components only happens in development to help you find Effects that need cleanup. You can turn off [Strict Mode](/apis/strictmode) to opt out of the development behavior, but we recommend keeping it on. This lets you find many bugs like the one above.
**In production, you would only see `"Connecting..."` printed once.** Remounting components only happens in development to help you find Effects that need cleanup. You can turn off [Strict Mode](/apis/strictmode) to opt out of the development behavior, but we recommend keeping it on. This lets you find many bugs like the one above.
## How to handle the Effect firing twice in development? {/*how-to-handle-the-effect-firing-twice-in-development*/}

4
beta/src/pages/learn/updating-objects-in-state.md

@ -84,7 +84,7 @@ export default function MovingDot() {
height: 20,
}} />
</div>
)
);
}
```
@ -156,7 +156,7 @@ export default function MovingDot() {
height: 20,
}} />
</div>
)
);
}
```

100
beta/src/pages/learn/you-might-not-need-an-effect.md

@ -358,6 +358,98 @@ function Form() {
When you choose whether to put some logic into an event handler or an Effect, the main question you need to answer is _what kind of logic_ it is from the user's perspective. If this logic is caused by a particular interaction, keep it in the event handler. If it's caused by the user _seeing_ the component on the screen, keep it in the Effect.
### Chains of computations {/*chains-of-computations*/}
Sometimes you might feel tempted to chain Effects that each adjust a piece of state based on other state:
```js {7-29}
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
const [isGameOver, setIsGameOver] = useState(false);
// 🔴 Avoid: Chains of Effects that adjust the state solely to trigger each other
useEffect(() => {
if (card !== null && card.gold) {
setGoldCardCount(c => c + 1);
}
}, [card]);
useEffect(() => {
if (goldCardCount > 3) {
setRound(r => r + 1)
setGoldCardCount(0);
}
}, [goldCardCount]);
useEffect(() => {
if (round > 5) {
setIsGameOver(true);
}
}, [round]);
useEffect(() => {
alert('Good game!');
}, [isGameOver]);
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
} else {
setCard(nextCard);
}
}
// ...
```
There are two problems with this code.
One problem is that it is very inefficient: the component (and its children) have to re-render between each `set` call in the chain. In the example above, in the worst case (`setCard` → render → `setGoldCardCount` → render → `setRound` → render → `setIsGameOver` → render) there are three unnecessary re-renders of the tree below.
Even if it weren't slow, as your code evolves, you will run into cases where the "chain" you wrote doesn't fit the new requirements. Imagine you are adding a way to step through the history of the game moves. You'd do it by updating each state variable to a value from the past. However, setting the `card` state to a value from the past would trigger the Effect chain again and change the data you're showing. Code like this is often rigid and fragile.
In this case, it's better to calculate what you can during rendering, and adjust the state in the event handler:
```js {6-7,14-26}
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
// ✅ Calculate what you can during rendering
const isGameOver = round > 5;
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
}
// ✅ Calculate all the next state in the event handler
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount <= 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert('Good game!');
}
}
}
}
// ...
```
This is a lot more efficient. Also, if you implement a way to view game history, now you will be able to set each state variable to a move from the past without triggering the Effect chain that adjusts every other value. If you need to reuse logic between several event handlers, you can [extract a function](#sharing-logic-between-event-handlers) and call it from those handlers.
Remember that inside event handlers, [state behaves like a snapshot](/learn/state-as-a-snapshot). For example, even after you call `setRound(rount + 1)`, the `round` variable will reflect the value at the time the user clicked the button. If you need to use the next value for calculations, define it manually like `const nextRound = round + 1`.
In some cases, you *can't* calculate the next state directly in the event handler. For example, imagine a form with multiple dropdowns where the options of each next dropdown depend on the selected value of the previous dropdown. Then, [a chain of Effects fetching data](/learn/adjusting-effect-dependencies#splitting-an-effect-in-two) is appropriate because you are synchronizing with network.
### Initializing the application {/*initializing-the-application*/}
Some logic should only run once when the app loads. You might place it in an Effect in the top-level component:
@ -651,7 +743,7 @@ This ensures that when your Effect fetches data, all responses except the last r
Handling race conditions is not the only difficulty with implementing data fetching. You might also want to think about how to cache the responses (so that the user can click Back and see the previous screen instantly instead of a spinner), how to fetch them on the server (so that the initial server-rendered HTML contains the fetched content instead of a spinner), and how to avoid network waterfalls (so that a child component that needs to fetch data doesn't have to wait for every parent above it to finish fetching their data before it can start). **These issues apply to any UI library, not just React. Solving them is not trivial, which is why modern [frameworks](/learn/start-a-new-react-project#building-with-a-full-featured-framework) provide more efficient built-in data fetching mechanisms than writing Effects directly in your components.**
If you don't use a framework (and don't want to build your own) but would like to make data fetching from Effects more ergonomic, consider extracting your fetching logic into a custom Hook like in this example:
If you don't use a framework (and don't want to build your own) but would like to make data fetching from Effects more ergonomic, consider extracting [your fetching logic into a custom Hook](/learn/adjusting-effect-dependencies#wrapping-an-effect-into-a-custom-hook) like in this example:
```js {4}
function SearchResults({ query }) {
@ -666,21 +758,21 @@ function SearchResults({ query }) {
}
function useData(url) {
const [result, setResult] = useState(null);
const [data, setData] = useState(null);
useEffect(() => {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setResult(json);
setData(json);
}
});
return () => {
ignore = true;
};
}, [url]);
return result;
return data;
}
```

4
beta/src/sidebarLearn.json

@ -170,6 +170,10 @@
{
"title": "You Might Not Need an Effect",
"path": "/learn/you-might-not-need-an-effect"
},
{
"title": "Lifecycle of Reactive Effects",
"path": "/learn/lifecycle-of-reactive-effects"
}
]
}

Loading…
Cancel
Save