I've written before about React's slavish devotion to declarative syntax (full article here: https://dev.to/bytebodger/react-s-odd-obsession-with-declarative-syntax-4k8h). So I'd like to give a real-life example from a very popular NPM package: react-table
.
In their "Quick Start" guide, they show an example of how to build a table with their package. Bear in mind that this is just the "Quick Start" example - so what we're seeing is, presumably, one of the most basic use-cases for the package. This is their example:
return (
<table {...getTableProps()} style={{ border: 'solid 1px blue' }}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th
{...column.getHeaderProps()}
style={{
borderBottom: 'solid 3px red',
background: 'aliceblue',
color: 'black',
fontWeight: 'bold',
}}
>
{column.render('Header')}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return (
<td
{...cell.getCellProps()}
style={{
padding: '10px',
border: 'solid 1px gray',
background: 'papayawhip',
}}
>
{cell.render('Cell')}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
)
Yikes...
I mean... I can certainly understand what's happening in this example. And if you're a React dev, I hope that you can, too. But for such a small bit of functionality, there sure is a lot going on here. And it's not necessarily simple to just trace the flow.
Differing Standards of Readability
First, let me make it clear that I'm in no way attacking react-table
. I could have chosen a thousand different examples from all over the web. This is just one that happened to jump out at me.
Second, I imagine that many of you are looking at the example above and thinking:
Yeah... so? What's wrong with it??
I've noticed that React devs, in general, seem to have naturally coalesced around this type of style. I see examples, like the one above, almost everywhere I look in React. And anytime we venture into the realm of coding styles, we're obviously veering hard into subjective, personal choices.
But even with all those caveats in mind, I still can't hide the fact that I hate this style of JSX. Personally, I find it very hard to read. There are four separate loops happening in the middle of our layout. And if it's hard to read, that means that it could also be hard to follow the logic and spot any inherent flaws. Personally, I never want to see a whole heap of logic stuffed into the middle of my JSX.
An Alternate Approach
Rather than argue about what is putatively good-or-bad in the above example, I think it's more useful to illustrate how this would look in my code:
const getCells = (cells) => {
return cells.map(cell => (
<td
{...cell.getCellProps()}
style={{
padding: '10px',
border: 'solid 1px gray',
background: 'papayawhip',
}}
>
{cell.render('Cell')}
</td>
));
};
const getHeaderGroups = () => {
return headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{getHeaders(headerGroup.headers)}
</tr>
));
};
const getHeaders = (headers) => {
return headers.map(header => (
<th
{...header.getHeaderProps()}
style={{
borderBottom: 'solid 3px red',
background: 'aliceblue',
color: 'black',
fontWeight: 'bold',
}}
>
{header.render('Header')}
</th>
));
};
const getRows = () => {
return rows.map(row => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{getCells(row.cells)}
</tr>
);
});
};
return (
<table {...getTableProps()} style={{ border: 'solid 1px blue' }}>
<thead>
{getHeaderGroups()}
</thead>
<tbody {...getTableBodyProps()}>
{getRows()}
</tbody>
</table>
)
Is my approach a wee bit longer? Yep. It sure is. But if your only standard of code quality is brevity, then I'm sure you're accustomed to writing some craptastic, cryptic code.
Buy why did I choose to break up the code the way that I did?
Declarative... When It Suits You
React devs talk about declarative code similarly to how other people talk about their first car. They get all starry-eyed. And... they get theoretical. They start spewing all the bits of code theory that they can pull out of their head to tell you why declarative is just soooooo much better than imperative.
Then they crank out a block of code like that initial example. And for some reason, this never seems to strike them as contradictory. But the first example is chuck full of imperative logic.
You see, for too many React devs, their definition of "declarative" just means: "It happens inside the return
statement." For those same devs, their definition of "imperative" is: "It happens outside the return
statement."
I've even seen (misguided) articles about "React Best Practices" claiming that: "You shouldn't have any JSX outside the return
statement." But that's just ignorant.
In the first example given above, we basically have 3 options for how to deal with our table logic:
Cram the header-aggregation, row-aggregation, and cell-aggregation into the
return
statement. (Which is what the original author did.) This isn't technically wrong - but it's a far cry from being declarative.Move the loops, conditionals, and other logic into their own standalone functions. (Which is what I did in the second example.) I would actually argue that my approach is more compliant with declarative practices than the first example.
Move the loops, conditionals, and other logic into completely separate standalone components. This is technically feasible - but it can lead to an insane explosion of single-use helper components.
I chose option #2 because I don't want any conditionals or loops inside my JSX. When I'm looking at a block of JSX, I want that block to represent, as much as possible, pure layout. Every time I start chucking loops and control statements into my JSX, the JSX gets farther away from being declarative layout, and closer to being a thinly-disguised imperative function.
Whenever you start reaching for loops and conditional statements inside of your
return
statement, it's an indication that this imperative logic may be better suited to its own helper function.
The Irony of Declarative Acolytes
I'm aware of a certain, bizarre irony anytime I'm engaged with someone who believes themselves to be a hardcore fan of declarative syntax. On one hand, they seem to despise any logic that can't be naturally represented with a JSX tag (which is, under the covers, just a function call). On the other hand, they will happily crank out return
statements that are stuffed to the gills with imperative code.
Maybe you think I'm being silly, but I swear that many of React's declarative fanboys (yeah - there's that word again...) would be perfectly fine with this:
export default function DeclarativeUserInfo() {
return (
<>
{[1].map((render, index) => {
if (someCondition)
return null;
const getNames = () => {
// all the get-names logic here
return [firstName, lastName, middleInitial];
};
const getStreetAddress = () => {
// all the get-address logic here
return [streetAddress1, streetAddress2];
};
const getCity = () => {
// all the get-city logic here
return city;
};
const getStateOrProvince = () => {
// all the get-state/province logic here
return stateOrProvince;
};
const getCountry = () => {
// all the get-country logic here
return country;
};
const getPostalCode= () => {
// all the get-postal-code logic here
return postalCode;
};
const [firstName, lastName, middleInitial] = getNames();
if (!lastName)
return <div>I like sammiches.</div>;
const [streetAddress1, streetAddress2] = getStreetAddress();
const city = getCity();
const stateOrProvince = getStateOrProvince();
const country = getCountry();
const postalCode = getPostalCode();
return (
<div key={'imperative-mess-' + index}>
<div>
{firstName} {middleInitial}. {lastName}
</div>
<div>{streetAddress1}</div>
<div>{streetAddress1}</div>
<div>
{city}, {stateOrProvince} {postalCode}
</div>
<div>{country}</div>
</div>
);
})}
</>
);
}
I'm not even trying to be funny here. Show this example to some of your React friends and ask them if it's imperative or declarative? You might be surprised by some of the answers that you get. Cuz there are more-than-a-few of my React colleagues out there who'd swear that this is declarative code.
Granted, it is, sorta, "declarative" from the perspective that we're declaring that, whatever is produced by the (silly) map()
function will, in turn, be rendered at the place where the function is invoked. But that's a pretty-weak standard for "declarative".
Room For Nuance
If I sound like I'm extremely pedantic about my personal imperative-vs-declarative rules, believe me, I'm not. There's more-than-enough room in good React code for declarative and imperative constructs - even inside JSX.
Let me give you one small example of where I use imperative techniques inside JSX all the time:
export default function Foo() {
return (
<>
<div style={{display : global.isLoggedIn ? 'block' : 'none'}}>
Here is all the stuff we show to logged-in users.
</div>
<div style={{display : global.isLoggedIn ? 'none' : 'block'}}>
Here is what we show to non-logged-in users.
</div>
</>
);
}
IMHO, this approach is just sooooooo elegant. No need to spin up helper functions. And I greatly prefer this to the more-common approach like this:
export default function Foo() {
return (
<>
{global.isLoggedIn &&
<div>
Here is all the stuff we show to logged-in users.
</div>
}
{!global.isLoggedIn &&
<div>
Here is what we show to non-logged-in users.
</div>
}
</>
);
}
In fact, CSS in general represents an excellent use case for inline ternary operators. It's not limited to display
. You can efficiently switch an element's color
, or width
, or... anything by just setting the CSS value with a ternary operator.
Can I give you any empirical reason why my approach is "better"?? Umm... no. I admit, it's largely a question of coding style. But for whatever reason, my approach feels much more declarative to me. While the second approach feels more like imperative code that's been stuffed in a faux-declarative wrapper.
Conclusions
I'm not one of those React devs who thinks that every bit of logic should be shoehorned into a declarative JSX tag. I also fail to see how wrapping a block of imperative logic with { }
inside your JSX magically makes it declarative.
One of the allures of a declarative style is its inherent readability. The more imperative code you try to cram into a supposedly-declarative container, the more you sacrifice that readability.
And there's a certain contradiction I've often scene in the Functional Programming Drones. They preach endlessly about the ability to break features down into smaller-and-smaller functions. But then, when it comes time to crank out some layout inside React's JSX, it's almost like they're afraid (or can't be bothered) to follow their own dictates and split things off into those separate functions. Instead, they just cram all the imperative logic right into the middle of the JSX.
Top comments (3)
cracks open my copy of The Structure and Interpretation of Computer Programs ...
and
The horror of React development for me is that it is full of people who are quite happy to bandy around big words without knowing their meaning or value. I guess they think that functional programming means using arrow functions, and must be better because it's new. And the nauseating over use of chained ternaries? Sheesh.
Maybe OO will be cool again next month?
Yes this is good comment. FP has its high now, I think amount of hype is huge. But there is truth that besides definition you make, FP is also about piping data through functions, and this is a product of lack of assignment. Even though in JS you cannot make a program without assignment (and you should not even try), React quite nicely represents data piping through components/functions. And yes most of these functions are not functions in mathematical sense, as whenever u use hooks, you get into environment state, but most of FP languages also makes runtime tricks like IO Monad or Elm Architecture.
You think you write "functional" and stateless, but only the high abstraction make this magic possible. React is a runtime, programming in React is React programming, what is some composition of many styles. Programming in React is different than any other.
Awesome post !! this post shed light on me about how to handle the refactoring of the app I am working on !!