Advice #1: useHook appropriately
The React community has ever been so high when Hooks first came out. Hooks are cool! Hooks are trendy! Hooks is the future of React!
Some of my colleagues even attempted to migrate their project codebase to Hooks’ fantasy land. But be careful! Don’t use Hooks if you don’t understand the motivation why React team invented Hooks. Please spend at least 30 minutes to read and think deeply about it.
Here to sum up, I can give you 2 reasons why Hooks exists:
- To extract stateful logic from a component, so it can be tested independently and reused easily.
- To offer an alternative to
class
, becausethis
is full of confuse and surprise.
I admit that Hooks have fulfilled its duty and deserves praises. I do love Hooks! But my advice is if you don’t have any particular problem with those reasons mentioned above, and if your code is already concise and effective, then there is no need to rewrite all your components with React Hooks, unless you truly want to experiment the hype.
Advice #2: React Hooks will not replace Redux
Hooks are Great, but No.
Eric Elliott wrote an excellent article here to explain why Hooks is not an absolute replacement of Redux.
Hooks is a set of APIs (or functions) that helps you play with state and other React features. Meanwhile, Redux is an architecture that you use to organize your code and control the flow in a predictable manner.
When talking about Redux, we not just mention about a state management library, we mention about a huge Redux ecosystem with plenty of open source libraries and powerful tools to help you deal with side effects, immutability, persistence, …
Moreover, you can use Redux with Hooks together. So instead of reinventing the wheel, you can use both to achieve a greater good.
Advice #3: Avoid state mutation
Similar to this.setState
in class component, you should not modify state directly:
const [user, setUser] = useState({name: "Jane"})// wrong
user.name = "John"// will not re-render
// because the reference of user is still the same
setUser(user)
Instead, always create a new state:
const [user, setUser] = useState({name: "Jane"})// will trigger re-render
setUser({...user, name: "Jane"})
Advice #4: Understand how dependencies array works
Many React developers are used to component lifecycle, especially with componentDidMount
and componentDidUpdate
. So when learning Hooks, their first question usually is: “How to run this piece of code just one?”.
In other words, how to make useEffect
run only one time after render
(like componentDidMount
). And the answer is dependencies array.
It’s the array you pass as the second argument to useEffect
:
useEffect(() => {}), depArr)
Dependencies array is a list of values that are used in shallow comparison to determine when to re-run the effect. We can imagine a pseudo code like so:
// this aims to illustrate the idea.
// not the exact implementation of useEffect.
// the actual comparison algorithm React uses is Object.islastDeps = nulluseEffect = (func, deps) => {
if (!deps)
return func()
if (!lastDeps) {
lastDeps = [...deps] // shallow copy
return func()
}
for (i = 0; i < deps.length; ++i)
if (lastDeps[i] !== deps[i]) // shallow compare
return func()
}
Look at this pseudo implementation of useEffect
, you can better understand why and when a useEffect
will re-run in the following code:
// run every time after render
useEffect(() => {
console.log('run effect')
})// run only one time after first render
useEffect(() => {
console.log('run effect')
}, [])
In the code below, useEffect
performs a referential equality check, so it cannot determine if any properties of user
have changed or not.
const [user, setUser] = useState({name: "Jane"})// run after first render, and re-run when user changed
useEffect(() => {
console.log('run effect') // perform effect here
}, [user])...// press the button somewhere
onPress = () => {
setUser({name: "Jane"})
}
Although name
maintains the same value “Jane”, this will trigger a re-render and re-rerun the effect function.
Advice #5: Understand useCallback and useMemo
- If you pass a function to a child component as callback, you should memoize that function with useCallback.
- If you have data that is expensive to compute, you should memoize it with useMemo.
Example: Generate and display a random number.
Without useCallback
:
- anytime you click
RandomButton
, it callssetNumber
, which triggers re-render onContainer
(see log) - when
Container
re-renders, it initiates a newrandomize
function, then pass this function down toRandomButton
as prop. RandomButton
receives a new prop and decides to re-render (see log).
const Container = () => {
const [number, setNumber] = useState(0)
const randomize = () => setNumber(Math.random()) console.log("render Container")
return (
<>
<p>{number}</p>
<RandomButton randomize={randomize}/>
</>
)
}const RandomButton = ({randomize}) => {
console.log("render RandomButton")
return <button onClick={randomize}/>
}
Now, with useCallback
and React.memo
:
- when
Container
re-renders,useCallback
checks its dependencies array to determine whether to return the last memoized function or create a new one. - but in this case, we provide an empty array, which means
useCallback
will always return the samerandomize
function. - when passing
randomize
function as prop toRandomButton
, with the help ofReact.memo
, it checks and sees thatrandomize
prop is the same, so no need to re-render (see log).
const Container = () => {
const [number, setNumber] = useState(0)
const randomize = useCallback(() => setNumber(Math.random()), [])
console.log("render Container")
return (
<>
<p>{number}</p>
<RandomButton randomize={randomize}/>
</>
)
}const RandomButton = React.memo(({randomize}) => {
console.log("render RandomButton")
return <button onClick={randomize}/>
})
In this special occasion, we pass an empty array to useCallback
because its memoized function () => setNumber(Math.random())
doesn’t need any extra information to execute.
But what if we want to use number
state in the randomize
function, like so:
() => setNumber(number + Math.random())
In this situation, randomize
depends on number
state to execute correctly. So we specify number
as an dependency in useCallback
:
const randomize = useCallback(
() => setNumber(number + Math.random()), [number]
)
But still, this may not be a perfect solution because when randomize
calls setNumber
, it will update number
with a new value, then useCallback
checks that its dependencies have changed and create a new randomize
function, resulting in an unnecessary re-render of RandomButton
again.
In order to deal with this dilemma, we can use functional updates. This is especially useful when the new state is computed using the previous state.
A perfect solution is as follow:
const randomize = useCallback(
() => setNumber(prevNumber => prevNumber + Math.random()), []
)