An Introduction to State in React

December 29, 2021

Preface

In any application with a GUI, independent from the target platform (terminal, web, mobile native, desktop native etc.), handling the UI state is arguably one of the most difficult thing to manage. Most of the time a certain action that trigger a state change causes a re-render or a side effect and those changes can be quickly get out of hand by causing inconsistent representation in different parts of GUI if they are not handled with care. Additionally, it can be also quite painful to debug the app, if the data flow is not very clear.

React as a “library” offers an abstraction to simplify the state management. With this, any view (small or big) can be generated with a function of props (short of properties) and state. State is not explicitly passed to the render function for the reasons I’m not going to elaborate here, but the view is simply generated using props and state.

React Render Function Diagram

However, this is a bit simplistic way of looking at render functions in web platform. Before we take a look at the ways of dealing with complexity, let’s give an overview for intrinsic sources of state in browsers.

Sources of State in a Web Browser

When we code our components, we tend to think of them as isolated from the other sources of state. But the truth is, in browser there are many sources of truth that impact the way the app behaves.

Actually the diagram below is a more accurate representation of render function in React. The implicit usage of those sources of state can alter the behavior of the component as well.

React Render Function Diagram with Implicit Sources of State

To understand when and where it is necessary to use them, let’s go over them briefly.

Location

Location is arguable one of the most underutilized and misused sources of state. It is one of the tools that can enhance the UX greatly. It simply consists of four main components and each have their own purpose.

  1. Hostname

Hostname is the domain that the application is served. For example, we can instruct our web app to behave differently while working in different domains like localhost and deliberately turn off tracking events for development environment.

  1. Pathname

Pathname is the path of the URL. It helps us to structure pages of the application we serve. In client side rendered apps, pathname can decide what page will be rendered with the help of libraries like ReactRouter and ReactLocation.

  1. Search

Known as also query string, search contains the parameters followed by ? . The parameters are key value pairs, but a single key can have multiple values. Leveraging the parameters can be an effective tool to improve UX. A page can dynamically update the parameters when a certain filter applied to the data shown on the screen. This allows users to share a certain state of the app with others or even bookmark it for themselves using just the url. Since the changes are stored in history, the user is enabled to easily switch between possible states of the page with the help of back and forward buttons. Additionally storing an modal’s open/closed state provides an experience similar to native apps. When a modal is opened, most users click instinctively the back button in mobile web browsers and expect modal to be closed.

  1. Hash

Hash property helps us signify a certain HTML element with an id. Browsers are smart enough to scroll to the element specified in the location, if the element with the id is present in the page during the first render. Since client side rendered web applications is not fully booted in first render, it is not possible to have the same behavior without client side scripting.

Note:

Some libraries overcome this solution by setting a DOM wide MutationObserver and then traversing the DOM to scroll to the mounted element with the specific id if it exists. Instead, if it is known that a certain component's content can be targeted with hash, it is possible to trigger scrolling when a given component mounted. By doing so, the polling strategy can be avoided.

History:

Location is not a scalar value. It is a linked-list like structure. In every node, URL and a generic JS object called state are stored. The state of history affects the behavior of the app when the user clicks back or forward buttons.

Application Storage

Browsers have various instruments to store client side data. Here are some of them:

Cookies

Cookies are set by HTTP Response Headers and mainly controlled by responses. It is also possible to set cookies with JS, but it is rarely utilized this way. For security reasons, not every cookie is accessible with JS.

Session Storage

This storage type is a tab specific key value store. The content can be only accessed with JS. It is not highly persistent, and the content is removed once the tab is closed.

Local Storage

Compared to the session storage, local storage is persistent and shared across multiple instances of the same website operate in the same domain.

CacheStorage

This API helps to store HTTP responses. It is mostly used by Service Workers to provide consistent caching for assets and documents, and have control over the network calls and caching strategies.

IndexedDB

To store relational data on client side, IndexedDB provides an API to store structural data. It is not a RDBMS though, it doesn't enforce a fixed column tables.

Device Properties

The device that the app is rendered can have many characteristics that change the behavior of the app. Here are some of them:

  • Screen Orientation
  • Screen Dimensions
  • Aspect Ratio
  • DPI
  • Locale Settings
  • Touch Support

These can be queried from CSS or JS and alter the behavior of the application.

Handling Global State

Before diving deep into different strategies to handle state in React, we have to go over certain principles. These principles are going to help us to find the best strategy for dealing with state in a React application.

Immutability

One of the most important concepts of React is the immutability. The props and state should never modified directly under any condition. For example, the following is not the right way to trigger changes:

function NameInput() {
  const [name] = React.useState();

  return (
    <>
      <p>{name}</p>
      <input type="text" onChange={(ev) => (name = ev.target.value)} />
    </>
  );
}

