· By Wataru Ito · Front End  · 5 min read

React: Why Changing a Variable Does Not Trigger a Re-render

A short note on variables, props, and React re-renders, with a small example of subscribing to state outside a component.

A short note on variables, props, and React re-renders, with a small example of subscribing to state outside a component.

Changing a Variable and Passing New Props

I wrote this short note to explain how a component can subscribe to state outside React. First, I needed to separate two things I used to mix up: changing a variable and passing new props during a render.

React does not watch ordinary variables for changes. A parent reads those variables when it renders and passes their values as props. Changing a variable after that does not ask React to render the parent again.

Consider the following example:

import { useEffect } from "react";

let count = 0;

const Parent = () => {
  useEffect(() => {
    const intervalId = setInterval(() => {
      count++;
      console.log("count: ", count);
    }, 300);
    return () => {
      clearInterval(intervalId);
    };
  }, []);

  return <Child count={count} />;
};

const Child = (props) => {
  return <p>{props.count}</p>;
};

The interval changes the outer count variable, but Child still has the number we passed during the last render. Its count prop has not changed. With no other updates, the page keeps showing 0 while the console shows the increasing count.

If the parent renders again for another reason, it reads the current count and passes that number to Child. For a counter that should update the screen, we would use useState and its setter instead.

React can compare old and new props during an update. It does not monitor the variable we used to produce those props.

When the parent re-renders

By default, React renders a parent’s child components when the parent renders, even if their props have the same values. With memo, React can skip a child when its props are unchanged. New props matter to that decision.

When the context updates

A change to a provider’s value asks components that read that context to re-render. React compares the old and new values with Object.is. This does not force every component below the provider to re-render because of context; other children may still render as part of a normal parent update.

When state updates

Calling a state setter asks React to render the component with the new state. React can skip the update if the value is the same according to Object.is. Updating a child’s state does not require React to call all its ancestor components again.

Rendering means calling components to work out their output. React then commits any needed changes to the DOM. A re-render does not mean the DOM must change.

How do state libraries update subscribers?

With a library like Jotai, components can subscribe to the same stored value from different parts of the tree. The store notifies its subscribers when that value changes, and the subscribed hooks ask React for an update. Their children may render too, following the usual rules.

The React hook used for this depends on the library. Jotai’s useAtomValue uses a subscription and useReducer; saying these libraries all use only a useState setter would be inaccurate.

We can show the basic idea with useState. In this small browser-only PoC, we share a number outside the components. Each reader keeps local state so its subscription can ask React to render it again:

import { useEffect, useState } from "react";

let value = 0;
const listeners = new Set();

const useMyValue = () => {
  // This local state is not used as the source of truth.
  // It only exists to give React a reason to re-render this component.
  const [, setTrigger] = useState(value);

  useEffect(() => {
    // Register this component as a subscriber.
    // When the shared value changes, this callback will run.
    const listener = (v) => setTrigger(v);
    listeners.add(listener);

    return () => {
      // Remove the subscription when the component unmounts.
      listeners.delete(listener);
    };
  }, []);

  // Read the shared value during this render.
  return value;
};

const useSetMyValue = () => {
  // This hook only returns a writer.
  // It does not subscribe to changes in the shared value.
  return (myValue) => {
    // Update the external value first.
    value = myValue;

    // Then notify every subscribed component.
    // Each subscriber will call its own setTrigger, which asks React to re-render it.
    listeners.forEach((fn) => fn(myValue));
  };
};

This is a demonstration of notification, not a complete store implementation. For example, an update between rendering and the Effect subscribing can be missed. For an external store in an app, React provides useSyncExternalStore to handle the subscription and keep the value read during rendering consistent. A state library handles more of these details for us.

// Sample usage:
const MyComponent = () => {
  return (
    <div>
      <DisplayValue />
      <UpdateValue />
    </div>
  );
};

const UpdateValue = () => {
  // This component only gets the setter.
  // In this example, updating the shared value does not ask this component to re-render.
  const setValue = useSetMyValue();

  return <button onClick={() => setValue(Math.random())}>Click</button>;
};

const DisplayValue = () => {
  // This component subscribes to the shared value.
  // When the button updates the value, this component receives the notification and re-renders.
  const myValue = useMyValue();

  return <div> my value is: {myValue} </div>;
};

In this example, clicking the button notifies DisplayValue, which calls its local setter. UpdateValue does not subscribe, so this store update does not ask it to render. It can still re-render for other reasons, such as a parent update. This separation between reading and writing is the part I wanted to keep in mind for other articles.

Key Takeaways

  • Changing an ordinary variable does not schedule a React update. The parent passes new props when it renders again.
  • A store subscription connects changes outside React to updates in the components that read them.
  • A component that only writes to the store does not need to subscribe to its value.

I used AI to help check the accuracy of the explanations in this article.

References

Share:
Back to Articles

Related Articles

View All Articles »
Three Image Tools I Wanted for Myself

Three Image Tools I Wanted for Myself

A birthday poster led to three small browser tools: remove a background, convert an image, and make it bigger. No account, and your image stays on your device.