What's the Best React Grid Component?

Many web applications rely on data from external sources in order to deliver a good user experience. Knowing about the different data fetching techniques in React will enable you make informed decisions on the best use case for your application.

In this article, we will explore various techniques of fetching data in web applications built with React. We will focus on using Hooks introduced in React 16.8 to fetch data from the popular JSON Placeholder API to be displayed in a React application.

Getting Started

Open a terminal and run the command below to create a react application

1# using npm
2npx create-react-app project_name
3
4# using yarn
5yarn create react-app project_name

The command above bootstraps a React application using the create-react-app tool.

Navigate to the project directory

1cd project_name

Start the application

1# using npm
2npm start
3
4# using yarn
5yarn start

Fetch API

The Fetch API is a promise-based Web API that allows you to make network requests in web applications. It is very similar to XMLHttpRequest (XHR) but improves on it by making it easier to make asynchronous requests. It is also better at handling responses.

The fetch API is relatively easy to get started with. Below is an example of using the fetch API to get data:

1fetch('http://jsonplaceholder.typicode.com/posts')
2 .then(response => response.json())
3 .then(data => console.log(data))
4 .catch(error => console.log(error));

In the example above we are fetching data from JSON Placeholder API and outputting it to the console. The fetch takes the location of the resource as an argument and returns a promise as response. We use the response.json() function to convert the response received from the API into JSON. The response can also be converted into other formats as needed by your application. If the promise is rejected due to an error the catch block is executed.

To integrate the fetch API into the React application we created earlier navigate to the src directory and create a directory called components, this folder will hold all the React components. Create a Notes.js file in the components directory, this file is responsible for displaying the data gotten from an external API. Open the file and paste the code below into the file

1import React from 'react';
2const Notes = ({ data }) => {
3 return (
4 <div>
5 <ul>
6 {data && data.map((item, index) => <li key={index}>{item.title}</li>)}
7 </ul>
8 </div>
9 );
10};
11export default Notes;

Create another file called FetchDemo.js and paste the code below

1import React, { useState, useEffect } from 'react';
2import Notes from './Notes';
3const FetchDemo = () => {
4 const [notes, setNotes] = useState([]);
5 const [isLoading, setIsLoading] = useState(false);
6 const [isError, setIsError] = useState(false);
7 const fetchData = () => {
8 fetch('http://jsonplaceholder.typicode.com/posts')
9 .then((response) => response.json())
10 .then((data) => {
11 setIsLoading(false);
12 setNotes(data);
13 })
14 .catch((error) => {
15 setIsLoading(false);
16 setIsError(true);
17 console.log(error);
18 });
19 };
20 useEffect(() => {
21 fetchData();
22 }, []);
23 if (isLoading) {
24 return <div>Loading...</div>;
25 }
26 return (
27 <div>
28 <h1>Fetch Example</h1>
29 {notes && <Notes data={notes} />}
30 {isError && <div>Error fetching data.</div>}
31 </div>
32 );
33};
34export default FetchDemo;

In this component, we’ve created a fetchData function that is responsible for getting a list of notes from JSON Placeholder. The response gotten from the data is then stored in the notes state which is passed as props {notes && <Notes data={notes} />} to the Notes component we created earlier for displaying the notes. If there is an error encountered when fetching the data we modify the isError state and display an error message to the user {isError && <div>Error fetching data.</div>}. We also have a state isLoading which we use to display a loading message to the user while we wait for the response from the network request made.

The fetchData function is called in the useEffect hook which runs once when the component is mounted (for more on useEffects check here).

Go to the App.js file and replace the existing code with the code below

1import React from 'react';
2import FetchDemo from './components/FetchDemo';
3function App() {
4 return (
5 <div className="App">
6 <h1>Posts</h1>
7 <FetchDemo />
8 </div>
9 );
10}
11export default App;

What we’ve done here is to import the FetchDemo component we created and render it in the App component. Save the file and see the results in your browser, the results should be similar to the screenshot below:

null

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

Axios

