useState
anduseEffect
useState
and useEffect
are the basics that drive React hooks
. useState
is used to manage state, while useEffect
is used to handle side effects. Understanding the operation principles of useState
and useEffect
by writing them manually.
A simple usage of useState
is as follows.
When the page is first rendered, it will render the <App />
function component. In reality, it calls the App()
method, obtains a virtual DOM
element, and renders it on the browser page. When the user clicks the button
, it calls the addCount
method, and then re-renders the <App />
function component. In reality, it calls the App()
method again, obtains a new virtual DOM
element, and then React
executes the DOM diff
algorithm, updating the changed parts to the browser page. In other words, each time setCount
is called, it will re-execute the App()
function. This can be seen in the effect of console.log("refresh")
, which prints "refresh" to the console every time the button is clicked.
So the question is, when the page is first rendered and when we perform the +1
operation, both times the App()
function is called to execute the line const [count, setCount] = useState(0);
, how does it manage to execute the same code after the +
operation without reinitializing the variable n
(which continues to be 0
), and instead, it retrieves the latest value of n
. Considering this problem, we can simply implement a useMyState
function. This brings up the issue raised in Hooks
about hooking into a function's scope. Therefore, we can completely implement a Hooks
function to hook into a scope. In essence, in useMyState
, we save a variable within a closure, and this variable holds the previous value. When called again, we simply retrieve this previously saved value, https://codesandbox.io/s/fancy-dust-kbd1i?file=/src/use-my-state-version-1.ts
.
You can see in code sandbox
that now we can click the button to perform the ++
operation, instead of always getting 0
no matter how many times you click. However, the above situation is too simple because there is only one state
. If multiple variables are used, you need to call useState
twice. We need to make some improvements, otherwise it will cause multiple variables to exist in one saveState
, resulting in conflicts and overrides. There are two ways to improve: 1
making it into an object, such as saveState = { n:0, m:0 }
, but this way does not quite meet the requirement because useState
only passes an initial value parameter and does not pass a name. 2
make saveState
into an array, such as saveState: [0, 0]
. In fact, in React
, it is replaced by a form similar to a singly linked list, sequentially linking all the hooks
with next
. Using an array is a similar operation because both depend on the order of defining Hooks
, https://codesandbox.io/s/fancy-dust-kbd1i?file=/src/use-my-state-version-2.ts
.
You can see that independent state updates in multiple State
have been achieved. Then a new problem arises. <App />
uses saveState
and index
, but what about other components? In other words, how do multiple components resolve their independent scopes? One solution is for each component to create its own saveState
and index
, but several components in one file will lead to conflicts between saveState
and index
. Another solution is to place them on the corresponding virtual node objects for the components. This is also the approach adopted by React
, which places the saveState
and index
variables on the corresponding virtual node object FiberNode
. In React
, the specific implementation of saveState
is called memoizedState
. In reality, React
replaces arrays with a form similar to a singly linked list, linking all the hooks in order through next
.
It's evident that useState
relies heavily on the defined order. The order in which values are saved in the useState
array is crucial, as it allows the corresponding state value to be obtained by incrementing the index when executing function components. Since the order is maintained, it forces you to not change the order of useState
. For instance, using conditional judgment to determine whether to execute useState
will cause the obtained values to differ from the expected values. This problem also occurs with React.useState
itself. Therefore, React
does not allow you to use conditional judgment to control the order of useState
in function components, as it would result in chaotic values. The code snippet below would throw an exception similar to the following:
Of course, this is only a simple implementation of useState
. For the actual implementation in React
, you can refer to packages/react-reconciler/src/ReactFiberHooks.js
. The current version of React
is 16.10.2
, and you can also take a brief look at the related types.
Here's a simple use case of useEffect
.
In the same way, every time addCount1
is executed, the App()
function is re-executed. The console will print refresh
every time the button is clicked. Here, the change in count1
is used as a side effect to print count1 -> effect ${count1}
. However, clicking addCount2
does not trigger the side effect printing. The obvious reason is that we only specified the side effect of count1
. It can be seen that useEffect
can be used to achieve more granular side effect handling.
Here, we continue the train of thought of implementing useState
by storing the previous data and comparing whether the data has changed when the function is executed. If a change has occurred, the function is executed. Of course, we also need to complete the side effect cleanup function. Example Link.
With this implementation, we can also use the method of storing dependencies and cleanup functions to achieve the functionality of useEffect
. By comparing the previously passed dependency values with the current ones, we can determine whether to execute the passed function. Since we cannot know when the last Effect
of the React.FC
component function is completed, we need to manually assign the index
marker to 0
. In React
, useEffect
is also mounted on Fiber
and the required dependency values are stored in the memorizedState
of the current Fiber
. This is achieved by implementing a linked list and judging the initial load in order to connect all the hooks
in sequence through next
, thus determining which is the last Hooks
. Furthermore, useEffect
also strongly depends on the defined order, allowing React
to align the dependencies when executing the component function multiple times.
When I was first learning Hooks
, I always had a question about the difference between using React Hooks
and regular function calls, and at that time, I even answered a question on Zhihu.
In my understanding of React
after a few days of study, the difference between custom Hooks
and regular functions lies in:
Hooks
should only be called within React
function components and not in regular function calls.Hooks
can call functions such as useState
, useEffect
, and useContext
, while regular functions cannot.Therefore, I feel that Hooks
are like mixins
, a way to share state and side effects between components. So, functions related to component lifecycle and other component-related functions should be considered as Hooks
, rather than just regular utils
functions.
Regarding the first question, if a function is declared as a Hook
but does not fulfill the role of a Hook, then I believe it should not be called a Hook
. To avoid confusion, it is advisable to use the use
prefix when calling other Hooks
. Of course, for instance, even if you implement a useState
function that does not call other Hooks
, but is closely related to the functionality of function components, it definitely falls under the category of Hooks
.
As for the second question, the requirement to use a prefix of use
is not a syntax rule or a mandatory solution. Beginning with use
is more of a convention, similar to the convention that GET
requests do not carry a Body
. Its main purpose is still to constrain syntax. If you try to implement a simple functionality similar to useState
by yourself, you will understand why code like if (xxx) const [a, setA] = useState(0);
cannot appear. The React
documentation clearly states the rules for using Hooks
, and the purpose of using the use
prefix is to make React
recognize that it is a Hook
in order to check these rules. Typically, ESlint
is used in conjunction with eslint-plugin-react-hooks
to check these rules.
Later on, I gained a new understanding of this issue. If you define a truly custom Hook
, it usually needs to use useState
, useEffect
, and other Hooks
. This means that a custom Hook
is composed of official Hooks
, and by combining these official Hooks
, you can achieve the mounting of data onto nodes. In other words, the actual memorizedState
mentioned earlier is within the Fiber
, and functions self-implemented, such as the Hooks
implementation mentioned earlier, cannot achieve this. This means that through custom Hooks
, we encapsulate some state or other aspects of logic within the nodes by combining official Hooks
and our own logic, while using regular functions with a syntax similar to Hooks
can only encapsulate global state and logic. In simple terms, it provides an interface for us to encapsulate logic on nodes.
As an example, if we want to encapsulate a useUpdateEffect
to prevent the effect from being executed when a function component is first mounted, we should use useRef
or useState
rather than simply defining a variable to store state, as shown in this sandbox.
When we switch between use-update-effect-ref
and use-update-effect-var
of useUpdateEffect
, we will find that when refreshing the page, using use-update-effect-ref
will not print any value, while use-update-effect-var
will print count2 -> effect 0
. Clicking Count1++
or Count2++
works normally, indicating that use-update-effect-ref
can achieve the functionality we want, while use-update-effect-var
cannot correctly implement the functionality due to the problem of variable sharing. Of course, we can also solve this problem by using array-like methods, but when it comes to the sharing between specific components, we cannot achieve it based on the Hooks
syntax. We must manually register a closure to achieve similar functionality, and similar to updating the component and its child components when using useState
to refresh when set
is called, we must rely on useState
to achieve this.