- "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
fnormally 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
dataparameter 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
stateas 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);