diff --git a/src/components/ButtonLink.tsx b/src/components/ButtonLink.tsx index bd98d5b38..dff09ed3a 100644 --- a/src/components/ButtonLink.tsx +++ b/src/components/ButtonLink.tsx @@ -33,7 +33,7 @@ function ButtonLink({ className, 'active:scale-[.98] transition-transform inline-flex font-bold items-center outline-none focus:outline-none focus-visible:outline focus-visible:outline-link focus:outline-offset-2 focus-visible:dark:focus:outline-link-dark leading-snug', { - 'bg-link text-white dark:bg-brand-dark dark:text-secondary hover:bg-opacity-80': + 'bg-link text-white dark:bg-brand-dark dark:text-gray-90 hover:bg-opacity-80': type === 'primary', 'text-primary dark:text-primary-dark shadow-secondary-button-stroke dark:shadow-secondary-button-stroke-dark hover:bg-gray-40/5 active:bg-gray-40/10 hover:dark:bg-gray-60/5 active:dark:bg-gray-60/10': type === 'secondary', diff --git a/src/content/reference/react-dom/components/form.md b/src/content/reference/react-dom/components/form.md index f4df10742..df40353a1 100644 --- a/src/content/reference/react-dom/components/form.md +++ b/src/content/reference/react-dom/components/form.md @@ -48,9 +48,53 @@ title: "
" ## 사용법 {/*usage*/} +<<<<<<< HEAD ### 클라이언트에서 폼 제출 처리하기 {/*handle-form-submission-on-the-client*/} 폼이 제출될 때 함수를 실행하기 위해, 폼의 `action` 프로퍼티에 함수를 전달하세요. [`formData`](https://developer.mozilla.org/ko/docs/Web/API/FormData)가 함수에 인수로 전달되어, 폼에서 전달된 데이터에 접근할 수 있습니다. 이 점이 URL만 받던 기존 [HTML action](https://developer.mozilla.org/ko/docs/Web/HTML/Element/form)과의 차이점입니다. After the `action` function succeeds, all uncontrolled field elements in the form are reset. +======= +### Handle form submission with an event handler {/*handle-form-submission-with-an-event-handler*/} + +Pass a function to the `onSubmit` event handler to run code when the form is submitted. By default, the browser sends the form data to the current URL and refreshes the page, so call [`e.preventDefault()`](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault) to override that behavior. + +This example reads the submitted values with [`new FormData(e.target)`](https://developer.mozilla.org/en-US/docs/Web/API/FormData), which collects every field by its `name`. This keeps the inputs [uncontrolled](/reference/react-dom/components/input#reading-the-input-values-when-submitting-a-form). If you instead [control an input with state](/reference/react-dom/components/input#controlling-an-input-with-a-state-variable), read from that state on submit rather than from `FormData`. + + + +```js src/App.js +export default function Search() { + function handleSubmit(e) { + // Prevent the browser from reloading the page + e.preventDefault(); + + // Read the form data + const form = e.target; + const formData = new FormData(form); + const query = formData.get("query"); + alert(`You searched for '${query}'`); + } + + return ( + + + + + ); +} +``` + +
+ + + +Reading form data with `onSubmit` works in every version of React and gives you direct access to the [submit event](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit_event), so you can call `e.preventDefault()` and read the data yourself. Passing the function to the `action` prop instead runs the submission in a [Transition](/reference/react/useTransition). React then tracks the pending state, sends thrown errors to the nearest error boundary, and lets the form work with [`useActionState`](/reference/react/useActionState) and [`useOptimistic`](/reference/react/useOptimistic). An `action` can also be a [Server Function](/reference/rsc/server-functions), which `onSubmit` does not support. + + + +### Handle form submission with an action prop {/*handle-form-submission-with-an-action-prop*/} + +Pass a function to the `action` prop of form to run the function when the form is submitted. [`formData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) will be passed to the function as an argument so you can access the data submitted by the form. This differs from the conventional [HTML action](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#action), which only accepts URLs. Unlike `onSubmit`, an `action` runs in a [Transition](/reference/react/useTransition) and calling `e.preventDefault()` isn't needed. After the `action` function succeeds, all uncontrolled field elements in the form are reset. +>>>>>>> 152a471aa9ac2f6f0f3e64c04f39da790d40cf61 diff --git a/src/content/reference/react/Component.md b/src/content/reference/react/Component.md index 3481e474f..c4f411e50 100644 --- a/src/content/reference/react/Component.md +++ b/src/content/reference/react/Component.md @@ -1009,7 +1009,11 @@ state를 파생하면 코드가 장황해지고 컴포넌트에 대해 생각하 #### 주의 사항 {/*static-getderivedstatefromprops-caveats*/} +<<<<<<< HEAD - 이 메서드는 원인에 관계없이 *모든* 렌더링에서 호출됩니다. 이는 부모가 다시 렌더링을 일으킬 때만 발동하고 로컬 `setState`의 결과가 아닐 때만 발동하는 [`UNSAFE_componentWillReceiveProps`](#unsafe_cmoponentwillreceiveprops)와는 다릅니다. +======= +- This method is fired on *every* render, regardless of the cause. This is different from [`UNSAFE_componentWillReceiveProps`](#unsafe_componentwillreceiveprops), which only fires when the parent causes a re-render and not as a result of a local `setState`. +>>>>>>> 152a471aa9ac2f6f0f3e64c04f39da790d40cf61 - 이 메서드에는 컴포넌트 인스턴스에 대한 액세스 권한이 없습니다. 원하는 경우 클래스 정의 외부 컴포넌트 props 및 state의 순수 함수를 추출하여 `static getDerivedStateFromProps`와 다른 클래스 메서드 사이에 일부 코드를 재사용할 수 있습니다. diff --git a/src/content/reference/react/use.md b/src/content/reference/react/use.md index 0c3ea03cb..57445e10e 100644 --- a/src/content/reference/react/use.md +++ b/src/content/reference/react/use.md @@ -4,7 +4,11 @@ title: use +<<<<<<< HEAD `use`는 [Promise](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise)나 [Context](/learn/passing-data-deeply-with-context)와 같은 데이터를 참조하는 React API입니다. +======= +`use` is a React API that lets you read the value of a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) or [context](/learn/passing-data-deeply-with-context). +>>>>>>> 152a471aa9ac2f6f0f3e64c04f39da790d40cf61 ```js const value = use(resource); @@ -18,19 +22,23 @@ const value = use(resource); ## 레퍼런스 {/*reference*/} -### `use(resource)` {/*use*/} +### `use(context)` {/*use-context*/} +<<<<<<< HEAD 컴포넌트에서 [Promise](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise)나 [Context](/learn/passing-data-deeply-with-context)와 같은 데이터를 참조하려면 `use`를 사용하세요. +======= +Call `use` with a [context](/learn/passing-data-deeply-with-context) to read its value. Unlike [`useContext`](/reference/react/useContext), `use` can be called within loops and conditional statements like `if`. +>>>>>>> 152a471aa9ac2f6f0f3e64c04f39da790d40cf61 -```jsx +```js import { use } from 'react'; -function MessageComponent({ messagePromise }) { - const message = use(messagePromise); +function Button() { const theme = use(ThemeContext); // ... ``` +<<<<<<< HEAD 다른 React Hook과 달리 `use`는 `if`와 같은 조건문과 반복문 내부에서 호출할 수 있습니다. 다만, 다른 React Hook과 같이 `use`는 컴포넌트 또는 Hook에서만 호출해야 합니다. Promise와 함께 호출될 때 `use` API는 [`Suspense`](/reference/react/Suspense) 및 [Error Boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary)와 통합됩니다. `use`에 전달된 Promise가 대기Pending하는 동안 `use`를 호출하는 컴포넌트는 *Suspend*됩니다. `use`를 호출하는 컴포넌트가 Suspense 경계로 둘러싸여 있으면 Fallback이 표시됩니다. Promise가 리졸브되면 Suspense Fallback은 `use` API가 반환한 컴포넌트로 대체됩니다. `use`에 전달된 Promise가 Reject되면 가장 가까운 Error Boundary의 Fallback이 표시됩니다. @@ -54,10 +62,67 @@ Promise와 함께 호출될 때 `use` API는 [`Suspense`](/reference/react/Suspe --- ## 사용법 {/*usage*/} +======= +[See more examples below.](#usage-context) + +#### Parameters {/*context-parameters*/} + +* `context`: A [context](/learn/passing-data-deeply-with-context) created with [`createContext`](/reference/react/createContext). + +#### Returns {/*context-returns*/} + +The context value for the passed context, determined by the closest context provider above the calling component. If there is no provider, the returned value is the `defaultValue` passed to [`createContext`](/reference/react/createContext). + +#### Caveats {/*context-caveats*/} + +* `use` must be called inside a Component or a Hook. +* Reading context with `use` is not supported in [Server Components](/reference/rsc/server-components). + +--- + +### `use(promise)` {/*use-promise*/} + +Call `use` with a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) to read its resolved value. The component calling `use` *suspends* while the Promise is pending. Despite its name, `use` is not a Hook. Unlike Hooks, it can be called inside loops and conditional statements like `if`. + +```js +import { use } from 'react'; + +function MessageComponent({ messagePromise }) { + const message = use(messagePromise); + // ... +``` + +If the component that calls `use` is wrapped in a [Suspense](/reference/react/Suspense) boundary, the fallback will be displayed while the Promise is pending. Once the Promise is resolved, the Suspense fallback is replaced by the rendered components using the data returned by `use`. If the Promise is rejected, the fallback of the nearest [Error Boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary) will be displayed. + +[See more examples below.](#usage-promises) + +#### Parameters {/*promise-parameters*/} + +* `promise`: A [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) whose resolved value you want to read. The Promise must be [cached](#caching-promises-for-client-components) so that the same instance is reused across re-renders. + +#### Returns {/*promise-returns*/} + +The resolved value of the Promise. + +#### Caveats {/*promise-caveats*/} + +* `use` must be called inside a Component or a Hook. +* `use` cannot be called inside a try-catch block. Instead, wrap your component in an [Error Boundary](#displaying-an-error-with-an-error-boundary) to catch the error and display a fallback. +* Promises passed to `use` must be cached so the same Promise instance is reused across re-renders. [See caching Promises below.](#caching-promises-for-client-components) +* When passing a Promise from a Server Component to a Client Component, its resolved value must be [serializable](/reference/rsc/use-client#serializable-types). + +--- + +## Usage (Context) {/*usage-context*/} +>>>>>>> 152a471aa9ac2f6f0f3e64c04f39da790d40cf61 ### `use`를 사용하여 Context 참조하기 {/*reading-context-with-use*/} +<<<<<<< HEAD [Context](/learn/passing-data-deeply-with-context)가 `use`에 전달되면 [`useContext`](/reference/react/useContext)와 유사하게 작동합니다. `useContext`는 컴포넌트의 최상위 수준에서 호출해야 하지만, `use`는 `if`와 같은 조건문이나 `for`와 같은 반복문 내부에서 호출할 수 있습니다. `use`는 유연하므로 `useContext`보다 선호됩니다. +======= +When a [context](/learn/passing-data-deeply-with-context) is passed to `use`, it works similarly to [`useContext`](/reference/react/useContext). While `useContext` must be called at the top level of your component, `use` can be called inside conditionals like `if` and loops like `for`. +>>>>>>> 152a471aa9ac2f6f0f3e64c04f39da790d40cf61 ```js [[2, 4, "theme"], [1, 4, "ThemeContext"]] import { use } from 'react'; @@ -194,11 +259,807 @@ function Button({ show, children }) { +<<<<<<< HEAD ### 서버에서 클라이언트로 데이터 스트리밍하기 {/*streaming-data-from-server-to-client*/} 서버 컴포넌트에서 클라이언트 컴포넌트로 Promise Prop을 전달하여 서버에서 클라이언트로 데이터를 스트리밍할 수 있습니다. +======= +### Reading a Promise from context {/*reading-a-promise-from-context*/} + +To share asynchronous data without prop drilling, set a Promise as a context value, then read it with `use(context)` and resolve it with `use(promise)`: +>>>>>>> 152a471aa9ac2f6f0f3e64c04f39da790d40cf61 + +```js +import { use } from 'react'; +import { UserContext } from './UserContext'; + +function Profile() { + const userPromise = use(UserContext); + const user = use(userPromise); + return

{user.name}

; +} +``` + +Reading the value requires two `use` calls because the context value itself isn't awaited. See [Before you use context](/learn/passing-data-deeply-with-context#before-you-use-context) for alternatives to consider before reaching for context. + +Wrap the components that read the Promise in a [Suspense](/reference/react/Suspense) boundary so only that subtree suspends while the Promise is pending. See [Usage (Promises)](#usage-promises) below for more on reading Promises with `use`. + + + +When this pattern is used with [Server Components](/reference/rsc/server-components), refetching the Promise requires refetching the Server Component that sets the Promise in context. Avoid setting the Promise in context high in the tree, since that would refetch large parts of the app unnecessarily. + + + +--- + +## Usage (Promises) {/*usage-promises*/} + +### Reading a Promise with `use` {/*reading-a-promise-with-use*/} + +Call `use` with a Promise to read its resolved value. The component will [suspend](/reference/react/Suspense) while the Promise is pending. + +```js [[1, 4, "use(albumsPromise)"]] +import { use } from 'react'; + +function Albums({ albumsPromise }) { + const albums = use(albumsPromise); + return ( + + ); +} +``` + +Wrap the component that calls `use` in a [Suspense](/reference/react/Suspense) boundary so React can show a fallback while the Promise is pending. The closest Suspense boundary above the suspending component shows its fallback. Once the Promise resolves, React reads the value with `use` and replaces the fallback with the rendered component. + + + +#### Fetching data with `use` {/*fetching-data-with-use*/} + +In this example, `Albums` calls `use` with a cached Promise. The component suspends while the Promise is pending, and React displays the nearest Suspense fallback. Rejected Promises propagate to the nearest [Error Boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary). + + + +```js src/App.js active +import { use, Suspense } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { fetchData } from './data.js'; + +export default function App() { + return ( + Could not fetch albums.

}> + }> + + +
+ ); +} + +function Albums() { + const albums = use(fetchData('/albums')); + return ( + + ); +} + +function Loading() { + return

Loading...

; +} +``` + +```js src/data.js hidden +// Note: the way you would do data fetching depends on +// the framework that you use together with Suspense. +// Normally, the caching logic would be inside a framework. + +let cache = new Map(); + +export function fetchData(url) { + if (!cache.has(url)) { + cache.set(url, getData(url)); + } + return cache.get(url); +} + +async function getData(url) { + if (url === '/albums') { + return await getAlbums(); + } else { + throw Error('Not implemented'); + } +} + +async function getAlbums() { + // Add a fake delay to make waiting noticeable. + await new Promise(resolve => { + setTimeout(resolve, 1000); + }); + + return [{ + id: 13, + title: 'Let It Be', + year: 1970 + }, { + id: 12, + title: 'Abbey Road', + year: 1969 + }, { + id: 11, + title: 'Yellow Submarine', + year: 1969 + }, { + id: 10, + title: 'The Beatles', + year: 1968 + }]; +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "19.0.0", + "react-dom": "19.0.0", + "react-scripts": "^5.0.0", + "react-error-boundary": "4.0.3" + }, + "main": "/index.js" +} +``` + +
+ + -```js [[1, 4, "App"], [2, 2, "Message"], [3, 7, "Suspense"], [4, 8, "messagePromise", 30], [4, 5, "messagePromise"]] +#### Fetching data with `useEffect` {/*fetching-data-with-useeffect*/} + +Before `use`, a common approach was to fetch data in an Effect and update state when the data arrives. Compared to `use`, this approach requires managing loading and error states manually. For more details on why fetching in an Effect is discouraged, see [You Might Not Need an Effect](/learn/you-might-not-need-an-effect#fetching-data). + + + +```js src/App.js active +import { useState, useEffect } from 'react'; +import { fetchAlbums } from './data.js'; + +export default function App() { + const [albums, setAlbums] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetchAlbums() + .then(data => { + setAlbums(data); + setIsLoading(false); + }) + .catch(err => { + setError(err); + setIsLoading(false); + }); + }, []); + + if (isLoading) { + return

Loading...

; + } + + if (error) { + return

Error: {error.message}

; + } + + return ( +
    + {albums.map(album => ( +
  • + {album.title} ({album.year}) +
  • + ))} +
+ ); +} +``` + +```js src/data.js hidden +export async function fetchAlbums() { + // Add a fake delay to make waiting noticeable. + await new Promise(resolve => { + setTimeout(resolve, 1000); + }); + + return [{ + id: 13, + title: 'Let It Be', + year: 1970 + }, { + id: 12, + title: 'Abbey Road', + year: 1969 + }, { + id: 11, + title: 'Yellow Submarine', + year: 1969 + }, { + id: 10, + title: 'The Beatles', + year: 1968 + }]; +} +``` + +
+ + + +
+ + + +##### Promises passed to `use` must be cached {/*promises-must-cached*/} + +Promises created during render are recreated on every render, which causes React to show the Suspense fallback repeatedly and prevents content from appearing. + +```js +function Albums() { + // 🔴 `fetch` creates a new Promise on every render. + const albums = use(fetch('/albums')); + // ... +} +``` + +Instead, pass a Promise from a cache, a Suspense-enabled framework, or a Server Component: + +```js +// ✅ fetchData reads the Promise from a cache. +const albums = use(fetchData('/albums')); +``` + + + + + +#### Why are Promises recreated on every render? {/*why-promises-recreated*/} + +[React doesn't preserve state for renders that suspended before mounting](/reference/react/Suspense#caveats). After each suspension, React retries rendering from scratch, so any Promise created during render is recreated. + +Common ways a Promise can be unintentionally recreated during render: + +```js +function Albums() { + // 🔴 `fetch` creates a new Promise on every render. + const albums = use(fetch('/albums')); + + // 🔴 Uncached `async` function calls create a new Promise on every render. + const albums = use((async () => { + const res = await fetch('/albums'); + return res.json(); + })()); + + // 🔴 Adding `.then` returns a new Promise on every render, + // even if `fetchData` is cached. + const albums = use(fetchData('/albums').then(res => res.json())); + // ... +} +``` + +Ideally, Promises are created before rendering, such as in an event handler, a route loader, or a Server Component, and passed to the component that calls `use`. Fetching lazily in render delays network requests and can create waterfalls. + +```js +// ✅ fetchData reads the Promise from a cache. +const albums = use(fetchData('/albums')); +``` + + + +--- + +### Caching Promises for Client Components {/*caching-promises-for-client-components*/} + +Promises passed to `use` in Client Components must be cached so the same Promise instance is reused across re-renders. If a new Promise is created directly in render, React will display the Suspense fallback on every re-render. + +```js +// ✅ Cache the Promise so the same one is reused across renders +let cache = new Map(); + +export function fetchData(url) { + if (!cache.has(url)) { + cache.set(url, getData(url)); + } + return cache.get(url); +} +``` + +The `fetchData` function returns the same Promise each time it's called with the same URL. When `use` receives the same Promise on a re-render, it reads the already-resolved value synchronously without suspending. + + + +The way you cache Promises depends on the framework you use with Suspense. Frameworks typically provide built-in caching mechanisms. If you don't use a framework, you can use a simple module-level cache like the one above, or a [Suspense-enabled data source](/reference/react/Suspense#displaying-a-fallback-while-content-is-loading). + + + +In the example below, clicking "Re-render" updates state in `App` and triggers a re-render. Because `fetchData` returns the same cached Promise, `Albums` reads the value synchronously instead of showing the Suspense fallback again. + + + +```js src/App.js active +import { use, Suspense, useState } from 'react'; +import { fetchData } from './data.js'; + +export default function App() { + const [count, setCount] = useState(0); + return ( + <> + +

Render count: {count}

+ Loading...

}> + +
+ + ); +} + +function Albums() { + const albums = use(fetchData('/albums')); + return ( + + ); +} +``` + +```js src/data.js hidden +// Note: the way you would do data fetching depends on +// the framework that you use together with Suspense. +// Normally, the caching logic would be inside a framework. + +let cache = new Map(); + +export function fetchData(url) { + if (!cache.has(url)) { + cache.set(url, getData(url)); + } + return cache.get(url); +} + +async function getData(url) { + if (url === '/albums') { + return await getAlbums(); + } else { + throw Error('Not implemented'); + } +} + +async function getAlbums() { + // Add a fake delay to make waiting noticeable. + await new Promise(resolve => { + setTimeout(resolve, 1000); + }); + + return [{ + id: 13, + title: 'Let It Be', + year: 1970 + }, { + id: 12, + title: 'Abbey Road', + year: 1969 + }, { + id: 11, + title: 'Yellow Submarine', + year: 1969 + }]; +} +``` + +
+ + + +#### How to implement a promise cache {/*how-to-implement-a-promise-cache*/} + +A basic cache stores the Promise keyed by URL so the same instance is reused across renders. To also avoid unnecessary Suspense fallbacks when data is already available, you can set `status` and `value` (or `reason`) fields on the Promise. React checks these fields when `use` is called: if `status` is `'fulfilled'`, it reads `value` synchronously without suspending. If `status` is `'rejected'`, it throws `reason`. If the field is missing or `'pending'`, it suspends. + +```js +let cache = new Map(); + +function fetchData(url) { + if (!cache.has(url)) { + const promise = getData(url); + promise.status = 'pending'; + promise.then( + value => { + promise.status = 'fulfilled'; + promise.value = value; + }, + reason => { + promise.status = 'rejected'; + promise.reason = reason; + }, + ); + cache.set(url, promise); + } + return cache.get(url); +} +``` + +This is primarily useful for library authors building Suspense-compatible data layers. React will set the `status` field itself on Promises that don't have it, but setting it yourself avoids an extra render when the data is already available. + +This cache pattern is the foundation for [re-fetching data](#re-fetching-data-in-client-components) (where changing the cache key triggers a new fetch) and [preloading data on hover](#preloading-data-on-hover) (where calling `fetchData` early means the Promise may already be resolved by the time `use` reads it). + + + + + +Don't skip calling `use` based on whether a Promise is already settled. + +Unlike other hooks, `use` can be called inside conditions and loops — but it must always be called for the Promise itself. Never read `promise.status` or `promise.value` directly to bypass `use`; always pass the Promise to `use` and let React handle it. + + +```js +// 🔴 Don't bypass `use` by reading promise status directly +if (promise.status === 'fulfilled') { + return promise.value; +} +const value = use(promise); +``` + +```js +// ✅ Pass the promise to `use` and let React track the promise +const value = use(promise); +``` + +Bypassing `use` this way can break React Suspense optimizations and Suspense features for React DevTools. You can `use(promise)` conditionally, but don't conditionally `use(promise)` based on the promise itself. + + + +--- + +### Re-fetching data in Client Components {/*re-fetching-data-in-client-components*/} + +To refresh data at the same URL (for example, with a "Refresh" button), invalidate the cache entry and start a new fetch inside a [`startTransition`](/reference/react/startTransition). Store the resulting Promise in state to trigger a re-render. While the new Promise is pending, React keeps showing the existing content because the update is inside a Transition. + +```js +function App() { + const [albumsPromise, setAlbumsPromise] = useState(fetchData('/albums')); + const [isPending, startTransition] = useTransition(); + + function handleRefresh() { + startTransition(() => { + setAlbumsPromise(refetchData('/albums')); + }); + } + // ... +} +``` + +`refetchData` clears the old cache entry and starts a new fetch at the same URL. Storing the resulting Promise in state triggers a re-render inside the Transition. On re-render, `Albums` receives the new Promise and `use` suspends on it while React keeps showing the old content. + + + +```js src/App.js active +import { Suspense, useState, useTransition } from 'react'; +import { use } from 'react'; +import { fetchData, refetchData } from './data.js'; + +export default function App() { + const [albumsPromise, setAlbumsPromise] = useState( + () => fetchData('/the-beatles/albums') + ); + const [isPending, startTransition] = useTransition(); + + function handleRefresh() { + startTransition(() => { + setAlbumsPromise(refetchData('/the-beatles/albums')); + }); + } + + return ( + <> + +
+ }> + + +
+ + ); +} + +function Albums({ albumsPromise }) { + const albums = use(albumsPromise); + return ( + + ); +} + +function Loading() { + return

Loading...

; +} +``` + +```js src/data.js hidden +// Note: the way you would do data fetching depends on +// the framework that you use together with Suspense. +// Normally, the caching logic would be inside a framework. + +let cache = new Map(); + +export function fetchData(url) { + if (!cache.has(url)) { + cache.set(url, getData(url)); + } + return cache.get(url); +} + +export function refetchData(url) { + cache.delete(url); + return fetchData(url); +} + +async function getData(url) { + if (url.startsWith('/the-beatles/albums')) { + return await getAlbums(); + } else { + throw Error('Not implemented'); + } +} + +async function getAlbums() { + // Add a fake delay to make waiting noticeable. + await new Promise(resolve => { + setTimeout(resolve, 1000); + }); + + return [{ + id: 13, + title: 'Let It Be', + year: 1970 + }, { + id: 12, + title: 'Abbey Road', + year: 1969 + }, { + id: 11, + title: 'Yellow Submarine', + year: 1969 + }, { + id: 10, + title: 'The Beatles', + year: 1968 + }, { + id: 9, + title: 'Magical Mystery Tour', + year: 1967 + }]; +} +``` + +```css +button { margin-bottom: 10px; } +``` + +
+ + + +Frameworks that support Suspense typically provide their own caching and invalidation mechanisms. The custom cache above is useful for understanding the pattern, but in practice prefer your framework's data fetching solution. + + + +--- + +### Preloading data on hover {/*preloading-data-on-hover*/} + +You can start loading data before it's needed by calling `fetchData` during a hover event. Since `fetchData` caches the Promise, the data may already be available by the time the user clicks. If the Promise has resolved by the time `use` reads it, React renders the component immediately without showing a Suspense fallback. + +```js + + ))} + + }> + + + + ); +} + +function Loading() { + return

Loading...

; +} +``` + +```js src/Albums.js +import { use } from 'react'; +import { fetchData } from './data.js'; + +export default function Albums({ artistId }) { + const albums = use(fetchData(`/${artistId}/albums`)); + return ( + + ); +} +``` + +```js src/data.js hidden +// Note: the way you would do data fetching depends on +// the framework that you use together with Suspense. +// Normally, the caching logic would be inside a framework. + +let cache = new Map(); + +export function fetchData(url) { + if (!cache.has(url)) { + const promise = getData(url); + // Set status fields so React can read the value + // synchronously if the Promise resolves before + // `use` is called (e.g. when preloading on hover). + promise.status = 'pending'; + promise.then( + value => { + promise.status = 'fulfilled'; + promise.value = value; + }, + reason => { + promise.status = 'rejected'; + promise.reason = reason; + }, + ); + cache.set(url, promise); + } + return cache.get(url); +} + +async function getData(url) { + if (url.startsWith('/the-beatles/albums')) { + return await getAlbums('the-beatles'); + } else if (url.startsWith('/led-zeppelin/albums')) { + return await getAlbums('led-zeppelin'); + } else if (url.startsWith('/pink-floyd/albums')) { + return await getAlbums('pink-floyd'); + } else { + throw Error('Not implemented'); + } +} + +async function getAlbums(artistId) { + // Add a fake delay to make waiting noticeable. + await new Promise(resolve => { + setTimeout(resolve, 800); + }); + + if (artistId === 'the-beatles') { + return [{ + id: 13, + title: 'Let It Be', + year: 1970 + }, { + id: 12, + title: 'Abbey Road', + year: 1969 + }, { + id: 11, + title: 'Yellow Submarine', + year: 1969 + }]; + } else if (artistId === 'led-zeppelin') { + return [{ + id: 10, + title: 'Coda', + year: 1982 + }, { + id: 9, + title: 'In Through the Out Door', + year: 1979 + }, { + id: 8, + title: 'Presence', + year: 1976 + }]; + } else { + return [{ + id: 7, + title: 'The Wall', + year: 1979 + }, { + id: 6, + title: 'Animals', + year: 1977 + }, { + id: 5, + title: 'Wish You Were Here', + year: 1975 + }]; + } +} +``` + +```css +button { margin-right: 10px; } +``` + + + +--- + +### Streaming data from server to client {/*streaming-data-from-server-to-client*/} + +Data can be streamed from the server to the client by passing a Promise as a prop from a Server Component to a Client Component. + +```js import { fetchMessage } from './lib.js'; import { Message } from './message.js'; @@ -212,9 +1073,13 @@ export default function App() { } ``` +<<<<<<< HEAD 클라이언트 컴포넌트 Prop으로 받은 Promise`use` API에 전달합니다. 클라이언트 컴포넌트는 서버 컴포넌트가 처음에 생성한 Promise에서 값을 읽을 수 있습니다. +======= +The Client Component then takes the Promise it received as a prop and passes it to the `use` API. This allows the Client Component to read the value from the Promise that was initially created by the Server Component. +>>>>>>> 152a471aa9ac2f6f0f3e64c04f39da790d40cf61 -```js [[2, 6, "Message"], [4, 6, "messagePromise"], [4, 7, "messagePromise"], [5, 7, "use"]] +```js // message.js 'use client'; @@ -225,7 +1090,11 @@ export function Message({ messagePromise }) { return

Here is the message: {messageContent}

; } ``` +<<<<<<< HEAD `Message`[`Suspense`](/reference/react/Suspense)로 래핑되어 있으므로 Promise가 리졸브될 때까지 Fallback이 표시됩니다. Promise가 리졸브되면 `use` Hook이 값을 참조하고 `Message` 컴포넌트가 Suspense Fallback을 대체합니다. +======= +Because `Message` is wrapped in a [Suspense](/reference/react/Suspense) boundary, the fallback will be displayed until the Promise is resolved. When the Promise is resolved, the value will be read by the `use` API and the `Message` component will replace the Suspense fallback. +>>>>>>> 152a471aa9ac2f6f0f3e64c04f39da790d40cf61 @@ -290,6 +1159,7 @@ root.render( +<<<<<<< HEAD 서버 컴포넌트에서 클라이언트 컴포넌트로 Promise를 전달할 때 리졸브된 값이 직렬화 가능해야 합니다. 함수는 직렬화할 수 없으므로 Promise의 리졸브 값이 될 수 없습니다. @@ -297,19 +1167,30 @@ root.render( +======= +>>>>>>> 152a471aa9ac2f6f0f3e64c04f39da790d40cf61 #### Promise를 서버 컴포넌트에서 처리해야 하나요, 아니면 클라이언트 컴포넌트에서 처리해야 하나요? {/*resolve-promise-in-server-or-client-component*/} +<<<<<<< HEAD Promise는 서버 컴포넌트에서 클라이언트 컴포넌트로 전달할 수 있으며 `use` API를 통해 클라이언트 컴포넌트에서 리졸브됩니다. 또한 서버 컴포넌트에서 `await`을 사용하여 Promise를 리졸브하고 데이터를 클라이언트 컴포넌트에 Prop으로 전달하는 방법도 존재합니다. +======= +A Promise can be resolved with `await` in a Server Component, or passed as a prop to a Client Component and resolved there with `use`. + +Using `await` in a Server Component suspends the Server Component itself, and the Client Component receives the resolved value as a prop: +>>>>>>> 152a471aa9ac2f6f0f3e64c04f39da790d40cf61 ```js +// Server Component export default async function App() { + // Will suspend the Server Component. const messageContent = await fetchMessage(); - return + return ; } ``` +<<<<<<< HEAD 하지만 [서버 컴포넌트](/reference/rsc/server-components)에서 `await`을 사용하면 `await` 문이 완료될 때까지 렌더링이 차단됩니다. 서버 컴포넌트에서 클라이언트 컴포넌트로 Promise를 Prop으로 전달하면 Promise가 서버 컴포넌트의 렌더링을 차단하는 것을 방지할 수 있습니다. @@ -329,55 +1210,118 @@ export default async function App() { #### Error Boundary를 사용하여 오류 표시하기 {/*displaying-an-error-to-users-with-error-boundary*/} Promise가 거부될 때 오류를 표시하고 싶다면 [Error Boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary)를 사용합니다. Error Boundary를 사용하려면 `use` API 를 호출하는 컴포넌트를 Error Boundary로 래핑합니다. `use`에 전달된 Promise가 거부되면 Error Boundary에 대한 Fallback이 표시됩니다. +======= +A Server Component can also start a Promise without awaiting it and pass the Promise to a Client Component. The Server Component returns immediately, and the Client Component suspends when it calls `use`: - +```js +// Server Component +export default function App() { + // Not awaited: starts here, resolves on the client. + const messagePromise = fetchMessage(); + return ; +} +``` -```js src/message.js active -"use client"; +```js +// Client Component +'use client'; +import { use } from 'react'; -import { use, Suspense } from "react"; +export function Message({ messagePromise }) { + // Will suspend until the data is available. + const messageContent = use(messagePromise); + return

