When considering the separation of concerns between Container and Presentational Components (stateful / stateless components), I find it useful to leverage the core concepts of these patterns in order to define a clear boundary between where Immutable data types are used, and where raw JavaScript types are referenced exclusively.
By having a clear separation which compartmentalizes where Immutable types are used and where they are not, team members are afforded the ability to easily determine a components propTypes; as, without having a clear cut-off point, one must give thought as to if a prop passed down to a component will be an Immutable object, or not.
It’s no stretch of the imagination to see how this can quickly lead to code which becomes much harder to maintain than it needs to be. As such, the Container / Presentational Component pattern provides a rather natural boundary for separating these concerns.
Unfortunately; however, while such a boundary may seem rather obvious, it may not always be clearly defined, and this tends to lead to overly complex propType declarations.
For instance, on a number of occasions I’ve seen propTypes declared similar to the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | SomePresentationalComponent.propTypes = { someList: oneOfType([array, object]).isRequired, someItem: object.isRequired } // or SomePresentationalComponent.propTypes = { someList: oneOfType([array, instanceOf(Immutable.List)]).isRequired, someItem: oneOfType([object, instanceOf(Immutable.Map)]).isRequired } // or ... SomePresentationalComponent.propTypes = { someList: oneOfType([array, instanceOf(Immutable.List)]).isRequired, someItem: shape({ id: number, get: func }) } // etc ... |
Given the above example, it’s obvious that it was unclear to the original implementor (or current maintainer) of SomePresentationalComponent
as to what the expected propTypes will ultimately be. In certain cases, it appears someList
could be of type array; whereas, in other cases, it could be of type object (e.g. Immutable.List). Likewise, in some cases someItem
could be an object, whereas in others it could be an Immutable.Map.
As you can see, this is obviously problematic and indeed a very good candidate for a bug (not to mention, a maintenance headache indeed).
Moreover, it results in all sorts of unnecessary type check permutations before accessing properties. For example, just to check the length of the list:
| const len = typeof(someList.size) !== 'undefined' ? someList.size : someList.length; // or ... const len = someList instanceof(Immutable.List) ? someList.size : someList.length; // or ... const len = Immutable.Iterable.isIterable(someList) ? someList.size : someList.length; // or ... const len = _.isArray(someList) ? someList.length : someList.size; // etc ... const len = !_.isUndefined(someList.size) ? someList.size : someList.length; // etc ... |
Likewise, just to get the id
of someItem
:
| const id = someObject.get ? someObject.get('id') : someObject.id; // or ... const id = typeof(someObject.get) === 'function' ? someObject.get('id') : someObject.id; // or ... const id = _.isFunction(someObject.get) ? someObject.get('id') : someObject.id; // or ... const id = !_.isUndefined(someObject.id) ? someObject.id : someObject.get('id'); // etc ... |
At best, this is far from ideal, to say the very least …
Now, obviously the developer could simply define a single propType
and refactor Containers which are passing an invalid type; however, it may not always be clear what the type should be, if say, the component is being used by multiple applications to which the developer does not have access, and some of those applications are not using Immutable.js, in which case, it would be best to simply disallow Immutable from the component all together and have consumers of the component update their Containers. In any event, it’s symptomatic of a team not having a clear understanding of what kind of components work exclusively with Immutable data types, and which do not.
Solutions
Fortunately, as one might imagine, there are a couple of very simple solutions to this problem:
- Only use Immutable types throughout the entire application.
- Segment which components use Immutable types, and which do not.
Now, in some cases the argument for Option #1 may very well be a valid one; however, I find Option #2 to be much more feasible (and flexible) as, it helps to ensure Presentational component are kept pure, and that means only using JavaScript types. For my purposes, this is especially important as I have to maintain a shared library which must limit dependencies as much as possible; and some projects are using Immutable, Redux, etc., and some are not. As always – consider the context.
Pros
By having an internal design contract (or convention) which mandates that Container components are only ever to work with Immutable types and, Presentational components are only ever to be passed JavaScript data types, it becomes much clearer to team members where the boundary is defined, and thus, much easier to maintain a large application over time.
Furthermore, it allows less experienced developers to gradually become acclimated with the React Ecosystem by assigning them tasks focused on presentational features. This can be very useful as it only requires knowledge of core concepts without being inundated with additional libraries and APIs. This approach also affords team members with more experience to focus on the more complex portions of the application (application logic, reducers, containers, etc.).
In addition, destructuring, …rest parameters and related ES6 features can be used much more extensively to simplify implementation when using JavaScript types exclusively, helping to ensure Presentational components are kept intentionally “dumb”. Not to mention, in doing so, testing becomes considerably less complex when working with native JavaScript types – and this is equally important when helping newer developers become productive while still getting up to speed.
And, while not always likely, by reducing our dependency on Immutable.js, we position ourselves for a much more easier migration path in the event we decide to swap out Immutable for another library in the future.
Cons
Arguably, one could be justified in the assertion that only Immutable Data types should be used by both Container and Presentational components (Option #1), and indeed that would be a fair argument if you will be calling toJS() frequently when passing props down to Presentational Components (as there is obviously an inherent expense in doing so).
That being said, there is no reason why one would need to call toJS
when passing props to Presentational Components as the Immutable API can be utilized to reduce the given props before being passed down to child components. In such cases, a Higher Order Component can be defined for doing either, which can simplify implementation considerably.
Summary
Like most design decisions, there is rarely a one-size-fits-all approach that perfectly solves any given problem, and what ultimately makes sense in one context, may not always be appropriate in another. However, in the context of when and where Immutable types are used, in most cases it is fair to say there should always be a clear boundary defined, regardless of where that boundary must be.