DEV Community

Cover image for 101 React Tips & Tricks For Beginners To Experts ✨

101 React Tips & Tricks For Beginners To Experts ✨

Ndeye Fatou Diop on August 05, 2024

I have been working professionally with React for the past +5 years. In this article, I share the 101 best tips & tricks I learned over the ye...
Collapse
 
thethirdrace profile image
TheThirdRace

Very great article. I agree this should definitely be a must-read for anyone working with React

I would like to point out a few improvements to the examples.

#8 - Use IIFE to keep your code clean and avoid lingering variables

I would strongly discourage the use of IIFE. It's too easy to miss while reading the code and you don't really get much benefits from them anymore.

Readability is better than performance unless performance is absolutely required. In my career, I've come across maybe 2 situations where performance needed to trump readability.

#35 - React Context: Introduce a Provider component when the value computation is not straightforward

While it's pretty nice to abstract the logic for a provider outside the actual component code, I believe your example shows the wrong abstraction. A custom hook (#63) would have been more appropriate here.

The problem with "self-contained context provider" components is they are harder to use in tests. We embed logic in the self-contained function that needs to be mocked for a lot of tests depending on the values in the context. It's easier to keep your context provider "raw" and then you can simply use the same context provider in your tests and pass whatever you want for values. Still some mocking, but so much simpler that way.

#37 Simplify state updates with useImmer or useImmerReducer

While I fully see how easier it is with these hooks, it also trains you to manipulate an object without thinking about immutability.

This is a huge crutch! I've seen many developers being completely lost once they had to actually code the real thing. They didn't understand whatever principle they were replacing with these libraries, and they made a huge mess in the code base.

These libraries also tend to complicate the typings of your objects unnecessarily. I still have nightmares with Redux and ImmerJs, the "cure" was worse than the "problem".

My advice would be to avoid these kinds of libraries like the plague. They will not serve you well in your career in the long term.

#45 - Memoize callbacks or values returned from utility hooks to avoid performance issues

The example is not the best. In this case, fetchData should have already been memoized before being passed to useLoadData. Doing it in the children is "wrong" as it will be much less efficient this way.

#66 - Generate unique IDs for accessibility attributes with the useId hook

Your example shows 2 HTML elements with the same id... (input/span)

I recommend calling useId() once and composing your id for different components, ex:

const inputId = `input-${id}`
const spanId = `span-${id}`
Enter fullscreen mode Exit fullscreen mode

Obviously, I wouldn't name them inputId and spanId, but for illustrative purpose this is fine...

#82 Use ReactNode instead of JSX.Element | null | undefined | ...

I agree 100% with the notion, but the example is not the best one.

This should be used like slots are used in web-components.

children is pretty much the exception and should always be used with PropsWithChildren instead. You did use it right after in #83

Conclusion

Overall, an amazing article, kudos!!!

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop • Edited

Thanks for the comments @thethirdrace 😀.

8: The reason why I recommend IIFE is that I don't like it when there are variables polluting the component. Ideally, we can extract the logic into a function, but sometimes it is not the best if the logic takes so many arguments (and it is hard to find a name for the function 😅). My example is indeed too simple to show the problem, but I can see code like this inside a component.

  // ... First part of the component

  // startTime and endTime are just polluting the component scope
  const { startTime, endTime } = getEventTimes(eventData, schedule, timeframe);

  let duration;

  if (!startTime && !endTime) {
    duration = 0; // No time data available
  } else if (!startTime) {
    duration = endTime - schedule.start; // Use the schedule's start time
  } else if (!endTime) {
    duration = schedule.end - startTime; // Use the schedule's end time
  } else {
    duration = endTime - startTime; // Calculate based on both start and end times
  }
 // ... Rest of the component
Enter fullscreen mode Exit fullscreen mode

In these cases, I am ok with someone either using IIFE or extracting the logic into a function.

35 You are right. As pointed out, I am indeed using the hook 😅. Will change it to make it much clearer!

37 So I was actually thinking like you before. But recently, I have been using the redux toolkit, and damn, the experience is so much nicer. That is why I think they are still worth exploring.

45 I actually disagree here. When I write code, I want to make it super easy for consumers. And if we require every consumer to memoize the function passed, it won't work every time. That is why I am using a ref here to make sure my callback doesn't change while still using the correct value of the function when called

