Skip to content

Instantly share code, notes, and snippets.

@markalfred
Last active March 28, 2017 18:45
Show Gist options
  • Save markalfred/e2c5845e82f249c0883b7222a77fdcf8 to your computer and use it in GitHub Desktop.
Save markalfred/e2c5845e82f249c0883b7222a77fdcf8 to your computer and use it in GitHub Desktop.
The trouble with ES6 named import/exports

The trouble with ES6 named import/exports

-or-

Regarding renaming SET_RELATIONSHIP_INFORMATION_ERRORS to SET_ERRORS:

I've noticed a particular issue that seems to have come about with the advent of named import/exports in ES6 — that is, that variable names have grown in length by orders of magnitude. This seems to be the result of the need to namespace. For instance, let's say you have a set of user actions...

// user-actions.js
export function setUser(user) { return { type: 'SET_USER', payload: user } }
export function setUserErrors(errors) { return { type: 'SET_USER_ERRORS', payload: errors } }

// user-component.js
import { setUser, setUserErrors } from 'actions'
setUser(user)
setUserErrors(errors)

If we weren't using ES6 style import/exports, though, and we wanted to keep these functions named the same way, we'd do this:

// user-actions.js
module.exports = {
  setUser: function(user) { return { type: 'SET_USER', payload: user } }
  setUserErrors: function(errors) { return { type: 'SET_USER_ERRORS', payload: errors } }
}

// user-component.js
const userActions = require('user-actions')
userActions.setUser(user)
userActions.setUserError(error)

Notice the redundancy here. The way you'd actually do this, if we weren't using ES6 style import/exports, would be closer to this:

// user-actions.js
module.exports = {
  set: function(user) { return { type: 'SET_USER', payload: user } }
  setErrors: function(errors) { return { type: 'SET_USER_ERRORS', payload: errors } }
}

// user-component.js
const userActions = require('user-actions')
userActions.set(user)
userActions.setError(error)

And, of course, since we can namespace our flux types, we could reduce the redundancy even further:

// user-actions.js
const SET = 'user/SET'
const SET_ERRORS = 'user/SET_ERRORS'

module.exports = {
  set: function(user) { return { type: SET, payload: user } }
  setErrors: function(errors) { return { type: SET_ERRORS, payload: errors } }
}

Now, from the perspective of our User Component, the User Actions are propertly namespaced. It's obvious that calling userActions.set will call the "set" function of the actions associated to the "user". requireing all functions in the user-actions module and referencing that module by its name userActions gives us all the namespacing we need sort of automatically.

In my opinion, the latter example is nicer to read than the former. But in such a simple example, the trade-off is pretty tiny. It gets more interesting as the namespacing gets more complex, and particularly interesting when namespacing is only sometimes necessary. For instance, let's say you structured your app such that components, styles, actions, reducer, and selectors are all bundled together.

So, rather than this:

|- components
  |- post.js
  |- user.js
|- styles
  |- post.css
  |- user.css
|- actions
  |- post.js
  |- user.js
|- reducer
  |- post.js
  |- user.js
|- selectors
  |- post.js
  |- user.js

We do this:

|- features
  |- post
    |- component.js
    |- styles.css
    |- actions.js
    |- reducer.js
    |- selectors.js
  |- user
    |- component.js
    |- styles.css
    |- actions.js
    |- reducer.js
    |- selectors.js

In this case, we'd probably be able to assume that imported libraries are implicitly tied to the user, unless otherwise stated.

// features/user/component.js
const actions = require('./actions')
actions.set(user)

const postActions = require('features/post/actions')
postActions.setErrors(postErrors)

Or, even better maybe, is that each module is rigid enough that it should explicitly export bits of itself for consumption by other modules. In that case, we'd have an index.js file in each module directory, so Post's would look like:

// features/post/index.js
module.exports = {
  component: require('./component')
  actions: require('./actions')
  selectors: require('./selectors')
}