Axios is a promise-based lightweight HTTP client for the browser and Node.js. It is similar to the Fetch API as it is also used in making network requests. Unlike fetch, it transforms all responses to JSON.

Below is an example of using the fetch API to get data

1axios.get('http://jsonplaceholder.typicode.com/posts')
2 .then(response => console.log(response))
3 .catch(error => console.log(error));

This example is very similar to fetch as Axios is also promise-based. The major difference as mentioned earlier is we don’t have to convert the responses to JSON as Axios does that automatically. Some other features of Axios include:

  • Intercept request and response
  • Transform request and response data
  • Cancel requests
  • Client-side support for protecting against XSRF

To integrate Axios into a React application open a terminal in the applications root directory and run the code below

1# using npm
2npm install axios
3
4# using yarn
5yarn add axios

The above commands installs Axios into a React application.

Create a file called AxiosDemo.js in the components directory and paste the code below

1import React, { useState, useEffect } from 'react';
2import axios from 'axios';
3import Notes from './Notes';
4const AxiosDemo = () => {
5 const [notes, setNotes] = useState([]);
6 const [isLoading, setIsLoading] = useState(false);
7 const [isError, setIsError] = useState(false);
8 const fetchData = () => {
9 axios
10 .get('http://jsonplaceholder.typicode.com/posts')
11 .then((response) => {
12 setIsLoading(false);
13 setNotes(response.data);
14 })
15 .catch((error) => {
16 setIsLoading(false);
17 setIsError(true);
18 console.log(error);
19 });
20 };
21 useEffect(() => {
22 fetchData();
23 }, []);
24 if (isLoading) {
25 return <div>Loading...</div>;
26 }
27 return (
28 <div>
29 <h1>Using Axios</h1>
30 {notes && <Notes data={notes} />}
31 {isError && <div>Error fetching data.</div>}
32 </div>
33 );
34};
35export default AxiosDemo;

The code above shows how to fetch data using Axios. The first step was importing the Axios library we installed import axios from 'axios' the Axios library is then utilized in the fetchData function where we make a request to get data from the JSON Placeholder library. We store the response gotten in the notes state and if we encounter any error, we render an appropriate error message to the user.

Go to the App.js file and replace the existing code with the code below

1import React from 'react';
2import FetchDemo from './components/FetchDemo';
3import AxiosDemo from './components/AxiosDemo';
4function App() {
5 return (
6 <div className="App">
7 <AxiosDemo />
8 </div>
9 );
10}
11export default App;

In the App component we’ve rendered the AxiosDemo component. Save and open the application in a browser

null

Fetching data with Async/Await

“async/await” makes asynchronous programming easier in JavaScript. It provides a way of writing promises cleanly and concisely and takes away all .then() blocks. There are two parts to “async/await”:

  • async: The async is a keyword placed before a function declaration which makes the function return a promise. For example: async function hello() { return “hello”; } hello().then(console.log) //hello
  • await: The await keyword is used in an async function and makes the function wait for a promise to be resolved. let value = await promise;

“async/await” is less of a data fetching technique and more of a better way of using the existing data techniques mentioned earlier. We can refactor the existing Axios network call using promises in the previous section from the code below

1axios
2 .get('http://jsonplaceholder.typicode.com/posts')
3 .then((response) => {
4 setIsLoading(false);
5 setNotes(response.data);
6 })
7 .catch((error) => {
8 setIsLoading(false);
9 setIsError(true);
10 console.log(error);
11 });

to:

1try {
2 const response = await axios.get(
3 'http://jsonplaceholder.typicode.com/posts'
4 );
5 setIsLoading(false);
6 setNotes(response.data);
7} catch (error) {
8 setIsLoading(false);
9 setIsError(true);
10 console.log(error);
11}

This makes the code less complex and easier to read.

Custom Data Fetching Hooks

According to the React docs:

A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks

Custom hooks allow you to extract component logic into reusable functions that can then be used in different components. For example, if we have to fetch data across different components instead of having the data fetching logic in each of these components we can abstract that logic into a custom hook which can then be reused across the different components.

