DEV Community

Cover image for React Anti-Patterns and Best Practices - Do's and Don'ts
Dennis Persson
Dennis Persson

Posted on • Originally published at perssondennis.com

React Anti-Patterns and Best Practices - Do's and Don'ts

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

  1. Use useState Instead of Variables
  2. Declare CSS Outside Components
  3. Use useCallback To Prevent Function Recreations
  4. Use useCallback To Prevent Dependency Changes
  5. Use useCallback To Prevent useEffect Triggers
  6. Add an Empty Dependency List to useEffect When No Dependencies Are Required
  7. Always Add All Dependencies to useEffects and Other React Hooks
  8. Do Not Use useEffect To Initiate External Code
  9. Do Not Wrap External Functions in a useCallback
  10. Do Not Use useMemo With Empty Dependency List
  11. Do Not Declare Components Within Other Components
  12. Do Not Use Hooks in If Statements (No Conditional Hooks)
  13. Do Not Use Hooks After Return (No Conditional Hooks)
  14. Let Child Components Decide if They Should Render
  15. Use useReducer Instead of Multiple useState
  16. Write Initial States as Functions Rather Than Objects
  17. 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} />
}
Enter fullscreen mode Exit fullscreen mode

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} />
}
Enter fullscreen mode Exit fullscreen mode

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} />
}
Enter fullscreen mode Exit fullscreen mode

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% })} />
}
Enter fullscreen mode Exit fullscreen mode

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} />
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode
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>
}
Enter fullscreen mode Exit fullscreen mode

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.

Another js framework meme
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>
   </>)
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode
import { useEffect } from 'react'

const Component = () => {
  useEffect(() => {
    // Some code.

    // Do this.
  }, [])

  return <div>Example</div>
}
Enter fullscreen mode Exit fullscreen mode

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.

Revengeful bug
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>
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode
import initLibrary from '/libraries/initLibrary'

// Do this instead.
initLibrary()

const Component = () => {
  return <div>Example</div>
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode
import externalFunction from '/services/externalFunction'

const Component = () => {
  // Do this instead.
  return <button onClick={externalFunction}>Click me</button>
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

// Do this instead.
const memoizedValue = 3 + 5

const Component = () => {
  return <div>{memoizedValue}</div>
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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.

  1. A function will have to be instantiated on every render.
  2. React won't be able to decide when to do any kind of component optimizations.
  3. If hooks are used in ChildComponent, they will be reinitiated on every render.
  4. 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>
}
Enter fullscreen mode Exit fullscreen mode

Or even better, in a separate file.

// Do this instead.
import ChildComponent from 'components/ChildComponent'

const Component = () => {
  return <div><ChildComponent /></div>
}
Enter fullscreen mode Exit fullscreen mode

Unstable build meme
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>
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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} /> }
  </>
}
Enter fullscreen mode Exit fullscreen mode

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} />
}
Enter fullscreen mode Exit fullscreen mode

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} />
}
Enter fullscreen mode Exit fullscreen mode

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} />
}
Enter fullscreen mode Exit fullscreen mode

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>
  </>
}
Enter fullscreen mode Exit fullscreen mode
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>
  </>
}
Enter fullscreen mode Exit fullscreen mode

Typescript meme
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.
}
Enter fullscreen mode Exit fullscreen mode

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.
}
Enter fullscreen mode Exit fullscreen mode

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])
}
Enter fullscreen mode Exit fullscreen mode

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.
  }, [])
}
Enter fullscreen mode Exit fullscreen mode

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...
    }
  }, [])
}
Enter fullscreen mode Exit fullscreen mode

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 (28)

Collapse
 
leob profile image
leob • Edited

Agreed, my impression of React (having used it, and having read numerous articles similar to this one) is that it requires an inordinate amount of discipline and low-level knowledge to build anything even moderately complex with React which is (a) going to perform (i.e. not doing a ton of re-renders) and (b) going to be maintainable ...

I said it before, this kind of low-level stuff should be taken care of by the framework

Thread Thread
 
perssondennis profile image
Dennis Persson

Same thoughts here. I do like React, but one shouldn't really have to think about optimizations for everything one does. That's what frameworks should do.

Thread Thread
 
adevinwild profile image
adil

Absolutely not agree with you, React is a great library to design web apps

I feel like you're talking about premature optimization, honestly I never use useCallback and I never count the number of re-renders...
I just develop my features, and then I think about optimizing them and imo, it's the best way to not struggle with React

Once you know more about the patterns, the do's and don'ts, your last concern should be optimization... but maybe I'm wrong.

Thread Thread
 
leob profile image
leob • Edited

Okay, but why then do people write THAT MUCH about THIS very topic (React component re-rendering and how to prevent it), here on dev.to (and elsewhere)?

I'd almost say it's counter-productive - it definitely gives off the impression that React is all about low-level "nerding" to manually optimize one's code for the least amount of re-renders, rather than being able to focus on "business value".

If, in many (most?) cases it doesn't really matter, then why does it seem the #1 React topic on dev.to ... it does give me second thoughts about the framework, and I'm probably not the only one.

P.S. I find the "solution" which consists of sprinkling useMemo and useCallback all over my codebase super ugly - if that's what the "experts" recommend (and they do) then it only confirms the reservations I have :)

Collapse
 
leob profile image
leob

Two big takeaways:

1) Do yourself a big favor by doing 'dynamic' stuff (or anything, really) OUTSIDE a (function) component, not INSIDE of it - because if you do it "inside" the component declaration, then chances are it will always cause the component to re-render (I remember being bitten by this myself, heavily even)

2) React "the library" needs eternal baby-sitting, by the developer, in order to optimize (minimize) the amount of re-rendering - I'd argue that most of the "deep expertise" of expert React developers consists of intimate knowledge of what causes re-renders, and ways to avoid that :-D

Collapse
 
pankajsanam profile image
Pankaj • Edited

This is a really good and detailed article, but it just reinforced my dislike for React. I don't want to have to worry about all these optimizations; I expect the framework or library to handle them. I truly hope that Vue and Svelte will gain more traction and eventually surpass React.

Unfortunately, I have to use React because of its market share, popularity, and the size of its community. If given the chance, I would switch away from this library in a heartbeat

Collapse
 
jankapunkt profile image
Jan Küster

I completely misunderstood useEffect, for example: I often use useEffect to listen for a specific prop and then reset the internal state and after that send back and "ok" to the parent.

Now React is really annoyed that I did not put all deps into the effect deps list, because that was my intention: run the hook only when this single prop changed... Now I have to work around all this with useCallback etc...

Collapse
 
perssondennis profile image
Dennis Persson

Me too thought about it that way before. The transition from class components to function components naturally lead to thinking about functional components in life cycles. And in the beginning, that might have been the intention, but today, that doesn't work very well.

useEffect is a bit flawed in its design. I would rather see it being replaced with other hooks which are easier to find perfect use cases for.

Collapse
 
leob profile image
leob

The first one had me confused:

Use useState Instead of Variables

until you said "move the variable declaration outside the component" and THEN it made sense ... :)

Collapse
 
bsides profile image
Rafael Pereira

Same here, I was so worried for the whole article ;)

Collapse
 
leob profile image
leob

Yeah that one looked genuinely weird :)

Collapse
 
raibtoffoletto profile image
Raí B. Toffoletto

Nice article! There are good tips here, some I have a different opinion on, but well done 🎉

If your reducer logic is good and prevents mutability you don't need a function to get its initial state. And you shouldn't use that variable anywhere else but to initiate state on mount... just my opinion. 😉

 
leob profile image
leob • Edited

Yeah well of course React is still dominant, very dominant, in the market (that is, the job market, and market share in general) - so, people's "low level" expertise regarding how to avoid all these performance pitfalls is worth gold - in other words, people who've "invested" in React obviously have a lot to lose ... :)

Riot.js may be great, but there's half a dozen other more or less new-ish frameworks (Svelte, SolidJS, I could go on) which are also great, each with a small or tiny market share - fragmentation which makes React stronger :)

Inertia ... the 900 pound gorilla effect of the "incumbent" ... the "it's the safe choice" effect ... the "nobody ever got fired for recommending IBM" effect ... and more classics like that :-D

Thread Thread
 
adevinwild profile image
adil

"why then do people write THAT MUCH about THIS very topic (React component re-rendering and how to prevent it)"

I guess people have discovered the value of certain hooks and really want to put them forward
But personally , it's absolutely not the thing I spend most of my time on when I create.

I used SolidJS, and really liked it, I even find the concept of signals much less limiting than states with React

On the other hand, I never tested Vue or Svelte and never heard of Riot.js 🤔

Collapse
 
truemail785 profile image
Ozair • Edited

Is it a good idea to leave the dependency array of useCallback empty so that the function is only initialized once? Because the callback will be generating output OR performing something based on the up-to-date state variables. Why do we reinitialize the function on the value changes if it is using the up-to-date state values directly. Or is it? Maybe we are re-initializing the function so that it doesn't use the old values. Is that the case here?

Collapse
 
perssondennis profile image
Dennis Persson

It is fine to leave the dependency array empty if you really don't have any dependencies it is dependent on. If it is dependent on state variables or functions, you should add all of them. If you don't do that, the function you have wrapped in useCallback will use an old instance of the dependency if the dependency reference updates.

Note that if you have a function as a dependency to a useCallback, that function would also need to be a useCallback function, because if that would be a normal function in a React component, the useCallback function would be recreated every render since the normal function's reference changes every time.

As I wrote in the article, you should always add all dependencies to all hooks with dependencies (useEffect, useCallback, useMemo). Normally it's completely safe to add all dependencies to useCallback and useMemo, it won't infer any bugs (unless you are doing strange things in your code, for example if it's very important for the useMemo to run once).

With useEffects it's more difficult. You should still add all dependencies to it. But as described in the article (tip number 7), it can be tricky to do. If you see a useEffect with a missing dependency in some old code. Don't just add it without ensuring that the functionality still works, because the code's behavior may change.

Sometimes it is better to don't touch old code that works. And if you need to touch it, consider to refactor it. I have written another article describing how to deal with code changes. I think it's a good reading to learn about why it is important to be thoughtful when altering old code.

Collapse
 
truemail785 profile image
Ozair

Thanks for the detailed explanation.

Collapse
 
fruntend profile image
fruntend

Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍

Collapse
 
brense profile image
Rense Bakker

This is awesome! Hopefully everyone will read this article! 👍

Collapse
 
naucode profile image
Al - Naucode

Great article, you got my follow, keep writing!

Collapse
 
corners2wall profile image
Corners 2 Wall

holy shit this is awesome

Image description
Btw, same wrote in the documentation :-)

Collapse
 
bsides profile image
Rafael Pereira

Good explanation on each topic, keep it up! Thank you!

Collapse
 
thanhtanpm2000 profile image
ThanhTanPM2000

Thank you!

Collapse
 
perssondennis profile image
Dennis Persson