// features/user/component.js
const post = require('features/post')
post.actions.setErrors(errors)
post.selectors.getErrors(state)

Imagine the scenario where the post component, itself, has a public "setErrors" method that other components can call on it. I don't know why it would or what this would do, but I don't feel like re-constructing all the lead-up to this just to have a better theoretical situation here. So...

In the old pre-ES6 style, this is fine:

// features/user/component.js
const post = require('features/post')
post.actions.setErrors(errors)
post.component.setErrors(errors)

If we were doing this the ES6 way, we still need the same level of namespacing, but we're doing it in a more manual way.

// features/user/component.js
import { setPostErrors } from 'features/post/actions'
setPostErrors(errors)

Not terrible, but also not as nice as the pre-ES6 style in my opinion.

But we also have a potential namespace conflict now...

import { setPostErrors } from 'features/post/actions'
import { setPostErrors as setPostComponentErrors } from 'features/post/component'

Yuck. 😷

I think we're better off with the pre-ES6 style when it comes to these things. As an added bonus, we can keep things more concise when we're not traversing modules.

// features/user/component.js
const actions = require('./actions')
actions.set(user)
actions.setErrors(errors)

const post = require('features/post')
post.actions.set(post)
post.actions.setErrors(postErrors)
post.component.setErrors(postErrors)

// or maybe, depending on how we're structuring our data...
actions.setErrors(errors.user)
post.actions.setErrors(errors.post)

Again, User and Post are pretty simplistic, it starts to get hairy when we need to namespace more difficult things (and we do, to an extent, and will, even more, eventually). Let's say a spouse has a group of assets — ie. financial accounts. Each spouse has a different set, and there could be an error updating one of the spouse's financial accounts.

Pre-ES6, shortened, reduced redundancy style:

// features/spouse/non-originating/assets/financial-accounts/actions.js
const SET_ERRORS = 'spouse/nonOriginating/assets/financialAccounts/SET_ERRORS'
module.exports = {
  setErrors: function(errors) { return { type: SET_ERRORS, payload: errors } }
}

// features/spouse/non-originating/assets/financial-accounts/component.js
const actions = require('./actions')
actions.setErrors(errors)

// features/user/component.js
const spouse = require('features/spouse')
spouse.nonOriginating.assets.financialAccounts.actions.setErrors(errors)

More realistically, something like spouse/nonOriginating/assets/component.js would be importing the financial account actions, so this could be shorter, but the above is worst-case-scenario.

In our current, fully-namespaced-everywhere and using ES6 style:

// features/spouse/non-originating/assets/financial-accounts/actions.js
const SET_NON_ORIGINATING_SPOUSE_FINANCIAL_ACCOUNT_ASSET_ERRORS =
  'spouse/nonOriginating/assets/financialAccounts/SET_NON_ORIGINATING_SPOUSE_FINANCIAL_ACCOUNT_ASSET_ERRORS'

export const setNonOriginatingSpouseFinancialAccountAssetErrors = 
  function(errors) { return { type: SET_NON_ORIGINATING_SPOUSE_FINANCIAL_ACCOUNT_ASSET_ERRORS, payload: errors } }

// features/spouse/non-originating/assets/financial-accounts/component.js
import { setNonOriginatingSpouseFinancialAccountAssetErrors } from './actions'
setNonOriginatingSpouseFinancialAccountAssetErrors(errors)

// features/user/component.js
import { setNonOriginatingSpouseFinancialAccountAssetErrors } from 'features/spouse/non-originating/assets/financial-accounts/actions'
setNonOriginatingSpouseFinancialAccountAssetErrors(errors)

There's a ton more redundancy inside the actions file itself, a ton of redundancy that we could — debatably — get rid of inside the financial-accounts component, and even in the worst-case-scenario of the pre-ES6 stlye, I find it much easier to quickly see what this is doing:

spouse.nonOriginating.assets.financialAccounts.actions.setErrors(errors)

than what this is doing:

setNonOriginatingSpouseFinancialAccountAssetErrors(errors)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment