Destructuring Reconsidered

While working with React for the last five months, I’ve noticed that React developers make extensive use of object destructuring, especially in function signatures. The more I use React the less I like this trend, and here are a few, short reasons why.

There are countless books by wise industry sages1 that discuss how to write good functions. Functions should do one thing, and one thing only; they should be named concisely; their parameters should be closely related; etc. My observation is that destructured function parameters tend to quickly lead to violations of these best practices.

First, destructuring function parameters encourages “grab bag” functions where the destructured parameters are unrelated to each other. From a practical point of view, it is the destructured properties of the actual parameters that are considered, mentally, as parameters to a function. At least, the signature of a destructured function reads as if they are:

function foo({ bar, baz }, buzz) {}

A developer will read this as if bar, baz, and buzz are the actual parameters of the function (you could re-write the function this way, so they might as well be), but this is incorrect; the real parameters are buzz and some other object, which, according to best practice should be related to buzz. But because the first parameter (param1) is destructured, we now have properties bar and baz which are one step removed from buzz, and therefore the relationship between param1 and buzz is obscured.

This can go one of three ways:

  1. if param1 and buzz are related, we do not know why;
  2. if param1 and buzz are not related (but bar and baz are related to buzz) then the function is poorly written;
  3. if bar, baz, param1, and buzz are all closely related, then the function is still poorly written, as it now has three “virtual parameters” instead of just two actual parameters.

Second, destructured functions encourage an excessive number of “virtual parameters”. For some reason developers think this function signature is well written:

function sendMail({ firstName, lastName, email}, { address1, city, state, zip}, { sendSnailMail }) {}
// function sendMail(user, address, mailPreferences) {}

“But it only has three parameters!”, they say. While technically true, the point of short function signatures is to scope the function to a single, tangible task and to reduce cognitive overhead. For all practical purposes this function has eight parameters. And while the purpose of this function is fairly obvious based on its name, less expressive functions are far more difficult to grok.

Third, destructuring makes refactoring difficult. Sure, our tools will catch up some day. But from what I’ve seen modern editors and IDEs cannot intelligently refactor a function signature with destructured parameters, especially in a dynamic/weak typed language like JavaScript. The IDE or editor would need to infer the parameters passed into the function by examining invocations elsewhere in code, and then infer the assignments to those parameters to determine which constructor function or object literal produced them, then rewrite the properties within those objects… and you can see how this is a near impossible feat. Or at the very least, how even the best IDEs and editors would introduce so many bugs in the process that the feature would be avoided anyway.

Fourth. Often developers must trace the invocation of a function to its definition. In my experience, code bases typically have many functions with the same name used in different contexts. Modern tools are smart, and examine function signatures to try and link definitions to invocations, but destructuring makes this process far more difficult. Given the following function definition, the invocations would all be valid (since JS functions are variadic), but if a code base had more than one function named foo, determining which invocation is linked to which definition is something of a special nightmare.

// in the main module
function foo({ bar, baz}, { bin }, { buzz }) {}

// in the bakery module
function foo(bar, { baz }) {}

// invocations
foo({ bar, baz });

foo(anObject, anotherObject);

foo(1, { bin }, null);

In contrast, functions with explicitly named parameters (usually the signature parameters are named the same as the variables and properties used to invoke the function) make these functions an order of magnitude easier to trace.

Fifth, destructured parameters obscure the interfaces of the objects to which they belong, leaving the developer clueless as to the related properties and methods on the actual parameter that might have use within the function. For example:

function handle({ code }) {}

What else, besides code may exist in the first parameter that will allow me to more adequately “handle” whatever it is that I’m handling? The implicit assumption here is that code will be all I ever need to do my job, but any developer will smirk knowingly at the naivety of that assumption. To get the information I need about this parameter I have to scour the documentation (hahahahaha documentation) in hopes that it reveals the actual parameter being passed (and doesn’t just document the destructured property), or manually log the parameter to figure out what other members it possesses. Which brings me to my last point:

Logging. I cannot count the number of times I have had to de-destructure a function parameter in order to log the complete object being passed to the function, because I needed to know some contextual information about that object. The same applies for debugging with breakpoints. (I love when Webpack has to rebuild my client code because I just wanted to see what actual parameter was passed to a function. Good times.)

Don’t get me wrong – I’m not completely against destructuring. I actually like it quite a bit when used in a way that does not obscure code, hinder development, or hamstring debugging. Personally I avoid destructuring function parameters in the signature, and instead destructure them on the first line of the function, if I want to alias properties with shorter variable names within the function.

function sendEmail(user, address, mailPreferences) {
const { firstName, lastName, email } = user;
const { address1, city, state, zip } = address;
const { sendSnailMail } = preferences;

This pattern both conforms to best practices for defining functions, and also gives me a lightweight way to extract the bits of information I need from broader parameters, without making it painful to get additional information from those parameters if I need it.

Don’t use the new shiny just because it’s what all the cool kids do. Remember the wisdom that came before, because it came at a cost that we don’t want to pay again.

  1. Clean Code, Code Complete, etc.