diff --git a/beta/package.json b/beta/package.json index 21aefe42..6e2d373a 100644 --- a/beta/package.json +++ b/beta/package.json @@ -67,7 +67,7 @@ "eslint-plugin-import": "2.x", "eslint-plugin-jsx-a11y": "6.x", "eslint-plugin-react": "7.x", - "eslint-plugin-react-hooks": "experimental", + "eslint-plugin-react-hooks": "^0.0.0-experimental-fabef7a6b-20221215", "fs-extra": "^9.0.1", "globby": "^11.0.1", "gray-matter": "^4.0.2", diff --git a/beta/src/content/apis/react/useEffect.md b/beta/src/content/apis/react/useEffect.md index 514b4364..6ae7143d 100644 --- a/beta/src/content/apis/react/useEffect.md +++ b/beta/src/content/apis/react/useEffect.md @@ -1653,11 +1653,11 @@ function Page({ url, shoppingCart }) { } ``` -**What if you want to log a new page visit after every `url` change, but *not* if only the `shoppingCart` changes?** You can't exclude `shoppingCart` from dependencies without breaking the [reactivity rules.](#specifying-reactive-dependencies) However, you can express that you *don't want* a piece of code to "react" to changes even though it is called from inside an Effect. To do this, [declare an *Event function*](/learn/separating-events-from-effects#declaring-an-event-function) with the [`useEvent`](/apis/react/useEvent) Hook, and move the code that reads `shoppingCart` inside of it: +**What if you want to log a new page visit after every `url` change, but *not* if only the `shoppingCart` changes?** You can't exclude `shoppingCart` from dependencies without breaking the [reactivity rules.](#specifying-reactive-dependencies) However, you can express that you *don't want* a piece of code to "react" to changes even though it is called from inside an Effect. [Declare an *Effect Event*](/learn/separating-events-from-effects#declaring-an-effect-event) with the [`useEffectEvent`](/apis/react/useEffectEvent) Hook, and move the code that reads `shoppingCart` inside of it: ```js {2-4,7,8} function Page({ url, shoppingCart }) { - const onVisit = useEvent(visitedUrl => { + const onVisit = useEffectEvent(visitedUrl => { logVisit(visitedUrl, shoppingCart.length) }); @@ -1668,9 +1668,9 @@ function Page({ url, shoppingCart }) { } ``` -**Event functions are not reactive and don't need to be specified as dependencies of your Effect.** This is what lets you put non-reactive code (where you can read the latest value of some props and state) inside of them. For example, by reading `shoppingCart` inside of `onVisit`, you ensure that `shoppingCart` won't re-run your Effect. +**Effect Events are not reactive and must always be omitted from dependencies of your Effect.** This is what lets you put non-reactive code (where you can read the latest value of some props and state) inside of them. For example, by reading `shoppingCart` inside of `onVisit`, you ensure that `shoppingCart` won't re-run your Effect. In the future, the linter will support `useEffectEvent` and check that you omit Effect Events from dependencies. -[Read more about how Event functions let you separate reactive and non-reactive code.](/learn/separating-events-from-effects#reading-latest-props-and-state-with-event-functions) +[Read more about how Effect Events let you separate reactive and non-reactive code.](/learn/separating-events-from-effects#reading-latest-props-and-state-with-effect-events) --- diff --git a/beta/src/content/apis/react/useEffectEvent.md b/beta/src/content/apis/react/useEffectEvent.md new file mode 100644 index 00000000..c4db3fb9 --- /dev/null +++ b/beta/src/content/apis/react/useEffectEvent.md @@ -0,0 +1,24 @@ +--- +title: useEffectEvent +--- + + + +**This API is experimental and is not available in a stable version of React yet.** + +See [`useEffectEvent` RFC](https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md) for details. + + + + + + +`useEffectEvent` is a React Hook that lets you extract non-reactive logic into an [Effect Event.](/learn/separating-events-from-effects#declaring-an-effect-event) + +```js +const onSomething = useEffectEvent(callback) +``` + + + + diff --git a/beta/src/content/apis/react/useEvent.md b/beta/src/content/apis/react/useEvent.md deleted file mode 100644 index 8ad5e082..00000000 --- a/beta/src/content/apis/react/useEvent.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: useEvent ---- - - - -This section is incomplete, please see the RFC doc for [useEvent.](https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md) - - - - - - -`useEvent` is a React Hook that lets you extract non-reactive Effect logic into an [Event function.](/learn/separating-events-from-effects#declaring-an-event-function) - -```js -useEvent(callback) -``` - - - - diff --git a/beta/src/content/learn/removing-effect-dependencies.md b/beta/src/content/learn/removing-effect-dependencies.md index 85ac0bdd..8769a8fd 100644 --- a/beta/src/content/learn/removing-effect-dependencies.md +++ b/beta/src/content/learn/removing-effect-dependencies.md @@ -350,7 +350,7 @@ Let's say that you wanted to run the Effect "only on mount". You've read that [e This counter was supposed to increment every second by the amount configurable with the two buttons. However, since you "lied" to React that this Effect doesn't depend on anything, React forever keeps using the `onTick` function from the initial render. [During that render,](/learn/state-as-a-snapshot#rendering-takes-a-snapshot-in-time) `count` was `0` and `increment` was `1`. This is why `onTick` from that render always calls `setCount(0 + 1)` every second, and you always see `1`. Bugs like this are harder to fix when they're spread across multiple components. -There's always a better solution than ignoring the linter! To fix this code, you need to add `onTick` to the dependency list. (To ensure the interval is only setup once, [make `onTick` an Event function.](/learn/separating-events-from-effects#reading-latest-props-and-state-with-event-functions)) +There's always a better solution than ignoring the linter! To fix this code, you need to add `onTick` to the dependency list. (To ensure the interval is only setup once, [make `onTick` an Effect Event.](/learn/separating-events-from-effects#reading-latest-props-and-state-with-effect-events)) **We recommend to treat the dependency lint error as a compilation error. If you don't suppress it, you will never see bugs like this.** The rest of this page documents the alternatives for this and other cases. @@ -655,16 +655,16 @@ function ChatRoom({ roomId }) { The problem is that every time `isMuted` changes (for example, when the user presses the "Muted" toggle), the Effect will re-synchronize, and reconnect to the chat server. This is not the desired user experience! (In this example, even disabling the linter would not work--if you do that, `isMuted` would get "stuck" with its old value.) -To solve this problem, you need to extract the logic that shouldn't be reactive out of the Effect. You don't want this Effect to "react" to the changes in `isMuted`. [Move this non-reactive piece of logic into an Event function:](/learn/separating-events-from-effects#declaring-an-event-function) +To solve this problem, you need to extract the logic that shouldn't be reactive out of the Effect. You don't want this Effect to "react" to the changes in `isMuted`. [Move this non-reactive piece of logic into an Effect Event:](/learn/separating-events-from-effects#declaring-an-effect-event) ```js {1,7-12,18,21} -import { useState, useEffect, useEvent } from 'react'; +import { useState, useEffect, useEffectEvent } from 'react'; function ChatRoom({ roomId }) { const [messages, setMessages] = useState([]); const [isMuted, setIsMuted] = useState(false); - const onMessage = useEvent(receivedMessage => { + const onMessage = useEffectEvent(receivedMessage => { setMessages(msgs => [...msgs, receivedMessage]); if (!isMuted) { playSound(); @@ -682,7 +682,7 @@ function ChatRoom({ roomId }) { // ... ``` -Event functions let you split an Effect into reactive parts (which should "react" to reactive values like `roomId` and their changes) and non-reactive parts (which only read their latest values, like `onMessage` reads `isMuted`). **Now that you read `isMuted` inside an Event function, it doesn't need to be a dependency of your Effect.** As a result, the chat won't re-connect when you toggle the "Muted" setting on and off, solving the original issue! +Effect Events let you split an Effect into reactive parts (which should "react" to reactive values like `roomId` and their changes) and non-reactive parts (which only read their latest values, like `onMessage` reads `isMuted`). **Now that you read `isMuted` inside an Effect Event, it doesn't need to be a dependency of your Effect.** As a result, the chat won't re-connect when you toggle the "Muted" setting on and off, solving the original issue! #### Wrapping an event handler from the props {/*wrapping-an-event-handler-from-the-props*/} @@ -714,13 +714,13 @@ Suppose that the parent component passes a *different* `onReceiveMessage` functi /> ``` -Since `onReceiveMessage` is a dependency of your Effect, it would cause the Effect to re-synchronize after every parent re-render. This would make it re-connect to the chat. To solve this, wrap the call in an Event function: +Since `onReceiveMessage` is a dependency of your Effect, it would cause the Effect to re-synchronize after every parent re-render. This would make it re-connect to the chat. To solve this, wrap the call in an Effect Event: ```js {4-6,12,15} function ChatRoom({ roomId, onReceiveMessage }) { const [messages, setMessages] = useState([]); - const onMessage = useEvent(receivedMessage => { + const onMessage = useEffectEvent(receivedMessage => { onReceiveMessage(receivedMessage); }); @@ -735,17 +735,17 @@ function ChatRoom({ roomId, onReceiveMessage }) { // ... ``` -Event functions aren't reactive, so you don't need to specify them as dependencies. As a result, the chat will no longer re-connect even if the parent component passes a function that's different on every re-render. +Effect Events aren't reactive, so you don't need to specify them as dependencies. As a result, the chat will no longer re-connect even if the parent component passes a function that's different on every re-render. #### Separating reactive and non-reactive code {/*separating-reactive-and-non-reactive-code*/} In this example, you want to log a visit every time `roomId` changes. You want to include the current `notificationCount` with every log, but you *don't* want a change to `notificationCount` to trigger a log event. -The solution is again to split out the non-reactive code into an Event function: +The solution is again to split out the non-reactive code into an Effect Event: ```js {2-4,7} function Chat({ roomId, notificationCount }) { - const onVisit = useEvent(visitedRoomId => { + const onVisit = useEffectEvent(visitedRoomId => { logVisit(visitedRoomId, notificationCount); }); @@ -756,7 +756,7 @@ function Chat({ roomId, notificationCount }) { } ``` -You want your logic to be reactive with regards to `roomId`, so you read `roomId` inside of your Effect. However, you don't want a change to `notificationCount` to log an extra visit, so you read `notificationCount` inside of the Event function. [Learn more about reading the latest props and state from Effects using Event functions.](/learn/separating-events-from-effects#reading-latest-props-and-state-with-event-functions) +You want your logic to be reactive with regards to `roomId`, so you read `roomId` inside of your Effect. However, you don't want a change to `notificationCount` to log an extra visit, so you read `notificationCount` inside of the Effect Event. [Learn more about reading the latest props and state from Effects using Effect Events.](/learn/separating-events-from-effects#reading-latest-props-and-state-with-effect-events) ### Does some reactive value change unintentionally? {/*does-some-reactive-value-change-unintentionally*/} @@ -1150,7 +1150,7 @@ function ChatRoom({ getOptions }) { // ... ``` -This only works for [pure](/learn/keeping-components-pure) functions because they are safe to call during rendering. If your function is an event handler, but you don't want its changes to re-synchronize your Effect, [wrap it into an Event function instead.](#do-you-want-to-read-a-value-without-reacting-to-its-changes) +This only works for [pure](/learn/keeping-components-pure) functions because they are safe to call during rendering. If your function is an event handler, but you don't want its changes to re-synchronize your Effect, [wrap it into an Effect Event instead.](#do-you-want-to-read-a-value-without-reacting-to-its-changes) @@ -1161,7 +1161,7 @@ This only works for [pure](/learn/keeping-components-pure) functions because the - If the code in your Effect should run in response to a specific interaction, move that code to an event handler. - If different parts of your Effect should re-run for different reasons, split it into several Effects. - If you want to update some state based on the previous state, pass an updater function. -- If you want to read the latest value without "reacting" it, extract an Event function from your Effect. +- If you want to read the latest value without "reacting" it, extract an Effect Event from your Effect. - In JavaScript, objects and functions are considered different if they were created at different times. - Try to avoid object and function dependencies. Move them outside the component or inside the Effect. @@ -1273,7 +1273,7 @@ Is there a line of code inside the Effect that should not be reactive? How can y ```js import { useState, useEffect, useRef } from 'react'; -import { experimental_useEvent as useEvent } from 'react'; +import { experimental_useEffectEvent as useEffectEvent } from 'react'; import { FadeInAnimation } from './animation.js'; function Welcome({ duration }) { @@ -1378,7 +1378,7 @@ html, body { min-height: 300px; } -Your Effect needs to read the latest value of `duration`, but you don't want it to "react" to changes in `duration`. You use `duration` to start the animation, but starting animation isn't reactive. Extract the non-reactive line of code into an Event function, and call that function from your Effect. +Your Effect needs to read the latest value of `duration`, but you don't want it to "react" to changes in `duration`. You use `duration` to start the animation, but starting animation isn't reactive. Extract the non-reactive line of code into an Effect Event, and call that function from your Effect. @@ -1401,12 +1401,12 @@ Your Effect needs to read the latest value of `duration`, but you don't want it ```js import { useState, useEffect, useRef } from 'react'; import { FadeInAnimation } from './animation.js'; -import { experimental_useEvent as useEvent } from 'react'; +import { experimental_useEffectEvent as useEffectEvent } from 'react'; function Welcome({ duration }) { const ref = useRef(null); - const onAppear = useEvent(animation => { + const onAppear = useEffectEvent(animation => { animation.start(duration); }); @@ -1501,7 +1501,7 @@ html, body { min-height: 300px; } -Event functions like `onAppear` are not reactive, so you can read `duration` inside without retriggering the animation. +Effect Events like `onAppear` are not reactive, so you can read `duration` inside without retriggering the animation. @@ -1903,7 +1903,7 @@ export default function App() { ```js ChatRoom.js active import { useState, useEffect } from 'react'; -import { experimental_useEvent as useEvent } from 'react'; +import { experimental_useEffectEvent as useEffectEvent } from 'react'; export default function ChatRoom({ roomId, createConnection, onMessage }) { useEffect(() => { @@ -2031,11 +2031,11 @@ There's more than one correct way to solve this, but the here is one possible so In the original example, toggling the theme caused different `onMessage` and `createConnection` functions to be created and passed down. Since the Effect depended on these functions, the chat would re-connect every time you toggle the theme. -To fix the problem with `onMessage`, you needed to wrap it into an Event function: +To fix the problem with `onMessage`, you needed to wrap it into an Effect Event: ```js {1,2,6} export default function ChatRoom({ roomId, createConnection, onMessage }) { - const onReceiveMessage = useEvent(onMessage); + const onReceiveMessage = useEffectEvent(onMessage); useEffect(() => { const connection = createConnection(); @@ -2043,7 +2043,7 @@ export default function ChatRoom({ roomId, createConnection, onMessage }) { // ... ``` -Unlike the `onMessage` prop, the `onReceiveMessage` Event function is not reactive. This is why it doesn't need to be a dependency of your Effect. As a result, changes to `onMessage` won't cause the chat to re-connect. +Unlike the `onMessage` prop, the `onReceiveMessage` Effect Event is not reactive. This is why it doesn't need to be a dependency of your Effect. As a result, changes to `onMessage` won't cause the chat to re-connect. You can't do the same with `createConnection` because it *should* be reactive. You *want* the Effect to re-trigger if the user switches between an encrypted and an unencryption connection, or if the user switches the current room. However, because `createConnection` is a function, you can't check whether the information it reads has *actually* changed or not. To solve this, instead of passing `createConnection` down from the `App` component, pass the raw `roomId` and `isEncrypted` values: @@ -2066,7 +2066,7 @@ import { } from './chat.js'; export default function ChatRoom({ roomId, isEncrypted, onMessage }) { - const onReceiveMessage = useEvent(onMessage); + const onReceiveMessage = useEffectEvent(onMessage); useEffect(() => { function createConnection() { @@ -2087,7 +2087,7 @@ After these two changes, your Effect no longer depends on any function values: ```js {1,8,10,21} export default function ChatRoom({ roomId, isEncrypted, onMessage }) { // Reactive values - const onReceiveMessage = useEvent(onMessage); // Not reactive + const onReceiveMessage = useEffectEvent(onMessage); // Not reactive useEffect(() => { function createConnection() { @@ -2185,14 +2185,14 @@ export default function App() { ```js ChatRoom.js active import { useState, useEffect } from 'react'; -import { experimental_useEvent as useEvent } from 'react'; +import { experimental_useEffectEvent as useEffectEvent } from 'react'; import { createEncryptedConnection, createUnencryptedConnection, } from './chat.js'; export default function ChatRoom({ roomId, isEncrypted, onMessage }) { - const onReceiveMessage = useEvent(onMessage); + const onReceiveMessage = useEffectEvent(onMessage); useEffect(() => { function createConnection() { diff --git a/beta/src/content/learn/reusing-logic-with-custom-hooks.md b/beta/src/content/learn/reusing-logic-with-custom-hooks.md index 29cef98a..571aa7df 100644 --- a/beta/src/content/learn/reusing-logic-with-custom-hooks.md +++ b/beta/src/content/learn/reusing-logic-with-custom-hooks.md @@ -901,14 +901,14 @@ export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { This will work, but there's one more improvement you can do when your custom Hook accepts event handlers. -Adding a dependency on `onReceiveMessage` is not ideal because it will cause the chat to re-connect every time the component re-renders. [Wrap this event handler into an Event function to remove it from the dependencies:](/learn/removing-effect-dependencies#wrapping-an-event-handler-from-the-props) +Adding a dependency on `onReceiveMessage` is not ideal because it will cause the chat to re-connect every time the component re-renders. [Wrap this event handler into an Effect Event to remove it from the dependencies:](/learn/removing-effect-dependencies#wrapping-an-event-handler-from-the-props) ```js {1,4,5,15,18} -import { useEffect, useEvent } from 'react'; +import { useEffect, useEffectEvent } from 'react'; // ... export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { - const onMessage = useEvent(onReceiveMessage); + const onMessage = useEffectEvent(onReceiveMessage); useEffect(() => { const options = { @@ -987,11 +987,11 @@ export default function ChatRoom({ roomId }) { ```js useChatRoom.js import { useEffect } from 'react'; -import { experimental_useEvent as useEvent } from 'react'; +import { experimental_useEffectEvent as useEffectEvent } from 'react'; import { createConnection } from './chat.js'; export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) { - const onMessage = useEvent(onReceiveMessage); + const onMessage = useEffectEvent(onReceiveMessage); useEffect(() => { const options = { @@ -1647,7 +1647,7 @@ export default function App() { ```js useFadeIn.js active import { useState, useEffect } from 'react'; -import { experimental_useEvent as useEvent } from 'react'; +import { experimental_useEffectEvent as useEffectEvent } from 'react'; export function useFadeIn(ref, duration) { const [isRunning, setIsRunning] = useState(true); @@ -1662,7 +1662,7 @@ export function useFadeIn(ref, duration) { } function useAnimationLoop(isRunning, drawFrame) { - const onFrame = useEvent(drawFrame); + const onFrame = useEffectEvent(drawFrame); useEffect(() => { if (!isRunning) { @@ -1880,7 +1880,7 @@ Sometimes, you don't even need a Hook! - You can pass reactive values from one Hook to another, and they stay up-to-date. - All Hooks re-run every time your component re-renders. - The code of your custom Hooks should be pure, like your component's code. -- Wrap event handlers received by custom Hooks into Event functions. +- Wrap event handlers received by custom Hooks into Effect Events. - Don't create custom Hooks like `useMount`. Keep their purpose specific. - It's up to you how and where to choose the boundaries of your code. @@ -2234,7 +2234,7 @@ export function useCounter(delay) { ```js useInterval.js import { useEffect } from 'react'; -import { experimental_useEvent as useEvent } from 'react'; +import { experimental_useEffectEvent as useEffectEvent } from 'react'; export function useInterval(onTick, delay) { useEffect(() => { @@ -2250,7 +2250,7 @@ export function useInterval(onTick, delay) { -Inside `useInterval`, wrap the tick callback into an Event function, as you did [earlier on this page.](/learn/reusing-logic-with-custom-hooks#passing-event-handlers-to-custom-hooks) +Inside `useInterval`, wrap the tick callback into an Effect Event, as you did [earlier on this page.](/learn/reusing-logic-with-custom-hooks#passing-event-handlers-to-custom-hooks) This will allow you to omit `onTick` from dependencies of your Effect. The Effect won't re-synchronize on every re-render of the component, so the page background color change interval won't get reset every second before it has a chance to fire. @@ -2306,10 +2306,10 @@ export function useCounter(delay) { ```js useInterval.js active import { useEffect } from 'react'; -import { experimental_useEvent as useEvent } from 'react'; +import { experimental_useEffectEvent as useEffectEvent } from 'react'; export function useInterval(callback, delay) { - const onTick = useEvent(callback); + const onTick = useEffectEvent(callback); useEffect(() => { const id = setInterval(onTick, delay); return () => clearInterval(id); diff --git a/beta/src/content/learn/separating-events-from-effects.md b/beta/src/content/learn/separating-events-from-effects.md index 500b0369..becdd0e1 100644 --- a/beta/src/content/learn/separating-events-from-effects.md +++ b/beta/src/content/learn/separating-events-from-effects.md @@ -13,8 +13,8 @@ Event handlers only re-run when you perform the same interaction again. Unlike e - 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 +- What Effect Events are, and how to extract them from your Effects +- How to read the latest props and state from Effects using Effect Events @@ -398,7 +398,7 @@ In other words, you *don't* want this line to be reactive, even though it is ins You need a way to separate this non-reactive logic from the reactive Effect around it. -### Declaring an Event function {/*declaring-an-event-function*/} +### Declaring an Effect Event {/*declaring-an-effect-event*/} @@ -406,25 +406,25 @@ This section describes an **experimental API that has not yet been added to Reac -Use a special Hook called [`useEvent`](/apis/react/useEvent) to extract this non-reactive logic out of your Effect: +Use a special Hook called [`useEffectEvent`](/apis/react/useEffectEvent) to extract this non-reactive logic out of your Effect: ```js {1,4-6} -import { useEffect, useEvent } from 'react'; +import { useEffect, useEffectEvent } from 'react'; function ChatRoom({ roomId, theme }) { - const onConnected = useEvent(() => { + const onConnected = useEffectEvent(() => { 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. +Here, `onConnected` is called an *Effect Event.* 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: +Now you can call the `onConnected` Effect Event from inside your Effect: ```js {2-4,9,13} function ChatRoom({ roomId, theme }) { - const onConnected = useEvent(() => { + const onConnected = useEffectEvent(() => { showNotification('Connected!', theme); }); @@ -439,7 +439,7 @@ function ChatRoom({ roomId, theme }) { // ... ``` -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. +This solves the problem. Note that you had to *remove* `onConnected` from the list of your Effect's dependencies. **Effect Events are not reactive and must be omitted from dependencies. The linter will error if you include them.** Verify that the new behavior works as you would expect: @@ -464,14 +464,14 @@ Verify that the new behavior works as you would expect: ```js import { useState, useEffect } from 'react'; -import { experimental_useEvent as useEvent } from 'react'; +import { experimental_useEffectEvent as useEffectEvent } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId, theme }) { - const onConnected = useEvent(() => { + const onConnected = useEffectEvent(() => { showNotification('Connected!', theme); }); @@ -574,9 +574,9 @@ label { display: block; margin-top: 10px; } -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. +You can think of Effect Events as being very similar to event handlers. The main difference is that event handlers run in response to a user interactions, whereas Effect Events are triggered by you from Effects. Effect Events 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*/} +### Reading latest props and state with Effect Events {/*reading-latest-props-and-state-with-effect-events*/} @@ -584,7 +584,7 @@ This section describes an **experimental API that has not yet been added to Reac -Event functions let you fix many patterns where you might be tempted to suppress the dependency linter. +Effect Events 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: @@ -642,7 +642,7 @@ function Page({ url }) { const { items } = useContext(ShoppingCartContext); const numberOfItems = items.length; - const onVisit = useEvent(visitedUrl => { + const onVisit = useEffectEvent(visitedUrl => { logVisit(visitedUrl, numberOfItems); }); @@ -653,9 +653,9 @@ function Page({ url }) { } ``` -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. +Here, `onVisit` is an Effect Event. 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. +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` Effect Event. 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. @@ -664,7 +664,7 @@ As a result, you will call `logVisit` for every change to the `url`, and always You might be wondering if you could call `onVisit()` with no arguments, and read the `url` inside it: ```js {2,6} - const onVisit = useEvent(() => { + const onVisit = useEffectEvent(() => { logVisit(url, numberOfItems); }); @@ -673,10 +673,10 @@ You might be wondering if you could call `onVisit()` with no arguments, and read }, [url]); ``` -This would work, but it's better to pass this `url` to the Event function explicitly. **By passing `url` as an argument to your Event function, you are saying that visiting a page with a different `url` constitutes a separate "event" from the user's perspective.** The `visitedUrl` is a *part* of the "event" that happened: +This would work, but it's better to pass this `url` to the Effect Event explicitly. **By passing `url` as an argument to your Effect Event, you are saying that visiting a page with a different `url` constitutes a separate "event" from the user's perspective.** The `visitedUrl` is a *part* of the "event" that happened: ```js {1-2,6} - const onVisit = useEvent(visitedUrl => { + const onVisit = useEffectEvent(visitedUrl => { logVisit(visitedUrl, numberOfItems); }); @@ -685,12 +685,12 @@ This would work, but it's better to pass this `url` to the Event function explic }, [url]); ``` -Since your Event function explicitly "asks" for the `visitedUrl`, now you can't accidentally remove `url` from the Effect's dependencies. If you remove the `url` dependency (causing distinct page visits to be counted as one), the linter will warn you about it. You want `onVisit` to be reactive with regards to the `url`, so instead of reading the `url` inside (where it wouldn't be reactive), you pass it *from* your Effect. +Since your Effect Event explicitly "asks" for the `visitedUrl`, now you can't accidentally remove `url` from the Effect's dependencies. If you remove the `url` dependency (causing distinct page visits to be counted as one), the linter will warn you about it. You want `onVisit` to be reactive with regards to the `url`, so instead of reading the `url` inside (where it wouldn't be reactive), you pass it *from* your Effect. This becomes especially important if there is some asynchronous logic inside the Effect: ```js {6,8} - const onVisit = useEvent(visitedUrl => { + const onVisit = useEffectEvent(visitedUrl => { logVisit(visitedUrl, numberOfItems); }); @@ -725,7 +725,7 @@ function Page({ url }) { } ``` -After `useEvent` becomes a stable part of React, we recommend to **never suppress the linter** like this. +After `useEffectEvent` becomes a stable part of React, we recommend to **never suppress the linter** like this. The first downside of suppressing the rule is that React will no longer warn you when your Effect needs to "react" to a new reactive dependency you've introduced to your code. For example, in the earlier example, you added `url` to the dependencies *because* React reminded you to do it. You will no longer get such reminders for any future edits to that Effect if you disable the linter. This leads to bugs. @@ -794,7 +794,7 @@ The author of the original code has "lied" to React by saying that the Effect do **If you never suppress the linter, you will never see problems with stale values.** -With `useEvent`, there is no need to "lie" to the linter, and the code works as you would expect: +With `useEffectEvent`, there is no need to "lie" to the linter, and the code works as you would expect: @@ -816,13 +816,13 @@ With `useEvent`, there is no need to "lie" to the linter, and the code works as ```js import { useState, useEffect } from 'react'; -import { experimental_useEvent as useEvent } from 'react'; +import { experimental_useEffectEvent as useEffectEvent } from 'react'; export default function App() { const [position, setPosition] = useState({ x: 0, y: 0 }); const [canMove, setCanMove] = useState(true); - const onMove = useEvent(e => { + const onMove = useEffectEvent(e => { if (canMove) { setPosition({ x: e.clientX, y: e.clientY }); } @@ -868,13 +868,13 @@ body { -This doesn't mean that `useEvent` is *always* the correct solution. You should only apply it to the lines of code that you don't want to be reactive. For example, in the above sandbox, you didn't want the Effect's code to be reactive with regards to `canMove`. That's why it made sense to extract an Event function. +This doesn't mean that `useEffectEvent` is *always* the correct solution. You should only apply it to the lines of code that you don't want to be reactive. For example, in the above sandbox, you didn't want the Effect's code to be reactive with regards to `canMove`. That's why it made sense to extract an Effect Event. Read [Removing Effect Dependencies](/learn/removing-effect-dependencies) for other correct alternatives to suppressing the linter. -### Limitations of Event functions {/*limitations-of-event-functions*/} +### Limitations of Effect Events {/*limitations-of-effect-events*/} @@ -882,22 +882,22 @@ This section describes an **experimental API that has not yet been added to Reac -At the moment, Event functions are very limited in how you can use them: +Effect Events 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: +For example, don't declare and pass an Effect Event like this: ```js {4-6,8} function Timer() { const [count, setCount] = useState(0); - const onTick = useEvent(() => { + const onTick = useEffectEvent(() => { setCount(count + 1); }); - useTimer(onTick, 1000); // 🔴 Avoid: Passing event functions + useTimer(onTick, 1000); // 🔴 Avoid: Passing Effect Events return

{count}

} @@ -914,7 +914,7 @@ function useTimer(callback, delay) { } ``` -Instead, always declare Event functions directly next to the Effects that use them: +Instead, always declare Effect Events directly next to the Effects that use them: ```js {10-12,16,21} function Timer() { @@ -926,7 +926,7 @@ function Timer() { } function useTimer(callback, delay) { - const onTick = useEvent(() => { + const onTick = useEffectEvent(() => { callback(); }); @@ -937,11 +937,11 @@ function useTimer(callback, delay) { return () => { clearInterval(id); }; - }, [delay]); // No need to specify "onTick" (an Event function) as a dependency + }, [delay]); // No need to specify "onTick" (an Effect Event) 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. +Effect Events are non-reactive "pieces" of your Effect code. They should be next to the Effect using them. @@ -949,9 +949,9 @@ It's possible that in the future, some of these restrictions will be lifted. But - 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. +- You can move non-reactive logic from Effects into Effect Events. +- Only call Effect Events from inside Effects. +- Don't pass Effect Events to other components or Hooks. @@ -1104,7 +1104,7 @@ It seems like the Effect which sets up the timer "reacts" to the `increment` val ```js import { useState, useEffect } from 'react'; -import { experimental_useEvent as useEvent } from 'react'; +import { experimental_useEffectEvent as useEffectEvent } from 'react'; export default function Timer() { const [count, setCount] = useState(0); @@ -1151,7 +1151,7 @@ button { margin: 10px; } The issue is that the code inside the Effect uses the `increment` state variable. Since it's a dependency of your Effect, every change to `increment` causes the Effect to re-synchronize, which causes the interval to clear. If you keep clearing the interval every time before it has a chance to fire, it will appear as if the timer has stalled. -To solve the issue, extract an `onTick` Event function from the Effect: +To solve the issue, extract an `onTick` Effect Event from the Effect: @@ -1173,13 +1173,13 @@ To solve the issue, extract an `onTick` Event function from the Effect: ```js import { useState, useEffect } from 'react'; -import { experimental_useEvent as useEvent } from 'react'; +import { experimental_useEffectEvent as useEffectEvent } from 'react'; export default function Timer() { const [count, setCount] = useState(0); const [increment, setIncrement] = useState(1); - const onTick = useEvent(() => { + const onTick = useEffectEvent(() => { setCount(c => c + increment); }); @@ -1221,7 +1221,7 @@ button { margin: 10px; } -Since `onTick` is an Event function, the code inside it isn't reactive. The change to `increment` does not trigger any Effects. +Since `onTick` is an Effect Event, the code inside it isn't reactive. The change to `increment` does not trigger any Effects.
@@ -1231,7 +1231,7 @@ In this example, you can customize the interval delay. It's stored in a `delay` -Code inside Event functions is not reactive. Are there cases in which you would _want_ the `setInterval` call to re-run? +Code inside Effect Events is not reactive. Are there cases in which you would _want_ the `setInterval` call to re-run? @@ -1255,18 +1255,18 @@ Code inside Event functions is not reactive. Are there cases in which you would ```js import { useState, useEffect } from 'react'; -import { experimental_useEvent as useEvent } from 'react'; +import { experimental_useEffectEvent as useEffectEvent } from 'react'; export default function Timer() { const [count, setCount] = useState(0); const [increment, setIncrement] = useState(1); const [delay, setDelay] = useState(100); - const onTick = useEvent(() => { + const onTick = useEffectEvent(() => { setCount(c => c + increment); }); - const onMount = useEvent(() => { + const onMount = useEffectEvent(() => { return setInterval(() => { onTick(); }, delay); @@ -1277,7 +1277,7 @@ export default function Timer() { return () => { clearInterval(id); } - }, [onMount]); + }, []); return ( <> @@ -1320,7 +1320,7 @@ button { margin: 10px; } -The problem with the above example is that it extracted an Event function called `onMount` without considering what the code should actually be doing. You should only extract Event functions for a specific reason: when you want to make a part of your code non-reactive. However, the `setInterval` call *should* be reactive with respect to the `delay` state variable. If the `delay` changes, you want to set up the interval from scratch! To fix this code, pull all the reactive code back inside the Effect: +The problem with the above example is that it extracted an Effect Event called `onMount` without considering what the code should actually be doing. You should only extract Effect Events for a specific reason: when you want to make a part of your code non-reactive. However, the `setInterval` call *should* be reactive with respect to the `delay` state variable. If the `delay` changes, you want to set up the interval from scratch! To fix this code, pull all the reactive code back inside the Effect: @@ -1342,14 +1342,14 @@ The problem with the above example is that it extracted an Event function called ```js import { useState, useEffect } from 'react'; -import { experimental_useEvent as useEvent } from 'react'; +import { experimental_useEffectEvent as useEffectEvent } from 'react'; export default function Timer() { const [count, setCount] = useState(0); const [increment, setIncrement] = useState(1); const [delay, setDelay] = useState(100); - const onTick = useEvent(() => { + const onTick = useEffectEvent(() => { setCount(c => c + increment); }); @@ -1400,7 +1400,7 @@ button { margin: 10px; } -In general, you should be suspicious of functions like `onMount` that focus on the *timing* rather than the *purpose* of a piece of code. It may feel "more descriptive" at first but it obscures your intent. As a rule of thumb, Event functions should correspond to something that happens from the *user's* perspective. For example, `onMessage`, `onTick`, `onVisit`, or `onConnected` are good Event function names. Code inside them would likely not need to be reactive. On the other hand, `onMount`, `onUpdate`, `onUnmount`, or `onAfterRender` are so generic that it's easy to accidentally put code that *should* be reactive into them. This is why you should name your Event functions after *what the user thinks has happened,* not when some code happened to run. +In general, you should be suspicious of functions like `onMount` that focus on the *timing* rather than the *purpose* of a piece of code. It may feel "more descriptive" at first but it obscures your intent. As a rule of thumb, Effect Events should correspond to something that happens from the *user's* perspective. For example, `onMessage`, `onTick`, `onVisit`, or `onConnected` are good Effect Event names. Code inside them would likely not need to be reactive. On the other hand, `onMount`, `onUpdate`, `onUnmount`, or `onAfterRender` are so generic that it's easy to accidentally put code that *should* be reactive into them. This is why you should name your Effect Events after *what the user thinks has happened,* not when some code happened to run. @@ -1414,7 +1414,7 @@ Fix it so that when you switch from "general" to "travel" and then to "music" ve -Your Effect knows which room it connected to. Is there any information that you might want to pass to your Event function? +Your Effect knows which room it connected to. Is there any information that you might want to pass to your Effect Event? @@ -1439,14 +1439,14 @@ Your Effect knows which room it connected to. Is there any information that you ```js import { useState, useEffect } from 'react'; -import { experimental_useEvent as useEvent } from 'react'; +import { experimental_useEffectEvent as useEffectEvent } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId, theme }) { - const onConnected = useEvent(() => { + const onConnected = useEffectEvent(() => { showNotification('Welcome to ' + roomId, theme); }); @@ -1553,11 +1553,11 @@ label { display: block; margin-top: 10px; } -Inside your Event function, `roomId` is the value *at the time Event function was called.* +Inside your Effect Event, `roomId` is the value *at the time Effect Event was called.* -Your Event function is called with a two second delay. If you're quickly switching from the travel to the music room, by the time the travel room's notification shows, `roomId` is already `"music"`. This is why both notifications say "Welcome to music". +Your Effect Event is called with a two second delay. If you're quickly switching from the travel to the music room, by the time the travel room's notification shows, `roomId` is already `"music"`. This is why both notifications say "Welcome to music". -To fix the issue, instead of reading the *latest* `roomId` inside the Event function, make it a parameter of your Event function, like `connectedRoomId` below. Then pass `roomId` from your Effect by calling `onConnected(roomId)`: +To fix the issue, instead of reading the *latest* `roomId` inside the Effect Event, make it a parameter of your Effect Event, like `connectedRoomId` below. Then pass `roomId` from your Effect by calling `onConnected(roomId)`: @@ -1580,14 +1580,14 @@ To fix the issue, instead of reading the *latest* `roomId` inside the Event func ```js import { useState, useEffect } from 'react'; -import { experimental_useEvent as useEvent } from 'react'; +import { experimental_useEffectEvent as useEffectEvent } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId, theme }) { - const onConnected = useEvent(connectedRoomId => { + const onConnected = useEffectEvent(connectedRoomId => { showNotification('Welcome to ' + connectedRoomId, theme); }); @@ -1717,14 +1717,14 @@ To solve the additional challenge, save the notification timeout ID and clear it ```js import { useState, useEffect } from 'react'; -import { experimental_useEvent as useEvent } from 'react'; +import { experimental_useEffectEvent as useEffectEvent } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId, theme }) { - const onConnected = useEvent(connectedRoomId => { + const onConnected = useEffectEvent(connectedRoomId => { showNotification('Welcome to ' + connectedRoomId, theme); }); diff --git a/beta/yarn.lock b/beta/yarn.lock index 0972b6fa..9db85c1b 100644 --- a/beta/yarn.lock +++ b/beta/yarn.lock @@ -2433,16 +2433,16 @@ eslint-plugin-jsx-a11y@6.x, eslint-plugin-jsx-a11y@^6.4.1: language-tags "^1.0.5" minimatch "^3.0.4" +eslint-plugin-react-hooks@^0.0.0-experimental-fabef7a6b-20221215: + version "0.0.0-experimental-fabef7a6b-20221215" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-0.0.0-experimental-fabef7a6b-20221215.tgz#ceb8c59f1c363cc2f733b0be36661a8a722a89a1" + integrity sha512-y1lJAS4gWXyP6kXl2jA9ZJdFFfcMwNjMEZEEXn9LHOWEhnAgKgcqZ/NhNWAphiJLYOZ33kne1hbhDlGCcrdx5g== + eslint-plugin-react-hooks@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz#318dbf312e06fab1c835a4abef00121751ac1172" integrity sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA== -eslint-plugin-react-hooks@experimental: - version "0.0.0-experimental-cb5084d1c-20220924" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-0.0.0-experimental-cb5084d1c-20220924.tgz#66847d4c458198a1a38cbf437397dd461bfa7245" - integrity sha512-qk195a0V1Fz82DUESwAt8bSxwV3MBYGUh4cWezY21gaWderOcva8SdC6HNMAwk2Ad/HvaJbjp/qLYrYBQW7pGg== - eslint-plugin-react@7.x, eslint-plugin-react@^7.23.1: version "7.28.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.28.0.tgz#8f3ff450677571a659ce76efc6d80b6a525adbdf"