When to use the let keyword
Althought you should always prefer const
definitions in your application code, there’s one use case where let
declarations are useful.
There are some software development practices that can dramatically help you with with your quality of life, and (maybe) help your write better code. Test-driven development (TDD) is one of those. Functional programming is another.
What is functional programming? To me it’s about immutable data structures and a preference for composable list operations like map
, filter
and reduce
. I find that writing code in this way helps me write simple, concise and expressive code. (Of course, as with TDD, just because you use the technique doesn’t mean you’re guaranteed to write better code. Software design is a separate skill.)
From those principles come concrete ideas. “Do not use let
in your production code” is an example of a concrete rule that can be applied in any codebase. And in a React codebase, it’s extremely rarely that I’ll use a let
; about the only case I can think of is when I’m faced with cranky APIs that just don’t work with with map
and forEach
.
However, in test code I make an exception to enable a specific testing pattern.
describe("MyComponent", () => {
let myDependency;
beforeEach(() => {
myDependency = jest.fn(); // doesn't have to be a spy...
});
it("does something", () => {
// use myDependency
})
it("does something else", () => {
// use myDependency
})
})
The let
declaration in the describe
scope means that it myDependency
can be accessed in every test. But it can’t be const
because it isn’t defined with a value at the the point it is declared. Instead, it’s value is set in the beforeEach
block, which means that it’s reset for every test.
Resetting state is critically important with tests, because you have to got to ensure a consistent starting point for every test: they need to be independent of one another. A test that depends on the previous end state of another test is a useless test.
Overriding values within specific tests
Using let
means you can also override values within specific tests:
describe("MyComponent", () => {
let myDependency;
beforeEach(() => {
myDependency = <happy path value>;
});
it("does something", () => {
// use myDependency
})
it("does something evil", () => {
myDependency = <evil path value>;
})
})
And you can even use that with nested describe blocks:
describe("MyComponent", () => {
let myDependency;
beforeEach(() => {
myDependency = <happy path value>;
});
it("does something", () => {
// use myDependency
})
describe("when something evil happens", () => {
beforeEach(() => {
myDependency = <evil path value>;
});
it("does something evil", () => {
// use myDependency
})
});
})
This is a perfectly acceptable testing pattern. In fact, some other languages (like Ruby) have this pattern baked into their testing libraries (RSpec).
So while generally try to avoid let
in my code, this is one place where I’m happy to use it.
— Written by Daniel Irvine on August 23, 2022.