You probably want some parts of Lodash
note: If you're someone who already uses a relatively functional programming style, this post is going to be pretty boring!
You might not need Lodash, but that doesn't mean you don't want some subset of its functions in a utility library. Lodash doesn't tree-shake well, so you might not want to use Lodash's actual library, but I think versions of some of its utility functions—_.groupBy
, _.keyBy
, _.mapValues
, _.orderBy
, and _.uniqBy
—are essential for writing clean, expressive code.
I think example code is the best way of showing why these particular methods can make code more readable. Let's imagine that we're trying to take a list of classes and a list of teachers, organize those classes by grade level, and then print class names out in descending order of grade level with the name of the owner in parens and the number of co-teachers listed out:
### Grade:5
- Algebra [Mrs. Sprout] (1 co-teachers)
- Chemistry [Mr. Xvim Chao] (2 co-teachers)
- Geometry [Ms. Frizzle] (1 co-teachers)
### Grade:4
...
We have the following data:
- An array of classes:
{ id, name, icon, grade, coTeacherIds, classOwnerId }
- An array of teachers:
{ id, name, icon }
Let's assume that teachers
and classes
have enough rows that we want to avoid using Array.find
. If I were trying to write reasonable code without helper functions for that, it might look something like the following:
const teachersById = {};
for (const teacher of teachers) {
teachersById[teacher._id] = teacher;
}
const classesByGrade = {};
for (const c of classes) {
classesByGrade[c.grade] ??= [];
classesByGrade[c.grade].push(c);
}
for (const classes of Object.values(classesByGrade)) {
classes.sort((a, b) => a.name < b.name ? -1 : 1); // sorting classes by name alphabetically
}
const classGrades = Object.keys(classesByGrade);
classGrades.sort((a, b) => b.grade - a.grade);
const output = [];
for (const grade of classGrades) {
output.push(`### Grade:${grade}`);
output.push("");
output.push(...classesByGrade[grade].map((class) => `- ${c.name} [${teachersById[c.classOwnerId].name}] (${c.coTeacherIds.length} co-teachers)`);
output.push("");
}
return output.join("\n");
There are a few things I don't like about this version of the code:
- I can never remember how
.sort
works, so I'm not able to read (or write) the code and tell whether I'm ordering things correctly. - It's relatively verbose. While each
for
loop is only doing a single thing, you have to read the code to tell what that thing actually is.
Let's compare this to a more functional style that uses Lodash:
const teachersById = _.keyBy(teachers, "id");
const classesByGrade = _.groupBy(classes, "grade");
const orderedClassesByGrade = _.mapValues(classesByGrade, (classes) => {
return _.orderBy(classes, ["name"], ["asc"]);
});
const gradeOrder = _.orderBy(
Object.keys(orderedClassesByGrade), [(g) => g], ["desc"]
);
const output = gradeOrder.flatMap((grade) => {
return [
`### Grade:${grade}`,
"",
...orderedClassesByGrade[grade].map((c) => {
return `- ${c.name} [${teachersById[c.classOwnerId].name}] (${c.coTeacherIds.length} co-teachers)`;
}),
""
];
});
return output.join("\n)
If you're not used to these particular Lodash methods, this probably looks worse to you! I personally find it easier to read:
_.keyBy(teacher, "id")
means we want to be able to quickly look up a teacher by their id._.groupBy(classes, "grade")
means we want to have lists of classes for each grade._.mapValues(classes, ...)
means we want to transform those lists of classes in some way, and_.orderBy
tells me how we're doing the transformation.- I can immediately understand which way the
_.orderBy
calls will order the outputs.
You might not like either of these implementations! I'm not arguing that either implementation is the "right way" to tackle this particular problem, but I'm hoping that the comparison between them is useful to illustrate why a person might want those particular utility functions if they're regularly re-shaping data.
Still... you probably don't need all of Lodash, especially if you're working on the frontend. It's a relatively large dependency, and it doesn't tree-shake well. The methods that I personally think are essential are:
_.keyBy
comes up all the time if you're trying to avoidArray.find
to write performant code._.groupBy
comes up for similar reasons—avoiding writing ugly code with lots ofArray.filters
._.mapValues
is the necessary complement to both_.groupBy
and_.keyBy
. Once you have the values organized in the way that you'd like, you'll often need to transform them into an appropriate output shape._.orderBy
is essential becauseArray.sort
is terrible. Returning 0, 1, and -1 isn't nearly as ergonomic as just returning the key that you want to sort by._.uniqBy
is a nice utility when you need to dedupe an array- I quite like
_.filter
,_.reject
, and_.partition
.Array.filter
exists, but a lot of the time I'm filtering on a predicate, I'm also interested in the values that don't match that predicate, I like to have both_.reject
and_.partition
available.const [thingsWithX, thingsWithoutX] = _.partition(things, thing => thing.x)
feels clear to me. - A few other Lodash methods that once felt essential—
_.flatMap
,_.every
,_.extend
, and_.some
—are now part of the JS language.
Depending in the problem domain that you're working in, different lodash methods might prove to be the essential ones! But for whatever problem domain you're working in, I bet there's a set of standard utility functions that would make your code easier to read and work with. The list of Lodash functions is a good starting point.