Turn your data into a React superstar!
With make-reactive, you can magically make your Maps, Sets, and custom objects reactive — no fuss, no boring boilerplate, just pure reactivity!

🧠 The Problem

React exposes an API for storing state inside functional components via the useState hook. Which is great!

const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const [name, setName] = useState<string>();

.... Great — but only if you’re dealing with atomic serializable primitives.. Such as a simple "number", "boolean" or "string". Or best bet, an immutable object, that is there just as a reference.

What happens when you need a more complex data structure? Such as ES6 Maps, Sets, a JS Class Instance, or even a simpler Array?.. Let's experiment with what we have at hand.

💡 Naive Approach #1: Use useState anyways

I mean, mutable, immutable, primitive, non-primitive, who cares.. Why not just give it a go? Let's try to craft some ES6 Map state, that represents an association between Student ids and their Maths scores.

const [scores, setScores] = useState(() => new Map<string, number>());

Okay.. That was easy.. What's the catch? Let's implement some mutation logic for those scores. And while we're at it, add some rendering logic, so we can see those scores properly.

const assessExam = (studentId: string, score: number) => {
   scores.set(studentId, score);
}

return (
  <ul>
    {[...scores].map(([studentId, score]) => (
      <li key={studentId}>
         <span>{studentId} = {score}span>
      li>
    ))}
  ul>
);

Well that was it, wasn't it? Calling assessExam will do the trick?
WRONG.. It just mutates the value, however it does not trigger a re-render... Meaning, our updated (!) value will never reconstruct/mutate our VDOM, as there won't be any re-renders triggered.

Right (!) way to implement it would be this ugly piece of code:

const assessExam = (studentId: string, score: number) => {
   setScores(prev => {
      const next = new Map(prev);
      next.set(studentId, score);
      return next;
   });
}

Ugh.. That is just crazy to look at. Reallocating a whole new object, copying the previously stored memory state, leaving the older Map instance for the garbage collector to pick up, and having to imperatively re-write a very detailed and error prune piece of code that essentially does so little every time. 😵

💡 Naive Approach #2: Be more reasonable with useRef

useRef is yet another great hook that React exposes. All it does is to store some value, as a reference. Perfect for complex things you want to reference. Such as ES6 Maps! Let's store our Student scores as a ref, so we don't have to reconstruct a new one each time we intend to mutate.

const scores = useRef(new Map<string, number>());

Okay.. So far so good.. Now let's create assessExam once again.

const assessExam = (studentId: string, score: number) => {
   scores.current.set(studentId, score);
}

That's it! Just keep in mind, scores is a ref, so the value is stored under its current field. All good, right?
WRONG.. Now the problem is, in fact, mutating a ref won't trigger a re-render on the component...

Hold on..
So, we have a way to trigger re-renders — via useState
And, we have a way to store complex values as they are — via useRef

Can't we come up with a hybrid approach?

🧪 The Experiment: useState with useRef

The truth is, we can trigger re-render, by providing useState's dispatch function (the second argument returned) with a different value. Like so:

const [, forceUpdate] = useState(0);

// Can be triggered like so:
forceUpdate((i) => i + 1);

