logo
    • Home
    • Categories
    • About
  • en-languageEnglish
User interfaceBy Pierre Colart

React Data Management and Performance

Introduction

For proper data fetching, it's common to use communication patterns between a parent and a child with callbacks. We'll explore the React Context API and React Suspense to solve several known issues.

React Context API

The React Context API became official in version 16.3.0, whereas it was previously experimental. This new API has prompted many developers to move away from Redux in favor of the Context API, which allows for sharing data between components without passing props down to each child.

To get started, create a context by creating a folder named "contexts" and an example file named "Issue.tsx". Import the necessary dependencies and add the props interfaces, Issue and context:

 import { FC, createContext, useState, useEffect, ReactElement, useCallback } from 'react'
import axios from 'axios'

export type Issue = {
  number: number
  title: string
  url: string
  state: string
}

interface Issue_Context {
  issues: Issue[]
  url: string
}

interface Props {
  children: ReactElement
  url: string
}

Then, create the context with the createContext function and define the value to be exported:

 export const IssueContext = createContext<Issue_Context>({
  issues: [],
  url: ''
})

After creating the IssueContext, create a component to receive props, define states, and perform retrieval using useEffect. Then, return IssueContext.Provider, specifying the context (value) to be exported:

 const IssueProvider: FC<Props> = ({ children, url }) => {
  // State
  const [issues, setIssues] = useState<Issue[]>([])

  const fetchIssues = useCallback(async () => {
    const response = await axios(url)

    if (response) {
      setIssues(response.data)
    }
  }, [url])

  // Effects
  useEffect(() => {
    fetchIssues()
  }, [fetchIssues])

  const context = {
    issues,
    url
  }

  return <IssueContext.Provider value={context}>{children}</IssueContext.Provider>
}

export default IssueProvider

You should use useCallback to wrap a function used in useEffect. It is recommended to have a separate async/await function instead of including it directly in useEffect.

After retrieving the data and storing it in the "issues" state, add all the values to be exported as context. When rendering IssueContext.Provider, pass the context through props and render the component's children.

To consume a context, there are two steps:

  1. Encapsulate your application with the context provider. This code can be added to your APP:
 const App = () => {
  return (
    <IssueProvider url=
      "https://api.github.com/repos/ContentPI/ContentPI/issues">
      <Issues />
    </IssueProvider>
  )
}

As you can see, the Issues component is encapsulated by IssueProvider. Thus, inside the Issues component, you can consume the context and access the values of the issues.

❗️ This can be a source of confusion: if you forget to encapsulate your components with the provider, you won't be able to consume the context inside your components. The problem is that no error will be displayed, only undefined data, making detection difficult.

After adding IssueProvider in App.tsx, consume the context in the Issues component using useContext:

 // Dependencies
import { FC, useContext } from 'react'

// Contexts
import { IssueContext, Issue } from '../contexts/Issue'

const Issues: FC = () => {
  // Here you consume your Context, and you can grab the issues value.
  const { issues, url } = useContext(IssueContext)

  return (
    <>
      <h1>ContentPI Issues from Context</h1>

      {issues.map((issue: Issue) => (
        <p key={`issue-${issue.number}`}>
          <strong>#{issue.number}</strong> {' '}
          <a href={`${url}/${issue.number}`}>{issue.title}</a> {' '}
          {issue.state}
        </p>
      ))}
    </>
  )
}

export default Issues

Result:

React Context API example

The Context API is useful for separating application data and performing retrievals. It can be used for theming or passing functions, depending on the needs of the application.

Context API vs Redux:

Context API Redux
Integrated directly into React, requires minimal setup Requires an added dependency, more effort to integrate
Ideal for static data (updated infrequently) Suitable for both static and dynamic data
Adding new contexts means creating them from scratch Easily extensible, easy to add new data/actions
More difficult to debug Comes with debugging tools for easier debugging
UI logic and state management logic in the same component Better code organization with separate UI and state management logic

React Suspense (SWR)

React Suspense was introduced in React 16.6 and allows for suspending the rendering of components until a condition is met. A fallback component (such as a loading spinner or placeholder text) can be displayed as a fallback solution. Two use cases are:

  1. Code splitting: waiting for part of the application to download when a user wants to access it.
  2. Data fetching.

In both cases, a fallback (such as a spinner, loading text, or skeleton placeholder) can be displayed.

⚠️ Warning: React Suspense is still experimental and not recommended for production use.

Stale-While-Revalidate (SWR) is a React Hook for data fetching and an HTTP cache invalidation strategy. SWR first returns the data from the cache, sends the retrieval request, and then comes back with updated data. It was developed by Vercel, the company behind Next.js.

To illustrate this, we will create a Pokédex using the API: https://pokeapi.co.

Create the "Pokemon" directory: src/components/Pokemon. To use SWR, first create a retrieval file where requests will be made: fetcher.ts:```

 const fetcher = (url: string) => {
  return fetch(url).then((response) => {
    if (response.ok) {
      return response.json()
    }

    return {
      error: true
    }
  })
}

export default fetcher

Note that we return an object containing an error if the response fails. Indeed, it may happen that the API returns a 404 error, causing the application to malfunction. Now, let's configure SWR:

 // Dependencies
import { SWRConfig } from 'swr'

// Components
import PokeContainer from './Pokemon/PokeContainer'
import fetcher from './Pokemon/fetcher'

// Styles
import { StyledPokedex, StyledTitle } from './Pokemon/Pokemon.styled'

const App = () => {
  return (
    <> 
      <StyledTitle>Pokedex</StyledTitle> 

      <SWRConfig
        value={{
          fetcher,
          suspense: true,
        }}
      >
        <StyledPokedex>
          <PokeContainer />
        </StyledPokedex>
      </SWRConfig>
    </>
  )
}

As you can see, we need to encapsulate PokeContainer inside SWRConfig to retrieve the data. PokeContainer will be our parent component in which we will add our first Suspense:

 import { FC, Suspense } from 'react'

import Pokedex from './Pokedex'

const PokeContainer: FC = () => {
  return (
    <Suspense fallback={<h2>Loading Pokedex...</h2>}>
      <Pokedex />
    </Suspense>
  )
}

export default PokeContainer

As you can see, we set a loading message for our Suspense: "Loading Pokedex...". You can display whatever you want in it, React components or plain text. Then, we have our Pokedex component inside Suspense:

 // Dependencies
import { FC, Suspense } from 'react'
import useSWR from 'swr'

// Components
import LoadingSkeleton from './LoadingSkeleton'
import Pokemon from './Pokemon'

import { StyledGrid } from './Pokemon.styled'

const Pokedex: FC = () => {
  const { data: { results } } = 
    useSWR('https://pokeapi.co/api/v2/pokemon?limit=150')
  
  return (
    <>
      {results.map((pokemon: { name: string }) => (
        <Suspense fallback={<StyledGrid><LoadingSkeleton /></StyledGrid>}>
          <Pokemon key={pokemon.name} pokemonName={pokemon.name} />
        </Suspense>
      ))}
    </>
  )
}

export default Pokedex

We retrieve our data for the first time using the useSWR hook. As you can see, we are retrieving the first 150 Pokémon, because I'm a fan of the first generation. We use the map function to display each Pokémon, adding a Suspense component with a LoadingSkeleton for each one. We pass "pokemonName" to our component, because the first retrieval only provides us with the Pokémon's name. We need to perform another search to get the actual data of the Pokémon (name, types, power, etc.).

Finally, our Pokemon component will perform a specific retrieval by the name of the Pokémon and display the data:

 // Dependencies
import { FC } from 'react'
import useSWR from 'swr'

// Styles
import { StyledCard, StyledTypes, StyledType, StyledHeader } from './Pokemon.styled'

type Props = {
  pokemonName: string
}

const Pokemon: FC<Props> = ({ pokemonName }) => {
  const { data, error } = 
    useSWR(`https://pokeapi.co/api/v2/pokemon/${pokemonName}`)

  // Do you remember the error we set on the fetcher?
  if (error || data.error) {
    return <div />
  }

  if (!data) {
    return <div>Loading...</div>
  }

  const { id, name, sprites, types } = data
  const pokemonTypes = types.map((pokemonType: any) => 
    pokemonType.type.name)

  return (
    <StyledCard pokemonType={pokemonTypes[0]}>
      <StyledHeader>
        <h2>{name}</h2>
        <div>#{id}</div>
      </StyledHeader>
      
      <img alt={name} src={sprites.front_default} />

      <StyledTypes>
        {pokemonTypes.map((pokemonType: string) => (
          <StyledType key={pokemonType}>{pokemonType}</StyledType>
        ))}
      </StyledTypes>
    </StyledCard>
  )
} 

export default Pokemon

In this component, we are gathering all the data for each Pokemon, including:

  • id
  • name
  • sprites
  • types

As you can see, I'm using styled components which are very convenient. If you would like to see the styles I'm using for Pokedex, you can refer to the file Pokemon.styled.ts:

 import styled from 'styled-components'

