A Visual Guide to React Mental Models: UseState, UseEffect and Lifecycles

栏目: IT技术 · 发布时间: 4年前

内容简介:I love mental models. They’re crucial to understanding complex systems, allowing us to intuitively grasp and solve complex problems.This is the second of a three-part series of articles around React mental models. I’llI recommend you readpart 1 first, as t

I love mental models. They’re crucial to understanding complex systems, allowing us to intuitively grasp and solve complex problems.

This is the second of a three-part series of articles around React mental models. I’ll show you the exact mental models I use with complex React components by building them from the ground up and by using lots of visual explanations.

I recommend you readpart 1 first, as the mental models in this article are relying of the ones I explained there. If you want a refresher, here’s the complete mental model for part 1

Whether you’ve been working with React for years or are just starting, having a useful mental model is, in my opinion, the fastest way to feel confident working with it.

You’ll learn:

  • The useState hook : how it magically works and how to intuitively understand it.
  • The component’s lifecycle: Mounting, Rendering, Unmounting : the source of many bugs is a lack of a good mental model around these.
  • The useEffect hook : how this powerful Hook actually works?

Let’s start!

What are mental models and why are they important?

A mental model is a thought process or mental image that helps us understand complex systems and to solve hard problems intuitively by guiding us in the right direction. You use mental models everyday; think of how you imagine the internet, cars, or the immune system to work. You have a mental model for every complex system you interact with.

The mental model for React so far

Here’s a very quick overview of the React mental model I explained in part 1, or you can find the complete version for part 1 here .

A React component is just like a function, it receives props which are a function’s arguments, and it will re-execute whenever those props change. I imagine a component as a box that lives within another box.

Each box can have many children but only one parent, and apart from receiving props from its parent, it has an internal, special variable called state , which also makes it re-execute (re-render) when it changes.

A Visual Guide to React Mental Models: UseState, UseEffect and Lifecycles
When props or state changes the component re-renders

The useState hook: state in a bottle

I showed how state works in part 1 , and how it is is a special property inside a box. Unlike variables or functions which are re-declared on every render, the values that come out of useState always consistent across renders. They get initialized on mount with a default value, and can only be changed by a set state event.

But how can React prevent state from losing its value on each render? The answer is scope .

I explained the mental model for closures and scope in pat 1 . In short, a closure is like a semi-permeable box, letting information from the outside get in but never leaking anything out.

A Visual Guide to React Mental Models: UseState, UseEffect and Lifecycles
Javascript closures visualized

With useState , React scopes its value to the outermost closure, which is the React app containing all your components. In other words, whenever you use useState React returns a value that is stored outside your component and hence not changing on each render.

React manages to do this by keeping track of each component and the order in which each hook is declared. That’s the reason you can’t have a React Hook inside a conditional. If useState, useEffect, or any other hook is created conditionally then React cannot properly keep track of it.

This is best explained visually:

A Visual Guide to React Mental Models: UseState, UseEffect and Lifecycles
React state is scoped to the outer-most box, that way it doesn't change on every render

Whenever a component is re-rendered useState asks to get the state for the current component, React then checks a list containing all states for each component and returns the corresponding one. This list is stored outside the component because on each re-render variables and functions are created and destroyed.

Although this is a technical view of how state works, by understanding it I can transform some of React’s magic into something I can visualize. For my mental model I simplify things into a simpler idea.

My mental model when working with useState is this: since state is not affected by what happens to the box, I imagine it as a constant value within it. I know that no matter what happens state will remain consistent throughout the lifetime of my component.

A Visual Guide to React Mental Models: UseState, UseEffect and Lifecycles
State remains constant even though the component could change

How does state change?

Once we understand how state is preserved, it’s important to understand how it changes.

You may know that state updates are async , but what does that mean? How does it affect our everyday work?

A simplified explanation of sync and async is:

  • Sync ronous code blocks the JavaScript thread, where your apps runs, from doing any other work. Only one piece of code can be run at a time in the thread.
  • Async ronous code doesn’t block the thread because it gets moved to a queue and runs whenever there’s time available.

We use state as a variable, but updating it is async . This makes it easy to fall into the trap of thinking that a set state will change its value right away like a variable would, which leads to bugs and frustration, for example:

const Component = () => {
  const [searchValue, setSearchValue] = useState('');

  // search something when a user writes on an input
  const handleInput = e => {
    // Save value in state and then use it to fetch new data :x:
    setSearchValue(e.target.value);
    fetchSearch(searchValue).then(results => {
      // do something
    });
  };
};

