dan
2 years ago
committed by
GitHub
4 changed files with 772 additions and 8 deletions
@ -0,0 +1,760 @@ |
|||
--- |
|||
title: 'Separating Events from Effects' |
|||
--- |
|||
|
|||
<Intro> |
|||
|
|||
Event handlers only run when you perform the same interaction again. Unlike event handlers, Effects re-synchronize if some value they read, like a prop or a state variable, is different from what it was during the last render. Sometimes, you also want a mix of both behaviors: an Effect that re-runs in response to some values but not others. This page will teach you how to do that. |
|||
|
|||
</Intro> |
|||
|
|||
<YouWillLearn> |
|||
|
|||
- How to choose between an event handler and an Effect |
|||
- Why Effects are reactive, and event handlers are not |
|||
- What to do when you want a part of your Effect's code to not be reactive |
|||
- What Event functions are, and how to extract them from your Effects |
|||
- How to read the latest props and state from Effects using Event functions |
|||
|
|||
</YouWillLearn> |
|||
|
|||
## Choosing between event handlers and Effects {/*choosing-between-event-handlers-and-effects*/} |
|||
|
|||
First, let's recap the difference between event handlers and Effects. |
|||
|
|||
Imagine you're implementing a chat room component. Your requirements look like this: |
|||
|
|||
1. Your component should automatically connect to the selected chat room. |
|||
1. When you click the "Send" button, it should send a message to the chat. |
|||
|
|||
Let's say you've already implemented the code for them, but you're not sure where to put it. Should you use event handlers or Effects? Every time you need to answer this question, consider [*why* the code needs to run.](/learn/synchronizing-with-effects#what-are-effects-and-how-are-they-different-from-events) |
|||
|
|||
### Event handlers run in response to specific interactions {/*event-handlers-run-in-response-to-specific-interactions*/} |
|||
|
|||
From the user's perspective, sending a message should happen *because* the particular "Send" button was clicked. The user will get rather upset if you send their message at any other time or for any other reason. This is why sending a message should be an event handler. Event handlers let you handle specific interactions like clicks: |
|||
|
|||
```js {4-6} |
|||
function ChatRoom({ roomId }) { |
|||
const [message, setMessage] = useState(''); |
|||
// ... |
|||
function handleSendClick() { |
|||
sendMessage(message); |
|||
} |
|||
// ... |
|||
return ( |
|||
<> |
|||
<input value={message} onChange={e => setMessage(e.target.value)} /> |
|||
<button onClick={handleSendClick}>Send</button>; |
|||
</> |
|||
); |
|||
} |
|||
``` |
|||
|
|||
With an event handler, you can be sure that `sendMessage(message)` will *only* run if the user presses the button. |
|||
|
|||
### Effects run whenever synchronization is needed {/*effects-run-whenever-synchronization-is-needed*/} |
|||
|
|||
Recall that you also need to keep the component connected to the chat room. Where does that code go? |
|||
|
|||
The *reason* to run this code is not some particular interaction. It doesn't matter why or how the user navigated to the chat room screen. Now that they're looking at it and could interact with it, the component needs to stay connected to the selected chat server. Even if the chat room component was the initial screen of your app, and the user has not performed any interactions at all, you would *still* need to connect. This is why it's an Effect: |
|||
|
|||
```js {3-9} |
|||
function ChatRoom({ roomId }) { |
|||
// ... |
|||
useEffect(() => { |
|||
const connection = createConnection(serverUrl, roomId); |
|||
connection.connect(); |
|||
return () => { |
|||
connection.disconnect(); |
|||
}; |
|||
}, [roomId]); |
|||
// ... |
|||
} |
|||
``` |
|||
|
|||
With this code, you can be sure that there is always an active connection to the currently selected chat server, *regardless* of the specific interactions performed by the user. Whether the user has only opened your app, selected a different room, or navigated to another screen and back, your Effect will ensure that the component will *remain synchronized* with the currently selected room, and will [re-connect whenever it's necessary.](/learn/lifecycle-of-reactive-effects#why-synchronization-may-need-to-happen-more-than-once) |
|||
|
|||
<Sandpack> |
|||
|
|||
```js |
|||
import { useState, useEffect } from 'react'; |
|||
import { createConnection, sendMessage } from './chat.js'; |
|||
|
|||
const serverUrl = 'https://localhost:1234'; |
|||
|
|||
function ChatRoom({ roomId }) { |
|||
const [message, setMessage] = useState(''); |
|||
|
|||
useEffect(() => { |
|||
const connection = createConnection(serverUrl, roomId); |
|||
connection.connect(); |
|||
return () => connection.disconnect(); |
|||
}, [roomId]); |
|||
|
|||
function handleSendClick() { |
|||
sendMessage(message); |
|||
} |
|||
|
|||
return ( |
|||
<> |
|||
<h1>Welcome to the {roomId} room!</h1> |
|||
<input value={message} onChange={e => setMessage(e.target.value)} /> |
|||
<button onClick={handleSendClick}>Send</button> |
|||
</> |
|||
); |
|||
} |
|||
|
|||
export default function App() { |
|||
const [roomId, setRoomId] = useState('general'); |
|||
const [show, setShow] = useState(false); |
|||
return ( |
|||
<> |
|||
<label> |
|||
Choose the chat room:{' '} |
|||
<select |
|||
value={roomId} |
|||
onChange={e => setRoomId(e.target.value)} |
|||
> |
|||
<option value="general">general</option> |
|||
<option value="travel">travel</option> |
|||
<option value="music">music</option> |
|||
</select> |
|||
</label> |
|||
<button onClick={() => setShow(!show)}> |
|||
{show ? 'Close chat' : 'Open chat'} |
|||
</button> |
|||
{show && <hr />} |
|||
{show && <ChatRoom roomId={roomId} />} |
|||
</> |
|||
); |
|||
} |
|||
``` |
|||
|
|||
```js chat.js |
|||
export function sendMessage(message) { |
|||
console.log('🔵 You sent: ' + message); |
|||
} |
|||
|
|||
export function createConnection(serverUrl, roomId) { |
|||
// A real implementation would actually connect to the server |
|||
return { |
|||
connect() { |
|||
console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...'); |
|||
}, |
|||
disconnect() { |
|||
console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl); |
|||
} |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
```css |
|||
input, select { margin-right: 20px; } |
|||
``` |
|||
|
|||
</Sandpack> |
|||
|
|||
## Reactive values and reactive logic {/*reactive-values-and-reactive-logic*/} |
|||
|
|||
Intuitively, you could say that event handlers are always triggered "manually," for example by clicking a button. Effects, on the other hand, are "automatic": they run and re-run as often as it's needed to stay synchronized. |
|||
|
|||
There is a more precise way to think about this. |
|||
|
|||
Props, state, and variables declared inside your component's body are called <CodeStep step={2}>reactive values</CodeStep>. In this example, `serverUrl` is not a reactive value, but `roomId` and `message` are. They participate in the rendering data flow: |
|||
|
|||
```js [[2, 3, "roomId"], [2, 4, "message"]] |
|||
const serverUrl = 'https://localhost:1234'; |
|||
|
|||
function ChatRoom({ roomId }) { |
|||
const [message, setMessage] = useState(''); |
|||
|
|||
// ... |
|||
} |
|||
``` |
|||
|
|||
Reactive values like these can change due to a re-render. For example, the user may edit the `message` or choose a different `roomId` in a dropdown. Event handlers and Effects are different in how they respond to changes: |
|||
|
|||
- **Logic inside event handlers is *not reactive.*** It will not run again unless the user performs the same interaction (for example, a click) again. Event handlers can read reactive values, but they don't "react" to their changes. |
|||
- **Logic inside Effects is *reactive.*** If your Effect reads a reactive value, [you have to specify it as a dependency](/learn/lifecycle-of-reactive-effects#effects-react-to-reactive-values). Then, if a re-render causes that value to change, React will re-run your Effect's logic again with the new value. |
|||
|
|||
Let's revisit the previous example to illustrate this difference. |
|||
|
|||
### Logic inside event handlers is not reactive {/*logic-inside-event-handlers-is-not-reactive*/} |
|||
|
|||
Take a look at this line of code. Should this logic be reactive or not? |
|||
|
|||
```js [[2, 2, "message"]] |
|||
// ... |
|||
sendMessage(message); |
|||
// ... |
|||
``` |
|||
|
|||
From the user's perspective, **a change to the `message` does _not_ mean that they want to send a message.** It only means that the user is typing. In other words, the logic that sends a message should not be reactive. It should not run again only because the <CodeStep step={2}>reactive value</CodeStep> has changed. That's why you placed this logic in the event handler: |
|||
|
|||
```js {2} |
|||
function handleSendClick() { |
|||
sendMessage(message); |
|||
} |
|||
``` |
|||
|
|||
Event handlers aren't reactive, so `sendMessage(message)` will only run when the user clicks the Send button. |
|||
|
|||
### Logic inside Effects is reactive {/*logic-inside-effects-is-reactive*/} |
|||
|
|||
Now let's return to these lines: |
|||
|
|||
```js [[2, 2, "roomId"]] |
|||
// ... |
|||
const connection = createConnection(serverUrl, roomId); |
|||
connection.connect(); |
|||
// ... |
|||
``` |
|||
|
|||
From the user's prespective, **a change to the `roomId` *does* mean that they want to connect to a different room.** In other words, the logic for connecting to the room should be reactive. You *want* these lines of code to "keep up" with the <CodeStep step={2}>reactive value</CodeStep>, and to run again if that value is different. That's why you put this logic inside an Effect: |
|||
|
|||
```js {2-3} |
|||
useEffect(() => { |
|||
const connection = createConnection(serverUrl, roomId); |
|||
connection.connect(); |
|||
return () => { |
|||
connection.disconnect() |
|||
}; |
|||
}, [roomId]); |
|||
``` |
|||
|
|||
Effects are reactive, so `createConnection(serverUrl, roomId)` and `connection.connect()` will run for every distinct value of `roomId`. Your Effect keeps the chat connection synchronized to the currently selected room. |
|||
|
|||
## Extracting non-reactive logic out of Effects {/*extracting-non-reactive-logic-out-of-effects*/} |
|||
|
|||
Things get more tricky when you want to mix reactive logic with non-reactive logic. |
|||
|
|||
For example, imagine that you want to show a notification when the user connects to the chat. You read the current theme (dark or light) from the props so that you can show the notification in the correct color: |
|||
|
|||
```js {1,4-6} |
|||
function ChatRoom({ roomId, theme }) { |
|||
useEffect(() => { |
|||
const connection = createConnection(serverUrl, roomId); |
|||
connection.on('connected', () => { |
|||
showNotification('Connected!', theme); |
|||
}); |
|||
connection.connect(); |
|||
// ... |
|||
```` |
|||
|
|||
However, `theme` is a reactive value (it can change as a result of re-rendering), and [every reactive value read by an Effect must be declared as its dependency.](/learn/lifecycle-of-reactive-effects#react-verifies-that-you-specified-every-reactive-value-as-a-dependency) So now you have to specify `theme` as a dependency of your Effect: |
|||
|
|||
```js {5,11} |
|||
function ChatRoom({ roomId, theme }) { |
|||
useEffect(() => { |
|||
const connection = createConnection(serverUrl, roomId); |
|||
connection.on('connected', () => { |
|||
showNotification('Connected!', theme); |
|||
}); |
|||
connection.connect(); |
|||
return () => { |
|||
connection.disconnect() |
|||
}; |
|||
}, [roomId, theme]); // ✅ All dependencies are specified |
|||
// ... |
|||
```` |
|||
|
|||
Play with this example and see if you can spot the problem with this user experience: |
|||
|
|||
<Sandpack> |
|||
|
|||
```json package.json |
|||
{ |
|||
"dependencies": { |
|||
"react": "latest", |
|||
"react-dom": "latest", |
|||
"react-scripts": "latest", |
|||
"toastify-js": "1.12.0" |
|||
}, |
|||
"scripts": { |
|||
"start": "react-scripts start", |
|||
"build": "react-scripts build", |
|||
"test": "react-scripts test --env=jsdom", |
|||
"eject": "react-scripts eject" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
```js |
|||
import { useState, useEffect } from 'react'; |
|||
import { createConnection, sendMessage } from './chat.js'; |
|||
import { showNotification } from './notifications.js'; |
|||
|
|||
const serverUrl = 'https://localhost:1234'; |
|||
|
|||
function ChatRoom({ roomId, theme }) { |
|||
useEffect(() => { |
|||
const connection = createConnection(serverUrl, roomId); |
|||
connection.on('connected', () => { |
|||
showNotification('Connected!', theme); |
|||
}); |
|||
connection.connect(); |
|||
return () => connection.disconnect(); |
|||
}, [roomId, theme]); |
|||
|
|||
return <h1>Welcome to the {roomId} room!</h1> |
|||
} |
|||
|
|||
export default function App() { |
|||
const [roomId, setRoomId] = useState('general'); |
|||
const [isDark, setIsDark] = useState(false); |
|||
return ( |
|||
<> |
|||
<label> |
|||
Choose the chat room:{' '} |
|||
<select |
|||
value={roomId} |
|||
onChange={e => setRoomId(e.target.value)} |
|||
> |
|||
<option value="general">general</option> |
|||
<option value="travel">travel</option> |
|||
<option value="music">music</option> |
|||
</select> |
|||
</label> |
|||
<label> |
|||
<input |
|||
type="checkbox" |
|||
checked={isDark} |
|||
onChange={e => setIsDark(e.target.checked)} |
|||
/> |
|||
Use dark theme |
|||
</label> |
|||
<hr /> |
|||
<ChatRoom |
|||
roomId={roomId} |
|||
theme={isDark ? 'dark' : 'light'} |
|||
/> |
|||
</> |
|||
); |
|||
} |
|||
``` |
|||
|
|||
```js chat.js |
|||
export function createConnection(serverUrl, roomId) { |
|||
// A real implementation would actually connect to the server |
|||
let connectedCallback; |
|||
let timeout; |
|||
return { |
|||
connect() { |
|||
timeout = setTimeout(() => { |
|||
if (connectedCallback) { |
|||
connectedCallback(); |
|||
} |
|||
}, 100); |
|||
}, |
|||
on(event, callback) { |
|||
if (connectedCallback) { |
|||
throw Error('Cannot add the handler twice.'); |
|||
} |
|||
if (event !== 'connected') { |
|||
throw Error('Only "connected" event is supported.'); |
|||
} |
|||
connectedCallback = callback; |
|||
}, |
|||
disconnect() { |
|||
clearTimeout(timeout); |
|||
} |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
```js notifications.js |
|||
import Toastify from 'toastify-js'; |
|||
import 'toastify-js/src/toastify.css'; |
|||
|
|||
export function showNotification(message, theme) { |
|||
Toastify({ |
|||
text: message, |
|||
duration: 2000, |
|||
gravity: 'top', |
|||
position: 'right', |
|||
style: { |
|||
background: theme === 'dark' ? 'black' : 'white', |
|||
color: theme === 'dark' ? 'white' : 'black', |
|||
}, |
|||
}).showToast(); |
|||
} |
|||
``` |
|||
|
|||
```css |
|||
label { display: block; margin-top: 10px; } |
|||
``` |
|||
|
|||
</Sandpack> |
|||
|
|||
When the `roomId` changes, the chat re-connects as you would expect. But since `theme` is also a dependency, the chat *also* re-connects every time you switch between the dark and the light theme. That's not great! |
|||
|
|||
In other words, you *don't* want this line to be reactive, even though it is inside an Effect (which is reactive): |
|||
|
|||
```js |
|||
// ... |
|||
showNotification('Connected!', theme); |
|||
// ... |
|||
```` |
|||
|
|||
You need a way to separate this non-reactive logic from the reactive Effect around it. |
|||
|
|||
### Declaring an Event function {/*declaring-an-event-function*/} |
|||
|
|||
<Gotcha> |
|||
|
|||
This section describes an **experimental API that has not yet been added to React,** so you can't use it yet. |
|||
|
|||
</Gotcha> |
|||
|
|||
Use a special Hook called [`useEvent`](/apis/useEvent) to extract this non-reactive logic out of your Effect: |
|||
|
|||
```js {1,4-6} |
|||
import { useEffect, useEvent } from 'react'; |
|||
|
|||
function ChatRoom({ roomId, theme }) { |
|||
const onConnected = useEvent(() => { |
|||
showNotification('Connected!', theme); |
|||
}); |
|||
// ... |
|||
```` |
|||
|
|||
Here, `onConnected` is called an *Event function.* It's a part of your Effect logic, but it behaves a lot more like an event handler. The logic inside it is not reactive, and it always "sees" the latest values of your props and state. |
|||
|
|||
Now you can call the `onConnected` Event function from inside your Effect: |
|||
|
|||
```js {2-4,9,13} |
|||
function ChatRoom({ roomId, theme }) { |
|||
const onConnected = useEvent(() => { |
|||
showNotification('Connected!', theme); |
|||
}); |
|||
|
|||
useEffect(() => { |
|||
const connection = createConnection(serverUrl, roomId); |
|||
connection.on('connected', () => { |
|||
onConnected(); |
|||
}); |
|||
connection.connect(); |
|||
return () => connection.disconnect(); |
|||
}, [roomId]); // ✅ All dependencies are specified |
|||
// ... |
|||
``` |
|||
|
|||
This solves the problem. Similar to the `set` functions returned from `useState`, all Event functions are *stable:* they never change on a re-render. This is why you can skip them in the dependency list. They are not reactive. |
|||
|
|||
Verify that the new behavior works as you would expect: |
|||
|
|||
<Sandpack> |
|||
|
|||
```json package.json hidden |
|||
{ |
|||
"dependencies": { |
|||
"react": "latest", |
|||
"react-dom": "latest", |
|||
"react-scripts": "latest", |
|||
"toastify-js": "1.12.0" |
|||
}, |
|||
"scripts": { |
|||
"start": "react-scripts start", |
|||
"build": "react-scripts build", |
|||
"test": "react-scripts test --env=jsdom", |
|||
"eject": "react-scripts eject" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
```js |
|||
import { useState, useEffect } from 'react'; |
|||
import { useEvent } from './useEvent.js'; // Temporary |
|||
import { createConnection, sendMessage } from './chat.js'; |
|||
import { showNotification } from './notifications.js'; |
|||
|
|||
const serverUrl = 'https://localhost:1234'; |
|||
|
|||
function ChatRoom({ roomId, theme }) { |
|||
const onConnected = useEvent(() => { |
|||
showNotification('Connected!', theme); |
|||
}); |
|||
|
|||
useEffect(() => { |
|||
const connection = createConnection(serverUrl, roomId); |
|||
connection.on('connected', () => { |
|||
onConnected(); |
|||
}); |
|||
connection.connect(); |
|||
return () => connection.disconnect(); |
|||
}, [roomId, onConnected]); // TODO: Linter will allow [roomId] in the future |
|||
|
|||
return <h1>Welcome to the {roomId} room!</h1> |
|||
} |
|||
|
|||
export default function App() { |
|||
const [roomId, setRoomId] = useState('general'); |
|||
const [isDark, setIsDark] = useState(false); |
|||
return ( |
|||
<> |
|||
<label> |
|||
Choose the chat room:{' '} |
|||
<select |
|||
value={roomId} |
|||
onChange={e => setRoomId(e.target.value)} |
|||
> |
|||
<option value="general">general</option> |
|||
<option value="travel">travel</option> |
|||
<option value="music">music</option> |
|||
</select> |
|||
</label> |
|||
<label> |
|||
<input |
|||
type="checkbox" |
|||
checked={isDark} |
|||
onChange={e => setIsDark(e.target.checked)} |
|||
/> |
|||
Use dark theme |
|||
</label> |
|||
<hr /> |
|||
<ChatRoom |
|||
roomId={roomId} |
|||
theme={isDark ? 'dark' : 'light'} |
|||
/> |
|||
</> |
|||
); |
|||
} |
|||
``` |
|||
|
|||
```js chat.js |
|||
export function createConnection(serverUrl, roomId) { |
|||
// A real implementation would actually connect to the server |
|||
let connectedCallback; |
|||
let timeout; |
|||
return { |
|||
connect() { |
|||
timeout = setTimeout(() => { |
|||
if (connectedCallback) { |
|||
connectedCallback(); |
|||
} |
|||
}, 100); |
|||
}, |
|||
on(event, callback) { |
|||
if (connectedCallback) { |
|||
throw Error('Cannot add the handler twice.'); |
|||
} |
|||
if (event !== 'connected') { |
|||
throw Error('Only "connected" event is supported.'); |
|||
} |
|||
connectedCallback = callback; |
|||
}, |
|||
disconnect() { |
|||
clearTimeout(timeout); |
|||
} |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
```js notifications.js hidden |
|||
import Toastify from 'toastify-js'; |
|||
import 'toastify-js/src/toastify.css'; |
|||
|
|||
export function showNotification(message, theme) { |
|||
Toastify({ |
|||
text: message, |
|||
duration: 2000, |
|||
gravity: 'top', |
|||
position: 'right', |
|||
style: { |
|||
background: theme === 'dark' ? 'black' : 'white', |
|||
color: theme === 'dark' ? 'white' : 'black', |
|||
}, |
|||
}).showToast(); |
|||
} |
|||
``` |
|||
|
|||
```js useEvent.js |
|||
import { useRef, useInsertionEffect, useCallback } from 'react'; |
|||
|
|||
// The useEvent API has not yet been added to React, |
|||
// so this is a temporary shim to make this sandbox work. |
|||
// You're not expected to write code like this yourself. |
|||
|
|||
export function useEvent(fn) { |
|||
const ref = useRef(null); |
|||
useInsertionEffect(() => { |
|||
ref.current = fn; |
|||
}, [fn]); |
|||
return useCallback((...args) => { |
|||
const f = ref.current; |
|||
return f(...args); |
|||
}, []); |
|||
} |
|||
``` |
|||
|
|||
```css |
|||
label { display: block; margin-top: 10px; } |
|||
``` |
|||
|
|||
</Sandpack> |
|||
|
|||
You can think of Event functions as being very similar to event handlers. The main difference is that event handlers run in response to a user interactions, whereas Event functions are triggered by you from Effects. Event functions let you "break the chain" between the reactivity of Effects and some code that should not be reactive. |
|||
|
|||
### Reading latest props and state with Event functions {/*reading-latest-props-and-state-with-event-functions*/} |
|||
|
|||
<Gotcha> |
|||
|
|||
This section describes an **experimental API that has not yet been added to React,** so you can't use it yet. |
|||
|
|||
</Gotcha> |
|||
|
|||
Event functions let you fix many patterns where you might be tempted to suppress the dependency linter. |
|||
|
|||
For example, say you have an Effect to log the page visits: |
|||
|
|||
```js |
|||
function Page() { |
|||
useEffect(() => { |
|||
logVisit(); |
|||
}, []); |
|||
// ... |
|||
} |
|||
``` |
|||
|
|||
Later, you add multiple routes to your site. Now your `Page` component receives a `url` prop with the current path. You want to pass the `url` as a part of your `logVisit` call, but the dependency linter complains: |
|||
|
|||
```js {1,3} |
|||
function Page({ url }) { |
|||
useEffect(() => { |
|||
logVisit(url); |
|||
}, []); // 🔴 React Hook useEffect has a missing dependency: 'url' |
|||
// ... |
|||
} |
|||
``` |
|||
|
|||
Think about what you want the code to do. You *want* to log a separate visit for different URLs since each URL represents a different page. In other words, this `logVisit` call *should* be reactive with respect to the `url`. This is why, in this case, it makes sense to follow the dependency linter, and add `url` as a dependency: |
|||
|
|||
```js {4} |
|||
function Page({ url }) { |
|||
useEffect(() => { |
|||
logVisit(url); |
|||
}, [url]); // ✅ All dependencies are specified |
|||
// ... |
|||
} |
|||
``` |
|||
|
|||
Now let's say you want to include the number of items in the shopping cart together with every page visit: |
|||
|
|||
```js {2-3,6} |
|||
function Page({ url }) { |
|||
const { items } = useContext(ShoppingCartContext); |
|||
const numberOfItems = items.length; |
|||
|
|||
useEffect(() => { |
|||
logVisit(url, numberOfItems); |
|||
}, [url]); 🔴 React Hook useEffect has a missing dependency: 'numberOfItems' |
|||
// ... |
|||
} |
|||
``` |
|||
|
|||
You used `numberOfItems` inside the Effect, so the linter asks you to add it as a dependency. However, you *don't* want the `logVisit` call to be reactive with respect to `numberOfItems`. If the user puts something into the shopping cart, and the `numberOfItems` changes, this *does not mean* that the user visited the page again. In other words, *visiting the page* feels similar to an event. You want to be very precise about *when* you say it's happened. |
|||
|
|||
Split the code in two parts: |
|||
|
|||
```js {5-7,10} |
|||
function Page({ url }) { |
|||
const { items } = useContext(ShoppingCartContext); |
|||
const numberOfItems = items.length; |
|||
|
|||
const onVisit = useEvent((visitedUrl) => { |
|||
logVisit(url, numberOfItems); |
|||
}); |
|||
|
|||
useEffect(() => { |
|||
onVisit(url); |
|||
}, [url]); // ✅ All dependencies are specified |
|||
// ... |
|||
} |
|||
``` |
|||
|
|||
Here, `onVisit` is an Event function. The code inside it isn't reactive. This is why you can use `numberOfItems` (or any other reactive value!) without worrying that it will cause the surrounding code to re-execute on changes. |
|||
|
|||
On the other hand, the Effect itself remains reactive. Code inside the Effect uses the `url` prop, so the Effect will re-run after every re-render with a different `url`. This, in turn, will call the `onVisit` event function. |
|||
|
|||
As a result, you will call `logVisit` for every change to the `url`, and always read the latest `numberOfItems`. However, if `numberOfItems` changes on its own, this will not cause any of the code to re-run. |
|||
|
|||
### Limitations of Event functions {/*limitations-of-event-functions*/} |
|||
|
|||
<Gotcha> |
|||
|
|||
This section describes an **experimental API that has not yet been added to React,** so you can't use it yet. |
|||
|
|||
</Gotcha> |
|||
|
|||
At the moment, Event functions are very limited in how you can use them: |
|||
|
|||
* **Only call them from inside Effects.** |
|||
* **Never pass them to other components or Hooks.** |
|||
|
|||
For example, don't declare and pass an Event function like this: |
|||
|
|||
```js {4-6,8} |
|||
function Timer() { |
|||
const [count, setCount] = useState(0); |
|||
|
|||
const onTick = useEvent(() => { |
|||
setCount(count + 1); |
|||
}); |
|||
|
|||
useTimer(onTick, 1000); // 🔴 Avoid: Passing event functions |
|||
|
|||
return <h1>{count}</h1> |
|||
} |
|||
|
|||
function useTimer(callback, delay) { |
|||
useEffect(() => { |
|||
const id = setInterval(() => { |
|||
callback(); |
|||
}, delay); |
|||
return () => { |
|||
clearInterval(id); |
|||
}; |
|||
}, [delay, callback]); // Need to specify "callback" in dependencies |
|||
} |
|||
``` |
|||
|
|||
Instead, always declare Event functions directly next to the Effects that use them: |
|||
|
|||
```js {10-12,16,21} |
|||
function Timer() { |
|||
const [count, setCount] = useState(0); |
|||
useTimer(() => { |
|||
setCount(count + 1); |
|||
}, 1000); |
|||
return <h1>{count}</h1> |
|||
} |
|||
|
|||
function useTimer(callback, delay) { |
|||
const onTick = useEvent(() => { |
|||
callback(); |
|||
}); |
|||
|
|||
useEffect(() => { |
|||
const id = setInterval(() => { |
|||
onTick(); // ✅ Good: Only called locally inside an Effect |
|||
}, delay); |
|||
return () => { |
|||
clearInterval(id); |
|||
}; |
|||
}, [delay]); // No need to specify "onTick" (an Event function) as a dependency |
|||
} |
|||
``` |
|||
|
|||
It's possible that in the future, some of these restrictions will be lifted. But for now, you can think of Event functions as non-reactive "pieces" of your Effect code, so they should be close to the Effect using them. |
|||
|
|||
<Recap> |
|||
|
|||
- Event handlers run in response to specific interactions |
|||
- Effects run whenever synchronization is needed |
|||
- Logic inside event handlers is not reactive |
|||
- Logic inside Effects is reactive |
|||
- You can move non-reactive logic from Effects into Event functions |
|||
- Only call Event functions from inside Effects |
|||
- Don't pass Event functions to other components or Hooks |
|||
|
|||
</Recap> |
Loading…
Reference in new issue