React may seem to be one of the least opinionated frameworks in the Wild West Web. Despite that, there's a lot of mistakes you can do and even more things you can do to write clean and readable code. This article explains 17 common anti-patterns and best practices in React.
In This Article
- Use useState Instead of Variables
- Declare CSS Outside Components
- Use useCallback To Prevent Function Recreations
- Use useCallback To Prevent Dependency Changes
- Use useCallback To Prevent useEffect Triggers
- Add an Empty Dependency List to useEffect When No Dependencies Are Required
- Always Add All Dependencies to useEffects and Other React Hooks
- Do Not Use useEffect To Initiate External Code
- Do Not Wrap External Functions in a useCallback
- Do Not Use useMemo With Empty Dependency List
- Do Not Declare Components Within Other Components
- Do Not Use Hooks in If Statements (No Conditional Hooks)
- Do Not Use Hooks After Return (No Conditional Hooks)
- Let Child Components Decide if They Should Render
- Use useReducer Instead of Multiple useState
- Write Initial States as Functions Rather Than Objects
- Use useRef Instead of useState When a Component Should Not Rerender
Use useState Instead of Variables
This first one should be a basic one, but I still see developers doing this, sometimes even seniors. To store a state in React you should always use one of the React hooks, like useState or useReducer. Never declare the state directly as a variable in a component. Doing so will redeclare the variable on every render which means that React cannot memoize things it normally memoizes.
import AnotherComponent from 'components/AnotherComponent'
const Component = () => {
// Don't do this.
const value = { someKey: 'someValue' }
return <AnotherComponent value={value} />
}
In the case above, AnotherComponent and everything that depends on value will rerender on every render, even if they are memoized with memo, useMemo or useCallback.
If you would add a useEffect to your component with value as a dependency, it would trigger on every render. The reason for that is that the JavaScript reference for value will be different on every render.
By using React's useState, React will keep the same reference for value all until you update it with setValue. React will then be able to detect when to and when not to trigger effects and recalculate memoizations.
import { useState } from 'react'
import AnotherComponent from 'components/AnotherComponent'
const Component = () => {
// Do this instead.
const [value, setValue] = useState({ someKey: 'someValue' })
return <AnotherComponent value={value} />
}
If you only need a state that is initiated once, and then never updated, then declare the variable outside the component. When doing that, the JavaScript reference will never change.
// Do this if you never need to update the value.
const value = { someKey: 'someValue' }
const Component = () => {
return <AnotherComponent value={value} />
}
Declare CSS Outside Components
If you are using a CSS in JS solution, avoid declaring CSS within components.
import makeCss from 'some/css/in/js/library'
const Component = () => {
// Don't do this.
return <div className={makeCss({ background: red, width: 100% })} />
}
The reason why not to do it is because the object has to be recreated on every render. Instead, lift it out of the component.
import cssLibrary from 'some/css/in/js/library'
// Do this instead.
const someCssClass = makeCss({
background: red,
width: 100%
})
const Component = () => {
return <div className={someCssClass} />
}
Use useCallback To Prevent Function Recreations
Whenever a functional React component is rerendered, it will recreate all normal functions in the component. React provided a useCallback hook that can be used to avoid that. useCallback will keep the old instance of the function between renders as long as its dependencies doesn't change.
import { useCallback } from 'react'
const Component = () => {
const [value, setValue] = useState(false)
// This function will be recreated on each render.
const handleClick = () => {
setValue(true)
}
return <button onClick={handleClick}>Click me</button>
}
import { useCallback } from 'react'
const Component = () => {
const [value, setValue] = useState(false)
// This function will only be recreated when the variable value updates.
const handleClick = useCallback(() => {
setValue(true)
}, [value])
return <button onClick={handleClick}>Click me</button>
}
This time, I won't say do this or do that. Some people would tell you to optimize each function with a useCallback hook, but I won't. For small functions like the one in the example, I can't assure it really is better to wrap the function in useCallback.
Under the hood, React will have to check dependencies on every render to know if a new function needs to be created or not, and sometimes the dependencies changes frequently anyways. The optmization useCallback gives might therefore not always be needed.
If the dependencies to the function doesn't update a lot though, useCallback can be a good optimization to avoid recreating the function on each render.
Is React difficult? Join this dude creating another js framework!
Use useCallback To Prevent Dependency Changes
While useCallback can be used to avoid function instantiations, it can also be used for something even more important. Since useCallback keeps the same memory reference for the wrapped function between renders, it can be used to optimize usages of other useCallbacks and memoizations.
import { memo, useCallback, useMemo } from 'react'
const MemoizedChildComponent = memo({ onTriggerFn }) => {
// Some component code...
})
const Component = ({ someProp }) => {
// Reference to onTrigger function will only change when someProp does.
const onTrigger = useCallback(() => {
// Some code...
}, [someProp])
// This memoized value will only update when onTrigger function updates.
// The value would be recalculated on every render if onTrigger wasn't wrapper in useCallback.
const memoizedValue = useMemo(() => {
// Some code...
}, [onTrigger])
// MemoizedChildComponent will only rerender when onTrigger function updates.
// If onTrigger wasn't wrapped in a useCallback, MemoizedChildComponent would rerender every time this component renders.
return (<>
<MemoizedChildComponent onTriggerFn={onTrigger} />
<button onClick={onTrigger}>Click me</button>
</>)
}
Use useCallback To Prevent useEffect Triggers
The previous example showed how to optimize renders with help of useCallback, in the same way, it is also possible to avoid unnecessary useEffect triggers.
import { useCallback, useEffect } from 'react'
const Component = ({ someProp }) => {
// Reference to onTrigger function will only change when someProp does.
const onTrigger = useCallback(() => {
// Some code...
}, [someProp])
// useEffect will only run when onTrigger function updates.
// If onTrigger wasn't wrapped in a useCallback, useEffect would run every time this function renders.
useEffect(() => {
// Some code...
}, [onTrigger])
return <button onClick={onTrigger}>Click me</button>
}
Add an Empty Dependency List to useEffect When No Dependencies Are Required
If you have an effect which isn't dependent on any variables, make sure to an empty dependency list as the second argument to useEffect. If you don't do that, the effect will run on every render.
import { useEffect } from 'react'
const Component = () => {
useEffect(() => {
// Some code.
// Do not do this.
})
return <div>Example</div>
}
import { useEffect } from 'react'
const Component = () => {
useEffect(() => {
// Some code.
// Do this.
}, [])
return <div>Example</div>
}
The same logic applies to other React hooks, such as useCallback and useMemo. Although, as described later in this article, you may not need to use those hooks at all if you don't have any dependencies.
Always Add All Dependencies to useEffects and Other React Hooks
When dealing with dependency lists for built-in React hooks, such as useEffects and useCallback, make sure to always add all dependencies to the dependency list (second argument of the hooks). When a dependency is omitted, the effect or callback may use an old value of it which often results in bugs which can be hard to detect.
Adding all variables may be a very tricky thing to do, sometimes you simply don't want an effect to run again if a value updates, but trying to find a solution for it will not only save you from bugs, it usually leads to better written code as well.
Even more important, if you leave out a dependency to prevent a bug, that bug will come back for you when upgrading to newer React versions. In strict mode in React 18, updating hooks (e.g. useEffect, useMemo) are triggered twice in development, and that may happen in production in future React versions.
Better add all dependencies to react hooks to be on the safe side
import { useEffect } from 'react'
const Component = () => {
const [value, setValue] = useState()
useEffect(() => {
// Some code.
// Don't neglect adding variables to dependency list.
// The value variable should be added here.
}, [])
return <div>{value}</div>
}
You may wonder, how can you circumvent side effects when useEffects are triggered more times than you wish? Unfortunately, there isn't a one-for-all solution to that. Different scenarios requires different solutions. You can try to use hooks to only run code once, that can sometimes be useful, but it isn't a solution to recommend really.
Most often you can solve your problem using if-cases. You can look at the current state and logically decide whether or not you really need to run the code. For example, if your reason not to add the variable value as a dependency to the effect above was to only run the code when value is undefined, you can simply add an if-statement inside the effect.
import { useEffect } from 'react'
const Component = () => {
const [value, setValue] = useState()
useEffect(() => {
if (!value) {
// Some code to run when value isn't set.
}
// Do this, always add all dependencies.
}, [value])
return <div>{value}</div>
}
Other scenarios may be more complex, maybe it isn't very feasible to use if-statements to prevent effects from happening multiple times. And if it isn't easily done, you should avoid it to avoid bugs. When that's the case, you should first ask yourself, do you really need an effect? There are a lot of cases where developers use effect when they really shouldn't do that.
However, life is not trivial, let's say you really do need to use useEffect, and you don't manage to easily solve it with if-cases. What else options do you have? Actually, the easy way is potentially the best way in this case, just to add all dependencies and let the effect run more times than you want it to.
Instead of trying to prevent code from being executing you can write the code so it doesn't matter if it is called multiple times or not. Such code is called to be idempotent, and suits very well with functional programming. Such behavior can be achieved by using caches, throttles and debounce functions. I may write and article explaining this topic in detail in the future, but for now, I will leave it here.
Do Not Use useEffect To Initiate External Code
Let say you want to run some code to initialize a library. Plenty of times I have seen initializion code like that being placed in an useEffect with an empty dependency list, which is completely unnecessary and error prone. If the function you call isn't dependent on a component's internal state, it should be initialized outside the component.
import { useEffect } from 'react'
import initLibrary from '/libraries/initLibrary'
const Component = () => {
// Do not do this.
useEffect(() => {
initLibrary()
}, [])
return <div>Example</div>
}
import initLibrary from '/libraries/initLibrary'
// Do this instead.
initLibrary()
const Component = () => {
return <div>Example</div>
}
If the component's internal state is needed for the initialization, you can put it in an useEffect, but if you are doing that, make sure you are adding all the dependencies you use to the dependency list of useEffect, as described under the previous heading.
Do Not Wrap External Functions in a useCallback
Just like in the case with triggering init functions in a useEffect above, you don't need a useCallback to call an external function. Simply just invoke the external function as is. This saves React from having to check if the useCallback needs to be recreated or not, and it makes the code briefer.
import { useCallback } from 'react'
import externalFunction from '/services/externalFunction'
const Component = () => {
// Do not do this.
const handleClick = useCallback(() => {
externalFunction()
}, [])
return <button onClick={handleClick}>Click me</button>
}
import externalFunction from '/services/externalFunction'
const Component = () => {
// Do this instead.
return <button onClick={externalFunction}>Click me</button>
}
Valid use cases for using a useCallback are when the callback calls multiple functions or when it reads or updates an internal state, such as a value from useState hook or one of the components passed-in props.
import { useCallback } from 'react'
import { externalFunction, anotherExternalFunction } from '/services'
const Component = ({ passedInProp }) => {
const [value, setValue] = useState()
// This is okay...
const handleClick = useCallback(() => {
// ...because we call multiple functions.
externalFunction()
anotherExternalFunction()
// ...because we read and/or set an internal value or prop.
setValue(passedInProp)
}, [passedInProp, value])
return <button onClick={handleClick}>Click me</button>
}
Do Not Use useMemo With Empty Dependency List
If you ever add a useMemo with an empty dependency list, ask yourself why you are doing so.
Is it because it is dependent on a component's state variable and you don't want to add it? In that case, we have already discussed that, you should always list all dependency variables!
Is it because the useMemo doesn't really have any dependencies? Well, then just lift it out of the component, it doesn't belong in there!
import { useMemo } from 'react'
const Component = () => {
// Do not do this.
const memoizedValue = useMemo(() => {
return 3 + 5
}, [])
return <div>{memoizedValue}</div>
}
// Do this instead.
const memoizedValue = 3 + 5
const Component = () => {
return <div>{memoizedValue}</div>
}
Do Not Declare Components Within Other Components
I see this a lot, please stop doing it already.
const Component = () => {
// Don't do this.
const ChildComponent = () => {
return <div>I'm a child component</div>
}
return <div><ChildComponent /></div>
}
What is the problem with it? The problem is that you are misusing React. As discussed before, variables declared within a component will be redeclared every time the component renders. In this case, it means that the functional child component has to be recreated every time the parent rerenders.
This is problematic for multiple reasons.
- A function will have to be instantiated on every render.
- React won't be able to decide when to do any kind of component optimizations.
- If hooks are used in ChildComponent, they will be reinitiated on every render.
- The component's lines of code increases and it gets hard to read. I have seen files with tens or maybe twenties of these child components within a single React component!
What to do instead? Merely declare the child component outside the parent component.
// Do this instead.
const ChildComponent = () => {
return <div>I'm a child component</div>
}
const Component = () => {
return <div><ChildComponent /></div>
}
Or even better, in a separate file.
// Do this instead.
import ChildComponent from 'components/ChildComponent'
const Component = () => {
return <div><ChildComponent /></div>
}
Remember, I write this article for a reason
Do Not Use Hooks in If Statements (No Conditional Hooks)
This one is explained in React's Documentation. One should never write conditional hooks, simply as that.
import { useState } from 'react'
const Component = ({ propValue }) => {
if (!propValue) {
// Don't do this.
const [value, setValue] = useState(propValue)
}
return <div>{value}</div>
}
Do Not Use Hooks After Return (No Conditional Hooks)
If statements are conditional by definition, it's therefore easy to understand that you shouldn't place hooks within them when reading React's Documentation.
A little more sneaky keyword is the "return" keyword. Many people don't realize "return" can result in conditional hook renders. Look at this example.
import { useState } from 'react'
const Component = ({ propValue }) => {
if (!propValue) {
return null
}
// This hook is conditional, since it will only be called if propValue exists.
const [value, setValue] = useState(propValue)
return <div>{value}</div>
}
As you can see, a conditional return statement will make a succeeding hook conditional. To avoid this, put all your hooks above the component's first conditional rendering. Or, easier to remember, simply always put you hooks at the top of the component.
import { useState } from 'react'
const Component = ({ propValue }) => {
// Do this instead, place hooks before conditional renderings.
const [value, setValue] = useState(propValue)
if (!propValue) {
return null
}
return <div>{value}</div>
}
Let Child Components Decide if They Should Render
This one isn't something you always should do, but in many situations it's appropriate. Let's consider the following code.
import { useState } from 'react'
const ChildComponent = ({ shouldRender }) => {
return <div>Rendered: {shouldRender}</div>
}
const Component = () => {
const [shouldRender, setShouldRender] = useState(false)
return <>
{ !!shouldRender && <ChildComponent shouldRender={shouldRender} /> }
</>
}
Above is a common way to conditionally render a child component. The code is fine, apart from being a bit verbose when there are many child components. But dependent on what ChildComponent does, there may exist a better solution. Let's rewrite the code slightly.
import { useState } from 'react'
const ChildComponent = ({ shouldRender }) => {
if (!shouldRender) {
return null
}
return <div>Rendered: {shouldRender}</div>
}
const Component = () => {
const [shouldRender, setShouldRender] = useState(false)
return <ChildComponent shouldRender={shouldRender} />
}
In the example above, we have rewritten the two component's to move the conditional rendering into the child component. You may wonder, what's the benefit of moving conditional rendering into the child component?
The biggest benefit is that React can continue rendering ChildComponent even when it isn't visible. That means, ChildComponent can keep its state when it is hidden and then later getting rendered a second time without losing its state. It's always there, just not visible.
If the component instead would stop rendering, as it does with the first code, states saved in useState would be reset, and useEffects, useCallbacks and useMemos would all need to rerun and recalculate new values as soon as the component renders again.
If your code would trigger some network requests or doing some heavy calculations, those would also run when the component is rendered again. Likewise, if you would have some form data stored in the component's internal state, that would reset every time the component goes hidden.
As initially mentioned, this isn't something you always want to do. Sometimes you really want the component to unmount completely. For example, if you have a useEffect within the child component, you may not want to continue running it on rerenders. See the example below.
const ChildComponent = ({ shouldRender, someOtherPropThatChanges }) => {
useEffect(() => {
// If we don't want this code to run when shouldRender is false,
// then don't keep render this component when shouldRender is false.
}, [someOtherPropThatChanges])
if (!shouldRender) {
return null
}
return <div>Rendered: {shouldRender}</div>
}
const Component = () => {
const [shouldRender, setShouldRender] = useState(false)
return <ChildComponent
shouldRender={shouldRender}
someOtherPropThatChanges={someOtherPropThatChanges} />
}
We could of course use conditional logic inside the child component to make the code above to work, but that could be error-prone. And please recall, conditional hooks aren't allowed, so you cannot place the useEffect after the if statement.
const ChildComponent = ({ shouldRender, someOtherPropThatChanges }) => {
if (!shouldRender) {
return null
}
useEffect(() => {
// We cannot avoid running this useEffect by putting it after the
// null-render. Conditional hook rendering is not allowed in React!
}, [someOtherPropThatChanges])
return <div>Rendered: {shouldRender}</div>
}
const Component = () => {
const [shouldRender, setShouldRender] = useState(false)
return <ChildComponent
shouldRender={shouldRender}
someOtherPropThatChanges={someOtherPropThatChanges} />
}
Use useReducer Instead of Multiple useState
Instead of bloating the component with multiple useState, you can use one useReducer instead. It may be cumbersome to write, but it will both avoid unnecessary renders and can make the logic more understandable. Once you have a useReducer, it will be much easier to add new logic and states to your component.
There's no magical number of how many useState to write before refactoring to useReducer, but I would personally say around three.
import { useState } from 'react'
const Component = () => {
// Do not add a lot of useState.
const [text, setText] = useState(false)
const [error, setError] = useState('')
const [touched, setTouched] = useState(false)
const handleChange = (event) => {
const value = event.target.value
setText(value)
if (value.length < 6) {
setError('Too short')
} else {
setError('')
}
}
return <>
{!touched && <div>Write something...</div> }
<input type="text" value={text} onChange={handleChange} />
<div>Error: {error}</div>
</>
}
import { useReducers } from 'react'
const UPDATE_TEXT_ACTION = 'UPDATE_TEXT_ACTION'
const RESET_FORM = 'RESET_FORM'
const getInitialFormState = () => ({
text: '',
error: '',
touched: false
})
const formReducer = (state, action) => {
const { data, type } = action || {}
switch (type) {
case UPDATE_TEXT_ACTION:
const text = data?.text ?? ''
return {
...state,
text: text,
error: text.length < 6,
touched: true
}
case RESET_FORM:
return getInitialFormState()
default:
return state
}
}
const Component = () => {
const [state, dispatch] = useReducer(formReducer, getInitialFormState());
const { text, error, touched } = state
const handleChange = (event) => {
const value = event.target.value
dispatch({ type: UPDATE_TEXT_ACTION, text: value})
}
return <>
{!touched && <div>Write something...</div> }
<input type="text" value={text} onChange={handleChange} />
<div>Error: {error}</div>
</>
}
Need even more structure, consider using TypeScript!
Write Initial States as Functions Rather Than Objects
Note the code from the current tip. Look at getInitialFormState function.
// Code removed for brevity.
// Initial state is a function here.
const getInitialFormState = () => ({
text: '',
error: '',
touched: false
})
const formReducer = (state, action) => {
// Code removed for brevity.
}
const Component = () => {
const [state, dispatch] = useReducer(formReducer, getInitialFormState());
// Code removed for brevity.
}
See that I wrote the initial state as a function. I could rather have used an object directly.
// Code removed for brevity.
// Initial state is an object here.
const initialFormState = {
text: '',
error: '',
touched: false
}
const formReducer = (state, action) => {
// Code removed for brevity.
}
const Component = () => {
const [state, dispatch] = useReducer(formReducer, initialFormState);
// Code removed for brevity.
}
Why didn't I do that? The answer is simple, to avoid mutability. In the case above, when initialFormState is an object, we could happen to mutate the object somewhere in our code.
If that's the case, we wouldn't get the initial state back if we used the variable another time, for example when resetting the form. We would instead get the mutated object where for example touched could have a value of true.
That is also the case when running unit tests. When testing the code above, several tests could use the initialFormState and mutate it. Each test would then work when they run individually, while some of the tests would likely fail when all tests ran together in a test suite.
For that reason, it's a good practice to turn initial states into getter functions that returns the initial state object. Or even better, use libraries like Immer which is used to avoid writing mutable code.
Use useRef Instead of useState When a Component Should Not Rerender
Did you know you can optimize component renderings by replacing useState with useRef? Check this code.
import { useEffect } from 'react'
const Component = () => {
const [triggered, setTriggered] = useState(false)
useEffect(() => {
if (!triggered) {
setTriggered(true)
// Some code to run here...
}
}, [triggered])
}
When you run the code above, the component will rerender when setTriggered is invoked. In this case, triggered state variable could be a way to make sure that the effect only runs one time (which actually doesn't work in React 18, learn why in this article about useRunOnce hook).
Since the only use of triggered variable in this case, is to keep track if a function has been triggered or not, we do not need the component to render any new state. We can therefore replace useState with useRef, which won't trigger the component to rerender when it is updated.
import { useRef } from 'react'
const Component = () => {
// Do this instead.
const triggeredRef = useRef(false)
useEffect(() => {
if (!triggeredRef.current) {
triggeredRef.current = true
// Some code to run here...
}
// Note missing dependency. This isn't optimal.
}, [])
}
Note the missing dependency to the useEffect, that one is a bit tricker to fix when using useRef, but React explains it in their documentation.
In the case above, you may wonder why we need to use a useRef at all. Why can't we simply use a variable outside of the component?
// This does not work the same way!
const triggered = false
const Component = () => {
useEffect(() => {
if (!triggered) {
triggered = true
// Some code to run here...
}
}, [])
}
The reason we need a useRef is because the above code doesn't work in the same way! The above triggered variable will only be false once. If the component unmounts, the variable triggered will still be set to true when the component mounts again, because the triggered variable is not bound to React's life cycle.
When useRef is used, React will reset its value when a component unmounts and mounts again. In this case, we probably would want to uses useRef, but in other cases a variable outside the component may be what we are searching for.
Top comments (0)