Not long after Hooks was officially launched, I wrote an article On Hooks, which mainly provided a simple analysis and understanding of the concept. The new console developed during my internship fully utilized this feature, so I wanted to combine current practical experience to analyze best practices and comparisons for Hooks.
Capture Value#
Since the introduction of Hooks, functional components also possess state characteristics, making it more appropriate to refer to them as Function Components instead of Stateless Components. Here, I will first compare Function Components and Class Components based on the immutability of props:
Function Component:
function ProfilePage(props) {
setTimeout(() => {
// The parent component has rerendered, and props are still the initial ones
console.log(props)
}, 3000)
}
Class Component:
class ProfilePage extends React.Component {
render() {
setTimeout(() => {
// The parent component has rerendered, and this.props will change because this has changed
console.log(this.props)
}, 3000)
}
}
If you want to capture the initial props in a Class Component, you can use const props = this.props
, but this is somewhat of a hack, so to obtain stable props
, it is recommended to use Function Components.
Hooks:
function MessageThread() {
const [message, setMessage] = useState("")
const showMessage = () => {
alert("You said: " + message)
}
const handleSendClick = () => {
setTimeout(showMessage, 3000)
}
const handleMessageChange = e => {
setMessage(e.target.value)
}
return (
<>
<input value={message} onChange={handleMessageChange} />
<button onClick={handleSendClick}>Send</button>
</>
)
}
After clicking send
and modifying the input box value, the output after 3 seconds is still the value before clicking, indicating that Hooks also has the characteristic of Capture Value.
It can be considered that the content rendered each time forms a snapshot, and each Render state has its own fixed Props and State.
Not only objects, but functions are also independent during each render, which is the Capture Value characteristic.
In actual development, I have been caught by this feature, and to avoid capture value, you can use useRef
:
function MessageThread() {
const latestMessage = useRef("")
const showMessage = () => {
alert("You said: " + latestMessage.current)
}
const handleSendClick = () => {
setTimeout(showMessage, 3000)
}
const handleMessageChange = e => {
latestMessage.current = e.target.value
}
}
It can be considered that
ref
maintains a unique reference throughout all Render processes, so the value or assignment ofref
always retrieves a final state. It can also be simply understood thatref
is Mutable whilestate
is Immutable.
Lifecycle Method Alternatives#
- constructor: Function Components do not require an initial constructor; state can be initialized by calling
useState
. If the initial value computation is time-consuming, a function can be passed in, which will only execute once. - getDerivedStateFromProps: Reasonably schedule updates during rendering.
- shouldComponentUpdate: See
[React.memo](https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-shouldcomponentupdate)
. - render: This is the Function Component itself.
- componentDidMount, componentDidUpdate, componentWillUnmount: Their collection corresponds to
useEffect
. - componentDidCatch, getDerivedStateFromError: Corresponding Hook methods will be added in the near future.
Best Practices#
Component Definition#
const App: React.FC<{ title: string }> = ({ title }) => {
return React.useMemo(() => <div>{title}</div>, [title])
}
App.defaultProps = {
title: 'Function Component'
}
- Use
React.FC
to declare the Function Component type and define the Props parameter type. - Use
React.useMemo
to optimize rendering performance. - Use
App.defaultProps
to define default Props values.
Why not use React.memo?
Because there is a use of React.useContext
during component communication, which causes all components that use it to rerender. Only React.useMemo
can render as needed, and considering future maintenance, data may be injected at any time through useContext
, so it is recommended to use useMemo
even for components that do not have performance issues.
Why not use destructuring instead of defaultProps?
Although destructuring is more elegant in writing, there is a performance issue: for object types, the reference will change every time Rerender occurs.
Local State#
Arranged by common usage: useState
, useRef
, useReducer
.
useState
const [hide, setHide] = React.useState(false)
const [name, setName] = React.useState("jeremy")
State and function names should be self-explanatory, and it is recommended to declare them at the top for easy reference.
useRef
const dom = React.useRef(null)
useRef
should be used sparingly, as a large amount of Mutable data can affect code maintainability. It is recommended for objects that do not require repeated initialization.
useReducer
Local state is not recommended to use useReducer
, as it can lead to overly complex internal states. It is suggested to use it in conjunction with useContext
for communication between multiple components.
Is it appropriate to declare ordinary constants or functions directly within a function?
Since Function Components will re-execute on each render, constants are recommended to be placed outside the function to avoid performance issues. Functions should be declared using useCallback
to ensure accuracy and performance, and the second parameter of useCallback
must be filled in. The [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks)
will automatically fill in the dependencies.
Component Communication#
Simple component communication uses Props passing, while frequent communication between components uses useContext
.
useEffect Guide#
Function Components do not have lifecycle methods; they only describe the UI state, and then React synchronizes it to the DOM. The state rendered each time will be solidified, including state
, props
, useEffect
, and all internal functions.
The abandonment of lifecycle synchronization can lead to some performance issues, and we need to tell React how to compare Effects.
Dependencies of useEffect#
React will diff the content during DOM rendering, modifying only the changed parts. However, it cannot recognize incremental modifications to Effects, so developers must inform React of which external variables are used through the second parameter of useEffect
:
useEffect(() => {
document.title = 'Hello, ' + name
}, [name])
useEffect
will only execute again when the name
changes during Rerender. Manually maintaining this can be cumbersome, so you can use eslint for automatic prompts to fix.
Here, it is important to focus on the setting of dependencies:
Since useEffect conforms to the Capture Value characteristic, it is essential to handle dependencies properly to ensure the accuracy of the values obtained:
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => clearInterval(id)
}, [count])
If count
is not passed as a dependency here, the count
value obtained will always be the initialized 0
, making subsequent setCount
ineffective.
Passing count
allows access to the latest count
, but it leads to two problems:
- The timer becomes inaccurate because it will be destroyed and reset every time
count
changes. - The frequent creation and destruction of timers bring a certain performance burden.
There are issues whether to set dependencies or not; fundamentally, it is because we are relying on an external variable in an Effect that we only want to execute once.
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1)
}, 1000)
return () => clearInterval(id)
}, [])
setCount
also has a functional callback mode, which does not require concern for the current value; it only needs to modify the old value. This way, although the code always runs in the first Render, it can always access the latest state
.
The above solution does not completely resolve all scenarios, such as when two state
variables are simultaneously relied upon:
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step)
}, 1000)
return () => clearInterval(id)
}, [step])
Here, we have to rely on the step
variable. So how should we handle this now?
By using the useReducer
function to decouple updates from actions:
const [state, dispatch] = useReducer(reducer, initialState)
const { count, step } = state
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: "tick" })
}, 1000)
return () => clearInterval(id)
}, [dispatch])
This forms a local Redux, regardless of how many variables need to be relied upon during updates, no dependencies on any variables are needed during actual updates, and specific update operations can be written in the reducer
function.
Function and Effect#
If a function is not defined within useEffect
, it may not only miss dependencies, but the eslint plugin also cannot help automatically collect dependencies.
Any function that does not depend on variables within the Function Component can be directly extracted. But what about functions that depend on variables?
useCallback
:
function Parent() {
const [query, setQuery] = useState("react");
// ✅ Preserves identity until query changes
const fetchData = useCallback(() => {
const url = "https://hn.algolia.com/api/v1/search?query=" + query;
// ... Fetch data and return it ...
}, [query]); // ✅ Callback deps are OK
return <Child fetchData={fetchData} />;
}
function Child({ fetchData }) {
let [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, [fetchData]); // ✅ Effect deps are OK
// ...
}
Because functions also have the Capture Value characteristic, functions wrapped in useCallback
can be treated as ordinary variables for use as dependencies in useEffect
.
useCallback
returns a new function reference when its dependencies change, thus triggering the dependency change of useEffect
and activating it to re-execute.
In Class Components, if we want to refetch data when parameters change, we cannot directly compare the diff of the fetch function but must compare whether the fetch parameters have changed. Such code is not cohesive and difficult to maintain; in contrast, using useCallback
to encapsulate the fetch function in Function Components allows useEffect
to only care about whether this dependency changes, while the parameter changes are handled within useCallback
, making it easier to maintain with the help of the eslint plugin.
Cleanup Mechanism#
When a component is destroyed, listeners registered through useEffect
need to be cleaned up, which can be handled through its return value:
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
};
});
The callback function within the return value will execute when the component is destroyed. Due to the Capture Value characteristic, the values obtained during each registration and cleanup are paired fixed values.
If there is no reasonable cleanup return, it can easily lead to memory leaks. If an asynchronous function like async
is directly passed, useEffect will also warn. So how can we cancel the asynchronous function upon cleanup?
If the asynchronous method used supports cancellation, it can be directly canceled in the cleanup function. A simpler way is to use a boolean value:
function Article({ id }) {
const [article, setArticle] = useState(null);
useEffect(() => {
let didCancel = false;
async function fetchData() {
const article = await API.fetchArticle(id);
if (!didCancel) {
setArticle(article);
}
}
fetchData();
return () => {
didCancel = true;
};
}, [id]);
// ...
}
This article discusses more about handling error and loading states.
Other Advantages#
useEffect
executes at the end of rendering, which does not block the browser rendering process, aligning with the React Fiber philosophy. Fiber can pause or interrupt the rendering of different components based on circumstances. Code that follows the Capture Value characteristic can ensure safe access to values, and minimizing lifecycle methods can also resolve issues caused by interrupted execution.
React Hooks are still being improved and developed. Both official and community practices and libraries have valuable references and learning points. At the same time, Vue 3.0 has also borrowed ideas from React Hooks, resulting in Vue Hooks. Both have their strengths and weaknesses, and I will add more insights here after gaining a deeper understanding of this part.
References: