Using BEM Class Names To Guide Module Structure In ReactJS 15.6.1
Yesterday, I looked at getting React.js to work with TypeScript and Webpack. It's been a while since I worked with React, so I had forgotten how much granularity is required by the React mechanics. In order to avoid binding event-handlers on-the-fly, you have to decompose your UI (user interface) into components that map directly onto event targets. This leads to a lot of modules. Which, in and of itself is not a problem; but, I think it's easy to go too far (especially when you're inexperienced like myself). As I was working on my demo yesterday, I wondered if one could use a BEM (Block Element Modifier) mindset in order to bring some logic and constraint to the module structure of a React.js application.
To give some context, my demo yesterday contained a Contact List that accepted a Contact[] collection and an OnDelete callback so that contacts could be removed from the list (using a one-way data flow). Because the OnDelete callback had to be applied at the "item" level, I had to create a Contact List Item component that, for all intents and purposes, bound a single Contact to a specific invocation of the OnDelete callback:
export function ContactListComponent( props: Props ) : JSX.Element {
var contactListItemNodes = props.contacts.map(
( contact: Contact ) : JSX.Element => {
return(
<ContactListItemComponent
key={ contact.id }
contact={ contact }
onDelete={ props.onDelete }>
</ContactListItemComponent>
);
}
);
return(
<div className="contact-list">
{ contactListItemNodes }
</div>
);
}
As you can see, each instance of ContactListItemComponent is passed a Contact and the onDelete callback. This way, when the onClick() handler is bound internally (to the ContactListItemComponent), it is obvious which Contact instance the event applies to.
So far, so good - there's nothing wrong with this. But, the problem with my approach yesterday was that the ContactListComponent and the ContactListItemComponent were in two different modules. And, since I was using BEM (Block Element Modifier) to define my CSS classes, it meant that one module - ContactListComponent - had this class:
- contact-list
... while the other module - ContactListItemComponent - had these classes:
- contact-list__item
- contact-list__name
- contact-list__actions
- contact-list__action
On its face, this feels wrong. Spreading a cohesive set of CSS class names across multiple modules is a definite code smell. It's an indication that something went wrong in my organization. So, this morning, I took yesterday's demo and refactored it so that all of the "like" CSS class names were in the same module. In this case, it meant combining the ContactListComponent and the ContactListItemComponent elements in the same file:
// Import the core React modules.
import React = require( "react" );
// Import the application modules.
import { Contact } from "./interfaces";
export interface OnDelete {
( contact: Contact ) : void;
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
interface ContactListProps {
contacts: Contact[];
onDelete: OnDelete;
}
// NOTE: We are using a Function instead of a Class here because this is a stateless
// component that doesn't need to expose any additional methods. As such, we can provide
// what is essential just the render() method. The props are still type-checked against
// the Props {} interface.
export function ContactListComponent( props: ContactListProps ) : JSX.Element {
var contactListItemNodes = props.contacts.map(
( contact: Contact ) : JSX.Element => {
return(
<ContactListItemComponent
key={ contact.id }
contact={ contact }
onDelete={ props.onDelete }>
</ContactListItemComponent>
);
}
);
return(
<div className="contact-list">
{ contactListItemNodes }
</div>
);
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
interface ContactListItemProps {
contact: Contact;
onDelete: OnDelete;
}
interface ContactListItemState {}
class ContactListItemComponent extends React.Component<ContactListItemProps, ContactListItemState> {
// I render the component.
public render() : JSX.Element {
return(
<div title={ `Contact ID ${ this.props.contact.id }` } className="contact-list__item">
<div className="contact-list__name">
{ this.props.contact.name }
</div>
<div className="contact-list__actions">
<a onClick={ this.handleClick } className="contact-list__action">Delete</a>
</div>
</div>
);
}
// ---
// PRIVATE METHODS.
// ---
// I handle the delete click.
// --
// CAUTION: Using an instance-property to define the function so that we don't lose
// the "this" context when the method reference is passed into the React element.
private handleClick = ( event: React.MouseEvent<HTMLAnchorElement> ) : void => {
this.props.onDelete( this.props.contact );
}
}
This immediately feels much cleaner. The moment I moved both classes into the same module, I saw the following benefits:
- It is much easier to see how the two components relate to each other, providing top-down readability.
- It is much easier to see how all of the CSS classes relate to each other.
- I only have to define the OnDelete interface once.
- I only have to import the Contact interface once.
- I only have to export the one, top-level component.
- By never exporting the "private" component (ContactListItemComponent), it prevents anyone from accidentally importing it or testing it on its own (since it has no meaning on its own).
- It centralizes things that will likely change for the same reasons.
Of course, this is just one example, and I'm a React.js noob, so it's hard to know if this will work well in all situations. But, it feels right. Going forward, if related CSS classes are going to be hard-coded (as a means to differentiate from props-based CSS class names), I'm going to err on the side of keeping those components in the same module. And, I'm only going to export the components that truly need to be made available to the calling context.
Want to use code from this post? Check out the license.
Reader Comments
You probably wouldn't need to separate the components if you used arrow functions to pass the contact as argument. Something like:
onClick={() => this.deleteContact(contact)}
@Hugo,
Yes, this will work. But, it works because a new version of the callback is created every time the render() function executes. The React documentation suggests trying to avoid this approach due to the performance hit. Though, the documentation looks like it's really only a performance hit if you pass the callback through to a lower component (where it will shallow-compare the Props collection and see a new Fn reference on every render).
@Ben,
That I didn't know. I guess I'll take a better look at the docs about this subject.
Thanks man!
I would also suggests keeping them in separate files but combining components and related parts in a folder and have an index.tsx file that combines and reference the items needed to render.
@Hugo,
My pleasure, good sir!
@Rico,
I think that may help with the consumption of the component from the calling context; but, I think it still doesn't address the root issue which is that it becomes harder to see what the overall nature of the component is without having to open up several files. I guess what I'm saying that I don't see a value-add in having them in separate files. They will often change together since things like CSS classes and Props will likely change in-step. As such, I just think it'll be easier to understand how the components relate when they are in the same file.