Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Joe Mesot
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Joe Mesot@JoeMesot )

Decoupling Component And Layout Responsibilities In ReactJS

By Ben Nadel on

After my blog post yesterday on decoupling component directives from layout responsibilities in AngularJS, I wanted to take a quick look at the same concept in a ReactJS context. While the follow-up might seem redundant, ReactJS is a fundamentally different beast in the way that it composes and renders markup. Unlike most AngularJS code, ReactJS fully transcludes components, replacing them with new content. As such, thinking about the same problem in a ReactJS context will be rewarding in its own way.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

The problem that I've been noodling on is the commingling of responsibilities within the bounds of a single component. As we compose our components to build a single user interface (UI), it's easy to give a component double-duty, having it be responsible for both the layout of its content as well as for its own layout within the context of its parent. Doing this makes it harder to reuse the component and, in my opinion, makes it harder to reason about the code.

To see what I mean, let's take a look a small demo (repurposed from yesterday) in which I have a chat history that contains chat messages. In this version of the demo, the "ChatMessage" component is responsible for both the "message" and the history "line-item," forcing it to know about its use within a greater context.

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Decoupling Component And Layout Responsibilities In ReactJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./inline.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Decoupling Component And Layout Responsibilities In ReactJS
  • </h1>
  •  
  • <h2>
  • Coupled Version
  • </h2>
  •  
  • <div id="content">
  • <!-- App will be rendered here. -->
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/reactjs/react-0.13.3.min.js"></script>
  • <script type="text/javascript" src="../../vendor/reactjs/JSXTransformer-0.13.3.js"></script>
  • <script type="text/jsx">
  •  
  • // I manage the root component.
  • var Demo = React.createClass({
  •  
  • // I return the initial state of the component.
  • getInitialState: function() {
  •  
  • return({
  • messages: [
  • {
  • id: 1,
  • name: "Ben",
  • message: "Good morning, wanna grab some breakfast?"
  • },
  • {
  • id: 2,
  • name: "Kim",
  • message: "I just woke up, can you give me like 15 mins to shower and dress?"
  • },
  • {
  • id: 3,
  • name: "Ben",
  • message: "Yeah, no problemo, just text me."
  • },
  • {
  • id: 4,
  • name: "Kim",
  • message: "Will do."
  • },
  • {
  • id: 5,
  • name: "Kim",
  • message: "Ok, ready to rock. Meet outside?"
  • },
  • {
  • id: 6,
  • name: "Ben",
  • message: "Let's do this!"
  • }
  • ]
  • });
  •  
  • },
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I return the virtual DOM represented by the current component state.
  • render: function() {
  •  
  • var items = this.state.messages.map(
  • function operator( message ) {
  •  
  • // In this approach, the ChatMessage has to represent the
  • // actual chat history item as well as the chat message. A
  • // slight commingling of responsibilities.
  • return(
  • <ChatMessage
  • key={ message.id }
  • name={ message.name }
  • message={ message.message }
  • other={ ( message.name !== "Ben" ) }>
  • </ChatMessage>
  • );
  •  
  • }
  • );
  •  
  • return(
  • <div className="chat">
  • <div className="history">
  • { items }
  • </div>
  • </div>
  • );
  •  
  • }
  •  
  • });
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I manage the chat message component.
  • var ChatMessage = React.createClass({
  •  
  • // I return the virtual DOM represented by the current component state.
  • render: function() {
  •  
  • var classes = this.props.other
  • ? "m-message for-other"
  • : "m-message"
  • ;
  •  
  • return(
  • <div className={ classes }>
  •  
  • <img src={ "./" + this.props.name.toLowerCase() + ".jpg" } />
  •  
  • <div className="header">
  • { this.props.name }
  • </div>
  •  
  • <div className="content">
  • { this.props.message }
  • </div>
  •  
  • </div>
  • );
  •  
  • }
  •  
  • });
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // Render the root Demo and mount it inside the given element.
  • React.render( <Demo />, document.getElementById( "content" ) );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, the collection of messages maps directly onto a collection of ChatMessage components. Without seeing the CSS, it's hard to tell, but this requires the ChatMessage component to manage the layout of the history items, including things like "margin-bottom" and ":last-child" selectors (to prevent margin on the last item in the history):


 
 
 

 
 Decoupling component and layout responsibilities in ReactJS - commingled. 
 
 
 

