A functional approach to className management in ReactJS, Part I

Marcos Hernández
CodeSyndicate
Published in
3 min readMar 31, 2017

In the not-so-old days, before the “Virtual DOM” and external html templates, we use to do directly manipulate the DOM referencing the node and adding or removing classes from the element `classList`; usually using some library like jQuery or its counterparts and clones. With the arrival of ReactJS and other declarative libraries, this has become ill advised, discouraged and almost impossible.

There are several helpers in the wild, like Jed Watson’s classnames that work perfectly fine and are even more tested than the one I’m proposing here. The problems I’m addressing here are:

  1. How to manage class names with an API similarly to the DOM’s native ClassList.
  2. How to handle default and initial classes for an element.
  3. Avoid nasty errors and exceptions using the default classes.
  4. And finally address some ideas in Functional Programming (FP) I’ve been working on to create a pointfree API.

Let’s start with some definitions. I’m starting with a wrapper around the Array type in ES. Mostly because Array is the data structure closer to list in ES. I’m wrapping it in our own implementation because we want this object chainable and keep the same interface until we use the final value. I’m using some ideas inspired by functional programming and ADTs; though may not be fully compliant, and surely not the most performant, they are close enough and easy to reason about.

The ClassList Container

So let’s start with:

So, I started with a pointed functor with other fancy methods. Basically this is a container or box where I store a value and have a series of methods that know how to handle that value. To be more specific. I’m teaching this object how to retrieve a stored value (in the constructor) and how to store a value with the static method of. The of method allow us to put content inside a ClassList Container. In this specific case, we can get the value stored by calling getClasses(); What makes it a “Functor” is the map method that knows how to change the shape (type) of the data inside the container and return a new container with the new data. Since our ClassList shares with the type (and the value it stores is of type) Array we can just delegate the methods to the native implementation.

This already has a nice functionality. If we give an array of strings we can map over it, change it and use it later, or change it and use it in place. Think about adding a theme to all classes in the element. But wait… sometimes we don’t want to map over and change all the elements in the array, we need some kind of filter, and other handiness like map over the filtered results, and add new elements to the array. Let’s add that.

Notice how we can define filter by reducing the previous array into a new one. Now we have an enhanced Array functional API (there are other things from Array we aren’t implementing like shift, unshift, push and pop. They’re not relevant to the requirements and if nothing else they’re easy to add).

But notice the concat implementation. Though ES Array accepts any type (Number, String, Array, Object), just adding it to the existing array, that’s not very functional nor true in a “categorical” sense. It seems like behind the scene ES’s Array.prototype.concat takes the argument, checks its type and whether it’s an array, flattens the array and pushes the value; but concat has an specific mathematical meaning: is equivalent as addition or more technically aggregation and is supposed to work only on the same type. So [1,2,3].concat(4) should throw a TypeError, because we can’t aggregate an Array and a number, the correct way should be [1,2,3].concat([4]) that would yield: [1,2,3,4] This is important because in our initialClassList.prototype.concat implementation if we pass a ClassList instance we would get a mixed array. Consider this:

const list1 = ClassList.of(['btn'])
const laterDefinedList = ClassList.of(['btn-solarized'])
// we use our faulty concat:
list1.concat(laterDefinedList);
// we get a mixed type array
ClassList(['btn', ClassList(['btn-solarized'])])
.getClasses().join(' ');
"btn, [Object][Object]"// when we want an uniform array
ClassList(['btn', 'btn-solarized'])
.getClasses().join();
"btn btn-solarized"

We are going to fix the concat issue, add an interface to aggregate other types by cleaning the input and add a convenient method to get the actual value we want as a final result:

We have a convenient API that wraps an array. We’ll come next week with the II part of this series where we will add other niceties.

--

--