// Type colors
const type: any = {
  bug: '#2ADAB1',
  dark: '#636363',
  dragon: '#E9B057',
  electric: '#ffeb5b',
  fairy: '#ffdbdb',
  fighting: '#90a4b5',
  fire: '#F7786B',
  flying: '#E8DCB3',
  ghost: '#755097',
  grass: '#2ADAB1',
  ground: '#dbd3a2',
  ice: '#C8DDEA',
  normal: '#ccc',
  poison: '#cc89ff',
  psychic: '#705548',
  rock: '#b7b7b7',
  steel: '#999',
  water: '#58ABF6'
}

export const StyledPokedex = styled.div`
  display: flex;
  flex-wrap: wrap;
  flex-flow: row wrap;
  margin: 0 auto;
  width: 90%;

  &::after {
    content: '';
    flex: auto;
  }
`

type Props = {
  pokemonType: string
} 

export const StyledCard = styled.div<Props>`
  position: relative;
  ${({ pokemonType }) => `
    background: ${type[pokemonType]} url(./pokeball.png) no-repeat;
    background-size: 65%;
    background-position: center;
  `}
  color: #000;
  font-size: 13px;
  border-radius: 20px;
  margin: 5px;
  width: 200px;

  img {
    margin-left: auto;
    margin-right: auto;
    display: block;
  }
`

export const StyledTypes = styled.div`
  display: flex;
  margin-left: 6px;
  margin-bottom: 8px;
`

export const StyledType = styled.span`
  display: inline-block;
  background-color: black;
  border-radius: 20px;
  font-weight: bold;
  padding: 6px;
  color: white;
  margin-right: 3px;
  opacity: 0.4;
  text-transform: capitalize;
`

export const StyledHeader = styled.div`
  display: flex;
  justify-content: space-between;
  width: 90%;

  h2 {
    margin-left: 10px;
    margin-top: 5px;
    color: white;
    text-transform: capitalize;
  }

  div {
    color: white;
    font-size: 20px;
    font-weight: bold;
    margin-top: 5px;
  }
`

export const StyledTitle = styled.h1`
  text-align: center;
`

export const StyledGrid = styled.div`
  display: flex;
  flex-wrap: wrap;
  flex-flow: row wrap;
  div {
    margin-right: 5px;
    margin-bottom: 5px;
  }
`

Finally, our LoadingSkeleton component should look like this:

 import { FC } from 'react'
import Skeleton from 'react-loading-skeleton'

const LoadingSkeleton: FC = () => (
  <div>
    <Skeleton height={200} width={200} />
  </div>
)

export default LoadingSkeleton

This library is incredible. It allows you to create placeholders that are displayed while waiting for data. You can create as many shapes as you want. You have probably already seen this effect on sites like LinkedIn or YouTube.

Let's take a look at the result:

Skeleton loading animation

Next, you will see the Pokemon fallbacks rendered by SkeletonLoading:

Pokemon fallbacks

And then finally:

Rendered Pokemon

Pretty cool, right? But there's something else to mention: as previously stated, SWR first retrieves data from the cache, then constantly revalidates it to check for updates. This means that every time the data changes, SWR performs a new fetch to confirm whether the old data is still valid or needs to be replaced. You can observe this effect by switching from the Pokedex tab to another one, and then back.

Currently, React Suspense doesn't have a defined usage pattern, which means you can find different ways to use it and there are no established best practices yet. I found that SWR is the simplest and most understandable way to work with React Suspense, and I think it's a very powerful library that can be used even without Suspense.

Reconciliation

Most of the time, React is fast enough by default and you don't need to perform additional optimizations to improve your application's performance. React uses different techniques to optimize the display of components on the screen.

Whenever React needs to display a component, it calls its render method as well as those of its children recursively, and then creates a tree of elements in the DOM. When the component's state changes, these methods are called again on the nodes, and React compares the result with the previous tree of elements. The library is smart enough to determine the minimum number of operations required to apply the expected changes to the screen. This process is called reconciliation and is handled transparently by React.

React tries to apply the smallest possible number of operations on the DOM, as it is a costly operation. However, comparing two trees of elements is not free either, and React makes two assumptions to reduce its complexity:

  1. If two elements have a different type, they generate a different tree.
  2. Developers can use keys to mark children as stable across different render calls.

The second point is interesting for developers, as it provides a tool to help React display our views more quickly. By default, React iterates over both lists of a DOM node's children at the same time and creates a mutation whenever there is a difference. Let's look at some examples. The conversion between the following two trees will work well when adding an element at the end of the children:

 <ul>
  <li>Carlos</li>
  <li>Javier</li>
</ul>

<ul>
  <li>Carlos</li>
  <li>Javier</li>
  <li>Emmanuel</li>