To create a stricter separation for responsibilities, we can have the top level component manage the chat history and let the ChatMessage component deal with nothing but rendering the message content. The difference in the following code is subtle but meaningful:

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Decoupling Component And Layout Responsibilities In ReactJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./component.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Decoupling Component And Layout Responsibilities In ReactJS
  • </h1>
  •  
  • <h2>
  • Decoupled Version
  • </h2>
  •  
  • <div id="content">
  • <!-- App will be rendered here. -->
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/reactjs/react-0.13.3.min.js"></script>
  • <script type="text/javascript" src="../../vendor/reactjs/JSXTransformer-0.13.3.js"></script>
  • <script type="text/jsx">
  •  
  • // I manage the root component.
  • var Demo = React.createClass({
  •  
  • // I return the initial state of the component.
  • getInitialState: function() {
  •  
  • return({
  • messages: [
  • {
  • id: 1,
  • name: "Ben",
  • message: "Good morning, wanna grab some breakfast?"
  • },
  • {
  • id: 2,
  • name: "Kim",
  • message: "I just woke up, can you give me like 15 mins to shower and dress?"
  • },
  • {
  • id: 3,
  • name: "Ben",
  • message: "Yeah, no problemo, just text me."
  • },
  • {
  • id: 4,
  • name: "Kim",
  • message: "Will do."
  • },
  • {
  • id: 5,
  • name: "Kim",
  • message: "Ok, ready to rock. Meet outside?"
  • },
  • {
  • id: 6,
  • name: "Ben",
  • message: "Let's do this!"
  • }
  • ]
  • });
  •  
  • },
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I return the virtual DOM represented by the current component state.
  • render: function() {
  •  
  • var items = this.state.messages.map(
  • function operator( message ) {
  •  
  • // In this version, we have a clear distinction between the
  • // history-item and the embedded chat message. This allows the
  • // two components to retain different responsibilities.
  • return(
  • <div key={ message.id } className="history-item">
  •  
  • <ChatMessage
  • name={ message.name }
  • message={ message.message }
  • other={ ( message.name !== "Ben" ) }>
  • </ChatMessage>
  •  
  • </div>
  • );
  •  
  • }
  • );
  •  
  • return(
  • <div className="chat">
  • <div className="history">
  • { items }
  • </div>
  • </div>
  • );
  •  
  • }
  •  
  • });
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I manage the chat message component.
  • var ChatMessage = React.createClass({
  •  
  • // I return the virtual DOM represented by the current component state.
  • render: function() {
  •  
  • var classes = this.props.other
  • ? "m-message for-other"
  • : "m-message"
  • ;
  •  
  • return(
  • <div className={ classes }>
  •  
  • <img src={ "./" + this.props.name.toLowerCase() + ".jpg" } />
  •  
  • <div className="header">
  • { this.props.name }
  • </div>
  •  
  • <div className="content">
  • { this.props.message }
  • </div>
  •  
  • </div>
  • );
  •  
  • }
  •  
  • });
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // Render the root Demo and mount it inside the given element.
  • React.render( <Demo />, document.getElementById( "content" ) );
  •  
  • </script>
  •  
  • </body>
  • </html>

In this version, the actual markup of the ChatMessage component didn't change at all. The markup difference happens in the calling context, where the collection of messages is mapped onto a collection of "line items", which individually contain a ChatMessage instance. By creating this separation, however, the CSS for the ChatMessage did change, relinquishing the margin controls to the contextual layout.


 
 
 

 
 Decoupling component and layout responsibilities in ReactJS - decoupled. 
 
 
 

Again, the difference here is very small. But, I think it makes the code easier to understand and maintain because it creates a cleaner separation of responsibilities.

All of this makes me think about rendering HTML tables. If you look at my "Thinking in React in AngularJS" post, you will see that the entirety of the Table (including its rows and cells) are rendered by a single component. This differs from the original "Thinking in React" article which has each row rendered by a different component.

At first, I thought that this was a defect of AngularJS - a feature that it could not parity in ReactJS (at least not easily - it can be done with janky double-transclusion). But, when you think about the responsibilities of layout, distributing the structure of an HTML Table across multiple components also distributes the responsibilities of layout, which I think is a bad idea. In fact, the more that I think about it, the more I think that the AngularJS solution looks more natural and appropriate.

As I was digging into all of this, and thinking about reusing components in different contexts, I started to realize that ReactJS suffers from another problem that AngularJS doesn't have: lack of structural insight. By this, I mean that when you look at a component element, you have no idea what kind of structure it's going to generate without looking into the implementation of the component.

For example, take a look at the following JSX markup:

  • <div className="foo">
  • <Widget />
  • </div>
  •  
  • <div className="bar">
  • <Widget />
  • </div>

