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...
For further actions, you may consider blocking this person and/or reporting abuse
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 variablesI 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 straightforwardWhile 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 withuseImmer
oruseImmerReducer
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 withRedux
andImmerJs
, 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 issuesThe example is not the best. In this case,
fetchData
should have already been memoized before being passed touseLoadData
. Doing it in thechildren
is "wrong" as it will be much less efficient this way.#66
- Generate unique IDs for accessibility attributes with the useId hookYour example shows 2 HTML elements with the same
id
... (input/span)I recommend calling
useId()
once and composing yourid
for different components, ex:Obviously, I wouldn't name them
inputId
andspanId
, 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 withPropsWithChildren
instead. You did use it right after in#83
Conclusion
Overall, an amazing article, kudos!!!
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.
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 called66 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 :)
#8
- IIFEIt'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
inuseEffect
just because they needed anasync
function there. But that isn't immediately obvious, and you can easily fix it with a syntax like this:As you can see, the syntax is basically the same without the
IIFE
that is hard to catch. We define thefunction
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 thereturn
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
- ImmerRedux toolkit
doesn't requireImmerJS
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 usingImmer
, 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
MemoizeYou 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 memoizationre-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
- idsid
should be unique by element, it has nothing to do withname
. It's how theHTML
syntax works.The
span
and theinput
should not, at any point, have the sameid
. Browsers will let you do it just fine, but developers will have a hard time when querying byid
.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.
#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!#45
- MemoizeAh 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 stepsIf you simply wanted to memoize everything, you should do this:
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 (theuseEffect
). Plus, now the value contained infetchDataRef
is not calculated at render, but at theuseEffect
phase so you added an asynchronous step to the state of your component.You don't need the
ref
or theuseEffect
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 insynchronizing
the new function reference inuseLoadData
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 achildren
of theparent
).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
inuseLoadData
?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.
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.
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 thefetchDataRef
than actually redeclaring theloadData
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 theuseEffect
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 thefetchData
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 usememo()
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 👍
This article should be a must-read for anyone working with REACT!
So glad you like it Alberto ☺️
Thank you for the fantastic post!
Thanks 🙏
A very impressive article!
Thanks Nawael 🙏
Amazing article, very thorough. Kudos 👏
Thanks Cecile
Nice piece
Thanks!
Wow, really good piece. I came to read the post from your comment on it. dev.to/devteam/what-was-your-win-t...
Thanks so much! Glad you liked it ☺️
Great!! Mega-helpful. Thanks
So glad you like it ☺️
Thanks for taking time to put together the best practices and standards. This link will definitely go into our confluence page.
Thanks a lot GP!
Super glad you like it 🙏
Amazing
Wouaw ! Thanks 🙏
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.💪✊😄
Oh interesting! Feel free to connect on X
Neat article! I'll run it in the next issue of React Digest. Perhaps consider including in #100 if you'd like it. 😉
Wouaw thanks a lot Jakub. I would love it 😻
So Insightful!
Thanks! :)
It is so helpful article
Thanks 🙏