Skip to content

Instantly share code, notes, and snippets.

@sebmarkbage
Last active September 8, 2025 18:23
Show Gist options
  • Save sebmarkbage/75f0838967cd003cd7f9ab938eb1958f to your computer and use it in GitHub Desktop.
Save sebmarkbage/75f0838967cd003cd7f9ab938eb1958f to your computer and use it in GitHub Desktop.
The Rules of React

The Rules of React

All libraries have subtle rules that you have to follow for them to work well. Often these are implied and undocumented rules that you have to learn as you go. This is an attempt to document the rules of React renders. Ideally a type system could enforce it.

What Functions Are "Pure"?

A number of methods in React are assumed to be "pure".

On classes that's the constructor, getDerivedStateFromProps, shouldComponentUpdate and render.

class MyComponent extends React.Component {
  constructor() {
    // pure
  }
  
  static getDerivedStateFromProps() {
    // pure
  }

  shouldComponentUpdate() {
    // pure
  }

  render() {
    // pure
  }
}

However, componentDidMount, componentDidUpdate, componentWillUnmount and custom event handlers are not required to be pure.

class MyComponent extends React.Component {
  componentDidMount() {
    // not pure
  }
  componentDidUpdate() {
    // not pure
  }
  componentWillUnmount() {
    // not pure
  }
  handleClick = () => {
    // not pure
  }
  render() {
    // pure
    return <div onClick={this.handleClick} />;
  }
}

The first argument to setState has to be a pure function.

this.setState(() => {
  // pure
});

But not the second argument.

this.setState({}, () => {
  // not pure
});

Functional components are also pure functions.

function MyComponent() {
  // pure
}

Getters in State

setState also has another specific limitation. The object passed to setState or returned from the first argument cannot have any getters on it that might break any of the rules above. This is because they're also evaluated as part of a pure function.

What Does "Pure" Mean in React?

Pure in general has a specific meaning but can vary by definition. In React, it mostly means that it has to be idempotent. It will always return the same thing for the same input.

Forbidden

Specifically this means that within those functions you cannot:

  • Mutate a variable binding, unless that binding was "newly created".
  • Mutate a property on an object, unless that object was "newly created".
  • Mutate a property on this except in the constructor.
  • Read a property from an object, unless that object was "newly created" or if the property will never change after it is read once.
  • Read a variable binding, unless that binding was "newly created" or if its will never change after it is read once.
  • Call Math.random() or Date.now() since these read values that later change.
  • Call setState, since this mutates.
  • Issue a network request (POST), file system or other I/O that writes to a persistent store. E.g. impression logging, create, updates or deletes.
  • Create a new component type. A new function/class that is later used in JSX.
  • Call another function that might do anything of the above.

"Newly created" above means that the object, or the closure around a variable binding, was created inside the pure function call itself. Note that it is not enough that this is an object created by a previous invokation of this function. It has to be within each call to the function.

Allowed

There are a few things you might not consider as pure according to other definitions but which are ok in React:

  • Reading this.props or this.state in class components is ok even though they change over time.
  • Throwing errors or any other object is ok.
  • Mutating objects is ok as long as that object is "newly created".
  • Mutating bindings is ok as long as that binding was "newly created".
  • You can call a function which mutates an object you pass in, as long as that object is "newly created".
  • Mutating properties on this is ok in the constructor because it is "newly created" in class constructors.
  • Creating new functions that are not pure is ok, as long as they're not called until later, outside a pure function.
  • Issuing a network request (GET), a read from the file system or other I/O is ok if this doesn't cause a write, and as long as the result is not read by the pure function*.

*) Why would you load something you're not going to read? It can be used to prime a cache and the read is from the cache.

Lazy Initialization

An exception to these rules is that it is ok to read and mutate a value if it is for the purpose of lazy initialization. By that we mean that you only read to check if a property/binding is initialized, if not initialize it, write the result to the property/binding and from that point always return the same value.

var lazy;
function MyComponent() {
  if (!lazy) {
    lazy = function() { }; // ok
  }
  return lazy; // ok
}

This can also be used with multiple values in a Map as long as there is a unique key.

var cache = new Map();
function MyComponent(props) {
  if (!cache.has(props.id)) {
    cache.set(props.id, function() { }); // ok
  }
  return cache.get(props.id); // ok
}

If a value is not yet ready to be initialized, you can throw a Promise that resolves when it is ready to be initialized.

Once a value has been initialized, it can never change again. If you want to invalidate a cache, you can store the cache itself in "state".

Mutating Objects Created by Render

