Skip to content

Commit 6ef2aae

Browse files
authored
Immer documentation (#232)
1 parent fa9c379 commit 6ef2aae

File tree

1 file changed

+232
-0
lines changed

1 file changed

+232
-0
lines changed

packages/components/docs/immer.md

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# Immer
2+
3+
[Immer](https://immerjs.github.io/immer/docs/introduction) is a JavaScript package that provides immutability for normal
4+
JavaScript objects, arrays, Sets, and Maps. After internal review by several of our frontend engineers we've elected
5+
to add Immer as a dependency and start using it for new development in lieu of [ImmutableJS](https://immutable-js.github.io/immutable-js/).
6+
That said, we do not plan on actively migrating all usages away from ImmutableJS and it will remain a dependency for
7+
the foreseeable future.
8+
9+
This document intends to outline why we're moving to Immer, provide some links to good resources for Immer, and provide
10+
a couple of scenarios highlighting aspects of note.
11+
12+
## Rationale for switching
13+
14+
In April 2020 we took some time to start investigating how we could move
15+
away from ImmutableJS. We've utilized ImmutableJS since 2015
16+
to provide immutable data structures to work with on the client. At the time, it was one of the most well-supported
17+
immutablility packages out there. Alas, as things progressed ImmutableJS started to fall out of favor for
18+
several reasons.
19+
20+
#### Disadvantages of ImmutableJS
21+
22+
1. No longer actively developed. We use `v3.8.2` which was released in late 2016. `v4` has yet to get
23+
passed "release candidate" status with it's most recent candidate releasing in late 2018.
24+
1. API learning curve. Immutable provides all of its own data structures (e.g. `List`, `Map`, `Set`, etc) which are
25+
wholy different from native JS data structures. While the API for these structures is powerful, allowing for really
26+
complex mutations and iterations, it can be difficult to ramp up on understanding it all.
27+
1. Poorly constructed `Record`. A class we rely on heavily is `Immutable.Record`. Extending record and
28+
providing the correct typings annotations requires three declarations of each value. Additionally, due to the
29+
nature of ImmutableJS, the constructor isn't able to make any effectual modifications of what the user passes in,
30+
which lead to us using a `RecordType.create()` static method pattern. `Immutable.Record` was removed in `v4`.
31+
1. Difficult to debug. The Immutable data structures can be difficult to debug and generally requires the code to be
32+
modified to include `.toJS()` statements to understand what is actually held in a data structure.
33+
34+
#### Advantages of Immer
35+
36+
These are copied [directly from the website](https://immerjs.github.io/immer/docs/introduction#benefits):
37+
38+
1. Immutability with normal JavaScript objects, arrays, Sets and Maps. No new APIs to learn!
39+
1. Strongly typed, no string based paths selectors etc.
40+
1. Structural sharing out of the box
41+
1. Object freezing out of the box
42+
1. Deep updates are a breeze
43+
1. Boilerplate reduction. Less noise, more concise code.
44+
1. First class support for patches
45+
1. Small: 3KB gzipped
46+
47+
## Learning Immer
48+
49+
This document intends to provide some specific insights about using Immer in our LabKey client-side code. As such, it expects
50+
the reader to have an understanding of why Immer exists, what Immer does, and how Immer does it.
51+
52+
Before you read further it is **highly recommended** that you read (or watch) at least one the following:
53+
54+
- [Immer's official documentation](https://immerjs.github.io/immer/docs/introduction) - read the docs! Most up-to-date and covers lots of topics.
55+
- [Introducing Immer: Immutability the easy way](https://hackernoon.com/introducing-immer-immutability-the-easy-way-9d73d8f71cb3) - written by the author of Immer
56+
- [Simplify Creating Immutable Data Trees With Immer](https://egghead.io/lessons/redux-simplify-creating-immutable-data-trees-with-immer) - egghead.io video tutorial
57+
58+
## Scenarios
59+
60+
This section focuses on a couple of scenarios to help get a better understanding. These were written against `v6.0.3` of
61+
Immer so things may have changed if you're working with a more current version.
62+
63+
### Immutable class
64+
65+
This scenario highlights declaring an immutable class in TypeScript using Immer. By the end we'll have an immutable class
66+
that is both compile-time and run-time safe. To keep the class simple we're going to define a `Circle` class defined only
67+
by its `radius`:
68+
69+
```ts
70+
class Circle {
71+
radius: number;
72+
73+
constructor(r: number) {
74+
this.radius = r;
75+
}
76+
}
77+
```
78+
79+
#### Run-time safety
80+
81+
This initial declaration is fully mutable. You can externally modify the radius after construction.
82+
83+
```ts
84+
let circle = new Circle(5);
85+
circle.radius = 10; // radius now 10
86+
```
87+
88+
Let's try to use Immer on this class and see if it works:
89+
90+
```ts
91+
import { produce } from 'immer';
92+
93+
let circle = produce(new Circle(5), () => {});
94+
circle.radius = 10; // circle.radius is now 10! I thought using produce made it immutable!
95+
```
96+
97+
To make a class immutable with Immer you first annotate the class with a [Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol)
98+
provided by Immer called `immerable`:
99+
100+
```ts
101+
import { immerable } from 'immer';
102+
103+
class Circle {
104+
[immerable] = true;
105+
106+
radius: number;
107+
108+
constructor(r: number) {
109+
this.radius = r;
110+
}
111+
}
112+
```
113+
114+
What does this symbol do? To paraphrase the Immer docs:
115+
116+
> Classes must use the `immerable` symbol to mark itself as compatible with Immer.
117+
When one of these objects is mutated within a producer, its prototype is preserved between copies.
118+
119+
Now this class is ready to be used with Immer. Let's try again using `produce`:
120+
121+
```ts
122+
import { produce } from 'immer';
123+
124+
// without using produce the instance is still mutable
125+
let circle = new Circle(5);
126+
circle.radius = 20; // radius now 20.
127+
128+
// with produce the instance is now immutable
129+
circle = produce(new Circle(5), () => {});
130+
circle.radius = 10; // non-strict mode: fails silently. Radius still 5.
131+
circle.radius = 10; // strict mode: Run-time error: Cannot assign to read only property 'radius' of object '#<Circle>'
132+
```
133+
134+
Instances of this class declared via `produce` can only be mutated via `produce`. When an instance is passed through
135+
Immer's `produce` function it will [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)
136+
the object (when `immer.setAutoFreeze(true)`). Depending on the strict mode, any attempts to explicitly modify the object
137+
will either fail to modify or throw a run-time error.
138+
139+
Now, let's actually make an update to the immutable instance with Immer:
140+
141+
```ts
142+
import { produce } from 'immer';
143+
144+
// Create the initial instance
145+
const circle = produce(new Circle(5), () => {});
146+
147+
// Mutate and copy from `produce`
148+
const newCircle = produce(circle, (draft) => {
149+
draft.radius = 10;
150+
});
151+
console.log(circle.radius); // 5
152+
console.log(newCircle.radius); // 10
153+
```
154+
155+
The instance is now immutable, a mutated copy can be made via `produce`, and we have run-time safety via `Object.freeze`.
156+
157+
#### Compile-time safety
158+
159+
Immer results in run-time safety from mutations to your objects, but used in conjunction with Typescript you can
160+
also get compile-time safety. This has the advantages of catching errors earlier and applying to all code paths,
161+
even those not covered by tests.
162+
163+
To get started, let's first declare all the properties on the `Circle` class as [`readonly`](https://www.typescriptlang.org/docs/handbook/classes.html#readonly-modifier).
164+
165+
```ts
166+
import { immerable } from 'immer';
167+
168+
class Circle {
169+
[immerable] = true;
170+
171+
readonly radius: number;
172+
173+
constructor(r: number) {
174+
this.radius = r;
175+
}
176+
}
177+
```
178+
179+
The `radius` property is now read-only so if we attempt to modify it directly we receive an error:
180+
181+
```ts
182+
import { produce } from 'immer';
183+
184+
let circle = new Circle(5);
185+
circle.radius = 5; // Error: TS2540: Cannot assign to 'radius' because it is a read-only property.
186+
187+
// Same for the produced version
188+
let circle = produce(new Circle(5), () => {});
189+
circle.radius = 5; // Error: TS2540: Cannot assign to 'radius' because it is a read-only property.
190+
```
191+
192+
This gives us compile-time safety against invalid writes. The next feature we can use is the `Draft` utility from
193+
Immer. `Draft` To quote the docs:
194+
195+
> The `Draft` utility type can be used if the state argument type is immutable.
196+
197+
```ts
198+
import { Draft, produce } from 'immer';
199+
200+
// Without "Draft"
201+
let circle = produce(new Circle(5), (draft: Circle) => {
202+
draft.radius = 10; // Error: TS2540: Cannot assign to 'radius' because it is a read-only property.
203+
});
204+
205+
// With "Draft"
206+
let circle = produce(new Circle(5), (draft: Draft<Circle>) => {
207+
draft.radius = 10; // OK!
208+
});
209+
```
210+
211+
If you'd like to have your class instances be immutable without requiring use of `produce` you can directly call
212+
`Object.freeze` at the end of the constructor.
213+
214+
```ts
215+
import { immerable } from 'immer';
216+
217+
class Circle {
218+
[immerable] = true;
219+
220+
readonly radius: number;
221+
222+
constructor(r: number) {
223+
this.radius = r;
224+
225+
// Optionally, freeze the instance. Still works with produce but doesn't require it to have
226+
// an immutable instance via construction.
227+
Object.freeze(this);
228+
}
229+
}
230+
```
231+
232+
Now we have an immutable class that is compile-time safe, run-time safe, and can be utilized by Immer.

0 commit comments

Comments
 (0)