- "FP" stands for Functional Programming
- Submodule of Lodash's library (
import x from "lodash/fp/x"
versusimport x from "lodash/x"
) - Has a writeup on Lodash's Github page: https://github.com/lodash/lodash/wiki/FP-Guide
-
Immutable: All functions return a copy of the data instead of modifying it. Even functions which normally mutate data, like
_set
, are wrapped to make a copy first. -
Auto-curried, Data-last: This one's harder to explain and the value isn't obvious at first.
- Curried functions take their arguments one at a time, returning a callback function until it receives the last parameter. E.g. a curried version of
f(a, b, c)
is called asf(a)(b)(c)
. - Auto-curried functions can be called the normal way, but they produce curried versions of themselves until all their arguments are supplied. E.g. If
f
normally takes three arguments, you can call it asf(a,b,c)
,f(a,b)(c)
,f(a)(b)(c)
and so on. - "Data-last" simply means they've rearranged the arguments for every Lodash function so the
data
parameter is the last one instead of the first. - The result of this is, it enables a programming style where you compose multiple Lodash FP functions into a super-function, and then pass the
state
as the very last argument. This will make more sense later.
- Curried functions take their arguments one at a time, returning a callback function until it receives the last parameter. E.g. a curried version of
Consider a reducer where you simply need to set a value:
createReducer({
// State of a fictional running application
foos: {
123: {
loadingState: NOT_STARTED,
results: {
id: 123,
bar_id: 456,
},
}
},
editorState: {
id: 123,
bar_id: 456,
},
}, {
// Event handlers
FOO_EDITOR_SET_BAR: (state, { barId }) => {
// We can do this multiple ways:
// Strategy 0 (WRONG): Mutate the state
state.editorState.bar_id = barId;
return state;
// Strategy 1: Pyramid of exploded objects
return {
...state,
editorState: {
...state.editorState,
bar_id: barId,
}
}
// Strategy 2: Lodash to clone and set property
const newState = _cloneDeep(state);
_set(newState, 'editorState.bar_id', barId);
return state;
},
});
Strategy 1 does not scale well, we decided long ago.. Strategy 2 works, but for large deep-cloning a large piece of the state tree can cause many components to re-render unnecessarily, which slows down the app.
Lodash FP wraps data-mutating functions like _set()
so they clone the original data first, but unlike _cloneDeep()
they only clone the parts of the data structure that need to change.
import fpSet from 'lodash/fp/set';
const a = { foo: 'a', bar: [1,2,3] };
const b = fpSet('foo', 'b', a);
console.log(a); // {foo: "a", bar: [1, 2, 3]}
console.log(b); // {foo: "b", bar: [1, 2, 3]}
// Since *.bar was unaltered, `a` and `b` reference the same array, so React would assume it's unchanged.
console.log(a.bar === b.bar); // true
a.bar.push(4);
console.log(b); // {foo: "b", bar: [1, 2, 3, 4]}
You may notice this does the same thing as our deepAssign()
. But unlike deepAssign someone else maintains and tests this code.
import mapValues from 'lodash/mapValues';
import update from 'lodash/update';
import without from 'lodash/without';
// Non-Functional Programming solution
return {
...state,
results: _mapValues(state.results, (organization) => {
return {
...organization,
customerListIds: _without(organization.customerListIds, [customerListId]),
};
}),
};
import fpMapValues from 'lodash/fp/mapValues';
import fpUpdate from 'lodash/fp/update';
import fpWithout from 'lodash/fp/without';
// Use with Lodash FP functions.
// Same as regular Lodash, but the `data` parameter is LAST instead of FIRST
return fpUpdate(
'results',
results => fpMapValues(
organization => fpUpdate(
'customerListIds',
customerListIds => fpWithout(customerListId, customerListIds),
organization
),
results
),
state
);
// Lodash FP automatically *curries* -- meaning, if you don't give all the parameters,
// instead of erroring it returns a copy of itself with the arguments pre-bound.
// So instead of doing `f(a, b)` you can also do `f(a)(b)`.
return fpUpdate(
'results',
results => fpMapValues(
organization => fpUpdate(
'customerListIds',
customerListIds => fpWithout(customerListId)(customerListIds)
)(organization),
)(results)
)(state);
// Lambdas can be removed by substituting `x => f(x)` with `f`
return fpUpdate(
'results',
fpMapValues(
// Remove the deleted customer list id from each org
fpUpdate(
'customerListIds',
fpWithout(customerListId)
)
)
)(state);
// Let's collapse this a bit to see what it looks like.
return fpUpdate('results', fpMapValues(fpUpdate('customerListIds', fpWithout(customerListId))))(state);