DEV Community

Cover image for React's Odd Obsession With Declarative Syntax

React's Odd Obsession With Declarative Syntax

Adam Nathaniel Davis on March 23, 2020

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 ...
Collapse
 
isaachagoel profile image
Isaac Hagoel

My two cents:

  1. I suggest doing a search and replace - "shadow dom" -> "virtual dom". The shadow dom is a different beast altogether. it is part of the web components standards and has to do with styles encapsulation. You are not the first one to mix the two terms but still :). Good arguments against the virtual dom (you mentioned some here, I know it isn't your main point): svelte.dev/blog/virtual-dom-is-pur...
  2. With hooks there is useEffect that is supposed to replace componentDidMount and componentDidUpdate 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 render
  3. I agree with the sentiment expressed here but I think it is not all because of the declarative syntax. It is a combination of things:
  4. JSX - other declarative frameworks that don't use jsx (like Svelte :)) won't allow putting fancy javascript within the declarative description of the view
  5. All logic should reside within the render function - this is a problem whether you are using declarative or imperative syntax. You gave good examples for it.
  6. Time is not a thing - this is where declarative syntax can be get in the way because it simply ignores the existence of time. Sometimes it's fine. Sometimes not so much. "A then B than maybe C or maybe D" is by definition imperative and cannot be gracefully (or in some cases at all) expressed declaratively (example for a "solution": Redux thunk 😩)

At 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 and else, each and await) 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.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis • Edited

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 of componentDidUpdate and componentDidMount?

Collapse
 
isaachagoel profile image
Isaac Hagoel

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).

Collapse
 
bytebodger profile image
Adam Nathaniel Davis • Edited

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.

Collapse
 
isaachagoel profile image
Isaac Hagoel

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.

Thread Thread
 
bytebodger profile image
Adam Nathaniel Davis

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.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis • Edited

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!

Collapse
 
cbovis profile image
Craig Bovis

For the sake of the illustration, we'll also assume that the username is known at the point that the component is mounted, and that the username is immutable.

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.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

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.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis • Edited

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.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

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!

Collapse
 
andersclark profile image
Anders Clark

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"?

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

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.

Collapse
 
ryansolid profile image
Ryan Carniato

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.

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Interesting! And thank you for the thoughtful feedback. I will definitely give SolidJS a look.