We will be creating a useAxios hook that takes away the data fetching logic from the AxiosDemo component we created earlier. Navigate to the src directory and create a hooks directory. Now go to the hooks directory and create a file called useAxios.js. Open the file and paste the code below:

1import { useState, useEffect } from 'react';
2import axios from 'axios';
3
4const useAxios = (url) => {
5 const [data, setData] = useState([]);
6 const [isLoading, setIsLoading] = useState(false);
7 const [isError, setIsError] = useState(false);
8 const fetchData = async () => {
9 try {
10 const response = await axios.get(url);
11 setIsLoading(false);
12 setData(response.data);
13 } catch (error) {
14 setIsLoading(false);
15 setIsError(true);
16 console.log(error);
17 }
18 };
19 useEffect(() => {
20 fetchData();
21 }, []);
22 return { isLoading, isError, data };
23};
24export default useAxios;

In the code above we’ve extracted the states and data fetching logic needed from the AxiosDemo component, we’ve also added a parameter for url. What this means is we can use this hook in any component that needs to fetch data from anywhere not necessarily JSON Placeholder and return a response back to the component that needs the data.

Now let us refactor our AxiosDemo component to use the useAxios hook. Open AxiosDemo.js file and replace the existing code with the code below:

1import React from 'react';
2import useAxios from '../hooks/useAxios';
3import Notes from './Notes';
4
5const AxiosDemo = () => {
6 const { isLoading, isError, data: notes } = useAxios(
7 'http://jsonplaceholder.typicode.com/posts'
8 );
9 if (isLoading) {
10 return <div>Loading...</div>;
11 }
12 return (
13 <div>
14 <h1>Using Axios</h1>
15 {notes && <Notes data={notes} />}
16 {isError && <div>Error fetching data.</div>}
17 </div>
18 );
19};
20
21export default AxiosDemo;

We’ve imported the useAxios hook into the AxiosDemo component. We use the hook by passing the url argument:

1const { isLoading, isError, data: notes } = useAxios("http://jsonplaceholder.typicode.com/posts");

Once the data is fetched the hook returns an object which we have destructured to be used in the AxiosDemo component.

Concurrent Mode and Suspense

Concurrent mode and Suspense are experimental features introduced in React 16.6 to handle data fetching in React applications. To get started with concurrent mode, open a terminal in the root of your application and run the code below:

1# using npm
2npm install react@experimental react-dom@experimental
3
4# using yarn
5yarn add react@experimental react-dom@experimental

Go to index.js and replace the existing code with the code below:

1import ReactDOM from 'react-dom';
2
3// If you previously had:
4//
5// ReactDOM.render(<App />, document.getElementById('root'));
6//
7// You can opt into Concurrent Mode by writing:
8
9ReactDOM.unstable_createRoot(
10 document.getElementById('root')
11).render(<App />);

Concurrent mode is a set of new features that helps React apps stay responsive while waiting for the result of an operation, this includes Suspense which is a component that lets you specify a loading state while you wait for some code action to be done executing.

1// show a loading message while waiting for notes to be loaded
2const notes = fetchData()
3<Suspense fallback={<div>Loading...</div>}>
4 <Notes data={notes} />
5</Suspense>

In the example above we are displaying a loading message while waiting for the data from our API to be loaded. You might wonder how this differs from using a loading state as we’ve used in previous sections.

1if (isLoading) {
2 return <div>Loading...</div>;
3}

As an application gets bigger we might have to depend on different loading states when each component has to make a different asynchronous request which makes our code get messy. Since Suspense knows when our data is fetched it saves us a lot of time in boilerplate code and having to listen for loading state changes in our application.

To be clear Concurrent mode and Suspense is still experimental and shouldn’t be used in production-ready applications. It is also good to note that Suspense is not a data fetching mechanism but rather a way to delay the rendering of components while you wait for unavailable data. Dan Abramov gave a great demo on how Suspense works check it out here.

Conclusion

In this article we’ve looked at different data fetching techniques for React applications, while the approaches are similar it is left for you to choose the best approach to suit the use case of your application.

We have a few options for how we include jQuery and a few considerations:

Considerations

  1. jquery-ujs 13 monkey patches $.ajax to send the CSRF parameters. For more info on this, see: Rails Guides on CSRF 13. This is the key file that gets inserted: rails.js 5. This code uses the jQuery.ajaxPrefilter 4 from this line 6. If this file is not loaded in the Rails application.js, then the CSRF token is not added when when $.ajax (or it’s friends, like $.get) is called. This is part of the jquery-rails gem 21. Note, there is no separate gem for jquery-ujs. However, there’s a separate github repo, probably so that it can be used by npm, but I’m just guessing.
  2. We under NO CIRCUMSTANCES want to load 2 versions of jQuery. We want the version of jQuery used to be as transparent as possible. Is the reason obvious? Thus, it’s critical that we load jQuery from the gem, then we DO NOT load jquery in the webpack config.
  3. It turns out that there is a npm version of jquery-ujs 31!

#Options for jQuery Inclusion

A. jQuery loaded from Rails

Having jQuery included before loading the bundle from web, and specify jQuery as an “external” 43.

1. jquery-rails Gem

Use the jquery-rails gem 21 and be sure to specify what version of jQuery. This is somewhat obtuse, as it requires locking the jQuery version by the gem version. This is specified here 16.

2. jQuery from a CDN

Use the jquery-rails-cdn 10 gem. If we do that, we need to load it first in the Rails main application layout. This gem will use this version of jQuery:

jQuery version is automatically detected via jquery-rails

B. jquery-ujs and jquery from npm

Let’s load both jquery-ujs and jquery in our webpack configuration. The advantages to doing this:

  1. It’s clear what versions we’re using as they specified just like the other dependencies. This is the way we’re handling all other 3rd party JavaScript, so let’s be consistent.

  2. No chance of accidentally having a different version loaded from both Rails and Webpack.

  3. We simply need to expose jQuery as global so that other JavaScript or CoffeeScript code in the Rails project that’s not using the Webpack config can find jQuery and $. This is documented for Webpack 27:

  4. We need to expose the jquery-ujs part, through an addition to the entries so this gets loaded by webpack (PENDING EXAMPLE).

  5. You need to use the expose-loader.

npm install expose-loader --save

You can either do this when you require it:

require("expose?$!jquery");

or you can do this in your config:

loaders: [
    { test: /jquery\.js$/, loader: 'expose?$' },
    { test: /jquery\.js$/, loader: 'expose?jQuery' }
]

We will very shortly have this as an example here: https://github.com/shakacode/react-webpack-rails-tutorial/issues/51 92.

Questions

  1. How useful is the CDN for jQuery performance wise?

React version: 18.1.0

Summary
React seems to cause Chrome to leak core Blink (renderer) memory when a large number of divs are mounted. Significantly more memory is leaked than when an equivalent script is run using vanilla JS.

