Logic-less JSX

HTML is a declarative language with great readability. JSX, and templating engines in general, give us the power to mix logic and HTML. We've all experienced how JSX can become difficult to understand as our components grow in complexity. This article presents approaches that we can use to keep the mess out of our JSX, and make our code more readable and maintainable. Let's take a look at a simple example.

Here is a design and its markup:


            <ul>
              <li><a href="url/animal/1">Dog</a> - 4 legs - Friendly</li>
              <li><a href="url/animal/2">Bird</a> - 2 legs</li>
              <li><a href="url/animal/3">Snake</a> - 0 legs - Unfriendly</li>
              <li><a href="url/animal/4">Centipede</a> - ? legs - Not enough data!</li>
            </ul>
            

And here is the data that we are going to use to render it:


            const data = [
              { id: 1, name: 'Dog', legCount: 4, isFriendly: true },
              { id: 2, name: 'Bird', legCount: 2 },
              { id: 3, name: 'Snake', legCount: 0, isFriendly: false },
              { id: 4, name: 'Centipede' },
            ]
            

A first intuition to create the Animal component is to use the properties of the animal objects from the data as props:

Note: Turning numbers like legCount into strings for rendering is convenient to avoid accidental falsy conditions when their value is 0. I use Lodash's toString.

Here we've got some logic mixed with the markup and it's already quite messy, even for a small component like this. Wouldn't it be nicer to get rid of most of that logic and have the following more declarative JSX instead?

In order to use this lighter JSX, we need to transform the raw data into render data.

Where to transform the data

Our components might need the raw data for things like state management, styles, or handlers, so in my opinion it makes more sense to pass that raw data as props and have the transformation done within the component, rather than outside of it.

Here are 4 different approaches that we can use to transform the data within the components. They all have pros and cons, so it's up to your personal preference.

Approach 1: Variables

An intuitive approach to move some logic out of our JSX can be to create some intermediate variables in the body of the function of the component:


            const Animal = ({ id, name, legCount, isFriendly }) => {
              const url = `url/animal/${id}`
              const legCountStr = toString(legCount) || '?'
              const friendliness = { true: 'Friendly', false: 'Unfriendly' }[isFriendly]
              const hasNotEnoughData = legCount === undefined && isFriendly === undefined

              return (
                <li>
                  <a href={url}>{name}</a> - {legCountStr} legs
                  {friendliness && ` - ${friendliness}`}
                  {hasNotEnoughData && ' - Not enough data!'}
                </li>
              )
            }
            

The strength of this approach is that it is pretty simple to understand, and doesn't introduce any new concept. The body of the function can get pretty big if there is a lot of logic, but at least that logic is right there, accessible and clear.

Note: What I do in friendliness is a pseudo inline switch. If you use this you should keep in mind that any value you pass to the switch is converted to a string. Booleans, numbers, undefined, and null included. Also, if the right side of the switch cases produce side-effects, you need to put them in functions and execute the function at the end of the switch. See my other example below.

Approach 2: Second component

We can move that logic into a second component that will transform the props, and possibly do other smart things such as handling a state. I chose to add a Cmp suffix to the markup component. It looks like this:


            const AnimalCmp = ({ name, url, legCount, friendliness }) => (
              <li>
                <a href={url}>{name}</a> - {legCount} legs
                {friendliness && ` - ${friendliness}`}
                {hasNotEnoughData && ' - Not enough data!'}
              </li>
            )

            const Animal = ({ id, name, legCount, isFriendly }) => (
              <AnimalCmp
                name={name}
                url={`url/animal/${id}`}
                legCount={toString(legCount) || '?'}
                friendliness={{ true: 'Friendly', false: 'Unfriendly' }[isFriendly]}
                hasNotEnoughData={legCount === undefined && isFriendly === undefined}
              />
            )
            

