A new React concept: Effect Event

Each framework will have some unique concepts due to the difference in implementation principles. for example:

  • Vue3Due to its responsive implementation principle, concepts such as ref, , and so on are derivedreactive

  • SvelteHeavily dependent on its own compiler, so it derives compilation-related concepts (such as its labelinnovative use of tags)

In React, there is a "very easy" to be misused API -  useEffect, what I want to introduce today Effect Eventbelongs to useEffectthe 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 Eventwith Effect. useEffectIt is also easy to be misused because the two concepts are easily confused.

The concept of Event

In the code below, the click divtriggers the click event, onClickwhich is the click callback. Among them onClickbelong to Event:

function App() {
  const [num , update] = useState(0);
  
  function onClick() {
    update(num + 1);
  }
  
  return (
    <div onClick={onClick}>{num}</div>
  )
}

EventThe characteristic of is: "logic that is triggered by some behavior, not a state change" .

For example, in the above code, the logic onClickis triggered by the behavior of "click event"num , and the state change will not be triggered onClick.

The concept of Effect

EffectOn the contrary Event, he is "triggered by some state changes, not logic triggered by some behavior" .

For example, in the following code, when titlechanged , document.titleit will be updated to titlethe value:

function Title({title}) {
  useEffect(() => {
    document.title = title;
  }, [title])
  
  // ...
}

The logic in the above code useEffectbelongs to Effect, he is titletriggered by the change. In useEffectaddition, the following two Hookalso 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 Eventand is completely different?Effect

For example, in the first version of the project, we useEffecthad 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 optionsstate (save form data) and use it as useEffecta 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 onSubmitthe callback), the previous data initialization logic can be reused.

It is so convenient to do so that many students think that this is useEffectthe 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), Eventthe logic should be written in the event callback, not useEffectin. 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 fetchDataunder what circumstances the method will be executed because:

  1. useEffecttoo many dependencies

  2. Difficult to fully grasp the timing of each dependency change

Therefore, in React, we need to clearly distinguish Eventand 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 Eventand 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 roomIdneeds to be reconnected to the new chat room when it changes. In this scenario, the disconnection/reconnection of the chat room depends on roomIdthe state change, which obviously belongs to Effectthe 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. useEffectThe 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 themea change will cause the chat room to disconnect/reconnect. After all, themeso are useEffectdependencies.

In this example, although Effectdependent theme, it is not triggered Effectby a change (it is triggered by a change).themeroomId

In order to deal with this scenario, Reacta 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, themeit is moved to onConnected(he is a Effect Event), although the latest value of the latest value useEffectis used , it does not need to be used as a dependency.theme

useEffectEvent source code analysis

useEffectEventThe 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, refthe variable holds the "reference of callback" . For the above example:

const onConnected = useEffectEvent(() => {
  showNotification('连接成功!', theme);
});

refSave references to functions like:

() => {
  showNotification('连接成功!', theme);
}

useEffectEventImplThe method accepts refand callback的最新值as parameters, and will update the value saved in to useEffectbefore execution .refcallback引用callback的最新值

Therefore, when useEffectexecuted in onConnected, what is obtained is refthe latest value of the following closures saved in :

() => {
  showNotification('连接成功!', theme);
}

Naturally also the latest value in the closure theme.

useEffectEvent and useEvent

Carefully observe useEffectEventthe 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);
};

useEffectEventThe first limitation is more obvious - the return value of the following line of code restriction can only useEffectbe 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 useCallbackreturn value:

return useCallback((...args) => {
    const fn = ref.impl;
    return fn(...args);
}, []);

This will make useEffectEventthe 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 useEffectEventfollows useCallback:

const fn = useEffectEvent(() => a + b + c)

Compared with it useCallback, he has 2 advantages:

  1. without explicitly declaring dependencies

  2. Even if the dependency changes, fnthe reference will not change, which is simply the best choice for performance optimization

So Reactwhy useEffectEventlimit it?

In fact, useEffectEventthe predecessor of useEventis following the above implementation, but due to:

  1. useEventThe positioning should be Effect Event, but the actual use is wider (can be replaced useCallback), which does not meet his positioning

  2. Currently ( the official compiler that can generate code equivalent to ) is not considered , React Forgetif this is added , it will increase the difficulty of implementationuseMemouseCallbackuseEventhookReact Forget

So, useEventit didn't officially go into the standard. Instead, the more restrictive useEffectEventones 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: Executed Effectinside , but Effectdoes not depend on the logic of the state

The specific implementation of which is Effect Eventin . Compared to his predecessor , he has 2 additional restrictions:ReactuseEffectEventuseEvent

  1. Can only be executed Effectwithin

  2. always returns a different reference

In my opinion, Effect Eventthe emergence of is entirely due to Hooksthe complexity of the implementation mechanism (dependence must be explicitly specified) caused by the mental burden.

After all, those who also follow Hooksthe concept Vue Composition APIdon't have this problem.

References

[1]

React documentation: https://react.dev/learn/separating-events-from-effects

Guess you like

Origin blog.csdn.net/huihui_999/article/details/131720253