DEV Community

Cover image for The 5 most important takeaways from the new React Docs, for both newbie and experienced devs
Edgar Chavez
Edgar Chavez

Posted on • Updated on

The 5 most important takeaways from the new React Docs, for both newbie and experienced devs

The new React documentation is incredibly well written and is a great starting point to understand how React works under the hood. This is great if you're a beginner learning this framework for the first time, but in my opinion it's also a useful resource for more experienced developers trying to solidify their knowledge.

If you have a few hours to spare, my recommendation is that you go check out the documentation yourself. But if you don't, here are the 5 most valuable nuggets of knowledge I was able to take away from the notes I took as I read the new React docs:

1.- Understanding the React render

Understanding React rendering is crucial to creating well-written components that are performant. Let's remember that (functional) components are simply JavaScript functions that return JSX and are called by React to display that as HTML on the page.

Rendering happens when React calls a component function and its JSX is transformed into a React element so it can then be added to the virtual DOM. During the rendering phase, React will also compare the updated virtual DOM with the old one (diffing) to decide which nodes need to be changed in the real DOM.

Since the component function is called during the render phase, every statement or expression found in that function's body will also be evaluated again.

Commiting occurs when the updated component has been rendered (the function was called) and now it can be copied to the browser DOM if any changes were found. After the DOM is updated, the browser repaints the screen.

What triggers a render?
-The initial mounting of a component
-The state of the component changed
-The props of the component changed
-A parent component re-rendered
-A re-render was manually triggered

*Based on these rules, we can infer that re-rendering a component higher in the tree could cause a large portion of the component tree to re-render, if they are descendants of this component. We can solve this "rendering cascade" by wrapping our components with React.memo, but we should not over-optimize unless it's really necessary. React is "fast" by default.

*Let's keep in mind that a component's state persists throughout re-renders, so we can be sure that the state of a component will remain the same if the render was triggered by anything other than a state change in that component. Changing props will not change the state even if a prop is used to initialize a piece of state.

*Another thing to keep in mind is that React only commits changes to the real DOM if there is a difference between renders. This means that a component can re-render without commiting anything new to the DOM. Still, if a component performs an expensive calculation in its function body and React re-renders it, it could still block the UI even if no changes were committed to the DOM. That's why we need to be careful about not triggering unncessary re-renders, or keeping expensive calculations in components to a minimum.

Read more about this in the React docs.


2.- State as a snapshot

The most important thing to take away from this is:
A component's state is tied to each render cycle and won't change until the next render.

We can think of a component's state as a snapshot of a value in React's memory, and any operation or expression that uses state in the component will use this snapshot.

If we want to 'change' the snapshot, we have to change the value in memory (using a state setter function). This will in turn call the component function with a new snapshot (re-rendering), effectively using the new value both in the function body and its JSX.

I used to struggle with this logic as a newbie React dev, because I was always thinking "how can I await for the state to be updated so I can then trigger some other action that uses the new state?". This led me to believe that anything that 'reacted' to state changes needed to be wrapped in a useEffect, since it was the only way to ensure the state had been updated. In reality, this often leads to components with tons of performance issues and unnecessary re-renders.

You can be sure that every part of the component function that uses state will have the latest snapshot (unless you have created a closure somewhere, like a callback in a setTimeout). Setting state does not change the variable in the existing render, but instead requests a new render.

Read more about this in the React docs.


3.- Don't use props to set state unless you want to ignore all further updates to said prop

The most important thing to remember here is:
If you are passing a component's piece of state as props to a child component, don't use that value to initialize state in the child component.