The strength of this approach is the isolation of the Cmp component, which can be used for pure logic-less rendering in a Storybook. It also has a clean separation of concerns, and you can put those components in 2 different files if that's what you want. The downside of the intermediate component is that it can be confusing at first. It also adds one extra depth level per component which is a bit annoying when using React Devtools, and might have some performance impact that I haven't tested.

If the components have a state, I'd favor putting that state in the "transforming" component rather than the Cmp one, to keep the latter free from any logic.

Note: An alternative implementation is to use the HOC mapProps from Recompose (library currently unmaintained). It wouldn't be possible to use only mapProps if you want to hold a state though, you would also need a HOC like withState.

Approach 3: Function

If you extract the transforming logic into a function, you get this:


            const getAnimalRenderData = ({ id, name, legCount, isFriendly }) => ({
              name,
              url: `url/animal/${id}`,
              legCountStr: toString(legCount) || '?',
              friendliness: { true: 'Friendly', false: 'Unfriendly' }[isFriendly],
              hasNotEnoughData: legCount === undefined && isFriendly === undefined,
            })

            const Animal = props => {
              const {
                name,
                url,
                legCountStr,
                friendliness,
                hasNotEnoughData,
              } = getAnimalRenderData(props)

              return (
                <li>
                  <a href={url}>{name}</a> - {legCountStr} legs
                  {friendliness && ` - ${friendliness}`}
                  {hasNotEnoughData && ' - Not enough data!'}
                </li>
              )
            }
            

This version is a bit of a mix of the last two. It is simpler than introducing a new component, but doesn't give you the option to isolate the logic-less JSX code, and the function cannot hold a state. Its special feature however is that unlike the previous approaches, this function can be used outside of the context of React, by your server for instance, or if you switch to a different UI library than React one day.