In the following reproduction, 100k divs are mounted at a time. We loop through 50 "pages", each with 100k divs. We then render a blank page (or multiple pages with a single div, to ensure that React isn't holding onto the prior tree).

React Version:
400 MB of total "Memory Footprint" is retained. Note that the JS Memory usage is only 40MB. (Also note that the browser's total memory usage peaks significantly higher while these massive pages are loaded.)
image
(Screenshot from Chrome's "Task Manager")

In another run, 300 MB of total "Memory Footprint" is retained. Note that the live JS Memory usage is only 15MB.
image

Pure JS Version:
160 MB of total "Memory Footprint" is retained. Note that the JS Memory usage is only 2MB.
image

The browser is thus retaining an extra ~200MB (~300MB / 400MB vs ~150 MB) of memory when React is used.

Context:
I've dug through the JS Memory Heap dumps for both versions, but can't find a clear explanation for why Chrome is holding onto more memory here.

The issue still occurs when les divs are mounted, but mounting more divs makes the pattern easier to see.

I believe that this issue is the root-cause of a real-world issue I've been having in another application. In the real application we see an even bigger leak of > 1GB. In the following screenshot 1.5GB of total "Memory Footprint" is retained, while only 400MB of JS Memory is used.
image

Hypothesis:
I'm in contact with a Chrome-team developer who ran a separate reproduction script for our real-world application, and they hypothesized that Chrome was allocating a large amount of styles here. However, the issue still occurs when all CSS is removed. Is it possible that React is causing extra styles to be attached to these divs somehow? (Not sure if this is a red-herring or not. This might be a separate issue with our application.)

Quote from someone on the Chrome team:

I ran it through chrome://tracing and it looks like there are a lot of Blink (renderer) objects being allocated on PartitionAlloc, which is the allocator used for non-GC'd objects within the renderer. These tend to be performance-sensitive classes like strings, vectors, DOM nodes, style data, etc. The memory dump lists the bulk of this memory as (not web_cache, font_caches, site_storage, or parkable_strings).
I observed several OOM crashes while doing this, and the traces uploaded to Google's internal crash database all had StyleInheritedVariables::Copy() which doesn't necessarily mean that there's a leak there, but suggests that the app is causing a lot of copies of this object to be made. This object is allocated on PartitionAlloc, so that's a bit suspicious. I believe this is also not reflected in the JS heap size, so that's consistent with your observations. This object is used for the CSS Properties and Values API, in particular for storing custom properties that allow inheritance. Below is the relevant portion of the call stack. You can look up the code on source.chromium.org if you want. I took a quick look but all I can tell is that it probably has something to do with setting, changing, inheriting, or registering a CSS variable. I don't know if merely invoking var() causes the copy to be made.
So as a first step, I'd remove any uses of inherited CSS custom properties and see if that changes anything.

Steps To Reproduce

Note that you must give time for Chrome's GC to kick-in after the test stops. The test will take a few minutes to run.

Warning: These load heavy pages

React:


function App() {
  const [i, setI] = useState(0);

  // Re-render this component every 100ms
  useEffect(() => {
    setInterval(() => {
      setI((curI) => (curI += 1));
    }, 100);
  }, []);

  // 50 times, render a large list of 100k divs
  // We don't construct this large array on the initial run so that our `useEffect` closure 
  // doesn't accidentally include a large `c` array.
  let c = [];
  if (i > 2 && i < 50) {
    for (let j = 0; j < 100 * 1000; j++) {
      c.push(
        <div>
          {j} - {Math.random()}
        </div>
      );
    }
  }

  return (
    <div className="App">
      Memory Test - {i}
      <br />
      {c}
    </div>
  );
}

Pure JS:

<title>100k Divs Vertically, then Clear</title>

<div id="root"></div>

<script>
  const root = document.getElementById("root");

  let i = 0;
  const int = setInterval(() => {
    console.log(i);

    for (const child of [...root.childNodes].reverse()) {
      child.remove();
    }

    for (let j = 0; j < 100 * 1000; j += 1) {
      const d = document.createElement("div");
      d.innerText = "j" + j + "-" + Math.random();
      root.appendChild(d);
    }

    i += 1;

    if (i == 100) {
      clearInterval(int);
      root.remove();
    }
  }, 100);
</script>

 

This happens for any local React App, but I've provided a small CRA in case any automation is done.

  1. Visit the locally run app on Firefox Browser. In my case, v101.0.1 (64-bit)
  2. Set up open in editor URL in settings, set up open in Editor URL. I used "vscode://file/{path}"
  3. On the Components pane, select any element in the page
  4. Click on "Open in Editor" button

Actual result:
Nothing happens. The link does not open in my editor

Desired result:
The link should open in my VSCode editor

I think the reason why this is happening is because of a Firefox bug where window.open does not work in extensions. It is used in InspectedElement.js to open the source url. According to https://bugzilla.mozilla.org/show_bug.cgi?id=1282021, the extension should use browser.windows.create for Firefox