An additional restriction is that you cannot mutate objects or closure bindings created in any of these functions, after the render completes. They are implicitly frozen. That includes objects, and closures. The exception to this is mutable objects stored in the state object.

function Foo() {
  let x = {active: false};
  let y = 0;
  let clickHandler = () => {
    x.active = true; // not ok
    y++; // not ok
  };
  return <div onClick={clickHandler} />;
}
class Component extends React.Component {
  state = { mutableBox: { active: false, count: 0 } };
  clickHandler = () => {
    this.state.mutableBox.active = true; // ok, but discouraged
    this.state.mutableBox.count++; // ok, but discouraged
    this.forceUpdate();
  };
  render() {
    return <div onClick={this.clickHandler} />;
  }
}

But what if...?

Nope. If you break any of these rules, React will not work as advertised.

Yes, this is limiting but it also the tradeoff that allows us to do many complex things magically for you in React. Luckily we have patterns to help you avoid needing to break these rules.

@bvaughn
Copy link

bvaughn commented Apr 24, 2018

Mutate a property on this except in the constructor.

WRT "forbidden" things, I'd like to mention one of the ideas I've been toying about for react-virtualized regarding moving things out of state and into memoized helpers.

Given a prop that can be either a number or a function that returns a number:

type Props = {|
  cellSize: number | ((index: number) => number)
  // ...
|};

It can be useful to do property validation- and convert to a consistent type (the function type)- all in one place, like so:

class List extends React.Component {
  getCellSize: any = memoize((cellSize: CellSize) => {
    if (typeof cellSize === "function") {
      return cellSize;
    } else if (typeof cellSize === "number") {
      return () => cellSize;
    } else {
      throw Error(
        'An invalid "cellSize" prop has been specified. ' +
          "Value should be either a number or a function returning a number. " +
          `"${cellSize === null ? "null" : typeof cellSize}" was specified.`
      );
    }
  });
  // ...
}

Then this can be used in functions like render like so:

const getCellSize = this.getCellSize(this.props.cellSize);
const sizeOfFirstCell = getCellSize(0);

The above is idempotent, so I'm tempted to think it's safe- but it's also a side effect and not "pure", so I'm tempted to think it's not safe.

In this case, the memoized getCellSize is only ever called from render- so it's probably ok?

I think this applies more broadly to memoization, since memoized results may be attached to an instance.

@kevinSuttle
Copy link

This should be part of the Advanced Guides in the React docs, with added suggestions on how to avoid mutation in those situations where it's allowed.

@stephan-noel
Copy link

Was really surprised by the mutation of the closure variable y in the event handler.

function Foo() {
  let x = {active: false};
  let y = 0;
  let clickHandler = () => {
    x.active = true; // not ok
    y++; // not ok
  };
  return <div onClick={clickHandler} />;
}

If instead we had used a ref, would it be okay and why/why not?

import { useRef } from 'react';

function Foo() {
  let x = {active: false};
  let y = useRef(0);
  let clickHandler = () => {
    x.active = true; // not ok
    y.current++; // not ok  or ok?
  };
  return <div onClick={clickHandler} />;
}

I thought that mutations/side-effects inside useEffect and event handlers are expected/okay.

@mthurlin
Copy link

@stephan-noel - x and y will be recreated every render, so any changes made to whatever version the clickHandler happened to close over will be lost and probably not do what you expected. If you instead use a ref, the changes would be kept.

@drcmda
Copy link

drcmda commented Oct 30, 2020

Luckily we have patterns to help you avoid needing to break these rules.

Could the React team look into that one edge case we recently discussed, where a 3rd party constructor has side-effects, and it can be assumed the publisher will not change it?

