How does useOptimistic() work internally in React?

楽観的UIはUI開発でよく使われるトリックで、知覚されるパフォーマンスを向上させます。 useOptimistic() はそれを構築するのを支援するフックです。内部動作を調べてみましょう。

1. 楽観的UIとは何ですか?

ユーザーがクリックして「いいね」または「いいねを解除」できる「いいね」ボタンを実装するとします。どのようにしますか?

基本的な考え方は単純です。ボタンがクリックされるとAPIを取得し、APIの応答に応じてUIを更新します。これは機能しますが、非常に遅延が発生する可能性があります。次のデモを試してみてください。

import { Suspense, useState, use} from 'react';
import {Like} from './Like.js';

export default function App() {
  return <div>
    <h2>non-optimistic UI</h2>
    <p>Clicking following button always succeed</p>
    <Like forceResult="success"/>
    <p>Clicking following button always fail</p>
    <Like forceResult="failure"/>
  </div>
}

const mutateAPI = (payload, successOrFailure) => {
  return new Promise((resolve, reject) => {
    if (successOrFailure === 'success') {
      setTimeout(() => resolve(payload), 1000)
    } else {
      setTimeout(() => reject('something wrong'), 1000)
    }
  })
}

こちらがクリックハンドラーです。

const toggle = () => {
if (isFetching) return
setIsFetching(true)
mutateAPI({liked: !liked}, forceResult).then((data) => {
setLiked(data.liked)

APIの応答に基づいて状態を更新する

}).catch((reason) => {
toast(reason)
}).finally(() => {
setIsFetching(false)
})
}
const toggle = () => {
if (isFetching) return
setIsFetching(true)
mutateAPI({liked: !liked}, forceResult).then((data) => {
setLiked(data.liked)

APIの応答に基づいて状態を更新する

}).catch((reason) => {
toast(reason)
}).finally(() => {
setIsFetching(false)
})
}

以下は、インスタントフィードバックを提供したいが、APIリクエストの往復のため技術的に不可能であるため、 APIの応答を待たずにUIを更新することを除いては。 これは実際には良いことです、なぜならほとんどの場合、APIの取得が成功することを期待するからです。

そのため、クリックハンドラーでは以下のようにすることができます:

  1. UIを即座に更新する
  2. API呼び出しをトリガーする
  3. APIの応答(成功またはエラー)で、再びUIを更新する

以下は、楽観的UIを有効にしたデモです。これにより、認識されるパフォーマンスが大幅に向上します。

import { Suspense, useState, use} from 'react';
import {Like} from './Like.js';

export default function App() {
  return <div>
    <h2>optimistic UI</h2>
    <p>Clicking following button always succeed</p>
    <Like forceResult="success"/>
    <p>Clicking following button always fail</p>
    <Like forceResult="failure"/>
  </div>
}

const mutateAPI = (payload, successOrFailure) => {
  return new Promise((resolve, reject) => {
    if (successOrFailure === 'success') {
      setTimeout(() => resolve(payload), 1000)
    } else {
      setTimeout(() => reject('something wrong'), 1000)
    }
  })
}

