Vue tracks changes to plain objects with a Proxy. Write the two functions at the core of that: reactive and watchEffect.
reactive(object) returns a proxy for an object or an array. Reading and writing the proxy's properties reads and writes the original object. Calling reactive again with the same object, or with a proxy it returned, returns the same proxy.
Reading a property whose value is an object or an array, through a proxy, returns reactive(value). So nested data is reactive too, and reading it twice gives the same proxy.
watchEffect(fn) calls fn right away and tracks every property that fn reads through a proxy. When one of those properties is set to a value that isn't Object.is-equal to the old one, or is deleted, fn runs again, synchronously, during the write. watchEffect returns a stop function, and after stop(), fn never runs again.
Tracking is per object and per property: a write reruns only the effects that read that property of that object. The tracked properties are collected afresh on every run, so a property that a branch no longer reads stops rerunning the effect.
Reading a property that doesn't exist yet, or checking it with in, tracks it too, so adding it reruns the effect. Listing the keys, with Object.keys or for...in, tracks the list of keys: adding or deleting any property reruns the effect.
An array's length is a property like the others. When a write changes it, effects that read length rerun, including when push, pop, or a write past the end changes it.
An effect never reruns because of its own writes while it's running, so an effect may write to properties it reads.
const state = reactive({ user: { name: "Ada" }, todos: ["Write tests"] });
watchEffect(() => console.log(state.user.name + ": " + state.todos.length));
// logs "Ada: 1"
state.user.name = "Grace"; // logs "Grace: 1"
state.todos.push("Ship it"); // logs "Grace: 2"
state.user.name = "Grace"; // the same value: logs nothing
state.theme = "dark"; // the effect never read theme: logs nothing
const raw = { count: 1, user: { name: "Ada" } };
const state = reactive(raw);
expect(state.count).toBe(1);
state.count = 2;
expect(raw.count).toBe(2);
expect(state.user.name).toBe("Ada");
expect(state.user).toBe(state.user);
expect(reactive(raw)).toBe(state);
expect(reactive(state)).toBe(state);
const state = reactive({ count: 1 });
const seen = [];
const stop = watchEffect(() => seen.push(state.count));
expect(seen).toEqual([1]);
state.count = 2;
state.count++;
expect(seen).toEqual([1, 2, 3]);
stop();
state.count = 4;
expect(seen).toEqual([1, 2, 3]);
const first = reactive({ name: "Ada", age: 36 });
const second = reactive({ name: "Grace" });
const run = spy();
watchEffect(() => {
run();
first.name;
});
first.age = 37;
second.name = "Hopper";
expect(run).toHaveBeenCalledTimes(1);
first.name = "Lovelace";
expect(run).toHaveBeenCalledTimes(2);
const state = reactive({ n: NaN, word: "a", zero: 0 });
const run = spy();
watchEffect(() => {
run();
state.n;
state.word;
state.zero;
});
state.n = NaN;
state.word = "a";
expect(run).toHaveBeenCalledTimes(1);
state.zero = -0;
expect(run).toHaveBeenCalledTimes(2);
const state = reactive({ user: { name: "Ada" }, tags: ["a"] });
const seen = [];
watchEffect(() => seen.push(state.user.name + ":" + state.tags.join(",")));
state.user.name = "Grace";
state.tags[0] = "b";
expect(seen).toEqual(["Ada:a", "Grace:a", "Grace:b"]);
const state = reactive({ show: true, name: "a" });
const run = spy();
watchEffect(() => {
run();
if (state.show) state.name;
});
state.show = false;
state.name = "b";
expect(run).toHaveBeenCalledTimes(2);
state.show = true;
state.name = "c";
expect(run).toHaveBeenCalledTimes(4);
const state = reactive({ a: 1 });
const values = [];
const has = [];
const keys = [];
watchEffect(() => values.push(state.b));
watchEffect(() => has.push("b" in state));
watchEffect(() => keys.push(Object.keys(state).join(",")));
state.b = 2;
expect(values).toEqual([undefined, 2]);
expect(has).toEqual([false, true]);
expect(keys).toEqual(["a", "a,b"]);
delete state.b;
expect(values).toEqual([undefined, 2, undefined]);
expect(has).toEqual([false, true, false]);
expect(keys).toEqual(["a", "a,b", "a"]);
state.a = 5;
expect(keys).toEqual(["a", "a,b", "a"]);
const list = reactive([1, 2]);
const seen = [];
watchEffect(() => seen.push(list.length));
list.push(3);
expect(seen).toEqual([2, 3]);
list.pop();
expect(seen).toEqual([2, 3, 2]);
list[5] = 6;
expect(seen).toEqual([2, 3, 2, 6]);
list[0] = 10;
expect(seen).toEqual([2, 3, 2, 6]);
const state = reactive({ count: 0, runs: 0 });
const log = reactive([]);
watchEffect(() => {
state.runs++;
log.push(state.count);
});
expect(state.runs).toBe(1);
state.count = 5;
expect(state.runs).toBe(2);
expect(log.join(",")).toBe("0,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 Reactivity with Proxy. 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.