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 in the last several years, it's become my favorite tool and it's the primary skill I'm paid for in my current job. So any "critique" to be drawn from this post is in no way a slam on the framework itself. I love React.
But "love" doesn't have to be unconditional. I love my family. But I can still point out instances where my family members have done some really stupid stuff. And as a self-appointed "React Acolyte", I can easily point out some places where React - and its associated community - can get downright... odd. The declarative syntax is one of those places.
Declarative vs. Imperative Syntax
React devs loooooove to talk about declarative syntax. They speak of it lovingly, like some kind of magic spell, the way that Java devs talk about "encapsulation". As though merely uttering the word will somehow wash the bugs from their code and solve their greatest programming challenges.
Hell, I'll even admit that I'm a huge fan of declarative syntax. So much of UI development just feels more intuitive if you can declare the component (and its associated logic) in the place where that component will ultimately be rendered. Compare this to, for example, jQuery, where it felt like every bit of display logic was flung off into functions that seemed to have only loose connections to the eventual layout of the app. So in the interest of full disclosure, I think that the declarative syntax is, in most cases, pretty damn cool.
The problem isn't with React's declarative syntax in general. The problem is that, like so many other things in tech, the community gets ahold of something and decides that There Shall Be No Other Approach. The problem is that the community doesn't embrace the declarative syntax as a tool, to be deployed when it best suits the job.
Instead, the community too often looks at declarative syntax as some kind of religious dogma. React devs who stubbornly try to shove everything into a declarative syntax are like construction managers who show up on site and say, "This building will be constructed entirely with hammers! NO SCREWDRIVERS! Hammers are good! Screwdrivers are bad! So we will only be using hammers on this project."
Declarative = Rendering
So if React is fundamentally tied so closely to declarative syntax, and if I truly love React as much as I say that I do, then why would I ever get exasperated/annoyed with that syntax? Part of the problem is based on performance, and part of it is based on separation of concerns.
Performance
If you've spent any serious time around React, then you've also spent some serious time stressing out about unnecessary re-rendering. React's virtual DOM is a pretty cool bit of auto-magicalism. When it works properly - and it usually works quite properly - it just kinda "magically" updates the required display elements whenever they need to be updated.
React's re-rendering feels (to me) a lot like Java's garbage collection. One of Java's big selling points was that devs no longer had to do manual memory management. Java's garbage collector "auto-magically" frees up memory when it can be freed up, and releases the developer from having to worry about memory allocation. But if you've ever worked on a large enough Java project, at some point you found yourself wrestling with garbage collection, trying to force it to release some critical bit of memory that it just didn't seem to want to release on its own.
Similarly, React's virtual DOM means that devs no longer have to manually refresh every single DOM element that was dependent upon stateVariableX
every single time that variable is updated. React's virtual DOM "auto-magically" figures out what should-and-shouldn't be refreshed (re-rendered), and releases the developer from having to manually update all the dependent DOM elements. But if you've ever worked on a large enough React project, at some point you found yourself wrestling with the render cycle, trying to stop it from spawning unnecessary re-renders.
You see, "auto-magicalism" has a cost. At some point, it can be maddening to figure out why Java's garbage collection is-or-is-not triggering at a given moment. Similarly, it can be maddening to figure out why React insists on re-rendering a component, even when you swear that there should be no updates that would trigger such a re-render.
[Note: Somewhere, there's a C developer reading this and chortling. Seasoned C developers don't get angry about manual memory management. They prefer it. They even embrace it. But that's a topic for another post altogether...]
So what does any of this have to do with declarative syntax??
If there's any "problem" with declarative syntax, it's that I've seen far too many cases where there is business logic - logic that's normally represented in an imperative style, that's awkwardly shoved into a declarative syntax. What this means, in a practical sense, is that:
Any business logic that's been shoved into a declarative syntax gets run every single time the component is rendered.
Sometimes, this is "manageable". But other times... it just represents a needless recomputing of something that never needed to be recomputed. To put it another way, there are many potential algorithms that I don't want to be repeated every single time the component renders. But if all of your logic is anchored into a declarative syntax, then it's definitely in danger of being run on every single render.
An example might illustrate this better. Imagine that we want to show the user the encrypted equivalent of their username. 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 plenty of React solutions that attack this issue by saying, "Here's a component that you can use to display an encrypted value." Then they proceed to show you how to use this component declaratively, like so:
// Example 1
import React from 'react';
import ShowEncryptedValue from './ShowEncryptedValue';
export default class UserData extends React.Component {
render = () => {
const {name, username} = this.props;
return (
<>
<div>Name: {name}</div>
<div>Username: {username}</div>
<div>Encrypted username:
<ShowEncryptedValue value={username}/>
</div>
</>
);
};
}
When you've installed ShowEncryptedValue
from NPM, and imported it into this component, and then leveraged its functionality with a simple <ShowEncryptedValue value={username}/>
, you might be thinking, "Wow. That was easy. What a great solution for showing an encrypted value." But there's a problem that's lurking in this example.
There's some kind of calculation that must be done to determine the encrypted value. Furthermore, since we've already established that the username is immutable, this calculation should really only need to be completed once. But because we've tied this calculation to the render cycle (via declarative syntax), we now risk repeating this calculation on any future re-rendering.
Yes, I realize that some of that potential inefficiency will depend upon the quality of the code that lives inside of <ShowEncryptedValue>
component. But as programmers of quality solutions, we shouldn't be dependent upon the idea that these tools are doing the "right" processing on their own. When we know that we have a calculation, that depends upon an immutable value, we should only ever be running that calculation once.
To illustrate this, consider the following counter example:
// Example 2
import React from 'react';
export default class UserData extends React.Component {
encryptedUsername = null;
componentDidMount() {
const {username} = this.props;
/*
do some logic here that computes the encrypted username value
*/
this.encryptedUsername = whateverValueWasJustComputed;
}
render = () => {
const {name, username} = this.props;
return (
<>
<div>Name: {name}</div>
<div>Username: {username}</div>
<div>Encrypted username: {this.encryptedUsername}</div>
</>
);
};
}
Notice the difference here. In this example, the encrypted value can only ever be computed one time - when the component is mounted. But this example also depends upon a bit of imperative syntax. In other words, there's some implied logic in do some logic here that computes the encrypted username value
that is plain ol' function-based JavaScript. And from what I've seen, there are just sooooo many React devs who greatly prefer Example 1 over Example 2, even though Example 2 is probably much more efficient.
Separation of Concerns
For those who remember (or still adhere to) MVC, the return
statement (in a class-based component or in a functional component) is the "view". It's the place where we're actually dictating how things should be displayed.
For whatever reason, I've noticed that React devs love to cram all kinds of logic into the "view". They'll do stuff like this:
// Example 3
import React from 'react';
export default class UserData extends React.Component {
render = () => {
const {day, foos} = this.props;
return (
<>
{foos.map(foo => {
if (day === 'Monday')
return foo;
const newFoo = foo.replace(/./g, '');
return (
<div key={newFoo}>
`${newFoo} with periods removed`
</div>
);
})}
</>
);
};
}
I really don't expect any of you to agree with me on this. I see code like I've shown above, in Example 3, everywhere in React. But I'm gonna be honest with you here - I hate it. I think it's a convoluted mess. I truly dislike seeing all of that if/map/for/whatever logic crammed into the middle of a render()
function (or simply into the return()
, if it's a Hooks-based component).
It's hard (for me) to read. It feels (to me) like a violation of separation of concerns. And, to be completely honest, it just seems kinda lazy. It seems like the dev couldn't be bothered to encapsulate that logic into a separate function - so they just crammed it all into the body of the return
.
I know that many React devs don't share my viewpoint on this. But this feels to me like a bastardization of the declarative syntax. IMHO, it's not "declarative" if you've taken all of your normal imperative code and shoved it right into the middle of your render()/return()
.
APIs
This might feel a bit "theoretical" to you. So let me give you one simple example where I've seen the declarative syntax fail over and over again. I'm talking about APIs.
An API call is perhaps one of the best examples of logic that I absolutely do not want to be tied to the render cycle. API calls are slow. They're computationally expensive. When I'm building a Rich Internet Application, there is no excuse for spawning unnecessary API calls. The API should be called exactly when I want it to be called, and it should be called only as many times as are necessary.
Recently, I started diving more into GraphQL. If you're a React dev, and you start exploring GraphQL, it probably won't take you long to find Apollo. When I first loaded up Apollo, I looked at the docs and read this:
Declarative data fetching: Write a query and receive data without manually tracking loading states.
I'll be honest. As soon as I read this "feature", it gave me pause. But I figured, "Well, for such a well-supported package, they must have gone to great pains to avoid unnecessary API calls." I... was mistaken.
After getting everything installed, I spent the better part of two days trying to tightly constrain any stray renders. I did this because Apollo uses a declarative syntax for its API calls. This means that it tries to make a distinct API call for every render of the component.
Some of this just comes down to solid React application design. And there were certainly some optimizations I was able to make that removed a lot of unnecessary renders (and thus, a lot of unnecessary API calls). But even after great wailing and gnashing of teeth, I found that every time I loaded my app, it was making the core API calls TWICE.
To be frank, I'm sure that if I'd just slaved away at this task for an indeterminate period of time, I would have, eventually, figured out how to limit my API calls to a single request. But after a while, it felt increasingly silly.
I mean... Why on earth would you ever want to tie your API calls to the render function??? That's just a recipe for creating an ongoing flood of unnecessary API calls. But that's the way Apollo does it by default. And when you start to look at almost any other React/GraphQL library, you realize that they all try to do it the exact same way. They all do it that way because there is this odd... obsession in the React community with the declarative syntax.
My "answer" to this problem was to rip Apollo out altogether. Rather than depending on its built-in (declarative) components, I just created my own fetch()
calls, manually formatted in the GraphQL syntax, written in imperative functions, that could be called, with a high degree of control, whenever I wanted/needed them to be called.
Conclusion
Please note that this post is NOT a generalized complaint about Apollo or GraphQL. Nor is it any kind of complaint about the general idea of using declarative syntax. But like any other tool in the tool belt, a declarative syntax has ideal uses - and instances where it is not ideal. IMHO, React devs tend to lose sight of this basic concept.
Top comments (15)
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.