The Rules of React

React is a powerful and flexible JavaScript framework used to create user interfaces quickly. Just like any other frameworks, React has it's own ways of doing things. In this lesson, we are going to go over all the rules you must follow in order to create high quality React applications.

Components must be pure during render

One of the most important principle in React is that components should be pure functions, meaning when provided with the same input arguments, they should always return the same response, and contains no side effects.

By making your components pure, the app becomes predictable and easier to debug, and allows React to optimize them correctly.

As an example, the following component is not pure:

jsx
1function RandomNumber() {
2  return <span>{Math.random()}</span>;
3}

Because Math.random() will generate a different number every time it is called, causing RandomNumber() to return a different value on every render.

The following example is also not pure. This example is intended to retrieve a random quote from an API, and then display that quote. Because retrieving data from a remote source is a side effect, it makes this component not pure. If the remote data changes, the component will render differently.

jsx
1import { useState } from "react";
2
3function RandomQuote() {
4  const [quote, setQuote] = useState("");
5
6  async function fetchQuote() {
7    const response = await fetch("https://api.quotable.io/random");
8    const data = await response.json();
9    setQuote(data.content);
10  }
11
12  fetchQuote();
13
14  return <div>{quote ? quote : "Loading..."}</div>;
15}
16
17export default RandomQuote;

Side effects must run outside of render

However, this leads to a problem, because the side effects can be useful sometimes. What if we do need to retrieve data from external sources?

To solve this conundrum, you need to utilize the useEffect() hook whenever side effects are involved.

jsx
1import { useEffect, useState } from "react";
2
3function RandomQuote() {
4  const [quote, setQuote] = useState("");
5
6  useEffect(() => {
7    async function fetchQuote() {
8      const response = await fetch("https://api.quotable.io/random");
9      const data = await response.json();
10      setQuote(data.content);
11    }
12
13    fetchQuote();
14  }, []);
15
16  return <div>{quote ? quote : "Loading..."}</div>;
17}
18
19export default RandomQuote;

This will isolate the side effects and make sure the component is pure during the rendering process, and the retrieved data is only used to update the DOM after the component has been rendered.

Props, states, contexts, and hook arguments are immutable

Variables passed down to a component (props) should not be modified directly, or it might cause the component to produce inconsistent output, making it hard to debug and may lead to unwanted results in certain circumstance.

When you do need to modify the prop, make a copy first:

jsx
1function List({ items }) {
2  // Create a new array, put items inside with the spread syntax, and then add a new item
3  const newItems = [...items, "Orange"];
4
5  return (
6    <ul>
7      {newItems.map((item, index) => (
8        <li key={index}>{item}</li>
9      ))}
10    </ul>
11  );
12}
13
14export default List;

The same rule applies to states. You should never update them directly, or it will not trigger a rerender. Instead, you must always use the state update function.

jsx
1import { useState } from "react";
2
3function Counter() {
4  const [count, setCount] = useState(0);
5
6  return (
7    <button onClick={() => setCount((prevCount) => prevCount + 1)}>
8      Count: {count}
9    </button>
10  );
11}

Similar to props, arguments passed to hooks should not be updated directly.

jsx
1function useIconStyle(icon) {
2  const theme = useContext(ThemeContext);
3  const newIcon = { ...icon }; // ✅ Good: make a copy instead
4  if (icon.enabled) {
5    newIcon.className = computeStyle(icon, theme);
6  }
7  return newIcon;
8}

In general, data passed around in React should be immutable to ensure consistent results. When you need to modify them, make a local copy first. Your components and hooks should only modify local variables.

Values are immutable after being passed to JSX

After a value has been passed to JSX, they should not be modified. For instance:

jsx
1function CounterApp() {
2  let count = 0;
3
4  const counter1 = <Count count={count} />;
5  count = count + 1; // Modifies the value of count
6  const counter2 = <Count count={count} />;
7
8  return (
9    <>
10      {counter1}
11      {counter2}
12    </>
13  );
14}
15
16export default CounterApp;

In this case, we changed the value of count after it has been passed to the Count component.

This can be problematic because React will eagerly evaluate the JSX before the rendering starts. Modifying the value afterward will lead to outdated UI, and React won't know what to do with them.

The solution is similar, make a copy first, and then do the desired modifications to that copy instead.

jsx
1function CounterApp() {
2  let count = 0;
3
4  const counter1 = <Count count={count} />;
5  let newCount = count + 1;
6  const counter2 = <Count count={newCount} />;
7
8  return (
9    <>
10      {counter1}
11      {counter2}
12    </>
13  );
14}
15
16export default CounterApp;

Never call component functions directly

In React, components are defined as JavaScript functions. However, they should be treated as more than just that, and you should never call them directly like this:

jsx
1function Hello() {
2  return <h1>Hello, World!</h1>;
3}
4
5Hello();

Instead, when you need to use a component, always use the JSX syntax:

jsx
1function App() {
2  return <Hello />;
3}

This allows React to manage when these components should be called and rendered, optimizing your application and delivering a better user experience.

Never pass around hooks as regular values

Hooks are portals that allows you to "hook" into React's various features, such as states, contexts, side effects, and more, hence the name hook.

Similar to components, hooks are defined as functions, but they should also be treated differently.

We demonstrated in the higher-order function lesson that functions in JavaScript can be passed around as values. However, you should not to this with React hooks. Instead, each hook should be local to a component, and should not be passed from one to another.

Breaking this rule will not only increase the complexity of your application, but also cause React to not optimize the components properly.

Only call hooks at the top level

When you call a hook in a component, always make sure they are called at the top level, and not inside a sub-block such as if statements, loops, try catch blocks, and so on.

jsx
1if (someCondition) {
2  const [value, setValue] = useState(0);
3}

An error will be returned if you break this rule.

Only call hooks from React functions

Also, when you call a React hook, make sure it is from inside a React function, such as a component or a custom hook, not a regular JavaScript function.

jsx
1import { useState } from "react";
2
3const name = useState("Bob"); // Do not do this
4
5function MyComponent() {
6  const [name, setName] = useState("Alice");
7
8  return <h1>{name}</h1>;
9}