Now, imagine that in the "div.foo" context, I want the Widget to be "display:inline" and in the "div.bar" context, I want the Widget to be "display:block". In ReactJS, this is a difficult problem because the "Widget" component doesn't result in a "widget" element tag. In fact, there's nothing about the component name that indicates what type of structure will be rendered or what CSS classes it will have applied. As such, there's basically no way to provide contextual overrides without knowing the implementation of the component's render() method.

You could pass-in CSS classes or inline styles to the Widget component, providing an override hook. But, even so, doing this requires that the Widget component apply the injected classes and styles, which it won't by default - you have to program that into the DNA of the component's rendering algorithm.

In AngularJS, this type of situation is rarely a problem because you're always working with HTML. Even if you use a custom tag directive, it still results in direct access to the rendered element, to which you can apply additional CSS classes or contextual CSS rules.

It's always fascinating to see what fruit will be yielded from seemingly redundant exploration. Taking the concept of layout decoupling and applying it in a ReactJS context makes me think not only about the structure of my ReactJS component but, also, about the limitations that are inherent with the ReactJS virtual DOM.




Reader Comments

Interesting points. But I wouldn't say that the Angular approach is better. The contents of the Widget should be concealed, using HTML kind of defeats the purpose of that abstraction.

The better solution (like any HTML element) is to supply the top level style. If you use something like Radium you can use styles inline in the react component and pass the layout behavior down as a property from the parent. This then matches the behavior of HTML.

Reply to this Comment

@Joe,

I am not sure if I agree that passing in styles is a better solution than being able to supply contextual CSS properties. That feels like a slippery slope. If you start doing that for some things, how can you use CSS for other things.

For example, what about the context in which you want the top/bottom item in a collection to have a different margin. In CSS, this is easy with something like:

div.parent div.child:last-child { margin-bottom: 0px ; }

To argue that passing-in style is a better approach means that you have to include conditional inline styles based on the index of the element in the current collection.

Not to say you can't do that. But, at some point, it feels like you're defeating the purpose of CSS. Though, it does feel like there is a growing sentiment of anti-CSS methodologies that prefer inline styles over linked stylesheets.

To be honest, I'm not so good at CSS that I can argue for or against the shift in approaches.

Reply to this Comment

There are lots of issues with CSS though. Like class scope, ordering and performance. From an architectural perspective, I wouldn't want to rely on the implementation of a widget and assume that layout behavior will remain consistent. I would much prefer the widget to behave like any other HTML element and expose a standard set of properties that govern its layout behavior.

If you used an inline style library like Radium you can achieve the same functionality as last-child. For example - https://github.com/FormidableLabs/radium/issues/214

This is basically a lambda like expression that governs the styles applied. Radium isn't the only choice, there are many others - all typically offering SASS like functionality and media queries:
https://github.com/FormidableLabs/radium/tree/master/docs/comparison

Reply to this Comment

@Joe,

Maybe I am not sure what you mean by:

>> behave like any other HTML element and expose a standard set of properties that govern its layout behavior.

I was under the impression that HTML elements worked the same way, in that they use a native browser Stylesheet to define default layout behaviors. Then, you can override those with addition stylesheets / style attributes. And, if you inspect the shadow DOM in Google Chrome (caveat: I know next to nothing about the shadow DOM), it seems to also make use of stylesheets.

So, maybe you can clarify a bit what you mean?

Reply to this Comment

By 'behaving like regular HTML elements' - I just mean that you can set the style of a regular HTML element to modify the layout behavior. You can change the display style property, or use flex and you expect the element to adjust accordingly. These are essentially ambient properties used by the layout engine on all elements.

This approach is better than looking into the implementation of the element so you know how the top-level DIV is styled. If your layout is implementation dependent it is subject to changes in that implementation.

I had this problem recently with a component that I wanted to use in a flex based layout and also with a percentage height. I could have used a CSS class based approach, but then this opens the door to the other issues with CSS. It's better to expose the necessary style properties and control everything with properties, that's more 'the react way'. Plus, this lets me use the higher level components with React Native, which only uses inline styles.

Reply to this Comment

@Joe,

Ah, interesting - I don't know anything about React Native, except for what I've heard on podcasts. I can definitely see the advantage of using inline styles for a lot of things - CSS can get tough to keep in your head, especially with all the cascading based on precedence.

CSS is one of those things I am hoping to bone up on. I'm still mostly a CSS 2.0 person :)

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.