Why? Because state initialization occurs only when the component is mounted (it's rendered for the first time). This means that any updates to the parent's state won't trigger a state update in the child, effectively creating two independent states that will be out of sync. This is okay if you don't expect changes in the parent to be reflected in the child, but the majority of times we do.

If your intention is to keep a component in sync with its parent, then your child component should be a controlled component (it doesn't keep its own state). You can then use the value of the prop anywhere in your component, being sure that state updates in the parent will reflect correctly in the child.

Read more about this in the React docs.


4.- You need useEffect less thank you think

As I mentioned earlier in this post, useEffect tends to be overused by beginner React developers to trigger something in response to a state change.

State, props and JSX are the only key elements to building reactive component trees in React, and effects are simply designed as a way to step out of this paradigm to perform a 'side effect'. These side effects are usually actions that interact with some type of external system, not the application itself. Actions to be performed with effects include making an API call, controlling a non-React component, triggering some browser action, adding event listeners to non-React elements in the DOM, etc.

Here are some examples of common tasks where useEffect is not necessary:

  • Transforming state or props before displaying them

Most poorly written effects that I've seen are used to 'listen' to changes in a piece of state to then calculate a new state variable from it.

const PoorlyWrittenComponent = () => {
  const [data, setData] = useState([]);
  const [sortedData, setsortedData] = useState([]);

  useEffect(() => {
    fetchData();
  }, []);

  useEffect(() => {
    const sorted = [...data].sort((a, b) => {
     if (a.name > b.name) return 1;
     else if (a.name. < b.name) return -1;
     return 0;
    });
    setSortedData(sorted);
  }, [data]);

  const fetchData = async () => {
    // Data fetching
  };

  return (
    <div>
      <h1>Transformed Data Example</h1>
      {sortedData.map((item) => (
        <div key={item.id}>
          <h3>{item.name}</h3>
          <p>{item.description}</p>
        </div>
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Remember that all expressions and assignments in a component are evaluated again with each re-render. If we keep this in mind, it becomes clear that we can transform state by simply creating a new variable and assign it the expression that transforms the state. The expression will always use the latest state (see State as a Snapshot above). We can then use this variable in our JSX:

const GoodComponent = () => {
  const [data, setData] = useState([]);
  const sortedData = [...data].sort((a, b) => {
     if (a.name > b.name) return 1;
     else if (a.name. < b.name) return -1;
     return 0;
  });

  useEffect(() => {
    fetchData();
  }, []);

  const fetchData = async () => {
    // Data fetching
  };

  return (
    <div>
      <h1>Transformed Data Example</h1>
      {sortedData.map((item) => (
        <div key={item.id}>
          <h3>{item.name}</h3>
          <p>{item.description}</p>
        </div>
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode
  • Completely resetting the state when a prop changes

Let's say we pass a userId prop to a component to keep track of which user is being represented by the component. If we change the user represented in the component, we want to remove all current state and give the component a clear slate of state. To avoid unnecessary effects and re-renders, we can represent the userId that owns that component by also passing the userId as a key prop. Whenever a key changes in a component, we are requesting React to mount a different instance of that component with the new key.

const ParentComponent = ({ id }) => {
  const [selectedId, setSelectedId] = useState(id);

  const handleIdChange = (newId) => {
    setSelectedId(newId);
  };

  return (
    <div>
      <h1>Parent Component</h1>
      <button onClick={() => handleIdChange('newId')}>Change ID</button>
      <ChildComponent key={selectedId} id={selectedId} />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode
  • Responding to a change triggered by an event handler

Effects are often used to perform actions in response to a user action. In the next example, clicking on a submit button runs an event handler function that changes the state of isSubmitted and triggers an effect that displays a notification to the user.

const NotificationComponent = () => {
  const [isSubmitted, setIsSubmitted] = useState(false);

  useEffect(() => {
    if (isSubmitted) {
      // Display notification to the user
    }
  }, [isSubmitted]);

  const handleSubmit = () => {
    setIsSubmitted(true);
  };

  return (
    <div>
      <h1>Notification Example</h1>
      <button onClick={handleSubmit}>Submit</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

The useEffect in this piece of code is unnecessary, and we can simplify it by putting the code that displays the notification in the event handler.

const NotificationComponent = () => {
  const [isSubmitted, setIsSubmitted] = useState(false);

  const handleSubmit = () => {
    setIsSubmitted(true);
    // Display notification to the user
  };

  return (
    <div>
      <h1>Notification Example</h1>
      <button onClick={handleSubmit}>Submit</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

If a change in the component is triggered by an action from user input, it's better to handle all the logic related to the event in the event handler. In other cases, it makes sense to trigger some action in response to the component rendering, like sending a request to log data and analytics when the page is visited.

When deciding whether to put some behavior in an effect or in an event handler, think about what kind of logic it is from the user's perspective. If the action is performed as a result of user input, leave it inside the event handler. If it's performed as a result of the user seeing the page or the component, it might make sense to put it in an effect.

Read more about this in the React docs.


5.- Create custom hooks to reuse stateful logic in multiple parts of your application

Custom hooks are abstractions over built-in React hooks to keep track of a piece of state or trigger effects without displaying the implementation in the component directly. This helps you reuse a certain stateful logic in several components.

Be aware that built-in React hooks and custom hooks can only be called from the top level of React component functions or other custom hooks. A custom hook's name must always start with the word 'use'.

Custom hooks typically return a value that we want to keep track of. They behave similarly to components in the sense that state changes in the hook will cause it to re-render and therefore return the value again. You can think of custom hooks as components that return a value instead of JSX.

Here is an example of a custom hook that keeps track of a count that is incremented every second. The initial value and the value to increment the counter by are passed when initilializing the hook:

const useCounter = (initialValue, incrementBy) => {
  const [count, setCount] = useState(initialValue);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount((prevCount) => prevCount + incrementBy);
    }, 1000);

    return () => {
      clearInterval(interval);
    };
  }, [incrementBy]);

  return count;
};
Enter fullscreen mode Exit fullscreen mode

We can then use this hook in other components, abstracting the implementation of the counter and allowing us to simply use the value in the JSX:

const CounterComponent = () => {
  const count = useCounter(0, 1);

  return (
    <div>
      <h1>Counter: {count}</h1>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Read more about this in the React docs.

I hope you found some of this information useful. Let me know what were your main takeaways after reading the new React documentation. It's always a great feeling to have a stronger understanding of your frontend framework of choice, and I'd love to hear what you have learned.

Top comments (5)

Collapse
 
leandro_nnz profile image
Leandro Nuñez

Hi. One question about this point: “Transforming state or props before displaying them”.
You’re stating that you need to let the component re-render each time we need to show different sorted data (as in the example).
Isn’t that the magic of react? For us to update state without re-rendering the component?
Thanks in advance.

Collapse
 
edchavezb profile image
Edgar Chavez

Hi Leandro.

Not at all! In fact I meant the opposite. Sorry if I didn't explain myself clearly.

So for example, let's say we use a useEffect to listen to changes to a state variable called userList and transform it in some way, setting a transformedUserList state.

Effects run after the component rendered, and since the code inside the effect changes a state variable, we will trigger an additional render. Transforming the state with a useEffect takes a total of two renders.

But remember that when a re-render happens all the variables inside the component are created again and the expressions are evaluated one more time. So creating a variable that transforms the state happens WITH the render, not after it. This second approach takes just one render, the one caused by the state change.

Isn’t that the magic of react? For us to update state without re-rendering the component?

As for this part of your comment, keep in mind that updating state will ALWAYS trigger a render in React.

Hope that clears it up.

Collapse
 
leandro_nnz profile image
Leandro Nuñez

Ohhhh. I see now. Sorry for misunderstanding.
I get the rendering -I think- and re-rendering of the useEffect. I’m still a little confused about this:

  1. Why would component re-renderin those examples if not for changing data?
  2. I’m starting to think: is this why nextjs exists?
  3. Wouldn’t data sort happen on every re-render on GoodComponent and in the effect just when data changes?

Sorry for being such a load.

Thanks in advance!!

PS: this is a great article.

Thread Thread
 
edchavezb profile image
Edgar Chavez • Edited

No worries. Those are all good points and I guess there are some important ideas I didn't discuss in the post.

Wouldn’t data sort happen on every re-render on GoodComponent and in the effect just when data changes?

This is crucial to think about when you start optimizing your app for performance. A good way to prevent the sorting from happening in every render would be to wrap the function that sorts the data in a useMemo. We can use this hook to memoize the result of an operation and only re-calculate when the dependency changes.

const YourComponent = () => {
  const [data, setData] = useState([4, 6, 2, 3]);

  // Use useMemo to memoize the sortedData and only recompute it when data changes
  const sortedData = useMemo(() => {
    return dataList.slice().sort(/* Your sorting logic here */);
  }, [data]);

  // Other logic and rendering of your component using sortedData

  return (
    <div>
      {/* Component rendering */}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Why would component re-renderin those examples if not for changing data?

A component will re-render without any change in state if one of its parents re-renders. A re-render in a component always triggers a re-render of all of its children. But you can also memoize components to prevent this. To do that you have to wrap the component in React.memo.

const MyComponent = ({ value }) => {
  console.log('Rendering MyComponent...');
  return <div>{value}</div>;
};

// Use React.memo to memoize MyComponent
const MemoizedMyComponent = React.memo(MyComponent);

export default MemoizedMyComponent;
Enter fullscreen mode Exit fullscreen mode

When you do this, the component will only re-render when its props or state change, but will skip re-rendering if its parent triggered a render.

These are useful techniques to improve performance, but it's often unnecessary to apply them everywhere. It's better to first identify where your app has performance bottlenecks and apply them there.

I’m starting to think: is this why nextjs exists?

I personally don't have much experience with Next, but I would say one of its advantages is that you can offload a lot of work that is typically done in the client to the server. So in a sense you're right because Next completely changes what we should pay attention to when it comes to performance.

Thanks for your comment!

Thread Thread
 
leandro_nnz profile image
Leandro Nuñez

Thanks for the amazing explanation!