notes

Log | Files | Refs | README

proxy_based_reactivity.md (2622B)


      1 # Proxy-based Reactivity
      2 
      3 ```
      4                  Proxy (trap layer)
      5                 +------------------+
      6 state.count++   |   set trap fires |---> notify subscribers
      7 <-- direct -->  |   get trap fires |---> track dependencies
      8                 +------------------+
      9                        |
     10                 +------------------+
     11                 |   Raw state obj  |
     12                 +------------------+
     13 ```
     14 
     15 ## Reactive State with Proxy
     16 
     17 A JavaScript Proxy wraps your state object and intercepts get and set
     18 operations. Instead of calling setState() manually, any direct property
     19 assignment automatically triggers subscribers — the same mechanic Vue 3 and MobX
     20 use internally.
     21 
     22 ```ts
     23 type Subscriber<T> = (state: T) => void;
     24 
     25 function reactive<T extends object>(
     26   initial: T,
     27   onChange: Subscriber<T>,
     28 ): T {
     29   return new Proxy(initial, {
     30     set(target, prop, value) {
     31       (target as any)[prop] = value;
     32       onChange(target);
     33       return true;
     34     },
     35   });
     36 }
     37 
     38 // Usage
     39 interface AppState {
     40   count: number;
     41   user: string | null;
     42 }
     43 
     44 const state = reactive<AppState>(
     45   { count: 0, user: null },
     46   (s) => console.log("State changed:", s),
     47 );
     48 
     49 state.count++; // logs automatically
     50 state.user = "Alice"; // logs automatically
     51 ```
     52 
     53 For finer control, you can track which properties were accessed (get trap) and
     54 only notify subscribers that depend on that specific property — this is called
     55 dependency tracking:
     56 
     57 ```ts
     58 const subscribers = new Map<string | symbol, Set<() => void>>();
     59 let activeEffect: (() => void) | null = null;
     60 
     61 function reactive<T extends object>(initial: T): T {
     62   return new Proxy(initial, {
     63     get(target, prop) {
     64       if (activeEffect) {
     65         if (!subscribers.has(prop)) subscribers.set(prop, new Set());
     66         subscribers.get(prop)!.add(activeEffect);
     67       }
     68       return (target as any)[prop];
     69     },
     70     set(target, prop, value) {
     71       (target as any)[prop] = value;
     72       subscribers.get(prop)?.forEach((fn) => fn());
     73       return true;
     74     },
     75   });
     76 }
     77 
     78 function effect(fn: () => void) {
     79   activeEffect = fn;
     80   fn(); // run once to collect dependencies via get traps
     81   activeEffect = null;
     82 }
     83 
     84 // Usage
     85 const state = reactive({ count: 0, user: "Alice" });
     86 
     87 effect(() => {
     88   console.log(`Count is: ${state.count}`); // only re-runs when count changes
     89 });
     90 
     91 state.count = 5; // triggers effect
     92 state.user = "Bob"; // does NOT trigger effect (not accessed in effect)
     93 ```
     94 
     95 Watch out: Proxy only intercepts the top-level object by default. For deeply
     96 nested state you need to recursively wrap nested objects in their own Proxy —
     97 which is exactly what Vue 3's reactive() does under the hood.