« Scott Nonnenberg


Better async Redux, i18n, and Node.js versions

2016 Oct 11

It’s time for another edition of recent stack improvements! This time we’re primarily focused on React and Redux. But if you use Node.js at all, my comparison of Node.js version managers should be interesting!

redux-loop

In Redux, actions are passed through middleware, then reducers, then the new state resulting from those reducers is passed to your React components. It’s a single source of truth, and a single unidirectional update path through the system. Beautiful. But that entire process is synchronous!

Because there’s nothing in the core system addressing asynchronous actions, quite a few libraries have been released to try to address it:

  • redux-thunk gives every action creator direct access to dispatch(), and has no concept of when an async task is ‘done.’ But this is important for ensuring everything is ready for server rendering.
  • redux-promise-middleware gives you the power of promises, but chained async behavior gets very painful.
  • redux-saga is designed specifically to handle chained async behavior. But it again has the ‘done’ problem, and is probably too complex.

So each of the well-known libraries attempting to help have limitations. Is there a better way? Well, Redux was originally inspired by Elm. How does Elm do it? Let’s take a look at its ‘http’ example:

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    MorePlease ->
      (model, getRandomGif model.topic)

    FetchSucceed newUrl ->
      (Model model.topic newUrl, Cmd.none)

    FetchFail _ ->
      (model, Cmd.none)

First, note that the signature of the update function is Msg -> Model -> (Model, Cmd Msg). Like a Redux reducer, it mutates the Model (state) based on an incoming Msg (action). The difference is that instead of returning a plain Model it returns a tuple of Model and Cmd Msg.

In the MorePlease case, getRandomGif model.topic is the Cmd Msg. It’s not a function call, but the function and arguments which will be assembled into a function call. Cmd is a generic Elm operation and the Msg is its return type. When getRandomGif succeeds, it returns a FetchSucceed (a variant of Msg), which is then sent through the update function:

getRandomGif : String -> Cmd Msg
getRandomGif topic =
  let
    url =
      "https://api.giphy.com/v1/gifs/random?api_key=KEY&tag=" ++ topic
  in
    Task.perform FetchFail FetchSucceed (Http.get decodeGifUrl url)

Elegant, right?

Enter redux-loop, a Javascript project which describes itself like this:

A port of elm-effects and the Elm Architecture to Redux that allows you to sequence your effects naturally and purely by returning them from your reducers.

Let’s reimplement the above example in redux-loop:

import { loop, Effects } from 'redux-loop';
import { fromJS } from 'immutable';

// action creators
const morePlease = () => ({
  type: 'MORE_PLEASE',
});
const fetchSucceed = (url) => ({
  type: 'FETCH_SUCCEED',
  payload: url,
});

const fetchFail = () => ({
  type: 'FETCH_FAIL',
});

const getRandomGif = (topic) =>
  fetch(`https://api.giphy.com/v1/gifs/random?api_key=KEY&tag=${topic}`)
    .then(decodeUrl)
    .then(fetchSucceed)
    .catch(fetchFail);

const initialState = fromJS({
   topic: 'cats',
   url: null,
});

function reducer(state, action) {
  if (!state) {
    return initialState;
  }

  switch(action.type) {
    case 'MORE_PLEASE': {
      return loop(state, Effects.promise(getRandomGif, state.get('topic')))
    }

    case 'FETCH_SUCCEED': {
      return state.set('url', action.payload);
    }

    case 'FETCH_FAILED': {
      return state;
    }

    default: {
      return state;
    }
}

The user clicks a button and the MORE_PLEASE event is fired, which kicks off the fetch(). When that succeeds, the UI is updated because state.url has a new image location.

I like it! It allows for composition via chained tasks, expressed very clearly, in a very easy-to-test way. And the library itself isn’t much code!

Integrating it into your app

Having used redux-loop a good bit, there are some challenges with it:

Happily, you can fix all of these by reimplementing this method. You don’t even need a fork. :0)

On the other hand, if you have any Redux middleware which calls dispatch()/next() multiple times, like redux-api-middleware or redux-promise-middleware, things get a little more complicated.

Making these components play nicely takes some work but allows for more declarative action creators, even changing behavior client/server with different middleware. You’ll have to dig in and see what feels right for you and your team.

Finally, be aware that redux-loop uses Symbol, a new Javascript feature not present in PhantomJS or older browsers. You’ll need a polyfill of some kind.

react-intl

Internationalization (i18n) is painful. As a programmer, whenever I think about it, I think back to horrible string tables stored outside the code, with nothing but brittle IDs in the code itself. One typo in the ID and it breaks. Incorrectly reference any string interpolation hints in the separate string table file, and it breaks. Forget to update the string table when updating the code, and at minimum the UI is broken.

react-intl is the first i18n library I’ve used that feels right, feels natural. First, with the magic of Babel code analysis, your default language strings are in the code:

<FormattedMessage
  id="inbox.welcome"
  defaultMessage={`Hello {name}! Messages unread: {unreadCount, number}`}
  values={{
    name,
    unreadCount,
  }}
/>

The <FormattedMessage> React component will only use the defaultMessage (and warn on the console) if you haven’t provided any locale-specific translations.

