How does useOptimistic() work internally in React?
このフックをより簡単に理解するために、最初に以下の投稿をチェックすることをお勧めします。
楽観的UIはUI開発でよく使われるトリックで、知覚されるパフォーマンスを向上させます。
useOptimistic()
はそれを構築するのを支援するフックです。内部動作を調べてみましょう。
1. 楽観的UIとは何ですか?
ユーザーがクリックして「いいね」または「いいねを解除」できる「いいね」ボタンを実装するとします。どのようにしますか?
基本的な考え方は単純です。ボタンがクリックされるとAPIを取得し、APIの応答に応じてUIを更新します。これは機能しますが、非常に遅延が発生する可能性があります。次のデモを試してみてください。
こちらがクリックハンドラーです。
const toggle = () => {if (isFetching) returnsetIsFetching(true)mutateAPI({liked: !liked}, forceResult).then((data) => {setLiked(data.liked)APIの応答に基づいて状態を更新する
}).catch((reason) => {toast(reason)}).finally(() => {setIsFetching(false)})}
const toggle = () => {if (isFetching) returnsetIsFetching(true)mutateAPI({liked: !liked}, forceResult).then((data) => {setLiked(data.liked)APIの応答に基づいて状態を更新する
}).catch((reason) => {toast(reason)}).finally(() => {setIsFetching(false)})}
以下は、インスタントフィードバックを提供したいが、APIリクエストの往復のため技術的に不可能であるため、 APIの応答を待たずにUIを更新することを除いては。 これは実際には良いことです、なぜならほとんどの場合、APIの取得が成功することを期待するからです。
そのため、クリックハンドラーでは以下のようにすることができます:
- UIを即座に更新する
- API呼び出しをトリガーする
- APIの応答(成功またはエラー)で、再びUIを更新する
以下は、楽観的UIを有効にしたデモです。これにより、認識されるパフォーマンスが大幅に向上します。
変更点は以下の通りです。
const [liked, setLiked] = useState(false)const [optimisticLiked, setOptimisticLiked] = useState(liked)真の状態と楽観的状態を別々に保存する必要があります
競合状態を処理するために
// これはAbortControllerによって適切に処理されるはずです// デモ用に、// ここではユニークな呼び出しIDを使用して、最後のAPI呼び出しのみを信頼しますconst callIdRef = useRef(null)const toggle = () => {setOptimisticLiked(!optimisticLiked)const callId = uid()callIdRef.current = callIdmutateAPI({liked: !optimisticLiked}, forceResult).then((data) => {if (callIdRef.current === callId) {setOptimisticLiked(data.liked)setLiked(data.liked)}最後のリクエスト以外は破棄する
}).catch((reason) => {if (callIdRef.current === callId) {setOptimisticLiked(liked)}toast(reason)})}
const [liked, setLiked] = useState(false)const [optimisticLiked, setOptimisticLiked] = useState(liked)真の状態と楽観的状態を別々に保存する必要があります
競合状態を処理するために
// これはAbortControllerによって適切に処理されるはずです// デモ用に、// ここではユニークな呼び出しIDを使用して、最後のAPI呼び出しのみを信頼しますconst callIdRef = useRef(null)const toggle = () => {setOptimisticLiked(!optimisticLiked)const callId = uid()callIdRef.current = callIdmutateAPI({liked: !optimisticLiked}, forceResult).then((data) => {if (callIdRef.current === callId) {setOptimisticLiked(data.liked)setLiked(data.liked)}最後のリクエスト以外は破棄する
}).catch((reason) => {if (callIdRef.current === callId) {setOptimisticLiked(liked)}toast(reason)})}
上記のコードはデモ目的であり、バグフリーではありません。
連続クリック/レースコンディションは、両方のアプローチで適切に処理する必要があります。 それが実際には非常に難しいことがわかります。 最初のデモでは、複数の進行中のリクエストを防ぐためにローディング状態が追加されています。二番目のデモでは、最後のリクエスト以外は破棄されます。
2. データフェッチングライブラリには通常、Optimistic UIのサポートが組み込まれています。
私たちのコードを見ると、Optimistic UIはAPIリクエストの前に状態を更新することについてすべてです。 したがって、データフェッチングライブラリで楽観的な更新をサポートするのは自然です。
Relayは、ミューテーションのoptimisticUpdater()
によってこれをサポートします。
function StoryLikeButton({story}) {...function onLikeButtonClicked(newDoesLike) {commitMutation({variables: {id: data.id,doesViewerLike: newDoesLike,},optimisticUpdater: store => {// TODO 楽観的アップデータを記入する},})}...}
function StoryLikeButton({story}) {...function onLikeButtonClicked(newDoesLike) {commitMutation({variables: {id: data.id,doesViewerLike: newDoesLike,},optimisticUpdater: store => {// TODO 楽観的アップデータを記入する},})}...}
swrも、そのmutate()
のoptimisticData
オプションを通じてこれをサポートします。
<buttontype="submit"onClick={async () => {setText("");const newTodo = {id: Date.now(),text};try {// Update the local state immediately and fire the// request. Since the API will return the updated// data, there is no need to start a new revalidation// and we can directly populate the cache.await mutate(addTodo(newTodo), {optimisticData: [...data, newTodo],rollbackOnError: true,populateCache: true,revalidate: false});toast.success("Successfully added the new item.");} catch (e) {// If the API errors, the original data will be// rolled back by SWR automatically.toast.error("Failed to add the new item.");}}}>Add</button>
<buttontype="submit"onClick={async () => {setText("");const newTodo = {id: Date.now(),text};try {// Update the local state immediately and fire the// request. Since the API will return the updated// data, there is no need to start a new revalidation// and we can directly populate the cache.await mutate(addTodo(newTodo), {optimisticData: [...data, newTodo],rollbackOnError: true,populateCache: true,revalidate: false});toast.success("Successfully added the new item.");} catch (e) {// If the API errors, the original data will be// rolled back by SWR automatically.toast.error("Failed to add the new item.");}}}>Add</button>
私たちは、楽観的なアップデータをサポートするAPIラッパーを作成することで、いいねボタンのデモを書き直すことができます。
import {useState, useRef, useEffect} from 'react'export function useAPI(fetcher) {const [state, setState] = useState({})const [optimisticState, setOptimisticState] = useState(state)const mutateIdRef = useRef(null)useEffect(() => {}, [])const mutate = (updater, options = {}) => {const mutateId = uid()mutateIdRef.current = mutateIdif (options.optimisticData) {setOptimisticState(state => ({...optimisticState,data: options.optimisticData}))}楽観的な更新を処理する
updater().then((data) => {if (mutateIdRef.current === mutateId) {setState(state => ({...state,data}))setOptimisticState(state => ({...optimisticState,data}))}}).catch((error) => {if (options.onError) {options.onError(error)}if (mutateIdRef.current === mutateId && options.rollbackOnError) {setOptimisticState(state)真の状態にロールバックする
}})}return {data: optimisticState.data,error: state.error,mutate}}const uid = (() => {let i = 0return () => i++})()
import {useState, useRef, useEffect} from 'react'export function useAPI(fetcher) {const [state, setState] = useState({})const [optimisticState, setOptimisticState] = useState(state)const mutateIdRef = useRef(null)useEffect(() => {}, [])const mutate = (updater, options = {}) => {const mutateId = uid()mutateIdRef.current = mutateIdif (options.optimisticData) {setOptimisticState(state => ({...optimisticState,data: options.optimisticData}))}楽観的な更新を処理する
updater().then((data) => {if (mutateIdRef.current === mutateId) {setState(state => ({...state,data}))setOptimisticState(state => ({...optimisticState,data}))}}).catch((error) => {if (options.onError) {options.onError(error)}if (mutateIdRef.current === mutateId && options.rollbackOnError) {setOptimisticState(state)真の状態にロールバックする
}})}return {data: optimisticState.data,error: state.error,mutate}}const uid = (() => {let i = 0return () => i++})()
新しいuseAPI()
を使ったデモはこちらです。
デモは良好で、ボタンコンポーネントもさらにクリーンになりました。
const {data, mutate} = useAPI(() => Promise.resolve({liked: false}))const liked = !!data?.likedconst toggle = () => {const payload = {liked: !liked}mutate(() => mutateAPI(payload, forceResult),{optimisticData: payload,rollbackOnError: true,onError: toast})}
const {data, mutate} = useAPI(() => Promise.resolve({liked: false}))const liked = !!data?.likedconst toggle = () => {const payload = {liked: !liked}mutate(() => mutateAPI(payload, forceResult),{optimisticData: payload,rollbackOnError: true,onError: toast})}
素晴らしい、Optimistic UIについての背景知識は十分です。さあ、Reactの世界に飛び込みましょう。🤿
3. 自分たちでuseOptimistic()
を実装してみましょう。
こちらがuseOptimistic()
の公式デモです。
const [messages, setMessages] = useState([{ text: "Hello there!", sending: false, key: 1 }]);安定した状態の真実の源
async function sendMessage(formData) {const sentMessage = await deliverMessage(formData.get("message"));setMessages((messages) => [...messages, { text: sentMessage }]);}const [optimisticMessages, addOptimisticMessage] = useOptimistic(messages,(state, newMessage) => [...state,{text: newMessage,sending: true}]);楽観的に変更できる状態のコピー
async function formAction(formData) {addOptimisticMessage(formData.get("message"));楽観的な状態を更新する
formRef.current.reset();await sendMessage(formData);ミューテーションAPIをトリガーする
}<form action={formAction} ref={formRef}><input type="text" name="message" placeholder="Hello!" /><button type="submit">Send</button></form>
const [messages, setMessages] = useState([{ text: "Hello there!", sending: false, key: 1 }]);安定した状態の真実の源
async function sendMessage(formData) {const sentMessage = await deliverMessage(formData.get("message"));setMessages((messages) => [...messages, { text: sentMessage }]);}const [optimisticMessages, addOptimisticMessage] = useOptimistic(messages,(state, newMessage) => [...state,{text: newMessage,sending: true}]);楽観的に変更できる状態のコピー
async function formAction(formData) {addOptimisticMessage(formData.get("message"));楽観的な状態を更新する
formRef.current.reset();await sendMessage(formData);ミューテーションAPIをトリガーする
}<form action={formAction} ref={formRef}><input type="text" name="message" placeholder="Hello!" /><button type="submit">Send</button></form>
私たちのOptimistic UIの実装では、APIのレスポンスが受け取られたときに、楽観的な状態が実際の状態によって上書きされます。
上記のコードから、状態引数(messages
)が変更されたときに楽観的な状態がリセットされると推測できます。これはuseState()
やuseReducer()
では持っていない能力です -
setState()
なしで直接状態を変更することはできません。
同様のものを実装するように求められた場合、おそらく以下のコードになるでしょう。
function useOptimistic(state, optimisticDispatcher) {const [optimisticState, setState] = useState(state)useLayoutEffect(() => {setState(optimisticState)}, [state])私たちはsetState()を通じてのみ状態を更新できます
const dispatch = (action) => {setState((state) => optimisticDispatcher(state, action))}return [optimisticState,dispatch]}
function useOptimistic(state, optimisticDispatcher) {const [optimisticState, setState] = useState(state)useLayoutEffect(() => {setState(optimisticState)}, [state])私たちはsetState()を通じてのみ状態を更新できます
const dispatch = (action) => {setState((state) => optimisticDispatcher(state, action))}return [optimisticState,dispatch]}
こちらが、useOptimistic()
を私たちの実装に置き換えた公式デモです。
上記のデモを試してみると、実際には動作しないことに気づくでしょう - 楽観的な更新はまったくレンダリングされません!問題は<form/>
のaction
です!
古風なonSubmit()
に切り替えると、なんとなく動作します。
async function formAction(e) {e.preventDefault()const formData = new FormData(formRef.current)addOptimisticMessage(formData.get("message"));formRef.current.reset();await sendMessage(formData);}<form onSubmit={formAction} ref={formRef}><input type="text" name="message" placeholder="Hello!" /><button type="submit">Send</button></form>
async function formAction(e) {e.preventDefault()const formData = new FormData(formRef.current)addOptimisticMessage(formData.get("message"));formRef.current.reset();await sendMessage(formData);}<form onSubmit={formAction} ref={formRef}><input type="text" name="message" placeholder="Hello!" /><button type="submit">Send</button></form>
「なんとなく動作する」と言いましたが、実際にはuseOptimistic()
と全く同じようには動作しません - すばやく複数のメッセージを入力してみて、公式のデモとの違いを確認してみてください。
4. React内部でuseOptimistic()
はどのように動作するのか?
useOptimistic()
の内部はかなり複雑です。まずは公式の要約を見てみましょう。
それはある状態を引数として受け取り、ネットワークリクエストのような非同期アクションの期間中に異なる可能性があるその状態のコピーを返します。 あなたは、現在の状態とアクションへの入力を取る関数を提供し、アクションが保留中である間に使用される楽観的な状態を返します。
したがって、useOptimistic()
は非同期アクションの下で使用されることを想定しており、トランジションに関連している必要があります。
トランジションについての詳細は、React内部でuseTransition()はどのように動作するのか?を参照してください。
4.1 mountOptimistic()
いつものように、初期マウントから始めましょう。
function mountOptimistic<S, A>(passthrough: S,reducer: ?(S, A) => S,): [S, (A) => void] {const hook = mountWorkInProgressHook();hook.memoizedState = hook.baseState = passthrough;const queue: UpdateQueue<S, A> = {pending: null,lanes: NoLanes,dispatch: null,// Optimistic state does not use the eager update optimization.lastRenderedReducer: null,lastRenderedState: null,};hook.queue = queue;// This is different than the normal setState function.const dispatch: A => void = (dispatchOptimisticSetState.bind(null,currentlyRenderingFiber,true,queue,): any);queue.dispatch = dispatch;return [passthrough, dispatch];基本的には
useState()
と同じです!}function dispatchOptimisticSetState<S, A>(fiber: Fiber,throwIfDuringRender: boolean,queue: UpdateQueue<S, A>,action: A,): void {const transition = requestCurrentTransition();const update: Update<S, A> = {// An optimistic update commits synchronously.lane: SyncLane,
useState()
では、このレーンはrequestUpdateLane()
によって決定され、任意の優先度になる可能性があります
しかし、ここでは
SyncLane
のみを使用します。それは、useOptimistic()は非同期アクションで使用することを想定しているため、設定しないと
SynLaneになり、トランジションレーンになってしまい、コミットされることはありません
// After committing, the optimistic update is "reverted" using the same// lane as the transition it's associated with.revertLane: requestTransitionLane(transition),
revertLane
はuseOptimistic()
内にのみ存在します。これはReactに
revertLane
が作業中のときには、楽観的な更新を元に戻すべきであることを伝えるためのものです。
action,hasEagerState: false,eagerState: null,next: (null: any),};if (isRenderPhaseUpdate(fiber)) {} else {const root = enqueueConcurrentHookUpdate(fiber, queue, update, SyncLane);
useState()
と同様に、更新をキューに入れますif (root !== null) {// NOTE: The optimistic update implementation assumes that the transition// will never be attempted before the optimistic update. This currently// holds because the optimistic update is always synchronous. If we ever// change that, we'll need to account for this.scheduleUpdateOnFiber(root, fiber, SyncLane);SyncLaneでの再レンダリングがスケジュールされました
// Optimistic updates are always synchronous, so we don't need to call// entangleTransitionUpdate here.}}}
function mountOptimistic<S, A>(passthrough: S,reducer: ?(S, A) => S,): [S, (A) => void] {const hook = mountWorkInProgressHook();hook.memoizedState = hook.baseState = passthrough;const queue: UpdateQueue<S, A> = {pending: null,lanes: NoLanes,dispatch: null,// Optimistic state does not use the eager update optimization.lastRenderedReducer: null,lastRenderedState: null,};hook.queue = queue;// This is different than the normal setState function.const dispatch: A => void = (dispatchOptimisticSetState.bind(null,currentlyRenderingFiber,true,queue,): any);queue.dispatch = dispatch;return [passthrough, dispatch];基本的には
useState()
と同じです!}function dispatchOptimisticSetState<S, A>(fiber: Fiber,throwIfDuringRender: boolean,queue: UpdateQueue<S, A>,action: A,): void {const transition = requestCurrentTransition();const update: Update<S, A> = {// An optimistic update commits synchronously.lane: SyncLane,
useState()
では、このレーンはrequestUpdateLane()
によって決定され、任意の優先度になる可能性があります
しかし、ここでは
SyncLane
のみを使用します。それは、useOptimistic()は非同期アクションで使用することを想定しているため、設定しないと
SynLaneになり、トランジションレーンになってしまい、コミットされることはありません
// After committing, the optimistic update is "reverted" using the same// lane as the transition it's associated with.revertLane: requestTransitionLane(transition),
revertLane
はuseOptimistic()
内にのみ存在します。これはReactに
revertLane
が作業中のときには、楽観的な更新を元に戻すべきであることを伝えるためのものです。
action,hasEagerState: false,eagerState: null,next: (null: any),};if (isRenderPhaseUpdate(fiber)) {} else {const root = enqueueConcurrentHookUpdate(fiber, queue, update, SyncLane);
useState()
と同様に、更新をキューに入れますif (root !== null) {// NOTE: The optimistic update implementation assumes that the transition// will never be attempted before the optimistic update. This currently// holds because the optimistic update is always synchronous. If we ever// change that, we'll need to account for this.scheduleUpdateOnFiber(root, fiber, SyncLane);SyncLaneでの再レンダリングがスケジュールされました
// Optimistic updates are always synchronous, so we don't need to call// entangleTransitionUpdate here.}}}
したがって、一般的に初期マウントについては、updateOptimistic()
はuseState()
とそれほど違いはありません。主な違いは、その更新にrevertLane
があることです。
レンダリングレーンがrevertLane
でない場合、更新はコミットされるべきです。それ以外の場合は、元に戻されるべきです。
4.2 updateOptimistic()
初期マウント後のレンダリングで、revertLane
がどのように動作するかを見つけ出します。
function updateOptimistic<S, A>(passthrough: S,reducer: ?(S, A) => S,): [S, (A) => void] {const hook = updateWorkInProgressHook();return updateOptimisticImpl(hook,((currentHook: any): Hook),passthrough,reducer,);}function updateOptimisticImpl<S, A>(hook: Hook,current: Hook | null,passthrough: S,reducer: ?(S, A) => S,): [S, (A) => void] {// Optimistic updates are always rebased on top of the latest value passed in// as an argument. It's called a passthrough because if there are no pending// updates, it will be returned as-is.//// Reset the base state to the passthrough. Future updates will be applied// on top of this.hook.baseState = passthrough;これは
useState()
とは異なります。基本状態をリセットしますこれは、私たちが言及した問題を解決します - setState()以外で状態を更新することはできません
// If a reducer is not provided, default to the same one used by useState.const resolvedReducer: (S, A) => S =typeof reducer === 'function' ? reducer : (basicStateReducer: any);return updateReducerImpl(hook, ((currentHook: any): Hook), resolvedReducer);再び、これは
useState()
とほとんど同じです}
function updateOptimistic<S, A>(passthrough: S,reducer: ?(S, A) => S,): [S, (A) => void] {const hook = updateWorkInProgressHook();return updateOptimisticImpl(hook,((currentHook: any): Hook),passthrough,reducer,);}function updateOptimisticImpl<S, A>(hook: Hook,current: Hook | null,passthrough: S,reducer: ?(S, A) => S,): [S, (A) => void] {// Optimistic updates are always rebased on top of the latest value passed in// as an argument. It's called a passthrough because if there are no pending// updates, it will be returned as-is.//// Reset the base state to the passthrough. Future updates will be applied// on top of this.hook.baseState = passthrough;これは
useState()
とは異なります。基本状態をリセットしますこれは、私たちが言及した問題を解決します - setState()以外で状態を更新することはできません
// If a reducer is not provided, default to the same one used by useState.const resolvedReducer: (S, A) => S =typeof reducer === 'function' ? reducer : (basicStateReducer: any);return updateReducerImpl(hook, ((currentHook: any): Hook), resolvedReducer);再び、これは
useState()
とほとんど同じです}
ステートフックは、異なる優先度での更新のリストを保持し、 それらは異なるバッチで処理されるため、以下のプロパティが必要です。
baseQueue
: すべての更新baseState
: 更新のための基本状態、なぜなら更新はミドルウェアのように順序で処理されるからです。memoizedState
: レンダリングで使用される実際の値。この値は、 更新が保留中の場合、中間値となります。
updateReducer()
の動作の詳細については、React内部でuseState()はどのように動作するのか?を参照してください。 これは、なぜupdateOptimisticImpl()
でbaseState
を更新する必要があるのかを説明しています。
なぜなら、キュー内の更新は楽観的であることが想定されており、つまり、それらは
中間状態であるべきで、もし我々がこれらの更新の後に
重要な更新を置くと、それは誤った状態になるからです。
function updateReducerImpl<S, A>(
useState()
と同じで、内部的にはuseReducer()
と同じですhook: Hook,current: Hook,reducer: (S, A) => S,): [S, Dispatch<A>] {const baseState = hook.baseState;if (baseQueue === null) {} else {// We have a queue to process.const first = baseQueue.next;let newState = baseState;let newBaseState = null;let newBaseQueueFirst = null;let newBaseQueueLast: Update<S, A> | null = null;let update = first;let didReadFromEntangledAsyncAction = false;do {更新はこのループで処理されます
// An extra OffscreenLane bit is added to updates that were made to// a hidden tree, so that we can distinguish them from updates that were// already there when the tree was hidden.const updateLane = removeLanes(update.lane, OffscreenLane);const isHiddenUpdate = updateLane !== update.lane;// Check if this update was made while the tree was hidden. If so, then// it's not a "base" update and we should disregard the extra base lanes// that were added to renderLanes when we entered the Offscreen tree.const shouldSkipUpdate = isHiddenUpdate? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane): !isSubsetOfLanes(renderLanes, updateLane);この行は重要で、現在のレンダリングレーンが更新のレーンでない場合、
更新をスキップするべきであると言っています。
if (shouldSkipUpdate) {// Priority is insufficient. Skip this update. If this is the first// skipped update, the previous update/state is the new base// update/state.const clone: Update<S, A> = {lane: updateLane,revertLane: update.revertLane,action: update.action,hasEagerState: update.hasEagerState,eagerState: update.eagerState,next: (null: any),};if (newBaseQueueLast === null) {newBaseQueueFirst = newBaseQueueLast = clone;newBaseState = newState;} else {newBaseQueueLast = newBaseQueueLast.next = clone;}// Update the remaining priority in the queue.// TODO: Don't need to accumulate this. Instead, we can remove// renderLanes from the original lanes.currentlyRenderingFiber.lanes = mergeLanes(currentlyRenderingFiber.lanes,updateLane,);markSkippedUpdateLanes(updateLane);スキップとは無視することを意味せず、更新は複製されて次のレンダリングのためのキューに入れられます
} else {// This update does have sufficient priority.// Check if this is an optimistic update.const revertLane = update.revertLane;if (!enableAsyncActions || revertLane === NoLane) {楽観的な更新でない場合
// This is not an optimistic update, and we're going to apply it now.// But, if there were earlier updates that were skipped, we need to// leave this update in the queue so it can be rebased later.if (newBaseQueueLast !== null) {const clone: Update<S, A> = {// This update is going to be committed so we never want uncommit// it. Using NoLane works because 0 is a subset of all bitmasks, so// this will never be skipped by the check above.lane: NoLane,revertLane: NoLane,action: update.action,hasEagerState: update.hasEagerState,eagerState: update.eagerState,next: (null: any),};newBaseQueueLast = newBaseQueueLast.next = clone;}// Check if this update is part of a pending async action. If so,// we'll need to suspend until the action has finished, so that it's// batched together with future updates in the same action.if (updateLane === peekEntangledActionLane()) {didReadFromEntangledAsyncAction = true;}更新レーンが現在のトランジションレーンと同じ場合
このフラグは、状態変更が非同期アクションに依存することをマークするためのものです
このフラグは、この関数の最後で消費されます
} else {
revertLane
があるため、楽観的な更新// This is an optimistic update. If the "revert" priority is// sufficient, don't apply the update. Otherwise, apply the update,// but leave it in the queue so it can be either reverted or// rebased in a subsequent render.if (isSubsetOfLanes(renderLanes, revertLane)) {---------------------------------------前述の通り、revertLaneが現在のレンダリングレーンである場合
それは、更新を元に戻す必要があることを意味します
// The transition that this optimistic update is associated with// has finished. Pretend the update doesn't exist by skipping// over it.update = update.next;// Check if this update is part of a pending async action. If so,// we'll need to suspend until the action has finished, so that it's// batched together with future updates in the same action.if (revertLane === peekEntangledActionLane()) {didReadFromEntangledAsyncAction = true;}continue;ここでは、単純に無視されて次のキューには入れられないことで元に戻されます]
これはスキップとは異なります。なぜなら、スキップでは
更新はまだ複製されて次のキューに入れられます
} else {楽観的な更新でない場合、それをコミットする必要があります!
しかし、それをキューに入れる必要がまだあります。
なぜなら、非同期アクションが完了するまで、私たちは
楽観的な更新を設定することができる必要があります!したがって、楽観的な更新は非同期アクション中にキューに入れる必要があります
const clone: Update<S, A> = {// Once we commit an optimistic update, we shouldn't uncommit it// until the transition it is associated with has finished// (represented by revertLane). Using NoLane here works because 0// is a subset of all bitmasks, so this will never be skipped by// the check above.lane: NoLane,それをNoLaneに設定すると、次のレンダリングでスキップされなくなります
// Reuse the same revertLane so we know when the transition// has finished.revertLane: update.revertLane,action: update.action,hasEagerState: update.hasEagerState,eagerState: update.eagerState,next: (null: any),};if (newBaseQueueLast === null) {newBaseQueueFirst = newBaseQueueLast = clone;newBaseState = newState;} else {newBaseQueueLast = newBaseQueueLast.next = clone;}Chain up the updates for new render
// Update the remaining priority in the queue.// TODO: Don't need to accumulate this. Instead, we can remove// renderLanes from the original lanes.currentlyRenderingFiber.lanes = mergeLanes(currentlyRenderingFiber.lanes,revertLane,);markSkippedUpdateLanes(revertLane);}}// Process this update.const action = update.action;if (shouldDoubleInvokeUserFnsInHooksDEV) {reducer(newState, action);}if (update.hasEagerState) {// If this update is a state update (not a reducer) and was processed eagerly,// we can use the eagerly computed statenewState = ((update.eagerState: any): S);} else {newState = reducer(newState, action);Update is committed here
}}update = update.next;} while (update !== null && update !== first);if (newBaseQueueLast === null) {newBaseState = newState;} else {newBaseQueueLast.next = (newBaseQueueFirst: any);}// Mark that the fiber performed work, but only if the new state is// different from the current state.if (!is(newState, hook.memoizedState)) {markWorkInProgressReceivedUpdate();// Check if this update is part of a pending async action. If so, we'll// need to suspend until the action has finished, so that it's batched// together with future updates in the same action.// TODO: Once we support hooks inside useMemo (or an equivalent// memoization boundary like Forget), hoist this logic so that it only// suspends if the memo boundary produces a new value.if (didReadFromEntangledAsyncAction) {const entangledActionThenable = peekEntangledActionThenable();if (entangledActionThenable !== null) {// TODO: Instead of the throwing the thenable directly, throw a// special object like `use` does so we can detect if it's captured// by userspace.throw entangledActionThenable;}}この部分が最も重要です
状態が変更され、それが進行中の非同期アクションに依存している場合
それはプロミスをスローすることで一時停止します。
Suspenseについての詳細は、私のSuspenseについての投稿を参照してください
}hook.memoizedState = newState;hook.baseState = newBaseState;hook.baseQueue = newBaseQueueLast;最後にフックを更新します。
baseQueue
も更新されることに注意してください。queue.lastRenderedState = newState;}if (baseQueue === null) {// `queue.lanes` is used for entangling transitions. We can set it back to// zero once the queue is empty.queue.lanes = NoLanes;}const dispatch: Dispatch<A> = (queue.dispatch: any);return [hook.memoizedState, dispatch];}
function updateReducerImpl<S, A>(
useState()
と同じで、内部的にはuseReducer()
と同じですhook: Hook,current: Hook,reducer: (S, A) => S,): [S, Dispatch<A>] {const baseState = hook.baseState;if (baseQueue === null) {} else {// We have a queue to process.const first = baseQueue.next;let newState = baseState;let newBaseState = null;let newBaseQueueFirst = null;let newBaseQueueLast: Update<S, A> | null = null;let update = first;let didReadFromEntangledAsyncAction = false;do {更新はこのループで処理されます
// An extra OffscreenLane bit is added to updates that were made to// a hidden tree, so that we can distinguish them from updates that were// already there when the tree was hidden.const updateLane = removeLanes(update.lane, OffscreenLane);const isHiddenUpdate = updateLane !== update.lane;// Check if this update was made while the tree was hidden. If so, then// it's not a "base" update and we should disregard the extra base lanes// that were added to renderLanes when we entered the Offscreen tree.const shouldSkipUpdate = isHiddenUpdate? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane): !isSubsetOfLanes(renderLanes, updateLane);この行は重要で、現在のレンダリングレーンが更新のレーンでない場合、
更新をスキップするべきであると言っています。
if (shouldSkipUpdate) {// Priority is insufficient. Skip this update. If this is the first// skipped update, the previous update/state is the new base// update/state.const clone: Update<S, A> = {lane: updateLane,revertLane: update.revertLane,action: update.action,hasEagerState: update.hasEagerState,eagerState: update.eagerState,next: (null: any),};if (newBaseQueueLast === null) {newBaseQueueFirst = newBaseQueueLast = clone;newBaseState = newState;} else {newBaseQueueLast = newBaseQueueLast.next = clone;}// Update the remaining priority in the queue.// TODO: Don't need to accumulate this. Instead, we can remove// renderLanes from the original lanes.currentlyRenderingFiber.lanes = mergeLanes(currentlyRenderingFiber.lanes,updateLane,);markSkippedUpdateLanes(updateLane);スキップとは無視することを意味せず、更新は複製されて次のレンダリングのためのキューに入れられます
} else {// This update does have sufficient priority.// Check if this is an optimistic update.const revertLane = update.revertLane;if (!enableAsyncActions || revertLane === NoLane) {楽観的な更新でない場合
// This is not an optimistic update, and we're going to apply it now.// But, if there were earlier updates that were skipped, we need to// leave this update in the queue so it can be rebased later.if (newBaseQueueLast !== null) {const clone: Update<S, A> = {// This update is going to be committed so we never want uncommit// it. Using NoLane works because 0 is a subset of all bitmasks, so// this will never be skipped by the check above.lane: NoLane,revertLane: NoLane,action: update.action,hasEagerState: update.hasEagerState,eagerState: update.eagerState,next: (null: any),};newBaseQueueLast = newBaseQueueLast.next = clone;}// Check if this update is part of a pending async action. If so,// we'll need to suspend until the action has finished, so that it's// batched together with future updates in the same action.if (updateLane === peekEntangledActionLane()) {didReadFromEntangledAsyncAction = true;}更新レーンが現在のトランジションレーンと同じ場合
このフラグは、状態変更が非同期アクションに依存することをマークするためのものです
このフラグは、この関数の最後で消費されます
} else {
revertLane
があるため、楽観的な更新// This is an optimistic update. If the "revert" priority is// sufficient, don't apply the update. Otherwise, apply the update,// but leave it in the queue so it can be either reverted or// rebased in a subsequent render.if (isSubsetOfLanes(renderLanes, revertLane)) {---------------------------------------前述の通り、revertLaneが現在のレンダリングレーンである場合
それは、更新を元に戻す必要があることを意味します
// The transition that this optimistic update is associated with// has finished. Pretend the update doesn't exist by skipping// over it.update = update.next;// Check if this update is part of a pending async action. If so,// we'll need to suspend until the action has finished, so that it's// batched together with future updates in the same action.if (revertLane === peekEntangledActionLane()) {didReadFromEntangledAsyncAction = true;}continue;ここでは、単純に無視されて次のキューには入れられないことで元に戻されます]
これはスキップとは異なります。なぜなら、スキップでは
更新はまだ複製されて次のキューに入れられます
} else {楽観的な更新でない場合、それをコミットする必要があります!
しかし、それをキューに入れる必要がまだあります。
なぜなら、非同期アクションが完了するまで、私たちは
楽観的な更新を設定することができる必要があります!したがって、楽観的な更新は非同期アクション中にキューに入れる必要があります
const clone: Update<S, A> = {// Once we commit an optimistic update, we shouldn't uncommit it// until the transition it is associated with has finished// (represented by revertLane). Using NoLane here works because 0// is a subset of all bitmasks, so this will never be skipped by// the check above.lane: NoLane,それをNoLaneに設定すると、次のレンダリングでスキップされなくなります
// Reuse the same revertLane so we know when the transition// has finished.revertLane: update.revertLane,action: update.action,hasEagerState: update.hasEagerState,eagerState: update.eagerState,next: (null: any),};if (newBaseQueueLast === null) {newBaseQueueFirst = newBaseQueueLast = clone;newBaseState = newState;} else {newBaseQueueLast = newBaseQueueLast.next = clone;}Chain up the updates for new render
// Update the remaining priority in the queue.// TODO: Don't need to accumulate this. Instead, we can remove// renderLanes from the original lanes.currentlyRenderingFiber.lanes = mergeLanes(currentlyRenderingFiber.lanes,revertLane,);markSkippedUpdateLanes(revertLane);}}// Process this update.const action = update.action;if (shouldDoubleInvokeUserFnsInHooksDEV) {reducer(newState, action);}if (update.hasEagerState) {// If this update is a state update (not a reducer) and was processed eagerly,// we can use the eagerly computed statenewState = ((update.eagerState: any): S);} else {newState = reducer(newState, action);Update is committed here
}}update = update.next;} while (update !== null && update !== first);if (newBaseQueueLast === null) {newBaseState = newState;} else {newBaseQueueLast.next = (newBaseQueueFirst: any);}// Mark that the fiber performed work, but only if the new state is// different from the current state.if (!is(newState, hook.memoizedState)) {markWorkInProgressReceivedUpdate();// Check if this update is part of a pending async action. If so, we'll// need to suspend until the action has finished, so that it's batched// together with future updates in the same action.// TODO: Once we support hooks inside useMemo (or an equivalent// memoization boundary like Forget), hoist this logic so that it only// suspends if the memo boundary produces a new value.if (didReadFromEntangledAsyncAction) {const entangledActionThenable = peekEntangledActionThenable();if (entangledActionThenable !== null) {// TODO: Instead of the throwing the thenable directly, throw a// special object like `use` does so we can detect if it's captured// by userspace.throw entangledActionThenable;}}この部分が最も重要です
状態が変更され、それが進行中の非同期アクションに依存している場合
それはプロミスをスローすることで一時停止します。
Suspenseについての詳細は、私のSuspenseについての投稿を参照してください
}hook.memoizedState = newState;hook.baseState = newBaseState;hook.baseQueue = newBaseQueueLast;最後にフックを更新します。
baseQueue
も更新されることに注意してください。queue.lastRenderedState = newState;}if (baseQueue === null) {// `queue.lanes` is used for entangling transitions. We can set it back to// zero once the queue is empty.queue.lanes = NoLanes;}const dispatch: Dispatch<A> = (queue.dispatch: any);return [hook.memoizedState, dispatch];}
ここで少し流れをまとめてみましょう。
- 楽観的な更新は、
revertLane
がトランジションレーンで、lane
がSyncLane
を持っています - 更新は
SyncLane
で処理され、また、すべての後続のレンダリングでも処理されます 2.1 しかし、それはスキップされず、常に次の更新キューに保持されます - 更新は
revertLane
(低優先度のトランジションレーン)で、次のキューに追加されないことで元に戻されます 3.1 しかし、トランジションレーン上の非同期アクションがまだ完了していない場合、 それはプロミスをスローすることで一時停止します。非同期アクションが完了した後に再度元に戻すことが試みられます。
最後のパズルのピースは非同期アクションとは何か、そしてそれがどのようにトランジションと連携するかです。
4.3 トランジションについてのいくつかの背景
詳細については、React内部でuseTransition()はどのように動作するか を参照してください。簡単に言うと、
- グローバルな
transition
オブジェクトがあり、それは我々が低優先度の更新(トランジションレーン)をスケジュールする必要があるかどうかを示します startTransition()
は、このtransition
オブジェクトを非nullに設定するためのもので、その結果、 そのスコープ内のsetState()
呼び出しは低優先度の更新をスケジュールします。
4.4 <form/>
は現在、非同期アクションを受け入れます
<form>
についてすべてをカバーしていません。useOptimistic()
を理解するための必要な部分だけで、<form/>
については別のエピソードで詳しく説明します React.devによれば、
<form/>
はもはや単純なHTMLタグではなく、非同期アクションを受け入れることができるように拡張されました。デモコードでは、以下のように使用されています。
async function formAction(formData) {addOptimisticMessage(formData.get("message"));これはすぐに楽観的な状態を更新します
formRef.current.reset();await sendMessage(formData);ミューテーションAPIをトリガーする
}<form action={formAction} ref={formRef}><input type="text" name="message" placeholder="Hello!" /><button type="submit">Send</button></form>
async function formAction(formData) {addOptimisticMessage(formData.get("message"));これはすぐに楽観的な状態を更新します
formRef.current.reset();await sendMessage(formData);ミューテーションAPIをトリガーする
}<form action={formAction} ref={formRef}><input type="text" name="message" placeholder="Hello!" /><button type="submit">Send</button></form>
これは、<form/>
の送信動作が何らかの形で妨害されなければならないことを示唆しており、
実際にそうなっています。
function submitForm() {if (nativeEvent.defaultPrevented) {// We let earlier events to prevent the action from submitting.return;} // Prevent native navigation.event.preventDefault();var formData;if (submitter) {// The submitter's value should be included in the FormData.// It should be in the document order in the form.// Since the FormData constructor invokes the formdata event it also// needs to be available before that happens so after construction it's too// late. We use a temporary fake node for the duration of this event.// TODO: FormData takes a second argument that it's the submitter but this// is fairly new so not all browsers support it yet. Switch to that technique// when available.var temp = submitter.ownerDocument.createElement("input");temp.name = submitter.name;temp.value = submitter.value;submitter.parentNode.insertBefore(temp, submitter);formData = new FormData(form);temp.parentNode.removeChild(temp);} else {formData = new FormData(form);}var pendingState = {pending: true,data: formData,method: form.method,action: action,};{Object.freeze(pendingState);}startHostTransition(formInst, pendingState, action, formData);}
function submitForm() {if (nativeEvent.defaultPrevented) {// We let earlier events to prevent the action from submitting.return;} // Prevent native navigation.event.preventDefault();var formData;if (submitter) {// The submitter's value should be included in the FormData.// It should be in the document order in the form.// Since the FormData constructor invokes the formdata event it also// needs to be available before that happens so after construction it's too// late. We use a temporary fake node for the duration of this event.// TODO: FormData takes a second argument that it's the submitter but this// is fairly new so not all browsers support it yet. Switch to that technique// when available.var temp = submitter.ownerDocument.createElement("input");temp.name = submitter.name;temp.value = submitter.value;submitter.parentNode.insertBefore(temp, submitter);formData = new FormData(form);temp.parentNode.removeChild(temp);} else {formData = new FormData(form);}var pendingState = {pending: true,data: formData,method: form.method,action: action,};{Object.freeze(pendingState);}startHostTransition(formInst, pendingState, action, formData);}
pendingState
は、フォームコンポーネントの未来の状態です。トランジションが完了すると、<form/>
はReactランタイム内の内部状態を持つことになります。
startHostTransition()
は、コンポーネントに状態フックを注入するメソッドです。
function startHostTransition(formFiber, pendingState, callback, formData) {var queue;if (formFiber.memoizedState === null) {// Upgrade this host component fiber to be stateful. We're going to pretend// it was stateful all along so we can reuse most of the implementation// for function components and useTransition.//// Create the state hook used by TransitionAwareHostComponent. This is// essentially an inlined version of mountState.var newQueue = {pending: null,lanes: NoLanes,// We're going to cheat and intentionally not create a bound dispatch// method, because we can call it directly in startTransition.dispatch: null,lastRenderedReducer: basicStateReducer,lastRenderedState: NotPendingTransition,};queue = newQueue;var stateHook = {memoizedState: NotPendingTransition,baseState: NotPendingTransition,baseQueue: null,queue: newQueue,next: null,}; // Add the state hook to both fiber alternates. The idea is that the fiber// had this hook all along.formFiber.memoizedState = stateHook;var alternate = formFiber.alternate;if (alternate !== null) {alternate.memoizedState = stateHook;}デフォルトでは<form/>には状態がないため、ここでは新たな状態フックを作成します
これは<form/>自体の状態であり、保留中の状態などの情報を含んでいることを覚えておいてください
初期状態は
NotPendingTransition
であり、submitForm()
で作成されたpendingStateに遷移します} else {// This fiber was already upgraded to be stateful.var _stateHook = formFiber.memoizedState;queue = _stateHook.queue;}startTransition(formFiber,queue,pendingState,NotPendingTransition, // TODO: We can avoid this extra wrapper, somehow. Figure out layering// once more of this function is implemented.function () {return callback(formData);This callback is the async
formAction()
, in whichwe trigger the
setOptimisticState()
});Internally startTransition is called
}
function startHostTransition(formFiber, pendingState, callback, formData) {var queue;if (formFiber.memoizedState === null) {// Upgrade this host component fiber to be stateful. We're going to pretend// it was stateful all along so we can reuse most of the implementation// for function components and useTransition.//// Create the state hook used by TransitionAwareHostComponent. This is// essentially an inlined version of mountState.var newQueue = {pending: null,lanes: NoLanes,// We're going to cheat and intentionally not create a bound dispatch// method, because we can call it directly in startTransition.dispatch: null,lastRenderedReducer: basicStateReducer,lastRenderedState: NotPendingTransition,};queue = newQueue;var stateHook = {memoizedState: NotPendingTransition,baseState: NotPendingTransition,baseQueue: null,queue: newQueue,next: null,}; // Add the state hook to both fiber alternates. The idea is that the fiber// had this hook all along.formFiber.memoizedState = stateHook;var alternate = formFiber.alternate;if (alternate !== null) {alternate.memoizedState = stateHook;}デフォルトでは<form/>には状態がないため、ここでは新たな状態フックを作成します
これは<form/>自体の状態であり、保留中の状態などの情報を含んでいることを覚えておいてください
初期状態は
NotPendingTransition
であり、submitForm()
で作成されたpendingStateに遷移します} else {// This fiber was already upgraded to be stateful.var _stateHook = formFiber.memoizedState;queue = _stateHook.queue;}startTransition(formFiber,queue,pendingState,NotPendingTransition, // TODO: We can avoid this extra wrapper, somehow. Figure out layering// once more of this function is implemented.function () {return callback(formData);This callback is the async
formAction()
, in whichwe trigger the
setOptimisticState()
});Internally startTransition is called
}
今、物事が少し明確になりました。
<form/>
には非同期アクションがあり、それが送信されると、内部的に状態フックが作成され、トランジションが開始されます。async
アクションの間、楽観的な状態の更新がスケジュールされ、revertLane
がトランジションレーンになります。- 非同期アクションが完了するまで、
useOptimistic()
は変更を元に戻そうとするたびに一時停止します。
非同期アクションが完了したときに何が起こるのでしょうか?
function startTransition(fiber,queue,pendingState,finishedState,callback,options) {var previousPriority = getCurrentUpdatePriority();setCurrentUpdatePriority(higherEventPriority(previousPriority, ContinuousEventPriority));var prevTransition = ReactCurrentBatchConfig$3.transition;var currentTransition = {_callbacks: new Set(),};{// We don't really need to use an optimistic update here, because we// schedule a second "revert" update below (which we use to suspend the// transition until the async action scope has finished). But we'll use an// optimistic update anyway to make it less likely the behavior accidentally// diverges; for example, both an optimistic update and this one should// share the same lane.ReactCurrentBatchConfig$3.transition = currentTransition;As mentioned, here a new global transition is created
dispatchOptimisticSetState(fiber, false, queue, pendingState);}{ReactCurrentBatchConfig$3.transition._updatedFibers = new Set();}try {if (enableAsyncActions) {これは単なる機能フラグで、私たちの場合は真です
var returnValue = callback();ここで非同期の
formAction()
が呼び出され、Promiseを返します// Check if we're inside an async action scope. If so, we'll entangle// this new action with the existing scope.//// If we're not already inside an async action scope, and this action is// async, then we'll create a new async scope.//// In the async case, the resulting render will suspend until the async// action scope has finished.if (returnValue !== null &&typeof returnValue === "object" &&typeof returnValue.then === "function") {var thenable = returnValue;notifyTransitionCallbacks(currentTransition, thenable);// Create a thenable that resolves to `finishedState` once the async// action has completed.var thenableForFinishedState = chainThenableValue(thenable,finishedState);dispatchSetState(fiber, queue, thenableForFinishedState);} else {dispatchSetState(fiber, queue, finishedState);}したがって、
<form>
は新しい状態更新を受け取りますが、今回は楽観的ではありませんなぜ楽観的ではないのでしょうか?私はフォームの状態が重要で、すぐにコミットされるべきだと思うからです
}} catch (error) {}
function startTransition(fiber,queue,pendingState,finishedState,callback,options) {var previousPriority = getCurrentUpdatePriority();setCurrentUpdatePriority(higherEventPriority(previousPriority, ContinuousEventPriority));var prevTransition = ReactCurrentBatchConfig$3.transition;var currentTransition = {_callbacks: new Set(),};{// We don't really need to use an optimistic update here, because we// schedule a second "revert" update below (which we use to suspend the// transition until the async action scope has finished). But we'll use an// optimistic update anyway to make it less likely the behavior accidentally// diverges; for example, both an optimistic update and this one should// share the same lane.ReactCurrentBatchConfig$3.transition = currentTransition;As mentioned, here a new global transition is created
dispatchOptimisticSetState(fiber, false, queue, pendingState);}{ReactCurrentBatchConfig$3.transition._updatedFibers = new Set();}try {if (enableAsyncActions) {これは単なる機能フラグで、私たちの場合は真です
var returnValue = callback();ここで非同期の
formAction()
が呼び出され、Promiseを返します// Check if we're inside an async action scope. If so, we'll entangle// this new action with the existing scope.//// If we're not already inside an async action scope, and this action is// async, then we'll create a new async scope.//// In the async case, the resulting render will suspend until the async// action scope has finished.if (returnValue !== null &&typeof returnValue === "object" &&typeof returnValue.then === "function") {var thenable = returnValue;notifyTransitionCallbacks(currentTransition, thenable);// Create a thenable that resolves to `finishedState` once the async// action has completed.var thenableForFinishedState = chainThenableValue(thenable,finishedState);dispatchSetState(fiber, queue, thenableForFinishedState);} else {dispatchSetState(fiber, queue, finishedState);}したがって、
<form>
は新しい状態更新を受け取りますが、今回は楽観的ではありませんなぜ楽観的ではないのでしょうか?私はフォームの状態が重要で、すぐにコミットされるべきだと思うからです
}} catch (error) {}
ここでは少し複雑ですが、2つの更新がディスパッチされます。
- フォームの状態に対する楽観的な更新、アクションは
js
{action: {action : formAction,data: FormData {},method: "get"pending: true},lane: 2,revertLane: 128}
js
{action: {action : formAction,data: FormData {},method: "get"pending: true},lane: 2,revertLane: 128}
この更新は高優先度であるためコミットされ、revertLane
が作業されるのを待つためにレーンに保持されます。
- フォームの状態を解決するための別の更新。
js
{action: {reason: null,status: 'pending',then: function resolve() {},value: {pending: false, data: null, method: null, action: null}},lane: 128,revertLane: 0}
js
{action: {reason: null,status: 'pending',then: function resolve() {},value: {pending: false, data: null, method: null, action: null}},lane: 128,revertLane: 0}
これは通常の状態更新であり、この更新はトランジションの下でディスパッチされるため、
その更新レーンはトランジションレーンであり、revertLane
はありません。
これらの2つの更新の両方のアクションは単なるオブジェクトであり、関数ではないため、 後者は実際には縮小する際に最初のものを置き換えます。
トランジションレーンが作業されると、非同期アクションがまだ進行中の場合、最初の更新はスローします(しかし、その親
コンポーネント<Thread/>
はすでにスローします)。
非同期アクションが完了すると、Reactはすべてを再レンダリングします。今、最初の更新はスローしない、そして2つ目の更新が適用され、最終的には以下の状態を持つことになります
js
{reason: null,status: 'fulfilled',then: function resolve() {},value: {pending: false, data: null, method: null, action: null}}
js
{reason: null,status: 'fulfilled',then: function resolve() {},value: {pending: false, data: null, method: null, action: null}}
Reactが変更をコミットした後、更新キューはクリアされ、私たちはクリーンなスレートを持っています。
したがって、最初の更新は実際には中間的なもので、useTransition()
のisPending
オプションのようなものです。
5. 注意点:重複する楽観的な更新
公式デモで素早く複数のメッセージを入力してみてください。 非同期アクションが完了した後も、楽観的な更新がレンダリングされることに気づくでしょう。
実際には、1つのメッセージを入力するだけでも発生しますが、それほど目立ちません。問題は、useOptimistic()
が新しい状態引数(パススルー)を受け取ると、すぐに基本状態を更新するが、既存の楽観的な更新の元に戻す作業は将来の再レンダリングになるということです。言い換えれば、楽観的な更新の元に戻す作業はトランジションレーン(低優先度)にあるため、中断されて将来の任意の時間に実行される可能性があります。
これは既知の問題であり、Reactチームが取り組んでいます。解決するまで、ディスパッチャーを更新して重複を手動でフィルタリングすることができます。
const [optimisticMessages, addOptimisticMessage] = useOptimistic(messages,(state, newMessage) => [...state,...state.filter(msg => msg.text !== newMessage),{text: newMessage,sending: true}]);
const [optimisticMessages, addOptimisticMessage] = useOptimistic(messages,(state, newMessage) => [...state,...state.filter(msg => msg.text !== newMessage),{text: newMessage,sending: true}]);
ここでは、重複の問題がない公式デモの微調整版を示します。
6. まとめ
useOptimistic()
については多くのことがあります。なぜなら、それはステートフック、トランジション、サスペンスの上に構築されているからです。それはかなり複雑で、それを統合しているライブラリなしで使用するべきだとは思いません。これは並行モードの力を示す別のユースケースです。
以下は、useOptimistic()
がどのように動作するかについての簡単なまとめです。参考になれば幸いです!
<form/>
は非同期アクションを受け入れ、それが送信されると、<form/>
は内部に新しいステートフックを持ち、非同期アクションが完了するまでグローバルトランジションが開始されます。useOptimistic()
はパススルーステートを取る新しいステートフックを作成します。パススルーステートは更新の基本状態として使用され、これがsetState()
なしで状態が更新される方法です。- 楽観的な更新がディスパッチされると、その
revertLane
はトランジションレーンに設定されます。 - 楽観的な更新が処理されると、すぐにコミットされますが、後で低優先度(トランジションレーン)で元に戻すために更新キューに保持され続けます。
- 楽観的な更新がトランジションレーンで元に戻されると、グローバルトランジションがまだ進行中の場合はサスペンドします。

Want to know more about how React works internally?
Check out my series - React Internals Deep Dive!