function Foo = React.forwardRef((props, ref) => {
  const [connection] = useState(() => new Connection("google.com"))
  useImperativeHandle(ref, () => connection, [])
  ...

function App() {
  const ref = useRef()
  useReffect(() => console.log(ref.current), [])
  return <Foo ref={ref} />

The "official" solution that @gaearon and i worked out is this:

const Foo = React.forwardRef((props, ref) => {
  const [connection, set] = useState()
  useLayoutEffect(() => {
    const value = new Connection("google.com")
    set(value)
    if (typeof ref === 'function') {
      ref(value)
    } else if (ref != null) {
      ref.current = value
    }
    return () => {
      if (typeof ref === 'function') {
        ref(null)
      } else if (ref != null) {
        ref.current = null
      }
    }
  },[])
  ...

The pre-emptive ref call was necessary because otherwise foreign components wouldn't be able to put a reference onto the object in useEffect. But i think no one can be expected to deal with this in such a way.

If React could help us with a pattern that's more easily digestable that would be great!

@stephan-noel
Copy link

@mthurlin Okay that's what I thought thanks! I thought there was maybe something else that I could be missing.

@NikolayKovzik
Copy link

Are these rules still valid?

@gaearon
Copy link

gaearon commented Dec 17, 2022

yes

@edemotte
Copy link

are these rules still valid?

@kusiewicz
Copy link

yes

@ferretwithaberet
Copy link

Would you consider it fine passing a hook as a parameter to a function?
Example in this util I made.

@gmoniava
Copy link

gmoniava commented Sep 6, 2025

This should be in the official react docs!

@gaearon
Copy link

gaearon commented Sep 6, 2025

It is, in a slightly different shape: https://react.dev/reference/rules

@gmoniava
Copy link

gmoniava commented Sep 6, 2025

@gaearon Yes but that's not enough. See here - the code is reading window.__INITIAL_TIME__ in useState initializer function to solve a flicker problem and it seems an impure operation to me (that's why I opened issue), but apparently it is pure based on this list. Actually is it? So without this list I was not sure.

@gaearon
Copy link

gaearon commented Sep 6, 2025

cc @josephsavona re: above

@josephsavona
Copy link

josephsavona commented Sep 7, 2025

Yeah, we've actually been discussing the rules. In general reading from external data sources (including properties on window) is impure and against the rules. Your component should render the same result given the same props/state/context, if you read from an external mutable value then it can change the render value over time.

That said, state initializers are a bit of a weird case and we definitely need to clearly define our stance here and update the docs accordingly. In general, https://react.dev/reference/rules supercedes this document and if there are gaps in those docs please file a GitHub issue so we can fix!

@gmoniava
Copy link

gmoniava commented Sep 7, 2025

@josephsavona @gaearon

That said, state initializers are a bit of a weird case and we definitely need to clearly define our stance here and update the docs accordingly. In general, https://react.dev/reference/rules supercedes this document and if there are gaps in those docs please file a GitHub issue so we can fix!

Why should we open a new issue, when the gap is obvious no? For example take the "Forbidden" list from here. Such list is missing here.

The code I mentioned here is from this blog post, which people read and might use, so I think it is responsibility of react team to make it more precise what is pure/impure no?

Finally can you please confirm if that particular case from that post of reading window.__INITIAL_TIME__ in state initializer is pure or not (assuming it will not change after reading)? Based on the list from this gist I think is it allowed because of this forbidden rule (see bold part):

"Read a property from an object, unless that object was "newly created" or if the property will never change after it is read once."

Am I right?

Thank you.

@josephsavona
Copy link

@gmoniava There’s a few related things here:

  • The rule is, as stated in https://react.dev/reference/rules, that render has to be pure. That means render should always return the same result if props, state, and context haven’t changed. If you set window.__CONST__ once to a constant and never change or mutate that value, then we can deduce that it’s safe to access during render because re-rendering would always read the same value.

  • The definition of purity is deceptively simple, and the new docs page focuses on the fundamental rule so that developers can reason about it. It sounds like your feedback is that you’d find it helpful to see more concrete examples of what’s pure/impure, which is fair!

  • Re providing feedback, the best way to flag issues w the docs is by filing an issue. This is a gist and team members don’t get notified if there’s discussion here.

@gmoniava
Copy link

gmoniava commented Sep 7, 2025

@josephsavona

So you’re suggesting that one should deduce from this rule:

"React components are assumed to always return the same output with respect to their inputs – props, state, and context."

that this case of reading window.__INITIAL_TIME__ in a useState initializer is considered pure (mainly because that property won't be changed afterwards)? Maybe.

But I’m wondering: does this rule ("React components are assumed to always return the same output with respect to their inputs – props, state, and context.") apply only within a single component mount or does it also apply across multiple mounts?

Because if it’s only about a single mount, then any state initializer satisfies the rule — because it returns same value for entire lifetime of that component afaik. So why require that the state initializer itself be pure?

@gmoniava
Copy link

gmoniava commented Sep 8, 2025

@josephsavona

Also my bad when you showed me this link: https://react.dev/reference/rules, I only focused on that page, which is like an overview, but apparently it has sub pages (for some reason I did not click the links) which elaborate some topics like purity more. But yeah despite that it still seems that elaborate page does not answer all my questions such as from my previous comment for example.

PS. Also this page should be linked from the Learn page about purity imho.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment