Tangle — Event Driven State in React

Victor Pontis
Sep 9, 2021

DemoGitHub — Thanks to Stanislaw Chmiela for working on this with me.

While the hierarchical React state model works well most of the time, you can run into situations where the hierarchical structure gives your website very bad performance. This can often come up if you are changing state that affects deep leaf nodes since that can cause a re-render of the full React tree.

For example, we ran into performance issues when rendering presence status on a Luma Community page.

We render a green dot on someone’s avatar if they are currently in the Luma Community. After they’ve been inactive for 30 seconds, that avatar turns orange.

For Luma, it’s very important that presence is very real time because if you message someone who is online, you should expect they will see your message. That means in a large community we may need to re-render presence information every second.

Performance Issues

The traditional way of hosting presence state with React is as follows:

const CommunityHome = () => {
  const presenceInfos = usePresence();

  return (
    <CommunityHomeContent>
      <MainContent />
      <Sidebar>
        {presenceInfos.map((presence) => 
          <MemberRow key={presence.id} presence={presence} />
        )}
      </Sidebar>
    </CommunityHomeContent>
  )
}

Or here is a way that you can use React Context to avoid prop drilling:

const CommunityHome = () => {
  return (
    <CommunityHomeContent>
      <MainContent />
      <Sidebar>
        {members.map((member) => (
          <MemberRow key={member.id} member={member} />
        )}
      </Sidebar>
    </CommunityHomeContent>
  )
}

const MemberRow = ({member}) => {
  const {presenceInfos} = useContext(PresenceContext);
  const presenceInfo = presenceInfos.find((presence) => presence.member_id = member.id);

  return <div>...</div>;
}

But both of these have the same performance issue. Whenever one presenceInfo changes we have to re-render the whole page and all of the <MemberRow />s.

Introducing Tangle

So to solve this problem, we built Tangle. Tangle is a lightweight state management system that uses event listeners to only re-render affected leaf components.

This system is super simple, performant, and can be easily dropped into your existing codebase.

Here's an example of the results we can get with Tangle:

Usage

type HighlightEvent = {
  type: `highlight-${number}`;
  idx: number;
  color: typeof COLORS[number];
};
const HighlightStore = new TangleStore<HighlightEvent>();

const Home = () => {
  useEffect(() => {
    setInterval(() => {
      const randomIndex = Math.floor(Math.random() * NUM_CELLS);

      HighlightStore.dispatchEvent({
        type: `highlight-${randomIndex}`,
        idx: randomIndex,
        color: _.sample(COLORS) as typeof COLORS[number],
      });

      setTimeout(() => {
        HighlightStore.dispatchEvent({
          type: `highlight-${randomIndex}`,
          idx: randomIndex,
          color: "gray",
        });
      }, 600);
    }, 10);
  }, []);
  
  return (
    <Container>
      <Input />
      <Grid />
    </Container>
  );
}

// This is the leaf component
const Cell = ({ idx }: { idx: number }) => {
  const [color, setColor] = useState("gray");

  useTangle({
    store: HighlightStore,
    eventName: `highlight-${idx}`,
    handleEvent: (event) => {
      setColor(event.color);
    },
  });

  return (
    <div key={idx} className={"cell"} style={{backgroundColor: color}} />
  );
};

If you want to use Tangle, you can use the code from the Github Repo and copy it into your codebase. Reach out to us if you start using it and we can add the code to npm 🙂.

Existing Solutions

React has had this problem with deeply nested state since its release, so there are a lot of existing solutions.

Redux

Redux solves this by using connect() which allows a leaf component to listen to a selection of state.

MobX

MobX introduces an event driven model to state and provides decorators that allow you to listen to a specific state value.

Recoil

Recoil is a new library from Facebook that breaks down your state into different atoms. It is powerful and allows you to compose different atoms.

useContextSelector

This allows you to create a selector on a context to only re-render a component when that selector changes. React is considering bringing in this function into native React.

Unfortunately this will still require every component to run a check when the context changes before they can bail out of rendering.

Why these don’t work for us

While all these libraries are great, we felt they add significant educational overhead. They aren’t designed to be dropped into a project that is already using a different state management system.