</ul>

We have two trees initially:

  1. Carlos
  2. Javier

Then, React will insert a new tree:

  1. Emmanuel

Inserting an element at the beginning produces lower performance if implemented naively. If we look at the example, it works very poorly when converting between these two trees:

 <ul>
  <li>Carlos</li>
  <li>Javier</li>
</ul>

<ul>
  <li>Emmanuel</li>
  <li>Carlos</li>
  <li>Javier</li>
</ul>

Each child will be modified by React, rather than realizing that it can preserve the structure of the sub-trees. This problem can be solved by using the "key" attribute supported by React. Let's take a look at this.

The children have keys, and React uses these keys to match the children between the previous tree and the new tree. Updating the tree can be optimized by adding a key to our previous example:

 <ul>
  <li key="2018">Carlos</li>
  <li key="2019">Javier</li>
</ul>

<ul>
  <li key="2017">Emmanuel</li>
  <li key="2018">Carlos</li>
  <li key="2019">Javier</li>
</ul>

React can now identify that:

  • 2017 is new
  • 2018 and 2019 have been moved and are not new

Finding a key is not complicated. The element you are rendering may already have a unique identifier. Thus, the key can come directly from your data:

 <li key={element.id}>{element.title}</li>

React is now able to recognize that:

  • the key 2017 is new
  • the keys 2018 and 2019 have been moved and are not new.

Finding a key is not difficult. The element you are rendering may already have a unique identifier. Thus, the key can come directly from your data.

Optimization Techniques

Using the development version of React is very useful for coding and debugging as it provides all the necessary information to resolve various issues. However, all the checks and warnings have a cost that we want to avoid in production. Therefore, the first optimization we should make to our applications is bundling, by setting the NODE_ENV environment variable to production mode. This can be easily achieved with webpack, using the following method:

 new webpack.DefinePlugin({ 
  'process.env': { 
    NODE_ENV: JSON.stringify('production')
  }
})

To get the best performance, we don't just want to create the bundle with the production indicator enabled, but we also want to split our bundles into two: one for our application and another for the node_modules.

 optimization: {
  splitChunks: {
    cacheGroups: {
      default: false,
      commons: {
        test: /node_modules/,
        name: 'vendor',
        chunks: 'all'
      }
    }
  }
}

Webpack 4 has two modes:

  • development
  • production

The production mode is enabled by default. Thus, the code is minified and compressed when compiling the bundles. To specify the production mode, you can use the following code block:

 {
  mode: process.env.NODE_ENV === 'production' ? 'production' : 
    'development',
}

With this webpack configuration, we will get highly optimized bundles, one for our vendors and one for the actual application.

Anti-Patterns

Performance also involves avoiding doing anything wrong!

1. Initializing State with Props

In this section, we will see that initializing state with props received from the parent is generally considered an anti-pattern. However, we could still decide to use it once we have clearly identified the issues associated with this approach.

One of the best ways to learn is to look at code. So let's start by creating a simple component containing a button that increments a counter.

 import { FC, useState } from 'react'

type Props = {
  count: number
}

const Counter: FC<Props> = (props) => {}

export default Counter

Now, let's define our state:

 const [state, setState] = useState<any>(props.count)

The implementation of the click handler is quite simple:

 const handleClick = () => {
  setState({ count: state.count + 1 })
}

Finally:

 return (
  <div>
    {state.count}
    <button onClick={handleClick}>+</button>
  </div>
)

Now let's render this component, passing "1" as a property:

 <Counter count={1} />

The code works as expected: each click on the + button increments the current value. But what's the problem?

There are two main errors:

  • We have a duplicated source of truth.
  • If the count property passed to the component changes, the state is not updated.

If we inspect the Counter element using the React DevTools:

 <Counter>
Props
  count: 1
State
  count: 1

This does not provide a reliable and up-to-date value to use in the component and display to the user.

Worse still, clicking on + once causes the values to diverge. An example of this divergence is shown in the following code:

 <Counter>
Props
  count: 1
State
  count: 2

At this point, we can assume that the second value represents the current number, but this is not explicit and can lead to unexpected behavior or incorrect values in the tree.

The second problem focuses on how the class is created and instantiated by React. The useState function of the component is only called once during the creation of the component. In our component, we read the value of the property and store it in the state. If the value of this property changes during the application lifecycle, the component will never use the new value because it has already been initialized. This puts the component in an inconsistent state, which is suboptimal and difficult to debug.

What if we really want to use the value of the prop to initialize the component, and we are sure that the value will not change in the future?