変更点は以下の通りです。

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 = callId
mutateAPI({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 = callId
mutateAPI({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オプションを通じてこれをサポートします。

<button
type="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>
<button
type="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(() => {
{...}
fetcher().then((data) => {
setState(state => ({
...state,
data
}))
setOptimisticState(state => ({
...optimisticState,
data
}))
}).catch((error) => {
setState(state => ({
...state,
error
}))
setOptimisticState(state => ({
...optimisticState,
error
}))
})
}, [])
const mutate = (updater, options = {}) => {
const mutateId = uid()
mutateIdRef.current = mutateId
if (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 = 0
return () => 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(() => {
{...}
fetcher().then((data) => {
setState(state => ({
...state,
data
}))
setOptimisticState(state => ({
...optimisticState,
data
}))
}).catch((error) => {
setState(state => ({
...state,
error
}))
setOptimisticState(state => ({
...optimisticState,
error
}))
})
}, [])
const mutate = (updater, options = {}) => {
const mutateId = uid()
mutateIdRef.current = mutateId
if (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 = 0
return () => i++
})()

新しいuseAPI()を使ったデモはこちらです。

import { Suspense, useState, use} from 'react';
import {Like} from './Like.js';

export default function App() {
  return <div>
    <p>Clicking following button always succeed</p>
    <Like forceResult="success"/>
    <p>Clicking following button always fail</p>
    <Like forceResult="failure"/>
  </div>
}

const mutateAPI = (payload, successOrFailure) => {
  return new Promise((resolve, reject) => {
    if (successOrFailure === 'success') {
      setTimeout(() => resolve(payload), 1000)
    } else {
      setTimeout(() => reject('something wrong'), 1000)
    }
  })
}

デモは良好で、ボタンコンポーネントもさらにクリーンになりました。

const {data, mutate} = useAPI(() => Promise.resolve({liked: false}))
const liked = !!data?.liked
const 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?.liked
const toggle = () => {
const payload = {liked: !liked}
mutate(
() => mutateAPI(payload, forceResult),
{
optimisticData: payload,
rollbackOnError: true,
onError: toast
}
)
}

素晴らしい、Optimistic UIについての背景知識は十分です。さあ、Reactの世界に飛び込みましょう。🤿

3. 自分たちでuseOptimistic()を実装してみましょう。

こちらがuseOptimistic()の公式デモです。

import { useState, useRef, useOptimistic } from "react";
import { deliverMessage } from "./actions.js";

function Thread({ messages, sendMessage }) {
  const formRef = useRef();
  async function formAction(formData) {
    addOptimisticMessage(formData.get("message"));
    formRef.current.reset();
    await sendMessage(formData);
  }
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      ...state,
      {
        text: newMessage,
        sending: true
      }
    ]
  );

  return (
    <>
      {optimisticMessages.map((message, index) => (
        <div key={index}>
          {message.text}
          {!!message.sending && <small> (Sending...)</small>}
        </div>
      ))}
      <form action={formAction} ref={formRef}>
        <input type="text" name="message" placeholder="Hello!" />
        <button type="submit">Send</button>
      </form>
    </>
  );
}

export default function App() {
  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 }]);
  }
  return <Thread messages={messages} sendMessage={sendMessage} />;
}

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()を私たちの実装に置き換えた公式デモです。

import { useState, useRef } from "react";
import { deliverMessage } from "./actions.js";
import { useOptimistic } from "./useOptimistic.js";

function Thread({ messages, sendMessage }) {
  const formRef = useRef();
  async function formAction(formData) {
    addOptimisticMessage(formData.get("message"));
    formRef.current.reset();
    await sendMessage(formData);
  }
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      ...state,
      {
        text: newMessage,
        sending: true
      }
    ]
  );

  return (
    <>
      {optimisticMessages.map((message, index) => (
        <div key={index}>
          {message.text}
          {!!message.sending && <small> (Sending...)</small>}
        </div>
      ))}
      <form action={formAction} ref={formRef}>
        <input type="text" name="message" placeholder="Hello!" />
        <button type="submit">Send</button>
      </form>
    </>
  );
}

export default function App() {
  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 }]);
  }
  return <Thread messages={messages} sendMessage={sendMessage} />;
}

上記のデモを試してみると、実際には動作しないことに気づくでしょう - 楽観的な更新はまったくレンダリングされません!問題は<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>
import { useState, useRef } from "react";
import { deliverMessage } from "./actions.js";
import { useOptimistic } from "./useOptimistic.js";

function Thread({ messages, sendMessage }) {
  const formRef = useRef();
  async function formAction(e) {
    e.preventDefault()
    const formData = new FormData(formRef.current)
    addOptimisticMessage(formData.get("message"));
    formRef.current.reset();
    await sendMessage(formData);
  }
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      ...state,
      {
        text: newMessage,
        sending: true,
      },
    ]
  );

  return (
    <>
      {optimisticMessages.map((message, index) => (
        <div key={index}>
          {message.text}
          {!!message.sending && <small> (Sending...)</small>}
        </div>
      ))}
      <form onSubmit={formAction} ref={formRef}>
        <input type="text" name="message" placeholder="Hello!" />
        <button type="submit">Send</button>
      </form>
    </>
  );
}

export default function App() {
  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 }]);
  }
  return <Thread messages={messages} sendMessage={sendMessage} />;
}

「なんとなく動作する」と言いましたが、実際には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),

revertLaneuseOptimistic()内にのみ存在します。これはReactに

revertLaneが作業中のときには、楽観的な更新を

元に戻すべきであることを伝えるためのものです。

action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
{...}
// When calling startTransition during render, this warns instead of
// throwing because throwing would be a breaking change. setOptimisticState
// is a new API so it's OK to throw.
if (throwIfDuringRender) {
throw new Error('Cannot update optimistic state while rendering.');
} else {
// startTransition was called during render. We don't need to do anything
// besides warn here because the render phase update would be overidden by
// the second update, anyway. We can remove this branch and make it throw
// in a future release.
if (__DEV__) {
console.error('Cannot call startTransition while rendering.');
}
}
} 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),

revertLaneuseOptimistic()内にのみ存在します。これはReactに

revertLaneが作業中のときには、楽観的な更新を

元に戻すべきであることを伝えるためのものです。

action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
{...}
// When calling startTransition during render, this warns instead of
// throwing because throwing would be a breaking change. setOptimisticState
// is a new API so it's OK to throw.
if (throwIfDuringRender) {
throw new Error('Cannot update optimistic state while rendering.');
} else {
// startTransition was called during render. We don't need to do anything
// besides warn here because the render phase update would be overidden by
// the second update, anyway. We can remove this branch and make it throw
// in a future release.
if (__DEV__) {
console.error('Cannot call startTransition while rendering.');
}
}
} 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()とほとんど同じです

}

ステートフックは、異なる優先度での更新のリストを保持し、 それらは異なるバッチで処理されるため、以下のプロパティが必要です。

  1. baseQueue: すべての更新
  2. baseState: 更新のための基本状態、なぜなら更新はミドルウェアのように順序で処理されるからです。
  3. memoizedState: レンダリングで使用される実際の値。この値は、 更新が保留中の場合、中間値となります。

これは、なぜupdateOptimisticImpl()baseStateを更新する必要があるのかを説明しています。 なぜなら、キュー内の更新は楽観的であることが想定されており、つまり、それらは 中間状態であるべきで、もし我々がこれらの更新の後に 重要な更新を置くと、それは誤った状態になるからです。

function updateReducerImpl<S, A>(

useState()と同じで、内部的にはuseReducer()と同じです

hook: Hook,
current: Hook,
reducer: (S, A) => S,
): [S, Dispatch<A>] {
{...}
const queue = hook.queue;
if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}
queue.lastRenderedReducer = reducer;
// The last rebase update that is NOT part of the base state.
let baseQueue = hook.baseQueue;
// The last pending update that hasn't been processed yet.
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
const baseState = hook.baseState;
if (baseQueue === null) {
{...}
// If there are no pending updates, then the memoized state should be the
// same as the base state. Currently these only diverge in the case of
// useOptimistic, because useOptimistic accepts a new baseState on
// every render.
hook.memoizedState = baseState;
// We don't need to call markWorkInProgressReceivedUpdate because
// baseState is derived from other reactive values.
} 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 state
newState = ((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 queue = hook.queue;
if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}
queue.lastRenderedReducer = reducer;
// The last rebase update that is NOT part of the base state.
let baseQueue = hook.baseQueue;
// The last pending update that hasn't been processed yet.
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
const baseState = hook.baseState;
if (baseQueue === null) {
{...}
// If there are no pending updates, then the memoized state should be the
// same as the base state. Currently these only diverge in the case of
// useOptimistic, because useOptimistic accepts a new baseState on
// every render.
hook.memoizedState = baseState;
// We don't need to call markWorkInProgressReceivedUpdate because
// baseState is derived from other reactive values.
} 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 state
newState = ((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];
}

ここで少し流れをまとめてみましょう。

  1. 楽観的な更新は、revertLaneがトランジションレーンで、laneSyncLaneを持っています
  2. 更新はSyncLaneで処理され、また、すべての後続のレンダリングでも処理されます 2.1 しかし、それはスキップされず、常に次の更新キューに保持されます
  3. 更新はrevertLane(低優先度のトランジションレーン)で、次のキューに追加されないことで元に戻されます 3.1 しかし、トランジションレーン上の非同期アクションがまだ完了していない場合、 それはプロミスをスローすることで一時停止します。非同期アクションが完了した後に再度元に戻すことが試みられます。

最後のパズルのピースは非同期アクションとは何か、そしてそれがどのようにトランジションと連携するかです。

4.3 トランジションについてのいくつかの背景

詳細については、React内部でuseTransition()はどのように動作するか を参照してください。簡単に言うと、

  1. グローバルなtransitionオブジェクトがあり、それは我々が低優先度の更新(トランジションレーン)をスケジュールする必要があるかどうかを示します
  2. startTransition()は、このtransitionオブジェクトを非nullに設定するためのもので、その結果、 そのスコープ内のsetState()呼び出しは低優先度の更新をスケジュールします。

4.4 <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) {
{...}
if (formFiber.tag !== HostComponent) {
throw new Error(
"Expected the form instance to be a HostComponent. This " +
"is a bug in React."
);
}
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 which

we trigger the setOptimisticState()

}
);

Internally startTransition is called

}
function startHostTransition(formFiber, pendingState, callback, formData) {
{...}
if (formFiber.tag !== HostComponent) {
throw new Error(
"Expected the form instance to be a HostComponent. This " +
"is a bug in React."
);
}
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 which

we trigger the setOptimisticState()

}
);

Internally startTransition is called

}

今、物事が少し明確になりました。

  1. <form/>には非同期アクションがあり、それが送信されると、内部的に状態フックが作成され、トランジションが開始されます。
  2. asyncアクションの間、楽観的な状態の更新がスケジュールされ、revertLaneがトランジションレーンになります。
  3. 非同期アクションが完了するまで、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);

これはフォームの状態のためのもので、私たちが設定した楽観的な状態ではありません。

フォームの状態はuseFormStatus()で公開されています、

そして、それについては別のエピソードで取り上げます。

}
{
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) {
{...}
{
// This is a trick to get the `useTransition` hook to rethrow the error.
// When it unwraps the thenable with the `use` algorithm, the error
// will be thrown.
var rejectedThenable = {
then: function () {},
status: "rejected",
reason: error,
};
dispatchSetState(fiber, queue, rejectedThenable);
}
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig$3.transition = prevTransition;
{
if (prevTransition === null && currentTransition._updatedFibers) {
var updatedFibersCount = currentTransition._updatedFibers.size;
currentTransition._updatedFibers.clear();
if (updatedFibersCount > 10) {
warn(
"Detected a large number of updates inside startTransition. " +
"If this is due to a subscription please re-write it to use React provided hooks. " +
"Otherwise concurrent mode guarantees are off the table."
);
}
}
}
}
}
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);

これはフォームの状態のためのもので、私たちが設定した楽観的な状態ではありません。

フォームの状態はuseFormStatus()で公開されています、

そして、それについては別のエピソードで取り上げます。

}
{
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) {
{...}
{
// This is a trick to get the `useTransition` hook to rethrow the error.
// When it unwraps the thenable with the `use` algorithm, the error
// will be thrown.
var rejectedThenable = {
then: function () {},
status: "rejected",
reason: error,
};
dispatchSetState(fiber, queue, rejectedThenable);
}
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig$3.transition = prevTransition;
{
if (prevTransition === null && currentTransition._updatedFibers) {
var updatedFibersCount = currentTransition._updatedFibers.size;
currentTransition._updatedFibers.clear();
if (updatedFibersCount > 10) {
warn(
"Detected a large number of updates inside startTransition. " +
"If this is due to a subscription please re-write it to use React provided hooks. " +
"Otherwise concurrent mode guarantees are off the table."
);
}
}
}
}
}

ここでは少し複雑ですが、2つの更新がディスパッチされます。

  1. フォームの状態に対する楽観的な更新、アクションは
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が作業されるのを待つためにレーンに保持されます。

  1. フォームの状態を解決するための別の更新。
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
}
]
);

ここでは、重複の問題がない公式デモの微調整版を示します。

import { useState, useRef, useOptimistic } from "react";
import { deliverMessage } from "./actions.js";

function Thread({ messages, sendMessage }) {
  const formRef = useRef();
  async function formAction(formData) {
    addOptimisticMessage(formData.get("message"));
    formRef.current.reset();
    await sendMessage(formData);
  }
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      ...state.filter(msg => msg.text !== newMessage),
      {
        text: newMessage,
        sending: true
      }
    ]
  );

  return (
    <>
      {optimisticMessages.map((message, index) => (
        <div key={index}>
          {message.text}
          {!!message.sending && <small> (Sending...)</small>}
        </div>
      ))}
      <form action={formAction} ref={formRef}>
        <input type="text" name="message" placeholder="Hello!" />
        <button type="submit">Send</button>
      </form>
    </>
  );
}

export default function App() {
  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 }]);
  }
  return <Thread messages={messages} sendMessage={sendMessage} />;
}

6. まとめ

useOptimistic()については多くのことがあります。なぜなら、それはステートフック、トランジション、サスペンスの上に構築されているからです。それはかなり複雑で、それを統合しているライブラリなしで使用するべきだとは思いません。これは並行モードの力を示す別のユースケースです。

以下は、useOptimistic()がどのように動作するかについての簡単なまとめです。参考になれば幸いです!

  1. <form/>は非同期アクションを受け入れ、それが送信されると、<form/>は内部に新しいステートフックを持ち、非同期アクションが完了するまでグローバルトランジションが開始されます。
  2. useOptimistic()はパススルーステートを取る新しいステートフックを作成します。パススルーステートは更新の基本状態として使用され、これがsetState()なしで状態が更新される方法です。
  3. 楽観的な更新がディスパッチされると、そのrevertLaneはトランジションレーンに設定されます。
  4. 楽観的な更新が処理されると、すぐにコミットされますが、後で低優先度(トランジションレーン)で元に戻すために更新キューに保持され続けます。
  5. 楽観的な更新がトランジションレーンで元に戻されると、グローバルトランジションがまだ進行中の場合はサスペンドします。

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

😳 Share my post ?    
or sponsor me

Next: How does useOptimistic() work internally in React?