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!
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!
Having used redux-loop
a good bit, there are some challenges with it:
Effect
must resolve to a Redux action, errors are effectively swallowed. This leads to two problems. On the server, you might want to return a totally different error page. On the client, you probably want to capture and log the error centrally, not in every action creator.Effect
from one incoming action. All must resolve before any resultant action is provided to dispatch()
. This means that the shortest async operation must wait on the longest operation before any of it hits your reducers
.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.
dispatch()
before their natural order. So, either the redux-loop
extra dispatch()
call for an Effect
will miss your redux-promise-middleware
, or the redux-promise-middleware
extra call to dispatch()
when a promise resolves will not go through redux-loop
. You can solve this by putting the install()
of redux-loop
after your middleware, then providing that install()
method with access to the top-level store.dispatch
method, grabbing it after the store is created.redux-promise-middleware
does not return the promise that results from its *_PENDING
action - so Effects
generated from that action won’t be waited on like you would expect. You might need a fork here, to add a Promise.all()
.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.
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.
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.
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:
npm
at whatever version I wanted. This also applies for every other command-line node module I installed.nvm install 6
installs the latest 6.x.x
available. nvm install lts/*
installs the most recent LTS build.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
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!
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’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 »
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 »