This code is buggy. Imagine a person types Bye . The code will search for by instead of bye because each new stroke triggers a new setSearchValue and fetchSearch , but because state updates are async we’re going to fetch with an outdated searchValue . If a person types fast enough and we have other JavaScript running, we may even search for b since JavaScript didn’t have time to run the code from the queue yet.

Long story short, don’t expect state to be updated right away. This fixes the bug:

const Component = () => {
  const [searchValue, setSearchValue] = useState('');

  const handleInput = e => {
    // Saving the search in a variable makes it reliable :white_check_mark:
    const search = e.target.value;
    setSearchValue(search);
    fetchSearch(search).then(results => {
      // do something
    });
  };
};

One of the reasons state updates are async is to optimize them. If an app has hundreds of different states wanting to update at once React will try to batch as many of them as possible into a single async operation, instead of running many sync ones. Async operations, in general, are more performant too.

Another reason is consistency. If a state is updated many times in quick succession, React will only take the latest value for consistency’s sake. This would be difficult to do if the updates were sync and executed right away.

A Visual Guide to React Mental Models: UseState, UseEffect and Lifecycles
A mental model of how the JavaScript Thread works with state, React batches state updates together

In my mental model, I see individual state values as reliable but slow. Whenever I update one, I know it can take a while for it to change.

But what happens to state and the component itself, when it’s mounting and unmounting?

A Component’s lifecycle: mental models for mounting, rendering, and unmounting

We used to talk a lot about lifecycle methods when only class-components had access to state and control of what was happening to a component over its lifetime. But since Hooks came out and allowed us the same kind of power in functional components, the idea became less relevant.

What’s interesting is that each component still has a lifecycle: its mounted, rendered and unmounted, and each step must be taken into account for a fully-functional mental model around React components.

So let’s go through each phase and build a mental model for it, I promise it’ll make your understanding of a component much better.

Mounting: Creating Components

When React creates or renders a component for the first time it’s mounting it. Meaning it’s going to be added to the DOM and React will start keeping track of it.

I like to imagine mounting as a new box being or added inside its parent.

Mounting happens whenever a component hasn’t been rendered, and its parent decides to render it for the first time. In other words, mounting is a component being “born”.

A component can be created and destroyed many times, and each time it’s created, it will be mounted again.

const Component = () => {
  const [show, setShow] = useState(false);

  return (
    <div>
      <button onClick={() => setShow(!show)}>Show Menu</button>
      // Mounted with show = true and unomunted with show = false
      {show && <MenuDropdown />}
    </div>
  );
};

React renders components so fast it can look like its hiding them but in reality, it’s creating and deleting them very quickly. In the example above the <MenuDropdown /> component will be added and removed from the DOM every time the button is clicked.

Note how the component’s parent is the one deciding when to mount and unmount <MenuDropdown /> . This goes up the hierarchy too. If MenuDropdown has children components they will be mounted or unmounted too. The component itself never knows when it’s going to be mounted or unmounted.

A Visual Guide to React Mental Models: UseState, UseEffect and Lifecycles
The parent component re-renders with different logic, causing a child to mount

Once a component is mounted , it will do a few things:

  • Initialize useState with default values: this only happens on mount.
  • Execute the component’s logic.
  • Do an initial render, adding the elements to the DOM.
  • Run the useEffect hook.

Note that the useEffect hook runs after the initial render. That’s when you want to run code like creating event listeners, executing heavy logic, or fetching data. More on this in thebelow.

My mental model for mounting is this: whenever a parent box decides a child must be created, it mounts it, then the component will do three things: assign default values to useState , run its logic, render, and execute the useEffect hook.

The mount phase is very similar to a normal re-render , with the difference being initializing useState with default values and the elements being added to the DOM for the first time. After mount the component remains in the DOM and is updated further.

Once a component is mounted it will continue to live until it unmounts, doing any amount of renders in between.

Rendering: Updating What The User Sees

I explained the rendering mental model in part 1 , but let’s review it briefly as it’s an important phase.

After a component mounts, any changes to the props or state will cause it to re-render, re-executing all the code inside of it, including its children components. After each render the useEffect hook is evaluated again.

I imagine a component as a box and its ability to re-render makes it a re-usable box. Every render recycles the box, which could output different information while keeping the same state and code underneath.

