実装原則の違いにより、各フレームワークにはいくつかの固有の概念があります。例えば:
-
Vue3
応答性の高い実装原則により、ref
、reactive
などの概念が派生します。 -
Svelte
独自のコンパイラに大きく依存しているため、コンパイル関連の概念 (タグの革新的な使用などlabel
)を派生します。
には、誤用されやすい「非常に簡単」React
があります - 、今日紹介したいのは、から派生した概念に属します。API
useEffect
Effect Event
useEffect
悪用効果
この記事では、次の 3 つの概念について説明します。
-
Event
(イベント) -
Effect
(副作用) -
Effect Event
(副作用事象)
Event
まずはとチャットしましょうEffect
。useEffect
また、2 つの概念は混同されやすいため、誤用されやすくなります。
イベントのコンセプト
以下のコードでは、クリックによってdiv
クリック イベント (onClick
クリック コールバック) がトリガーされます。その中には以下のものonClick
に属しますEvent
:
function App() {
const [num , update] = useState(0);
function onClick() {
update(num + 1);
}
return (
<div onClick={onClick}>{num}</div>
)
}
Event
の特徴は、「状態変化ではなく、何らかの動作によってトリガーされるロジック」です。
たとえば、上記のコードでは、ロジックは「クリック イベント」の動作によってトリガーされonClick
、状態の変更はトリガーされません。num
onClick
エフェクトの概念
Effect
それどころかEvent
、彼は「何らかの行動によって引き起こされる論理ではなく、何らかの状態変化によって引き起こされる」のです。
たとえば、次のコードでは、title
変更されると、次の値document.title
に更新されます。title
function Title({title}) {
useEffect(() => {
document.title = title;
}, [title])
// ...
}
上記のコードのロジックuseEffect
は に属しておりEffect
、title
変更によってトリガーされます。さらにuseEffect
、次の 2 つもHook
属しますEffect
。
-
useLayoutEffect
(あまり使われない) -
useInsertionEffect
(非常にまれに使用されます)
なぜ悪用されやすいのでしょうか?
ここで問題は、Event
とEffect
の概念がまったく異なるのに、なぜ誤用されるのかということです。
たとえば、プロジェクトの最初のバージョンでは、useEffect
データを初期化するロジックが次のとおりでした。
function App() {
const [data, updateData] = useState(null);
useEffect(() => {
fetchData().then(data => {
// ...一些业务逻辑
// 更新data
updateData(data);
})
}, []);
// ...
}
プロジェクトが進行するにつれて、フォームの送信後にデータを更新するという別の要件が発生します。
前のロジックを再利用するには、新しいoptions
状態を追加し (フォーム データを保存)、それをuseEffect
依存関係として使用します。
function App() {
const [data, updateData] = useState(null);
const [options, updateOptions] = useState(null);
useEffect(() => {
fetchData(options).then(data => {
// ...一些业务逻辑
// 更新data
updateData(data);
})
}, [options]);
function onSubmit(opt) {
updateOptions(opt);
}
// ...
}
これで、フォームを送信した後 (onSubmit
コールバックをトリガーした後)、以前のデータ初期化ロジックを再利用できるようになります。
とても便利なので、これがuseEffect
ベストな使い方だと考える学生も多いです。しかし実際には、これは典型的な「useEffect の誤用」です。
注意深く分析した結果、 「フォームの送信」は明らかにEvent
(送信された動作によってトリガーされる) ものであり、Event
ロジックはイベント コールバックではなくイベント コールバックに記述する必要があることがわかりますuseEffect
。正しい書き方は次のようになります。
function App() {
const [data, updateData] = useState(null);
useEffect(() => {
fetchData().then(data => {
// ...一些业务逻辑
// 更新data
updateData(data);
})
}, []);
function onSubmit(opt) {
fetchData(opt).then(data => {
// ...一些业务逻辑
// 更新data
updateData(data);
})
}
// ...
}
上記の例のロジックは比較的単純であり、2 つの記述方法にほとんど違いはありません。ただし、実際のプロジェクトでは、プロジェクトが反復を続けると、次のコードが表示される場合があります。
useEffect(() => {
fetchData(options).then(data => {
// ...一些业务逻辑
// 更新data
updateData(data);
})
}, [options, xxx, yyy, zzz]);
fetchData
その時点では、次の理由から、メソッドがどのような状況で実行されるかを知るのは困難です。
-
useEffect
依存関係が多すぎる -
依存関係の変更のタイミングを完全に把握するのが難しい
したがって、 では、と をReact
明確に区別する必要があります。つまり、「ロジックの一部が動作によってトリガーされるのか、状態変化によってトリガーされるのか」を明確に区別する必要があります。Event
Effect
useEffect の依存関係の問題
Event
と を明確に区別できるようになったのでEffect
、プロジェクトの作成に問題がないことは当然です。しかし、 「効果の仕組みの問題」により、新たな問題にも直面しています。
roomId
変更されたときに新しいチャット ルームに再接続する必要があるチャット ルーム コードがあるとします。このシナリオでは、チャット ルームの切断/再接続はroomId
状態の変化に依存します。これは明らかにEffect
次のコードに属します。
function ChatRoom({roomId}) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId]);
// ...
}
次に、新しい要件を受け取ります。接続が成功すると、「グローバル リマインダー」がポップアップ表示されます。
「グローバルリマインダー」がダークモードであるかどうかがtheme props
影響します。useEffect
変更されたコードは次のとおりです。
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
connection.on('connected', () => {
showNotification('连接成功!', theme);
});
return () => connection.disconnect();
}, [roomId, theme]);
しかし、このコードには重大な問題があります。変更を引き起こすものはすべて、theme
チャット ルームの切断/再接続を引き起こします。結局のところ、依存関係theme
も同様ですuseEffect
。
この例では、Effect
依存していますがtheme
、変更Effect
によってtheme
トリガーされません (roomId
変更によってトリガーされます)。
このシナリオに対処するために、React
新しいコンセプト - 。彼は、上記の例のように、「Effect で実行されるが、Effect は状態のロジックに依存しない」ものを指します。 Effect Event
() => {
showNotification('连接成功!', theme);
}
useEffectEvent
(これは実験的なHook
) 定義を使用できますEffect Event
。
function ChatRoom({roomId, theme}) {
const onConnected = useEffectEvent(() => {
showNotification('连接成功!', theme);
});
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
connection.on('connected', () => {
onConnected();
});
return () => {
connection.disconnect()
};
}, [roomId]);
// ...
}
上記のコードでは(he is a )theme
に移動していますが、最新値の最新値が使用されていますが、依存関係として使用する必要はありません。onConnected
Effect Event
useEffect
theme
useEffectEvent ソースコード分析
useEffectEvent
実装は複雑ではなく、コアコードは次のとおりです。
function updateEvent(callback) {
const hook = updateWorkInProgressHook();
// 保存callback的引用
const ref = hook.memoizedState;
// 在useEffect执行前更新callback的引用
useEffectEventImpl({ref, nextImpl: callback});
return function eventFn() {
if (isInvalidExecutionContextForEventFunction()) {
throw new Error(
"A function wrapped in useEffectEvent can't be called during rendering.",
);
}
return ref.impl.apply(undefined, arguments);
};
}
このうち、ref
変数には「コールバックの参照」が保持されます。上の例の場合:
const onConnected = useEffectEvent(() => {
showNotification('连接成功!', theme);
});
ref
次のような関数への参照を保存します。
() => {
showNotification('连接成功!', theme);
}
useEffectEventImpl
このメソッドはパラメータとしてref
と を受け入れcallback的最新值
、useEffect
実行前にref
に保存されたcallback引用
値を更新しますcallback的最新值
。
したがって、 でuseEffect
実行すると、に保存されている次のクロージャの最新の値がonConnected
取得されます。ref
() => {
showNotification('连接成功!', theme);
}
当然、クロージャの最新値も表示されますtheme
。
useEffectEvent と useEvent
useEffectEvent
戻り値を注意深く観察してください。戻り値には 2 つの制限があります。
return function eventFn() {
if (isInvalidExecutionContextForEventFunction()) {
throw new Error(
"A function wrapped in useEffectEvent can't be called during rendering.",
);
}
return ref.impl.apply(undefined, arguments);
};
最初の制限はより明らかです。次のコード行のuseEffectEvent
戻り値の制限はuseEffect
、コールバック内でのみ実行できます (そうでない場合はエラーが報告されます)。
if (isInvalidExecutionContextForEventFunction()) {
// ...
}
もう 1 つの制限はさらに微妙です。戻り値はまったく新しい参照です。
return function eventFn() {
// ...
};
「新鮮な参照」が制限である理由がよくわからない場合は、useCallback
戻り値を返すことを検討してください。
return useCallback((...args) => {
const fn = ref.impl;
return fn(...args);
}, []);
これにより、useEffectEvent
関数の戻り値は不変の参照となり、「useEffect コールバック内でのみ実行可能」という制約がなくなるuseEffectEvent
と拡張版となりますuseCallback
。
たとえば、上記の制限に違反している場合は、次のコードが該当します。
function App({a, b}) {
const [c, updateC] = useState(0);
const fn = useCallback(() => a + b + c, [a, b, c])
// ...
}
代わりに、コードはuseEffectEvent
次のようになりますuseCallback
。
const fn = useEffectEvent(() => a + b + c)
それに比べてuseCallback
、彼には 2 つの利点があります。
-
依存関係を明示的に宣言せずに
-
依存関係が変化しても
fn
参照は変化しません。これは単にパフォーマンスの最適化にとって最良の選択です。
では、React
なぜuseEffectEvent
制限するのでしょうか?
実際、useEffectEvent
の前身はuseEvent
上記の実装に従っていますが、次の理由によります。
-
useEvent
ポジショニングは のはずですEffect Event
が、実際の用途はもっと広く(置き換え可能useCallback
)、彼のポジショニングと一致しません -
現時点では(と同等のコードを生成できる公式コンパイラ)は考慮されていません
React Forget
がuseMemo
、これを追加すると実装の難易度が上がりますuseCallback
useEvent
hook
React Forget
したがって、useEvent
正式には規格には入りませんでした。代わりに、より制限的なuseEffectEvent
ものが React ドキュメント [1] に組み込まれました。
要約する
今日は次の 3 つの概念を学びました。
-
Event
: 状態の変化ではなく、何らかの動作によってトリガーされるロジック -
Effect
: 何らかの動作ではなく、何らかの状態変化によってトリガーされるロジック -
Effect Event
:Effect
内部でが、Effect
状態のロジックには依存しません
その具体的な実装はEffect Event
にあります。前任者と比較して、彼には 2 つの追加の制限があります。React
useEffectEvent
useEvent
-
Effect
内でのみ実行可能 -
常に異なる参照を返します
私の考えでは、Effect Event
の出現はひとえにHooks
実装機構の複雑さ(依存関係を明示的に指定する必要がある)による精神的負担によるものだと考えています。
結局のところ、同じHooks
コンセプトに従っている人にはVue Composition API
この問題はありません。
参考文献
[1]
React ドキュメント: https://react.dev/learn/separating-events-from-Effects