{messageContent}

; +} +``` + +Prefer `await` in a Server Component when possible, since it keeps the data fetching on the server. If a Server Component above already awaits the data, pass the resolved value down as a prop instead of creating a new Promise to call `use`. + +You can also pass promise as a prop to a Client Component without awaiting it, and then read it with `use(promise)` to suspend deeper in the tree. This allows more of the surrounding UI to complete while the Promise is pending. A common case is interactive content like popovers and tooltips, where the data is needed only after a hover or click. Client Components can't `await`, so they rely on `use` to suspend on a Promise. + +In either case, wrap the component that reads the Promise in a Suspense boundary so React can show a fallback while the Promise is pending. See [Revealing content together at once](/reference/react/Suspense#revealing-content-together-at-once) for guidance on boundary placement. + + + +--- + +### Displaying an error with an Error Boundary {/*displaying-an-error-with-an-error-boundary*/} + +If the Promise passed to `use` is rejected, the error propagates to the nearest [Error Boundary](/reference/react/Component#catching-rendering-errors-with-an-error-boundary). Wrap the component that calls `use` in an Error Boundary to display a fallback when the Promise is rejected. + +In the example below, `fetchData` rejects on the first attempt and succeeds on retry. The Error Boundary catches the rejection and shows a fallback with a "Try again" button. +>>>>>>> 152a471aa9ac2f6f0f3e64c04f39da790d40cf61 + + + +```js src/App.js active +import { use, Suspense, useState, startTransition } from "react"; import { ErrorBoundary } from "react-error-boundary"; +import { fetchData, refetchData } from "./data.js"; + +export default function App() { + const [albumsPromise, setAlbumsPromise] = useState( + () => fetchData('/the-beatles/albums') + ); + + function handleRetry() { + startTransition(() => { + setAlbumsPromise(refetchData('/the-beatles/albums')); + }); + } -export function MessageContainer({ messagePromise }) { return ( - ⚠️Something went wrong

}> - ⌛Downloading message...

}> - + ( + <> +

⚠️ Something went wrong loading the albums.

+ + + )} + > + Loading...

}> +
); } -function Message({ messagePromise }) { - const content = use(messagePromise); - return

Here is the message: {content}

; +function Albums({ albumsPromise }) { + const albums = use(albumsPromise); + return ( +
    + {albums.map(album => ( +
  • + {album.title} ({album.year}) +
  • + ))} +
+ ); } ``` -```js src/App.js hidden -import { useState } from "react"; -import { MessageContainer } from "./message.js"; +```js src/data.js hidden +// Note: the way you would do data fetching depends on +// the framework that you use together with Suspense. +// Normally, the caching logic would be inside a framework. -function fetchMessage() { - return new Promise((resolve, reject) => setTimeout(reject, 1000)); -} +let cache = new Map(); +let retried = false; -export default function App() { - const [messagePromise, setMessagePromise] = useState(null); - const [show, setShow] = useState(false); - function download() { - setMessagePromise(fetchMessage()); - setShow(true); +export function fetchData(url) { + if (!cache.has(url)) { + cache.set(url, getData(url)); } + return cache.get(url); +} - if (show) { - return ; - } else { - return ; - } +export function refetchData(url) { + cache.delete(url); + retried = true; + return fetchData(url); } -``` +<<<<<<< HEAD ```js src/index.js hidden import React, { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; @@ -392,6 +1336,37 @@ root.render( ); +======= +async function getData(url) { + // Add a fake delay to make the loading state visible. + await new Promise(resolve => setTimeout(resolve, 1000)); + if (url === '/the-beatles/albums') { + // Fail the first attempt to demonstrate the Error Boundary, + // then succeed on retry. + if (!retried) { + throw new Error('Example Error: Failed to fetch albums'); + } + return [{ + id: 13, + title: 'Let It Be', + year: 1970 + }, { + id: 12, + title: 'Abbey Road', + year: 1969 + }, { + id: 11, + title: 'Yellow Submarine', + year: 1969 + }, { + id: 10, + title: 'The Beatles', + year: 1968 + }]; + } + throw new Error('Not implemented'); +} +>>>>>>> 152a471aa9ac2f6f0f3e64c04f39da790d40cf61 ``` ```json package.json hidden @@ -407,6 +1382,7 @@ root.render( ```
+<<<<<<< HEAD #### `Promise.catch`로 대체 값 제공하기 {/*providing-an-alternative-value-with-promise-catch*/} `use`에 전달된 Promise가 거부될 때 대체 값을 제공하려면 Promise의 [`catch`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch) 메서드를 사용합니다. @@ -431,13 +1407,16 @@ export default function App() { Promise의 `catch` 메서드를 사용하려면 Promise 객체에서 `catch`를 호출합니다. `catch`는 오류 메시지를 인수로 받는 함수를 인수로 받습니다. `catch`에 전달된 함수가 반환하는 값은 모두 Promise의 리졸브 값으로 사용됩니다. +======= +>>>>>>> 152a471aa9ac2f6f0f3e64c04f39da790d40cf61 --- ## 문제 해결 {/*troubleshooting*/} -### "Suspense Exception: This is not a real error!" {/*suspense-exception-error*/} +### I'm getting an error: "Suspense Exception: This is not a real error!" {/*suspense-exception-error*/} +<<<<<<< HEAD React 컴포넌트 또는 Hook 함수 외부에서, 혹은 `try`-`catch` 블록에서 `use`를 호출하고 있는 경우입니다. `try`-`catch` 블록 내에서 `use`를 호출하는 경우 컴포넌트를 Error Boundary로 래핑하거나 Promise의 `catch`를 호출하여 오류를 발견하고 Promise를 다른 값으로 리졸브합니다. [이러한 예시들을 확인하세요](#dealing-with-rejected-promises). @@ -455,5 +1434,58 @@ function MessageComponent({messagePromise}) { function MessageComponent({messagePromise}) { // ✅ `use`를 컴포넌트에서 호출하고 있습니다. const message = use(messagePromise); +======= +You are calling `use` inside a try-catch block. `use` throws internally to integrate with Suspense, so it cannot be wrapped in try-catch. Instead, wrap the component that calls `use` in an [Error Boundary](#displaying-an-error-with-an-error-boundary) to handle errors. + +```jsx +function Albums({ albumsPromise }) { + try { + // ❌ Don't wrap `use` in try-catch + const albums = use(albumsPromise); + } catch (e) { + return

Error

; + } +>>>>>>> 152a471aa9ac2f6f0f3e64c04f39da790d40cf61 + // ... +``` + +Instead, wrap the component in an Error Boundary: + +```jsx +function Albums({ albumsPromise }) { + // ✅ Call `use` without try-catch + const albums = use(albumsPromise); // ... ``` + +```jsx +// ✅ Use an Error Boundary to handle errors +Error

}> + +
+``` + +--- + +### I'm getting a warning: "A component was suspended by an uncached promise" {/*uncached-promise-error*/} + +The Promise passed to `use` is not cached, so React cannot reuse it across re-renders. + +This commonly happens when calling `fetch` or an `async` function directly in render: + +```js +function Albums() { + // 🔴 This creates a new Promise on every render + const albums = use(fetch('/albums')); + // ... +} +``` + +To fix this, cache the Promise so the same instance is reused: + +```js +// ✅ fetchData returns the same Promise for the same URL +const albums = use(fetchData('/albums')); +``` + +See [caching Promises for Client Components](#caching-promises-for-client-components) for more details. diff --git a/src/content/reference/react/useLayoutEffect.md b/src/content/reference/react/useLayoutEffect.md index 0221f4e65..7902a3cf4 100644 --- a/src/content/reference/react/useLayoutEffect.md +++ b/src/content/reference/react/useLayoutEffect.md @@ -48,7 +48,7 @@ function Tooltip() { #### 매개변수 {/*parameters*/} -* `setup`: The function with your Effect's logic. Your setup function may also optionally return a *cleanup* function. Before your [component commits](/learn/render-and-commit#step-3-react-commits-changes-to-the-dom), React will run your setup function. After every commit with changed dependencies, React will first run the cleanup function (if you provided it) with the old values, and then run your setup function with the new values. Before your component is removed from the DOM, React will run your cleanup function. +* `setup`: The function with your Effect's logic. Your setup function may also optionally return a *cleanup* function. After your [component commits](/learn/render-and-commit#step-3-react-commits-changes-to-the-dom) to the DOM and before the browser repaints the screen, React will run your setup function. After every commit with changed dependencies, React will first run the cleanup function (if you provided it) with the old values, and then run your setup function with the new values. Before your component is removed from the DOM, React will run your cleanup function. * `dependencies`**(선택사항)**: The list of all reactive values referenced inside of the `setup` code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. If your linter is [configured for React](/learn/editor-setup#linting), it will verify that every reactive value is correctly specified as a dependency. The list of dependencies must have a constant number of items and be written inline like `[dep1, dep2, dep3]`. React will compare each dependency with its previous value using the [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) comparison. If you omit this argument, your Effect will re-run after every commit of the component. diff --git a/src/content/versions.md b/src/content/versions.md index d22643e3d..50fb2f140 100644 --- a/src/content/versions.md +++ b/src/content/versions.md @@ -286,7 +286,7 @@ React 15 이전 버전의 경우, [15.react.dev](https://15.react.dev)를 참고 - [React v0.4.0](https://legacy.reactjs.org/blog/2013/07/17/react-v0-4-0.html) - [New in React v0.4: Prop Validation and Default Values](https://legacy.reactjs.org/blog/2013/07/11/react-v0-4-prop-validation-and-default-values.html) - [New in React v0.4: Autobind by Default](https://legacy.reactjs.org/blog/2013/07/02/react-v0-4-autobind-by-default.html) -- [React v0.3.3](https://legacy.reactjs.org/blog/2013/07/02/react-v0-4-autobind-by-default.html) +- [React v0.3.3](https://legacy.reactjs.org/blog/2013/06/21/react-v0-3-3.html) **Releases** - [v0.10.0 (March 2014)](https://github.com/facebook/react/blob/main/CHANGELOG.md#0100-march-21-2014) @@ -300,7 +300,7 @@ React 15 이전 버전의 경우, [15.react.dev](https://15.react.dev)를 참고 - [v0.3.3 (June 2013)](https://github.com/facebook/react/blob/main/CHANGELOG.md#033-june-20-2013) - [v0.3.2 (May 2013)](https://github.com/facebook/react/blob/main/CHANGELOG.md#032-may-31-2013) - [v0.3.1 (May 2013)](https://github.com/facebook/react/blob/main/CHANGELOG.md#031-may-30-2013) -- [v0.3.0 (May 2013)](https://github.com/facebook/react/blob/main/CHANGELOG.md#031-may-30-2013) +- [v0.3.0 (May 2013)](https://github.com/facebook/react/blob/main/CHANGELOG.md#030-may-29-2013) ### 최초의 커밋 {/*initial-commit*/} diff --git a/tailwind.config.js b/tailwind.config.js index 9450cfa59..85d0e7050 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -46,8 +46,8 @@ module.exports = { 'inner-border-dark': 'inset 0 0 0 1px rgba(255, 255, 255, 0.08)', 'outer-border': '0 0 0 1px rgba(0, 0, 0, 0.1)', 'outer-border-dark': '0 0 0 1px rgba(255, 255, 255, 0.1)', - 'secondary-button-stroke': 'inset 0 0 0 1px #D9DBE3', - 'secondary-button-stroke-dark': 'inset 0 0 0 1px #404756', + 'secondary-button-stroke': 'inset 0 0 0 1px #BCC1CD', + 'secondary-button-stroke-dark': 'inset 0 0 0 1px #4E5769', none: 'none', }, extend: {