The first thing you need to know is that I'm a React developer. I've been slinging code since long before there was anything called "React". But ...
For further actions, you may consider blocking this person and/or reporting abuse
My two cents:
useEffect
that is supposed to replacecomponentDidMount
andcomponentDidUpdate
and can be forced to run only once (by providing it with an empty "dependencies array"). There are also a host of other hooks (useMemo
,useCallback
, ...) who try to make up for the ridiculousness of running everything on every renderAt the risk of sounding like a fanboy (I am getting a more realistic view of its strengths and weaknesses these days so I don't consider myself a fanboy), I will mention Svelte again. I find it interesting that it takes a different approach. Some of it is declarative on steroids (reactive variables, basic declarative animations, template constructs for
if
,elseif
andelse
,each
andawait
) but some allows you to go full-on imperative (a good example is the "actions" feature which lets you manipulate a dom element directly or the low level control you have with custom transitions and animations). I guess it is as you said - the secret is in finding the perfect balance.2) I'll probably be doing more of my future examples in Hooks, since I'm writing more of them for work and that's where my minds been turning to lately. But AFAIK,
useEffect
doesn't particularly solve these issues at all? As you've pointed out, it just provides a Hooks equivalent ofcomponentDidUpdate
andcomponentDidMount
?I meant that even though useEffect (like every hook or anything else inside the render function) runs on every render it can be prevented from running its cleanup and effect code over and over again by passing in an explicit dependencies array (it still redundantly compares the dependencies on every render cycle to decide whether it needs to run but that's cheaper).
3-6) Agreed on all points, but I'm particularly nodding along to #6. Perhaps that encapsulates my "declarative angst" more than anything in my post: the declarative blindspot to time. As much as I love me some React, it can still drive me a little batty sometimes when I want to use The New Hot Package that someone told me about on NPM, and then I start looking at how it purports to help with all this complex logic, and its answer is: "Just drop this component here." And I'm thinking, "Wait a minute. I can't just drop the component there. There's gotta be logic governing when it gets used or what happens before it gets used and what happens after it gets used." That's why I used the example of APIs. Because IMHO, an API call must be a tightly controlled thing. It needs to happen at a very specific time - and no other time. Bundling an API call into a declaratively-called component and accepting that it will call (and re-call) an endpoint whenever the component's rendered just isn't acceptable to me.
I agree.
I think it was Rich Hickey who said in one of his talks that modern tools, frameworks and even programming languages seem to be optimised for beginners. Any tool that can't get you up and running in 3 minutes by copy pasting some examples into your code is automatically dismissed. Frameworks compete over which one makes it easier for an absolute beginner to build a toy version of a TODO list.
While I do appreciate it sometimes when I build some throwaway prototype or just want to play around with a tool to see what it's about, understanding is still the only real currency of our trade.
Amen. This was always my problem with Ruby on Rails. Everyone would show you this crazy-simple example of how they could build a RoR app from scratch in minutes. But if you start adding custom requirements (like, ohhhh... every real world app ever) and you can no longer settle for the "default" way that RoR wants to do everything, pretty soon that app takes just as long to build, and it's just as complex, as any app built in any other language.
1) Good catch on the shadow/virtual DOM thing. I've been using the terms interchangeably for quite some time. So it's good to have someone point out that logical oversight so I don't continue repeating the error!
I've seen this assumption fail time and time again in projects I've worked on. In my opinion it's always worth putting the effort into assuming a component's props can and will change. In this case that would mean deriving from React.PureComponent or using React.memo to avoid the unnecessary re-renders. By ensuring your rendered UI always reflects current props/state the component will be much better at standing the test of time.
You're absolutely correct, but the question of when/if the props change kinda misses my point. That's on me. I'm sure I didn't do a strong-enough job of describing the central concept I was trying to illustrate.
I only created those arbitrary rules for the sake of illustration. What I was trying to say is that: There are times when you want AlgorithmX to only run one time. Or other times when you only want AlgorithmX to run in response to a very specific event. But sooooo much of the React ecosphere that I run into takes the general approach of, "Just drop this component into the
render()/return()
and it'll all be fine. But it's not fine. That approach hands over the control of your logic to the virtual-DOM-update process.That's why, further down, I gave a more concrete use-case with regard to API/GraphQL/Apollo calls. With regard to API calls, I never want to leave their execution in the hands of React's re-rendering cycle. I know when I want an API call to run - and I only want it to run at that time.
In the other comments, Isaac Hagoel summarized it better than I did. It's a matter of time. A declarative syntax is like programming without regard to when a given call takes place. In many scenarios, that works wonderfully and can make your code much cleaner. But for some functions/algorithms/whatever, it's absolutely the wrong approach.
Your response does raise an interesting thought in my mind about memoization and its use in React. Memoization is powerful and is almost certainly underused. But it's possible that some React devs see this as a (rather lazy) way to chunk everything into the
render()
process, relying on the memoization (which is another way of saying "cache") to properly sort out when to re-run the algorithm.And that's all fine-and-good. But if I can choose between
A. Craft a single, imperative function that I know will only run once.
or
B. Memoize the function, throw it into the render process, and let the React engine figure out on every subsequent re-render whether that function needs to be called again.
I'll almost always choose A.
I suppose "wrong tool" is a matter of opinion. In the React core docs, they specifically call out
componentDidMount
as the place where you should populate data calls:reactjs.org/docs/faq-ajax.html
To be fair, there are other things in the React docs that I don't particularly agree with. So I'm not blindly pointing to their recommendation as the unquestionable "final answer". But it's kinda difficult to label
componentDidMount
as the "wrong tool" when it's the exact tool named for the task in their own docs.As for your broader point about memoization - it's a good one. I know what memoization is. But I might be able to make more practical use of it with regard to API calls. I'll have a look at your GitHub code. Thanks for the link!
Great post!
My team just refactored most of our frontend to split up business logic, Network calls, etc from the render methods. To me it looks great now and the app is actually faster as well.
One thing though: Have you tried memoization? Does it help with DOM "garbage collection"?
I have to admit that I haven't done too much with memoization - mostly because I fight pretty hard to keep my temporal-based logic in imperative code. But it's a great tool and it certainly addresses some of my concerns about over-use of declarative syntax.
These problems have nothing to do with Declarative. Simply the fact that React using a Virtual DOM is actually an imperative flow. It re-runs everything over and over on render. A sequence of commands. It reconstructs a new tree over and over and then diffs it. The solution is to only render stuff that doesn't change once. Then being declarative makes sense. There are no weirdness like Hook Rules. Hooks are basically trying to fix the model but they do it (very cleverly) backwards. They whitelist changes for components that render continuously. What if instead it was only the hooks and the bindings that re-ran over and over again, instead of the Component body?
So I actually think being Declarative is the goal. React's implementation just falls short. The expectation is that when you give up imperative control you have the most optimized things happening in the background. We assume that with HTML rendering and we assume that with a React tree. For React it just pushes us into a model built on constant reconstruction. We can prevent that but it still means playing into this top-down repeat rendering.
And now for the plug. This is exactly what SolidJS solves: dev.to/ryansolid/introducing-the-s.... Even other Reactive libraries like Svelte and Vue can suffer from this "React" problem, in that their granularity is at a Component level. They can be smarter on what Components to update but they still re-evaluate Components completely at a time. Solid is basically the React Syntax but fine granular updates that avoid this altogether for optimum performance.
Interesting! And thank you for the thoughtful feedback. I will definitely give SolidJS a look.