Next, the babel-plugin-react-intl package will extract all strings for your default language dictionary file. In your .babelrc:

{
  "plugins": [
    ["react-intl", {
        "messagesDir": "./build/messages/"
    }]
  ]
}

Now you have simple JSON files with all your strings! Ready to send to localizers, and ready to use as locale data in your app. The react-intl repo has a working example of all of this.

The ICU format

Okay, now that we’re managing our strings well, it’s time to do i18n right. react-intl does a lot for you regarding date and number formatting. But as you can start to see with the <FormattedPlural> react component, pluralization is where it gets really tricky. What are the zero, two, and few props for?

It turns out that each language has different rules for pluralization. Not just different words for item versus items, but more words and thresholds where they apply! FormatJS is the parent project of react-intl, and has this example on its homepage:

“Annie took 3 photos on October 11, 2016.”

In English there are two required states for this: ‘1 photo’, and ‘N photos.’ But FormatJS decided to make it nicer with a better option for zero: ‘no photos’:

{name} took {numPhotos, plural,
  =0 {no photos}
  =1 {one photo}
  other {# photos}
} on {takenDate, date, long}.

This is the International Components for Unicode (ICU) message format. Not only does it specify the translations, but it specifies the thresholds for where they should apply. This means that we can now, in the string itself, handle Polish properly. For that simple string, Polish has four possible states:

{takenDate, date, long} {name} {numPhotos, plural,
  =0 {nevyfotila}
  other {vyfotila}
} {numPhotos, plural,
  =0 {žádnou fotku}
  one {jednu fotku}
  few {# fotky}
  other {# fotek}
}.

The old ways aren’t adequate. Simple string interpolation (%s) isn’t enough because different languages might need the components in different orders, like the date first in Polish. Named string interpolation ({itemName}) isn’t enough because the pluralization rules themselves change by language, along with the words.

The ICU message format is what react-intl uses, and it’s the right way to do i18n. Use it! You don’t even have to use it with React, calling defineMessages() directly, along with the intl-messageformat node module.

nvm instead of n

You can’t necessarily upgrade all of your Node.js apps at once. Or perhaps a contract has locked its version to something older than what you generally use. Or perhaps you just want to try out the latest releases without a permanent commitment. You need a version manager.

When I was looking for a Node.js version manager a couple years ago, I seized upon n. It didn’t mess with environment variables, and it always installed binaries. No long builds from source. It replaced /usr/local/bin/node whenever I switched, so nothing else had to change. Nothing else needed to worry about different paths. It worked well.

That is, until I started upgrading npm beyond the default installed with Node.js. In particular, as soon as my projects required npm version 3, n became very painful. Every time I switched, n would replace npm with the default for that version of Node.js. And sometimes it would break npm, so I’d have to blow away /usr/local/lib/node_modules/npm manually.

I took another look, and realized that nvm had some distinct advantages:

  • It keeps a unique set of globally-installed node modules for each version of Node.js! That meant I could keep npm at whatever version I wanted. This also applies for every other command-line node module I installed.
  • It understands the concept of LTS builds and other shorthand aliases. nvm install 6 installs the latest 6.x.x available. nvm install lts/* installs the most recent LTS build.
  • It allows you to use different versions, at the same time, in different terminal windows. This allowed me to run my thisDayInCommits node script again, which has been broken by node-git for quite some time now.

There is one disadvantage, though. Anything that doesn’t run your shell setup script (like ~/.profile.sh or ~/.bash_profile) won’t have a node command in its path. For example, a GUI git application combined with ghooks/validate-commit-msg will give you a ‘node: command not found’ error. Here’s a fix for OSX or Linux: for a machine-wide node command to be used when nvm is not set up:

ln -s /user_home/.nvm/versions/node/vX.X.X/bin/node /usr/local/bin/node

moving from n to mvn

Okay, so maybe you’re convinced, but you’re currently using n. How to switch? Here are the steps I used to clean up:

# get rid of n binary
rm /usr/local/bin/n

# get rid of installed node versions
rm -rf /usr/local/n/

# get rid of n-managed commands
rm /usr/local/bin/iojs
rm /usr/local/bin/node
rm /usr/local/bin/npm

# get rid of headers
rm -rf /usr/local/include/node

Now you’re ready to install nvm!

Small improvements every X

I’m always discovering better ways of doing things every day, every week, every month, every year. Hopefully you’ve found these useful. Watch for more!

Have you found anything interesting lately? Let me know!

I won't share your email with anyone. See previous emails.

NEXT:

The technology side of Agile 2016 Oct 18

I’ve already written about why Agile is interesting in the first place, and how you might customize its application for your team. The hard truth is that you can’t become Agile overnight. People... Read more »

PREVIOUS:

Systems for collaboration 2016 Oct 04

A good workplace is a welcoming space for everyone, encourages open collaboration, and enables everyone to do their best work. Let’s talk about some of the techniques for collaboration I’ve learned... Read more »


Hi, I'm Scott. I've written both server and client code in many languages for many employers and clients. I've also got a bit of an unusual perspective, since I've spent time in roles outside the pure 'software developer.' You can find me on Mastodon.