66 I am using a single ID because the ID is associated with a single element: the name

82 You are right. The example is not the best. Will change it :)

Collapse
 
thethirdrace profile image
TheThirdRace • Edited

#8 - IIFE

It's more about the syntax that is really hard to pick up for developers, mistakes are too easy to make.

I've seen people do some IIFE in useEffect just because they needed an async function there. But that isn't immediately obvious, and you can easily fix it with a syntax like this:

function Grade() {
  const averageGrade = () => {
    let gradeSum = 0;
    let gradeCount = 0;
    grades.forEach((grade) => {
      gradeCount++;
      gradeSum += grade;
    });
    return gradeSum / gradeCount;
  };

  return <>{averageGrade()}</>;
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the syntax is basically the same without the IIFE that is hard to catch. We define the function normally and call it right after. Same principle, less mistakes possibilities.

While it may look like those 2 methods are the very same and there's no difference, there are in fact many.

The way people read text (western style => top->down & left->right), the IIFE will require 2 parallel contexts to load into memory, the function itself and the executing context (()()). This increase cognitive load as they need to be loaded at the same time in parallel.

Comparatively, by separating in 2 sections, you load those 2 contexts sequentially. You can read the whole function without worries and then you see it's called. This is already much easier to contextualize in memory because these things happen sequentially.

But there's another advantage to loading context sequentially like this. If you just want to have an idea of what the component does, you can easily see there's a function definition and skip to the next "block" without checking how the function does its work. Sequential context loading allows you to skim the code very fast without caring much about the details. This is not possible with IIFE because you need to figure out what that first parenthesis really means before closing the function context.

I want also to point out that the spacing is important to really delimitate the function from the return section. For the same reason we use spacing between paragraphs and after punctuation, spacing is essential to separate concepts in code.

These are very subtle things to take into account, but on large scale it makes your system so much more accessible and easier to work with.

#37 - Immer

Redux toolkit doesn't require ImmerJS to work, it's an option you can use out of the box, but not a necessity.

But I do get where you're coming from. It's much easier to read your state if there's a minimum amount of code.

I would argue it comes with a few problems though...

First, as mentioned in my previous post, you start to use that method everywhere because you're so used to it. Unfortunately, not all systems are built with Immer so you start to put bugs in other apps that don't use it because your brain isn't trained to see these mistakes. For your brain, all you can see is correct syntax. And the reviewers will not necessarily pick up on that because they never imagine someone would just assign the state like that because you just don't do that in their system. It's an unwritten rule for most likely 99.9% of production systems out there... The result is a bug in production, and not an obvious debug session because the coder and the review don't see the bug since their brain trick them into thinking the line is correct...

Second, you lose context of your state. When you do not use Immer and rebuild the state from the ground up in your reducer, you always see the whole story. But when using Immer, you only see whatever changes without any context. This is potentially very dangerous because you might not take into account everything you should.

Third, if you really need Immer to clean up the code, then I would argue your piece of state is way too complicated. You should split it up or flatten it, there's too much cognitive load in that state.

I used to always mutate in-place until I got these nasty bugs that took hours to debug. Immer will fix it in your code, but not in your head. I would argue fixing the concept in your head is going to be much more important for you than fixing it for that particular line of code.

#45 Memoize

When I write code, I want to make it super easy for consumers. And if we require every consumer to memoize the function passed, it won't work every time.

You definitely want to make your code super easy for consumers, no argument there. But I think you misunderstood what I meant here. I meant that the fetchData needs to be stable to begin with.

Even if you memoize the value at the consumer level, if the parent doesn't have a stable reference, then the memoization in the consumer will re-run on render.

I see you used a ref to avoid the memoization re-run in the consumer. Don't get me wrong, this is a clever pattern, but it's just a band-aid. You are going to have to do this in every consumer now, and for the lifetime of the app, and all developers in your app will have to do the same, etc. The technical debt balloons very fast.

By having a stable reference in the parent, the problem is solved at 1 place. You won't have to add code anywhere else; you solved it once and no consumer needs to worry about it. Simplicity at its finest.

I would also argue that a re-render is not the end of the world either. You can squeeze every ounce of performance out of your code, but if your performance budget is not totally used at the end of the render cycle, it's thrown out. So any excess in performance you got might not be perceivable by users anyway. You prematurely optimized code that didn't even needed it, this is time that could have been spent elsewhere. We all should strive for performant apps, but at some point there are diminishing returns and it's our job to figure out when it's necessary and when it's not.

Also, this might not even be necessary at all now with the new React Compiler. The function will have a stable reference in the parent without requiring you to do anything about it. On the other hand, your pattern will still leave a lot of code in lots of consumers, which is less performant and less maintainable than no code at all.

#66 - ids

id should be unique by element, it has nothing to do with name. It's how the HTML syntax works.

The span and the input should not, at any point, have the same id. Browsers will let you do it just fine, but developers will have a hard time when querying by id.

You should not ignore this because we're in React here. It doesn't matter, it is a bug. That's why I pointed it out.

Thread Thread
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop • Edited

#8 *IIFE * — I see your point. I am used to the syntax so that is probably why I missed that it could be hard for new devs. I will adapt it! Thanks :)

#37 Immer— I see your point. However, I will still leave it as a tip since it can really make life easier. I will defer to people's judgment.

#45 Memoize — I am not following your reasoning. The whole point with this hook is that consumers don't have any memoization to do. I am aware that React Compiler is coming: which gives even more sense to not having memoization be done by consumers. Whether or not memoization is needed, everyone's codebase is different. I prefer to memoize hooks' returns values (like libraries do) from experience and given my situation, but obviously, everyone is free to memoize or not ;)

#66 ids — Oh sorry, I missed that. You are right: ids should always be unique. Replacing it!

Thread Thread
 
thethirdrace profile image
TheThirdRace

#45 - Memoize

I am not following your reasoning. The whole point with this hook is that consumers don't have any memoization to do.

Ah I see, we simply are not focusing on the same thing😅

The problems with the example are manyfold.

1. The ref + useEffect is memoization with extra steps

If you simply wanted to memoize everything, you should do this:

function useLoadData(fetchData) {
  const [result, setResult] = useState({
    type: "notStarted",
  });

  const loadData = useCallback(async () => {
    setResult({ type: "loading" });
    try {
      const data = await fetchData();
      setResult({ type: "loaded", data });
    } catch (err) {
      setResult({ type: "error", error: err });
    }
  }, [fetchData, setResult]);

  return useMemo(() => ({ result, loadData }), [result, loadData])
}
Enter fullscreen mode Exit fullscreen mode

The useCallback dependency array solves all your requirements and is much simpler.

Instead, your "good" example replaced the standard dependency array method by 1 indirection (the ref) and 1 extra step for synchronization (the useEffect). Plus, now the value contained in fetchDataRef is not calculated at render, but at the useEffect phase so you added an asynchronous step to the state of your component.

You don't need the ref or the useEffect steps at all. The dependency array was enough to adjust the value without any extra step. As a bonus, the value is calculated at render and in a synchronous way.

Furthermore, the code is much simpler this way. You don't have to keep all these extra steps in your memory to understand how it works. The cognitive load is lower, and this makes it easier for people to understand the code.

2. fetchData is not memoized at all...

🚨 When you want to memoize a value, you need to do it when you define the variable that holds the value.

Given that fetchData is defined outside the hook, as it's passed by the consumer, memoizing it here doesn't have an impact on re-renders in the consumer.

If fetchData is defined in the consumer and you didn't memoize it at creation time, then it will be recalculated on every render of the consumer. What was the point in synchronizing the new function reference in useLoadData if you could have simply used the new function to begin with?

If fetchData came from the parent of the consumer, then any state change in the parent would re-render the consumer (because the consumer is a children of the parent).

Your memoization in the consumer itself didn't prevent a re-render of the consumer at all in this case. Again, what was the point in using all that extra code if it's going to be re-calculated anyway?

I would even ask what was the point of useMemo in useLoadData?

You need to execute the whole hook to know if 2 references have changed. If they did change, you are returning the 2 new references. If they didn't change, you are returning the 2 old references. In both case, you return 2 references, all the calculations have been done already and you memoized the tiniest and fastest possible data ever, so what did you save by adding so much code around it?

Don't get me wrong, I think your whole article is amazing. I'm simply pointing out that you might need to revisit the memoization concepts because there's a tiny gap there. And I cannot stress enough that you get it better than 99.99% of developers, even if you have a tiny gap missing.

This is also why most of the time it's better not to use memoization at all. It's a very easy concept to grasp, but it's a very hard one to apply correctly. And mistakes in applying it are worse than not applying it at all.

Thread Thread
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

So, I still think there is a misunderstanding here. I am using a ref here instead of useCallback dependencies because I explicitly don't want my callback to change, which will be the case if I use a dependencies array (all items will need to be memoized).

Here, what I like in this pattern is that the entire complexity lives in this single hook.

Check similar implementations like in here or here for example.

Thread Thread
 
thethirdrace profile image
TheThirdRace

I understand what you want to achieve, and I agree to a certain degree. If the hook didn't receive a parameter that is NOT memoized, I would be 100% behind the principle.

The flaw I'm pointing at is that the hook is getting a parameter that is not memoized. In turn, this has a few adverse effects...

One of them is that it's most likely slower to run the useEffect to synchronise the fetchDataRef than actually redeclaring the loadData function every render. Memoizing is supposed to be used for expansive compute, this is just a function declaration so there's no expansive compute.

Another adverse effect is you have complexified your code a lot. Developers now have to take into account the useCallback implications AND the useEffect synching. Your code would most likely be just as fast without any memoization at all and would be much simpler. The "bad" example is actually good because you didn't mesure what you saved; you did premature optimization without an ounce of evidence.

The worse adverse effect is the bug probability part. When reading a useCallback with an empty dependency array, most will take for granted that nothing can change the function. But this is not true because you can change the fetchData function and fetch something completely different between 2 calls. Making a wrong assumption here can lead to many bugs down the line. Your good intention is opening the door for many possible problems because you didn't use the standard dependency array. You can't expect developers to follow uncommon patterns without making mistakes.

Remember, any state update of the consumer component and all its hooks will re-render the consumer and all its children. The only way to stop a re-render down the chain is to either use {children} in the JSX or to use memo() on a component or function. Anything else needs to execute the function/component to know if they should or not apply changes to the DOM. What you saved in the "good" example is the reference pointer to the values, you're not memoizing the execution of any code here, which kind of defeat the point of the memoization in your example.

But at the end of the day, it's not the end of the world. I know I'm picking on a detail here, there are much worse things in the day to day code. I just hope that what I wrote and the "argument" we had will shed some light for people reading the article.

Thanks for your article and taking the time to explain more your ideas, it's really appreciated 👍

Collapse
 
albertocubeddu profile image
Alberto Cubeddu

This article should be a must-read for anyone working with REACT!

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

So glad you like it Alberto ☺️

Collapse
 
77pintu profile image
77pintu

Thank you for the fantastic post!

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

Thanks 🙏

Collapse
 
nawael profile image
Nawael

A very impressive article!

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

Thanks Nawael 🙏

Collapse
 
cfecherolle profile image
Cécile Fécherolle

Amazing article, very thorough. Kudos 👏

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

Thanks Cecile

Collapse
 
geesman profile image
Kenneth

Nice piece

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

Thanks!

Collapse
 
shricodev profile image
Shrijal Acharya

Wow, really good piece. I came to read the post from your comment on it. dev.to/devteam/what-was-your-win-t...

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

Thanks so much! Glad you liked it ☺️

Collapse
 
franio68 profile image
franio68

Great!! Mega-helpful. Thanks

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

So glad you like it ☺️

Collapse
 
getsetgopi profile image
GP

Thanks for taking time to put together the best practices and standards. This link will definitely go into our confluence page.

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

Thanks a lot GP!
Super glad you like it 🙏

Collapse
 
nguyenthanhan201 profile image
NguyenThanhAn201

Amazing

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

Wouaw ! Thanks 🙏

Collapse
 
zoujia profile image
zoujia

Mark👍I am a Vue developer, and I have been learning React for the past 3 months. I found it quite challenging and there are many concepts that are very different from Vue.💪✊😄

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

Oh interesting! Feel free to connect on X

Collapse
 
jakubgarfield profile image
Jakub Chodounsky

Neat article! I'll run it in the next issue of React Digest. Perhaps consider including in #100 if you'd like it. 😉

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

Wouaw thanks a lot Jakub. I would love it 😻

Collapse
 
shagun profile image
Shagun Bidawatka

So Insightful!

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

Thanks! :)

Collapse
 
raheleh_sepehri_588189b22 profile image
Raheleh sepehri

It is so helpful article

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

Thanks 🙏