In this case, it is better to make it explicit and give the property a name that clarifies your intentions, such as initialCount. For example:

 type Props = {
  initialCount: number
}

const Counter: FC<Props> = (props) => {
  const [count, setState] = useState<any>(props.initialCount)
  ...
}

If we use it like this, it is clear that the parent has only one way to initialize the counter:

 <Counter initialCount={1} />

2. Using the index as a key

The key property uniquely identifies an element in the DOM, and React uses it to check if the element is new or needs to be updated when the component's properties or state change. Using keys is always a good idea, and if you don't, React issues a warning in the console (in development mode). However, it's not just about using a key; sometimes, the value we decide to use as the key can make a difference. Indeed, using the wrong key can give us unexpected behavior in some cases. In this section, we will see one of these cases.

 import { FC, useState } from 'react'

const List: FC = () => {

}

export default List

Our state;

 const [items, setItems] = useState(['foo', 'bar'])

The implementation of the click handler is slightly different from the previous one because in this case, we need to insert a new element at the top of the list:

 const handleClick = () => { 
  const newItems = items.slice()
  newItems.unshift('baz')

  setItems(newItems)
}

Finally:

 return ( 
  <div> 
    <ul> 
      {items.map((item, index) => ( 
        <li key={index}>{item}</li> 
      ))} 
    </ul> 

    <button onClick={handleClick}>+</button> 
  </div> 
) 

If you run the component in the browser, you won't encounter any problems; clicking the button inserts a new element at the top of the list. However, let's do an experiment. Let's modify the render function as follows, adding an input field next to each element. We then use an input field because we can modify its content, which makes it easier to understand the problem:

 return ( 
  <div> 
    <ul> 
      {items.map((item, index) => ( 
        <li key={index}> 
          {item} 
          <input type="text" /> 
        </li> 
      ))} 
    </ul> 
    <button onClick={handleClick}>+</button> 
  </div> 
)

If we run this component again in the browser, copy the values of the elements in the input fields, and then click +, we will get unexpected behavior. As shown in the following screenshot, the elements shift while the input fields stay in the same position, so their value no longer corresponds to the value of the elements:

Incorrect behavior due to using index as key

If we run the component, click +, and check the console, we should have all the answers we need. What we can see is that React, instead of inserting the new element at the top, swaps the text of the two existing elements and inserts the last element at the bottom as if it were new. The reason for this is that we are using the index of the map function as the key.

This is a very common pattern as one might think that providing any key is always the best solution, but this is not the case at all. The key should be unique and stable, identifying one and only one element. To solve this problem, we can, for example, use the value of the element if we expect it to not repeat in the list, or create a unique identifier.

3. Propagating Props on DOM Elements

There is a common practice that has recently been described as an anti-pattern by Dan Abramov; it also triggers a warning in the console when you use it in your React application. It is a technique widely used in the community, and I have personally seen it several times in real projects. We usually resort to the technique of propagating props on elements to avoid writing each one of them manually, as shown below:

 <Component {...props} />

This works great and is transpiled into the following code by Babel:

 _jsx(Component, props)

However, when we spread properties on a DOM element, we run the risk of adding unknown HTML attributes, which is a bad practice. The problem is not only related to the spread operator; passing non-standard properties one by one leads to the same problems and warnings. Since the spread operator masks the individual properties that we are spreading, it's even harder to understand what we're passing to the element.

To see the warning in the console, a basic operation we can do is to render the following component:

 const Spread = () => <div foo="bar" />

Console:

 Unknown prop `foo` on <div> tag. Remove this prop from the element

In this case, as we mentioned, it's easy to determine which attribute we're passing and remove it. However, if we use the spread operator, as in the following example, we can't control which properties are passed from the parent:

 const Spread = props => <div {...props} />;

If we use the component in the following way, there are no problems:

 <Spread className="foo" />

This, however, is not the case if we do something like the following. React complains because we are applying a non-standard attribute to the DOM element:

 <Spread foo="bar" className="baz" />

One solution we can use to solve this problem is to create a property called safeProps that explicitly contains the valid DOM properties that we can safely spread to the component. For example, we can modify the component as follows:

 const Spread = props => <div {...props.domProps} />

We can then use it as follows:

 <Spread foo="bar" domProps={{ className: 'baz' }} />

As we've seen many times with React, it's always good to be explicit.

Pierre Colart

Passionate developer and architect who wants to share their world and discoveries in order to make things simpler for everyone.

See profil

URLs

Check les divers liens pour cet article

Github - Le code se trouve ici

Latest posts

Sequences, Time Series and Prediction

© 2023 Switch case. Made with by Pierre Colart