As you can see, if we deconstruct our render data, and if our line is too long, we might have to place each property on a single line, which takes a lot of space (at least that's how Prettier would format it). The first 2 approaches don't have this problem.

Approach 4: Class

This is very similar to the function approach, except that it is using a class:


            class AnimalRenderData {
              constructor(data) {
                this.data = data
              }
              get name() {
                return this.data.name
              }
              get url() {
                return `url/animal/${this.data.id}`
              }
              get legCountStr() {
                return toString(this.data.legCount) || '?'
              }
              get friendliness() {
                return { true: 'Friendly', false: 'Unfriendly' }[this.data.isFriendly]
              }
              get hasNotEnoughData() {
                return this.data.legCount === undefined && this.data.isFriendly === undefined,
              }
            }

            const Animal = props => {
              const {
                name,
                url,
                legCountStr,
                friendliness,
                hasNotEnoughData,
              } = new AnimalRenderData(props)

              return (
                <li>
                  <a href={url}>{name}</a> - {legCountStr} legs
                  {friendliness && ` - ${friendliness}`}
                  {hasNotEnoughData && ' - Not enough data!'}
                </li>
              )
            }
            

The main difference with the function implementation is that with a class, the transforming functions are only executed when those properties are accessed, which makes it more performant, at the cost of being more verbose. But if you intend to use this render data outside of React, to get the url of an animal on the server for instance, this is a more optimized choice than the function.

🎉 Special thanks to my friend and ex-colleague from Yelp Benjamin Knight, who came up with the class approach, reviewed this article and helped me improve it. He also coined the term "render data" that I'm using here.

What goes where – ⚠️ opinion

I am still trying to figure out what works for me in terms of what I should put in the JSX and what I should extract to the render data, but here is my current take on it.

What goes in my JSX

Code is clearer than words so there you go:


            str // strings
            <Cmp /> // components
            <Cmp {...{ a, b, }} /> // instead of a={a} b={b}. yes, it's less performant

            foo && <Cmp /> // shows if truthy
            foo === somevalue && <Cmp /> // shows if foo is somevalue

            !foo && <Cmp /> // shows if falsy
            foo !== somevalue && <Cmp /> // shows if foo is not somevalue

            isNil(foo) && <Cmp /> // shows if null or undefined (Lodash)
            exists(foo) && <Cmp /> // shows if not null or undefined (custom !isNil function)

            arr.length > 0 && <Cmp /> // shows if array has at least 1 item
            arr.length === 0 && <Cmp /> // shows if array is empty

            !isEmpty(obj) && <Cmp /> // shows if object has properties (Lodash)
            isEmpty(obj) && <Cmp /> // shows if object is empty (Lodash)

            arr.map(({ id }) => <Cmp key={id} />) // renders array

            foo ? <CmpA /> : <CmpB /> // show CmpA if foo is truthy, CmpB if not

            {
              caseA: () => <CmpA />,
              caseB: () => <CmpB />,
              undefined: () => <CmpC />
            }[switchValue]() // an inline switch of components based on switchValue

            condA ? (
              <CmpA />
            ) : condB ? (
              <CmpB />
            ) : condC ? (
              <CmpC />
            ) : (
              <CmpD />
            ) // chained ternaries - just think of them as else-ifs
            

If conditions are more complex than this, then I would probably put them in the render data. This list is very likely to change.

What goes in the render data

I guess most logic that derives the props and ends up going into the JSX could be "render data". It's pretty much just helpers to keep your JSX as lean as possible. It's difficult to make an exhaustive list of that one.

However I think it is a good idea to not put any JSX code in the render data. It can be tempting to do something like const foo = isFoo ? <Cmp1 /> : <Cmp2 /> and inject {foo} in the JSX, but if we do this, we break the separation of concerns and the features that come with it. Instead, keep the markup in the JSX, or create a new component const Foo = () => isFoo ? <Cmp1 /> : <Cmp2 /> and use <Foo />.

As pointed out by /u/nschubach, declaring 'Friendly' and 'Unfriendly' (as well as '?') in the render data does break the separation of concerns. It is similar to having markup there. So a purer version of the approach 1 would be:

            const Animal = ({ id, name, legCount, isFriendly }) => {
              const url = `url/animal/${id}`
              const legCountStr = toString(legCount)
              const hasNotEnoughData = legCount === undefined && isFriendly === undefined

              return (
                <li>
                  <a href={url}>{name}</a> - {legCountStr || '?'} legs
                  {isFriendly !== undefined &&
                    ` - ${isFriendly ? 'Friendly' : 'Unfriendly'}`}
                  {hasNotEnoughData && ' - Not enough data!'}
                </li>
              )
            }
            
Depending on how much separation of concerns you want, you might be okay with having some strings defined in the render data though.

Another thing that should probably not go in the render data is styles. Even if they are based on props.

Styles

I try to have as little styles in the JSX as possible, because just like logic, they can make a big mess in our JSX. And the whole point of logic-less JSX is to keep the JSX lean. Note that I am not only referring to inline styles, but also CSS-in-JS libraries that support writing styles in the JSX, like Emotion. Instead, I prefer using CSS-in-JS classes.

In my opinion, an essential feature for a CSS-in-JS library is to apply styles based on props. Styled Components, Emotion (with Styled Components), JSS, or Material UI support it for instance. With that feature, no need to put styles in the render data.

CSS-in-JS brings encapsulation and styles inheritance to CSS. So I think now is a good time to switch back to the old way of writing styles, with "semantic" classnames such as <div className={css.post} /> instead of things like <div class="block-list media-container media-container--dark" />. This helps keeping our JSX light too.

But that's a very big debate for an other day.

Closing words

Depending on your project, an approach or the other might work better for you. My personal preference goes to the first version, with variables in the body of the component. It is less modular than the other ones but it is the clearest and most compact, and should be enough in common cases.

But if I work in a very modular context, where front-end developers or web designers focus on the front of the front-end (HTML and CSS), the second approach with the pure Cmp would get my vote, as it provides the cleanest separation of concerns.

What's your personal preference? Is there something that I am missing? Let me know on Reddit or Twitter. I'm always happy to have my mind changed.

Posted on August 13th, 2019

Back to my site