A Visual Guide to React Mental Models: UseState, UseEffect and Lifecycles
A component re-renders whenever state or props change, or its parent re-renders

Once a component’s parent decides to stop rendering a child–because of a conditional, changes in data or any other reason–the component will need to be unmounted.

Unnmountig: Deleting Components

When a component is unmounted React will remove it from the DOM and stops keeping track of it. The component is deleted including any state it had.

Like explained in the mounting phase, a component is both mounted and unmounted by its parent, and if the component, in turn, has children it will unmount those too, and the cycle repeats until the last child is reached.

In my mental model, I see this as a parent-box trashing a child-box. If you throw a container to the trash everything inside of it will also go to the trash, this includes other boxes (components), state, variables, everything.

A Visual Guide to React Mental Models: UseState, UseEffect and Lifecycles
The parent component re-renders with different logic, causing a child to unmount

But a component can create code outside of itself. What happens to any subscription, web socket, or event listener created by a component that will be unmounted?

The answer is nothing. Those functions run outside the component and won’t be affected by it being deleted. That’s why is important for the component to clean up after itself before unmounting.

Each function drains resources. Failing to clean them up can lead to nasty bugs, degraded performance and even security risks.

I think of these functions as gears turning outside my box. They’re set in motion when the component mounts , and they must be stopped when it unmounts .

A Visual Guide to React Mental Models: UseState, UseEffect and Lifecycles
A function that lives outside your code won't be removed on `unmount` on its own.

We’re able to clean up or stop these gears through the return function of useEffect . I will explain in detail in the Effect hook section.

So let’s put all the lifecycle methods into a clear mental model

The Complete Component Lifecycle Mental Model

To summarize so far: a component is just a function, props are the function’s arguments and state is a special value that React makes sure to keep consistent across renders. All components must be within other components, and each parent can have many children within it.

A Visual Guide to React Mental Models: UseState, UseEffect and Lifecycles
A complete mental model for a simple stateful component

Each component has three phases in its lifecycle: mounting, rendering, and unmounting.

In my mental model, a component is a box and based on some logic it can decide to create or delete a child box. When it creates it a component is mounted and when it deletes it, it is unmounted .

A box mounting means it was created and executed. Here’s when useState is initialized with default values and React renders it so the user can see it, and starts keeping track of it.

The mounting phase is where we tend to connect to external services, fetch data or create event listeners.

Once mounted, whenever a box’s props or state changes it will be re-rendered, which I imagine as the box being recycled and everything but state is re-executed and re-calculated. What the user sees can change on every new render. Re-rendering is the second phase, which can happen any number of times, without limit.

Once a component’s parent decides to remove it, either because of logic, the parent itself was removed, or data changed, the component will unmount .

When a box unmounts it is thrown away, trashed with everything it contains, including children components (which in turn have their own unmount ). This is where we have the chance to clean up and delete any external function we initialized in a useEffect .

The cycle of mounting, re-rendering, and unmounting can happen thousands of times in your app without you noticing. React is incredibly fast and that’s why it’s useful to keep a mental model in mind when dealing with complex components since it’s so hard to see what’s going on in real-time.

But how do we take advantage of these phases in our code? The answer is through the powerful useEffect hook.

The UseEffect Hook: Unlimited Power!

The Effect hook allows us to run side effects in our components. Whenever you’re fetching data, connecting to a service or subscription or manually manipulating the DOM, you’re performing a side effect (also called simply effect).

A side effect in the context of functions is anything that will make the function unpredictable, like data or state. A function without side-effects will be predictable and pure –you might have heard of pure functions –always doing the exact same thing as long as the inputs remain constant.

An Effect hook always runs after every render. The reason being that side effects can contain heavy logic or take time, such as fetching data, so in general they’re better off running after render.

The hook receives two arguments: the function to execute and an array with values that will be evaluated after each render, these values are called dependencies.

// Option 1 - no dependencies
useEffect(() => {
  // heavy logic that runs after each render
});

// Option 2 - empty dependencies
useEffect(() => {
  // create an event listener, subscription, fetch one-time data
}, []);

// Option 3 - with dependencies
useEffect(() => {
  // fetch data whenever A, B or C changes.
}, [a, b, c]);