Now that gives us the super powers to trigger a re-render whenever we want. And the great nuance, React automatically batches, even if we call forceUpdate multiple times in a render call. (See https://react.dev/learn/queueing-a-series-of-state-updates)

Okay let's introduce some abstractions now. Let's generalize our ES6 Map usage under a hook for convenience.

export function useMap<K, V>() {
  const [, forceUpdate] = useState(0);
  const map = useRef(new Map<K, V>());

  const setMethod = (key: K, value: V) => {
    map.current.set(key, value);
    return forceUpdate((i) => i + 1);
  };

  const deleteMethod = (key: K) => {
    forceUpdate((i) => i + 1);
    return map.current.delete(key);
  };

  const clearMethod = () => {
    forceUpdate((i) => i + 1);
    return map.current.clear();
  };

  return {
    map,
    set: setMethod,
    delete: deleteMethod,
    clear: clearMethod,
  };
}

Perfect, just as we aimed! No redundant memory allocation, no wasteful memory copy operations, no repeated boilerplate each time you update. Let's try to use it back in our component now.

const { map: scores, set: setScore } = useMap<string, number>();

const assessExam = (studentId: string, score: number) => {
   setScore(studentId, score);
}

return (
  <ul>
    {[...scores].map(([studentId, score]) => (
      <li key={studentId}>
         <span>{studentId} = {score}span>
      li>
    ))}
  ul>
);

Well.. we still have a flaw..

😩 The Real Flaw: Hook Combinator Hell

Even though our useMap helper works, there's one giant catch.. You have to write a custom hook for every data structure...

Want a reactive Set? You’ll need to write a useSet.
Want a reactive Document with event hooks? Time to write another one.
What about Arrays, Objects, or your own Class instances?.. Pain..

Before you know it, your project is littered with repetitive logic:

  • A useX hook that wraps the value in a useRef
  • Mutation helpers that manually trigger forceUpdate
  • Boilerplate wiring just to mutate and rerender properly

You can do it for every single one of them. But why should you have to?
To be fair, there is no escape from having to implement an artifact per data structure. But! Thankfully, we can abstract most of the logic away!

🏆 Enter: ES6 Proxys

What if we could intercept reads and writes to any object without changing how we use it?

That’s exactly what ES6 Proxy objects are for.

With a Proxy, you can wrap any object — a Map, an Array, a plain object, even a class instance.

Instead of implementing a delegate for every method, we can wrap our object with a Proxy, and only trigger re-render on desired method calls. Here's how we can implement such a hook:

function useReactive<T extends object>(obj: T, desiredProperties: (keyof T)[]) {
  const [, forceUpdate] = useState(0);

  const proxyObj = useMemo(() => {
    return new Proxy(obj, {
      get(target, p, receiver) {
        if (desiredProperties.includes(p as keyof T)) {
          const method = target[p as keyof T];

          if (typeof method !== "function") {
            throw new Error("Whops, given desiredProperty is not a method...");
          }

          // Here we hook our force re-renderer
          return function (...args: any[]) {
            forceUpdate((i) => i + 1);
            return method.call(target, ...args);
          };
        }

        return Reflect.get(target, p, receiver);
      },
    });
  }, [obj]);

  return proxyObj;
}

That might be a bit too much to digest at first.
But focus on individual bits. So far this useReactive implementation has:

  1. A mechanism to trigger re-render on demand
  2. A Proxy encapsulating passed obj, and overriding its field accessing mechanisms
  3. The proxy encapsulates desired methods (provided by passed desiredProperties) and hooks in a re-render triggering before invoking the actual method.
  4. For properties other than desiredProperties, we just return whatever value there is, without wrapping/injecting anything.

And it can be easily used like so:

const scores = useMemo(() => new Map<string, number>(), []);
const reactiveScores = useReactive(scores, ["set", "clear", "delete"]);

const assessExam = (studentId: string, score: number) => {
   reactiveScores.set(studentId, score);
}

Perfect! Notice reactiveScores is type-safe, and IntelliSense suggestions work just like with a native ES6 Map! Which allows us to use it, without having to keep a whole new API in mind!

In fact, we can do even better!

🏆 Enter: @igoodie/make-reactive

I took my time, and abstracted most of those Proxy handler encapsulations under a library called make-reactive 🎉 (See https://github.com/iGoodie/make-reactive)

make-reactive provides a super easy utility called makeReactive, which allows you to compose a new hook as desired. Here's how we can implement our useReactiveMap with it:

// hooks/useReactiveMap.ts

export const useReactiveMap = makeReactive(<K, V>() => new Map<K, V>(), {
  methodHooks: {
    delete: true,
    clear: true,
    set: true,
  },
});

// Usage:
const scores = useReactiveMap<string, number>();
//    ^? Map

// This will automatically trigger a re-render!
scores.set("", 99);

Look at how clean it is... Okay what if I want clear not to trigger re-render if the map is empty? Easy! It allows you to feed in a config supplier, like so:

export const useReactiveMap = makeReactive(
  <K, V>() => new Map<K, V>(),
  (forceRerender) => ({
    methodHooks: {
      delete(self, key) {
        if (self.size !== 0) forceRerender();
        return self.delete(key);
      },
      clear: true,
      set: true,
    },
  })
);

It also includes ES6 Map, ES6 Set and Array implementations out-of-the-box! You can use them for convenience, or craft your very own reactive data structure hooks!

import makeReactive, {
  useReactiveArray,
  useReactiveMap,
  useReactiveSet
} from "@igoodie/make-reactive";

🧵 Closing Thoughts

React is optimized for immutability and serializable state — and for many apps, that’s a good thing. But sometimes you need more nuanced control. Whether you're working with Maps, Sets, custom classes, or just want cleaner state management for complex structures, React’s primitives (useState, useRef, etc.) start to show their limits.

That's exactly why I built make-reactive.

Instead of writing repetitive hooks for every data structure or giving up on reactivity entirely, I wanted a way to make any object reactive — without sacrificing performance or readability. The goal was to let developers work with native data structures as naturally as possible, while still enjoying the declarative rendering model of React.

If you've ever struggled with forcing re-renders after mutating a Map, or hated rebuilding an entire object just to flip a single boolean, this library is for you.

Check it out, break it, improve it — and let me know what you build with it.

That’s just one chapter of the journey I’ve been on.
Hope you had fun reading about it!
Thanks for reading 💙

⛓ Links & References

Github Repo: https://github.com/iGoodie/make-reactive
Demo Site: https://igoodie.github.io/make-reactive/
NPM Package: https://www.npmjs.com/package/@igoodie/make-reactive