Lodash is the library I reach for most when writing JavaScript. In this post I'll show you how the functional programming (FP) build of Lodash can really tidy up your reducers.
To get started, import the functions that we'll use: import {set, update, flow} from 'lodash/fp';
. Notice these are being imported from 'lodash/fp'
, not 'lodash'
.
Then have a look at this example:
const reducer = (state, action) => {
switch (action.type) {
case 'SET':
return set('some.deep.key', action.value, state);
case 'INCREMENT':
return update('some.deep.key', i => i + 1, state);
case 'FOO':
return flow(
set('some.deep.key', action.value),
update('another.deep.key', i => i * 2),
)(state);
}
};
Keen users of Lodash will see that the arguments for set
and update
are jumbled around! In the branch for FOO
we only pass in two arguments, which is odd since we passed in three arguments earlier! What's going on?
The arguments are in a different order due to the FP build of Lodash. This build is not as well documented as the standard one, which is a bummer, but there are some docs available here.
The lodash/fp module promotes a more functional programming (FP) friendly style by exporting an instance of lodash with its methods wrapped to produce immutable auto-curried iteratee-first data-last methods.
Hold on! It's not as scary as it sounds.
- immutable: Reducers must not modify the state, instead they must return a new state. Lodash FP and Redux go together like PB&J!
- auto-curried: A curried function takes a strict number of arguments. If it's given any less, it returns another function that takes the rest of the arguments before returning a value.
- iteratee-first: the "iteratee" is one of Lodash's superpowers. In our example, it's the dotted path inside of an object (our slice of state).
-
data-last: the "data" in our examples is
state
. In normal Lodash, the data is usually the first argument.
If set
is given a path and a value but not the state, it will return a function. That returned function takes one argument, the state, and then returns the new state. That's currying!
const setNameToRobby = set('name', 'Robby');
setNameToRobby({ name: 'Robert', location: 'Wellington' });
// => { name: 'Robby', location: 'Wellington' }
Often I find that a given action will do a few things in a single reducer. This is where flow
and curried functions comes in handy. flow
takes a number of functions and returns a single function that composes them all together. A number of set
s and update
s (and others you might find useful in Lodash FP) can be given to flow
as arguments and flow will produce a single function. That single takes in state
and gives it to the first of the original functions, and the value out of the first feeds into the second and so on.
flow(
set('some.deep.key', action.value),
update('another.deep.key', i => i * 2),
)(state);
// is equal to:
update(
'another.deep.key',
i => i * 2,
set(
'some.deep.key',
action.value,
state
),
)
// and quite a bit more readable!
Top comments (3)
One thing I noticed with
lodash/fp
set
, it behaves in an immutable fashion but just for the objects in the branch it touches - it's probably not a big deal, but good to know:-)@brian copying entire object is always more expensive than copying only a reference. When some part of the parent object is not modified like address in your example, it will be computationally cheaper to copy only a reference to it. The values for that part of the nested object were not modified so we don't have to alert any potential observers about any changes (this alert is done exactly by violation of reference equality). So for instance in React - components that were observing only the address attribute will get the same reference to it and they will treat it as the same value, hence no unnecessary rerender. We can actually say that the immutability for the entire object was preserved and it was done in performant manner, both from JS runtime perspective and potential rerenders in the browser / virtual DOM / etc.
Fun fact: Function currying is named for Haskell Curry