Each framework will have some unique concepts due to the difference in implementation principles. for example:
-
Vue3
Due to its responsive implementation principle, concepts such asref
, , and so on are derivedreactive
-
Svelte
Heavily dependent on its own compiler, so it derives compilation-related concepts (such as itslabel
innovative use of tags)
In React
, there is a "very easy" to be misused API
- useEffect
, what I want to introduce today Effect Event
belongs to useEffect
the concept derived from .
misused useEffect
This article will cover three concepts:
-
Event
(event) -
Effect
(side effect) -
Effect Event
(side effect event)
First let's chat Event
with Effect
. useEffect
It is also easy to be misused because the two concepts are easily confused.
The concept of Event
In the code below, the click div
triggers the click event, onClick
which is the click callback. Among them onClick
belong to Event
:
function App() {
const [num , update] = useState(0);
function onClick() {
update(num + 1);
}
return (
<div onClick={onClick}>{num}</div>
)
}
Event
The characteristic of is: "logic that is triggered by some behavior, not a state change" .
For example, in the above code, the logic onClick
is triggered by the behavior of "click event"num
, and the state change will not be triggered onClick
.
The concept of Effect
Effect
On the contrary Event
, he is "triggered by some state changes, not logic triggered by some behavior" .
For example, in the following code, when title
changed , document.title
it will be updated to title
the value:
function Title({title}) {
useEffect(() => {
document.title = title;
}, [title])
// ...
}
The logic in the above code useEffect
belongs to Effect
, he is title
triggered by the change. In useEffect
addition, the following two Hook
also belong to Effect
:
-
useLayoutEffect
(uncommonly used) -
useInsertionEffect
(very rarely used)
Why is it easy to misuse?
Now the question is: why is it misused because the concept of Event
and is completely different?Effect
For example, in the first version of the project, we useEffect
had logic for initializing data in:
function App() {
const [data, updateData] = useState(null);
useEffect(() => {
fetchData().then(data => {
// ...一些业务逻辑
// 更新data
updateData(data);
})
}, []);
// ...
}
As the project develops, you receive another requirement: update the data after submitting the form.
In order to reuse the previous logic, you add a new options
state (save form data) and use it as useEffect
a dependency:
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);
}
// ...
}
Now, after submitting the form (triggering onSubmit
the callback), the previous data initialization logic can be reused.
It is so convenient to do so that many students think that this is useEffect
the best usage. But in fact this is a typical "useEffect misuse" .
After careful analysis, we will find that: "Submitting the form" is obviously a Event
(triggered by the submitted behavior), Event
the logic should be written in the event callback, not useEffect
in. The correct way of writing should be like this:
function App() {
const [data, updateData] = useState(null);
useEffect(() => {
fetchData().then(data => {
// ...一些业务逻辑
// 更新data
updateData(data);
})
}, []);
function onSubmit(opt) {
fetchData(opt).then(data => {
// ...一些业务逻辑
// 更新data
updateData(data);
})
}
// ...
}
The logic of the above example is relatively simple, and there is little difference between the two writing methods. But in actual projects, as the project continues to iterate, the following code may appear:
useEffect(() => {
fetchData(options).then(data => {
// ...一些业务逻辑
// 更新data
updateData(data);
})
}, [options, xxx, yyy, zzz]);
At that point, it's hard to know fetchData
under what circumstances the method will be executed because:
-
useEffect
too many dependencies -
Difficult to fully grasp the timing of each dependency change
Therefore, in React
, we need to clearly distinguish Event
and Effect
, that is, clearly distinguish "is a piece of logic triggered by a behavior or a state change?"
Dependency problem of useEffect
Now, we have been able to clearly distinguish Event
and Effect
, it stands to reason that there will be no problem writing projects. However, due to "Effect's mechanism problem" , we also face a new problem.
Suppose we have a chat room code that roomId
needs to be reconnected to the new chat room when it changes. In this scenario, the disconnection/reconnection of the chat room depends on roomId
the state change, which obviously belongs to Effect
the following code:
function ChatRoom({roomId}) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId]);
// ...
}
Next, you receive a new requirement - when the connection is successful, a "global reminder" will pop up :
Whether "Global Reminder"theme props
is a dark mode is affected. useEffect
The modified code is as follows:
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
connection.on('connected', () => {
showNotification('连接成功!', theme);
});
return () => connection.disconnect();
}, [roomId, theme]);
But there is a serious problem with this code - anything that causes theme
a change will cause the chat room to disconnect/reconnect. After all, theme
so are useEffect
dependencies.
In this example, although Effect
dependent theme
, it is not triggered Effect
by a change (it is triggered by a change).theme
roomId
In order to deal with this scenario, React
a new concept - . He refers to those "executed in Effect, but Effect does not depend on the logic of the state" , such as in the above example: Effect Event
() => {
showNotification('连接成功!', theme);
}
We can use useEffectEvent
(this is an experimental Hook
) definition 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]);
// ...
}
In the above code, theme
it is moved to onConnected
(he is a Effect Event
), although the latest value of the latest value useEffect
is used , it does not need to be used as a dependency.theme
useEffectEvent source code analysis
useEffectEvent
The implementation is not complicated, the core code is as follows:
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);
};
}
Among them, ref
the variable holds the "reference of callback" . For the above example:
const onConnected = useEffectEvent(() => {
showNotification('连接成功!', theme);
});
ref
Save references to functions like:
() => {
showNotification('连接成功!', theme);
}
useEffectEventImpl
The method accepts ref
and callback的最新值
as parameters, and will update the value saved in to useEffect
before execution .ref
callback引用
callback的最新值
Therefore, when useEffect
executed in onConnected
, what is obtained is ref
the latest value of the following closures saved in :
() => {
showNotification('连接成功!', theme);
}
Naturally also the latest value in the closure theme
.
useEffectEvent and useEvent
Carefully observe useEffectEvent
the return value, it contains two restrictions:
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
The first limitation is more obvious - the return value of the following line of code restriction can only useEffect
be executed in the callback (otherwise an error will be reported)
if (isInvalidExecutionContextForEventFunction()) {
// ...
}
Another restriction is more subtle - the return value is a brand new reference:
return function eventFn() {
// ...
};
If you don't quite understand why "fresh reference" is a limitation, consider returning a useCallback
return value:
return useCallback((...args) => {
const fn = ref.impl;
return fn(...args);
}, []);
This will make useEffectEvent
the return value of the function an unchanging reference. If the restriction of "can only be executed in the useEffect callback"useEffectEvent
is removed, it will be an enhanced version useCallback
.
For example, if the above restrictions are broken, then for the following code:
function App({a, b}) {
const [c, updateC] = useState(0);
const fn = useCallback(() => a + b + c, [a, b, c])
// ...
}
Instead , the code is as useEffectEvent
follows useCallback
:
const fn = useEffectEvent(() => a + b + c)
Compared with it useCallback
, he has 2 advantages:
-
without explicitly declaring dependencies
-
Even if the dependency changes,
fn
the reference will not change, which is simply the best choice for performance optimization
So React
why useEffectEvent
limit it?
In fact, useEffectEvent
the predecessor of useEvent
is following the above implementation, but due to:
-
useEvent
The positioning should beEffect Event
, but the actual use is wider (can be replaceduseCallback
), which does not meet his positioning -
Currently ( the official compiler that can generate code equivalent to ) is not considered ,
React Forget
if this is added , it will increase the difficulty of implementationuseMemo
useCallback
useEvent
hook
React Forget
So, useEvent
it didn't officially go into the standard. Instead, the more restrictive useEffectEvent
ones found their way into the React documentation [1].
Summarize
Today we learned three concepts:
-
Event
: logic triggered by some behavior, not a state change -
Effect
: Logic triggered by some state change, not some behavior -
Effect Event
: ExecutedEffect
inside , butEffect
does not depend on the logic of the state
The specific implementation of which is Effect Event
in . Compared to his predecessor , he has 2 additional restrictions:React
useEffectEvent
useEvent
-
Can only be executed
Effect
within -
always returns a different reference
In my opinion, Effect Event
the emergence of is entirely due to Hooks
the complexity of the implementation mechanism (dependence must be explicitly specified) caused by the mental burden.
After all, those who also follow Hooks
the concept Vue Composition API
don't have this problem.
References
[1]
React documentation: https://react.dev/learn/separating-events-from-effects