React and Redux expect state to be immutable: you don't change an object that's already part of the state. To make a change, you build a new version of the state and leave the old one as it was. Copying the whole state for every small change would be slow. Structural sharing avoids that: the new version copies only the objects on the path from the top of the state down to the change, and reuses every other part of the old version, by reference.
Take this state, and mark the second todo as done:
const state = {
settings: { theme: "dark" },
todos: [
{ id: 1, text: "Shop", done: false },
{ id: 2, text: "Cook", done: false },
],
};
| Part | In the new version | The same object as before? |
|---|---|---|
| The state itself | A new object, holding the new todos | No: it's on the path to the change |
todos | A new array, holding the new second todo | No: it's on the path |
| The second todo | A new object, { id: 2, text: "Cook", done: true } | No: it's what changed |
| The first todo | The old object | Yes: shared |
settings | The old object | Yes: shared |
Because the unchanged parts are the very same objects, === tells you what changed without looking inside. If next is the new version, next.settings === state.settings is true, so nothing under settings changed, while next.todos !== state.todos says something in the list did. React's memo and React Redux's useSelector compare by reference like this, and skip work when nothing they depend on changed. The old version stays intact too, so undo can go back to it.
Changing an object in place breaks all this: it stays the same object, so === says nothing changed, and React may skip the update. Immer, the library Redux Toolkit uses, lets you write an update as if you were changing the state in place, such as state.todos[1].done = true, and builds the new, structurally shared version for you.
Write setIn(object, path, value), where path is an array of keys. It returns a new version of object with value at path, and leaves object and everything in it unchanged, the way state updates work in React or Redux. You can assume the containers are plain objects and arrays.
Copy only what's on the path: object itself and each object or array you step through (arrays stay arrays). Every branch off the path is shared, by reference, with the original, so comparing the old and new versions with === shows exactly what changed.
When a step is missing or isn't an object, create it: an array if the key that goes into it is a number, a plain object otherwise. If the value at path is already value (by Object.is), return object itself, since nothing changed. An empty path returns value.
const state = { user: { name: "Ada", address: { city: "London" } }, posts: [] };
const next = setIn(state, ["user", "name"], "Grace");
next.user.name; // "Grace"
state.user.name; // "Ada"
next.user === state.user; // false: on the path, so copied
next.user.address === state.user.address; // true: off the path, so shared
next.posts === state.posts; // true
const state = { user: { name: "Ada", age: 36 } };
expect(setIn(state, ["user", "name"], "Grace")).toEqual({ user: { name: "Grace", age: 36 } });
const state = { user: { name: "Ada" } };
const next = setIn(state, ["user", "name"], "Grace");
expect(state).toEqual({ user: { name: "Ada" } });
expect(next).not.toBe(state);
expect(next.user).not.toBe(state.user);
const state = { user: { name: "Ada", address: { city: "London" } }, posts: [{ id: 1 }], settings: { theme: "dark" } };
const next = setIn(state, ["user", "name"], "Grace");
expect(next.user.address).toBe(state.user.address);
expect(next.posts).toBe(state.posts);
expect(next.settings).toBe(state.settings);
const state = { list: [1, 2, 3] };
const next = setIn(state, ["list", 1], 20);
expect(Array.isArray(next.list)).toBe(true);
expect(next.list).toEqual([1, 20, 3]);
expect(state.list).toEqual([1, 2, 3]);
expect(setIn({}, ["a", "b", 0], "x")).toEqual({ a: { b: ["x"] } });
expect(setIn({ a: 1 }, ["a", "b"], 2)).toEqual({ a: { b: 2 } });
const state = { user: { name: "Ada" }, score: NaN };
expect(setIn(state, ["user", "name"], "Ada")).toBe(state);
expect(setIn(state, ["score"], NaN)).toBe(state);
expect(setIn({ a: 1 }, [], 5)).toBe(5);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Immutable setIn. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.