Depending on the second argument you have 3 options with different behavior. The logic for each option is:

  • If not present the effect will run after every render. This option is not commonly used, but it’s useful in some situations like needing to do heavy calculations after each render.
  • With an empty array [] the effect runs only once, after mounting and the first render. This is great for one-time effects such as creating an event listener.
  • An array with values [a, b, c] makes the effect evaluate the dependencies, whenever a dependency changes the effect will run. This is useful to run effect when props or state changes, like fetching new data.
A Visual Guide to React Mental Models: UseState, UseEffect and Lifecycles
A visual explanation of useEffect's dependency options

The dependency array gives useEffect its magic, and it’s important to use it correctly. You must include all variables used within useEffect , otherwise, the effect will reference stale values from previous renders when running, causing bugs.

The ESLint plugin eslint-plugin-react-hooks contains many useful Hooks-specific rules, including one that will warn you if you missed a dependency inside a useEffect .

My initial mental model for useEffect has it as a mini-box living inside its component, with three distinct behaviors depending on the usage of the dependency array: the effect either runs after every render if there are no dependencies, only after mount if it’s an empty array, or whenever a dependency changes if the array has values.

A Visual Guide to React Mental Models: UseState, UseEffect and Lifecycles
Each useEffect lives inside the component, accessing the same info, but as its own box

There’s another important feature of useEffect , it allows us to clean up before a new effect is run, or before unmount occurs.

UseEffect during unmount: cleaning up

Every time we create a subscription, event listener or open connections we must clean them up when they’re no longer needed, otherwise, we create a memory leak and degrade the performance of our app.

This is where useEffect comes in handy. By returning a function from it we can run code before applying the next effect, or if the effect runs only once then the code runs before unmounting the component.

// This effect will run once at mount, creating an event listener
// It will execute the return function at unmount, removing the event listening, cleaning up :white_check_mark:
useEffect(() => {
  const handleResize = () => setWindowWidth(window.innerWidth);
  window.addEventListener('resize', handleResize);

  return () => window.remoteEventListener('resize', handleResize);
}, []);

// This effect will run whenever `props.stream.id` changes
useEffect(() => {
  const handleStatusChange = streamData => {
    setStreamData(streamData);
  };

  streamingApi.subscribeToId(props.stream.id, handleStatusChange);

  // Unsubscribe to the current ID before running the next effect with the new ID
  return () =>
    streamingApi.unsubscribeToId(props.stream.id, handleStatusChange);
}, [props.stream.id]);

The Complete React UseEffect Hook Mental Model

I imagine useEffect as a small box within a component, living alongside the logic of the component. This box’s code (called an effect) only runs after React has rendered the component, and it’s the perfect place to run side-effect or heavy logic.

All of useEffect’s magic comes from its second argument, the dependency array, and it can have three behaviors from it:

  • No argument: the effect runs after each render
  • Empty array: the effect only runs after the initial render, and the return function before unmount.
  • Array with values: whenever a dependency changes, the effect will run, and the return function will run before the new effect.

I hope you’ve found my mental models useful! Explaining them clearly was a challenge. If you enjoyed reading please share this article, it’s all I ask for :heart:.

This was the second part of a three part series, the next and last part will cover higher-level concepts such as React context and how to better think of your app to prevent common performance issues.

You can subscribe to my newsletter if you want to be notified when the third part is out, as well as many more “Mental Models” articles I have planned (useEffect in-depth, Git, and more).

We understand so much better with visual aids, and there’s not enough material like this on the web, so I’m going to create many more articles such as this (and better, as I hone down my design skills :sweat_smile:).

What questions do you have? I’m always available in Twitter , hit me up!


以上所述就是小编给大家介绍的《A Visual Guide to React Mental Models: UseState, UseEffect and Lifecycles》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

C专家编程

C专家编程

[美] Peter Vander Linde / 徐波 / 人民邮电出版社 / 2002-12 / 40.00元

《C专家编程》展示了最优秀的C程序员所使用的编码技巧,并专门开辟了一章对C++的基础知识进行了介绍。 书中对C的历史、语言特性、声明、数组、指针、链接、运行时、内存,以及如何进一步学习C++等问题作了细致的讲解和深入的分析。全书撷取几十几个实例进行讲解,对C程序员具有非常高的实用价值。 这本《C专家编程》可以帮助有一定经验的C程序员成为C编程方面的专家,对于具备相当的C语言基础的程序员......一起来看看 《C专家编程》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

MD5 加密
MD5 加密

MD5 加密工具