To alter state in React, it is required to use setters or callback functions. This makes code more verbose, but it shows the way the app works more explicitly.

Note:

There are many libraries out there (e.g: mobx, immer) that allow direct modification of state. However these are still subject the rules of React and just hide the underlying mechanism.

By guaranteeing immutability of the props and state, React can optimize rendering and trigger effects only when they are necessary. Without that, the app becomes unstable.

Since the UI is a function of props and state, React can memoize (https://en.wikipedia.org/wiki/Memoization) the rendered output and cache them based on the input values (props and state). Below you can see a simplified version of memoized components. When React is asked to do memoization, it can skip a costly render operation when the props stay the same.

React Render Function Diagram for Memoized Renders

On the other hand, we need to initiate some side effects to make our app interactive. And having immutability there is also immensely helpful to prevent unnecessary triggers and find out the cases when the effects should be called. The mental modal for this is plainly:

Change of prop or state → Trigger an effect

Note:

That’s why the eslint rule react-hooks/exhaustive-deps is a must. This will catch all subtle bugs. There is a tendency to suppress and ignore the warnings for this rule. Here is the most upvoted comment in a GitHub issue that is against the use of the rule: https://github.com/facebook/create-react-app/issues/6880#issuecomment-485936545

const hideSelf = () => {
  // In our case, this simply dispatches a Redux action
};

// Automatically hide the notification
useEffect(() => {
  setTimeout(() => {
    hideSelf();
  }, 15000);
}, []);

Here the OP claims that it is unnecessary to put hideSelf to dependency list of useEffect. This might be true for this particular situation, but in many cases, a callback function like hideSelf contain curried variables. When those variables change, the effect will not be triggered when it is needed because there is no way for useEffect to know that a dependency has changed. For example, the following code will fetch the user data when the component mounts. Many developers think that it is enough to fetch the data when the component mounts. However, when userId changes, the data for the new user will never be fetched and the component will use the data that is invalid.

const { userId } = props;

const fetchUser = () => {
  return fetch(`/api/users/${userId}`);
};

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

The fix is actually quite trivial. You can also avoid memoizing the function by moving the function creation to the inner scope of useEffect.

const { userId } = props;

useEffect(() => {
  const fetchUser = () => {
    return fetch(`/api/users/${userId}`);
  };
  fetchUser();
}, [userId]);

When I migrated some class components to functional components in a codebase, I found and fixed so many subtle bugs thanks to dependency lists. Therefore, I take eslint rules for React Hooks very seriously, even though I’m pretty sure that I only need to call an effect after first render. The warnings for exhaustive-deps forces the developer to think again what your effect depends on.

Not All State Are Equal

As all things in software development, the treatment for state is not same all time. There are many solutions for different cases. In this section, I’ll try to give a brief overview for some of the techniques with their trade-offs.

In web applications, some state is tab specific and some state is app specific. Some state is imposed by server and have an async nature. As I mentioned, browser itself stores different types of state and therefore the solution sometimes may live outside of React. All different types of state must be handled with care. Unfortunately, there is no solution that works all the time.

Component State vs. App State

Let’s start with the basic building block: Component State. Whether it is an open closed status of a modal or the text value of an input element, Component State is the way the go to kick things off. But often we find ourselves in need to share the state information with other components. Before we let things out of control, there are two questions that will help us to make the right decision.

Question 1: Do I need to share that state with other components?

Often times, actually the answer is no. If there is a way to keep state encapsulated in a component, we make our code more maintainable by not coupling things. Just think about this: When we promote a state to be an app-wide accessible level, in a way we make it a global value. Just like we avoid making variables global at all costs, this should be also avoided. Once a global state is used between many components, they are tied with an invisible link. That link makes it harder to introduce changes, remove code and extend new functionality. So be very conservative when making things global.

Question 2: Do the components I want to share the state have a close parent?

If sharing state is unavoidable, we can only make it available for a certain branch of the render tree. The state can be moved to a parent of the component. This is also known as lifting the state to the parent. This change leads to prop drilling. Even though it seems tedious to pass the same value component to component, the app stays more maintainable and robust due to limited scope of the state in comparison to making a state global.

After these questions, we might still think that it is necessary to make some state global. For instance, imagine that there is an app with login functionality. It would be really handy in any component, if there is a way to access directly to the information of whether the user is logged in or not. So, here I would prefer to avoid prop drilling and find a way to inject the state. Injection might make it easier to code. Because we don’t need to drill the prop from top to bottom, we only need to touch only a handful of files. But there are some negative implications of making things global, be aware:

  • Coupled app state and component
  • Decreased mobility
  • Decreased testability

Conclusion

In this post, we’ve been over the various sources of state in web applications. We also mentioned some of the principles imposed by React and how it changes the way we deal with our app state. In the following post, we will take a look at various solutions for handling this complexity with ease.