Oct 2, 2020 · by Emiliano Javier González

Building a simple grid component with React & Redux

Design and performance considerations

React is a great library. It is very easy to learn how to use, but this process can become difficult if you come from other frameworks and move to a project that’s already running. You could come across class components, hooks, mapDispatch and mapState functions, action creators, reducers, and the like. Now, the pattern proposed by React is that all its components pass the dependencies they need to their children, this can become very taxing if you have a complex tree of components. That’s where Redux comes in. Redux allows us to keep the state of our application in one place, and modify it through encapsulated logic actions. 

Now, when should we use the state of a component and when should we save that state in the store? We will try to answer these questions by creating a dynamic list of files, and we will compare the renderings of the components using the profiler tool to understand a little better what happens in our components when we modify the state, either by modifying the component’s state, or by using Redux’s actions. Let’s start with a simple design that’ll help us to identify the components that we’re going to need:

We need to call our component with some sort of collection with column definitions in it, and other configuration values like API filter values. There are also two important sections on the component, the header and a body (in blue and red, respectively):

This might be the first important separation that requires a decision be made. We don’t want to re-render the header every time the body (which is going to hold a loading indicator at the very first render) re-renders over an action being dispatched. Also, for this example, headers do not have ordering capabilities, so there’s no need to connect those to the redux store. We can identify four main components here: FileList.js, FileListHeaders.js, FileListBody.js and, FIleListItem.js. 

Let’s start writing some code. In the following examples, the code is simple and uses HTML table instead of CSS tables or CSS flex boxes.

Writing a first version

import React from 'react';
import {connect} from 'react-redux';
import styled from 'styled-components';

const StyledFileList = styled.section`
padding: 15px;

width: 35%;
table {
width: -webkit-fill-available;
thead {
font-weight: bold;
}
td {
border: 1px solid grey;
padding: 5px 10px;
}
}
`;

// Fixed data simulating API response
const response = {
data: [
{fileId: 1, fileName: 'File1.xlsx', related: 151624},
{fileId: 2, fileName: 'File2.pdf', related: 151623}
]
};

// Configurable columns
const columns = [
{displayName: 'File name', source: 'fileName'},
{displayName: 'Related', source: 'related'}
];

export function FileList(props) {
// props destructuring
const {response} = props;

return (

{columns.map((column) => ({column.displayName}))}
{response.data.map((row) => ({columns.map((column) => ())}
))}

{row[column.source]}

);
}

const mapState = (state) => {
return {
response
};
};

export default connect(mapState)(FileList);

Adding a text filter

Now that we’ve got a first version running, let’s add a simple input for client text search:

...

<StyledFileList>
<input type="text" onChange={handleOnChange} />
<table>

// Since data is passed by the mapState function as a prop, we should clone this so we can
// alter it as we need, we can use the spread operator:
const [data, setData] = useState([...response.data]);

const handleOnChange = (event) => {
const value = event.target.value;

setData(
// We should use the whole data set to calculate new filter results
response.data.filter(
(file) => file.fileName.toLowerCase().search(value.toLowerCase()) !== -1
)
);
};

...

Everything is going well. But, what is happening with react re-rendering the components exactly? Let’s check the react’s plugin profiler:

Separating components–and responsibilities

This is a little difficult to decipher. What parts of the component are consuming what time, exactly? Let’s create the FileListBody component:

import React from 'react';

export const FileListBody = (props) => {
const {data, columns} = props;

return data.map((row) => (

{columns.map((column) => (

{row[column.source]}

))}

));
};

export default FileListBody;

FileList.js
...
<tbody>
<FileListBody data={data} columns={columns} />
</tbody>
...

And check the profiler again:

Now we see the separation clearly, but we are re-rendering everything with this distribution. This is not a problem right now, but when this begins to scale there will be many expensive re-renders if this is not under control. 

At this point we also realize we don’t know where FileListHeader is. It is necessary to separate that in a different component, so let’s do that and check the profiler again.

import React from 'react';

function FileListHeader(props) {
const {columns} = props;

return (
<thead>
{columns.map((column) => (
<td>{column.displayName}</td>
))}
</thead>
);
}

export default FileListHeader;

Now we can track that rendering in an easier manner. But there’s still that re-rendering issue. The most probable scenario is that text change (and other filters) events come from updating the parent component’s state, so what if we “memorize” the header for future renders?

useMemo

One of the tools we need to control re-renders is useMemo hook (or shouldComponentUpdate method for class components), so the FileListHeader depends only on the columns prop:

...

const thead = useMemo(() => <FileListHeader columns={columns} />, [columns]);

return (
<StyledFileList>
<input type="text" onChange={handleOnChange} />
<table>
{thead}
<tbody>

...

Now we have no header re-render. We should add the FileListItem component because we know that, in the long run, every row is going to have some render logic and its own lifecycle:

FileListBody.js
...
return data.map((row) => <FileListItem columns={columns} row={row} />);
...

And this is the profiler for this distribution when input search text changes:

Now, whenever data changes (for whatever reason), FileList component is getting re-rendered and FileListHeader is memoized, but FileList will handle more complex logic as this evolves. So, changes in the response object should be handled by the body instead. Let’s add the corresponding actions and reducers for getting the data from our data source and modify our component responsibility distribution.

Normalizing state shape

We’re going to create two actions for this example, ‘files/getAll/ and ‘files/update.’ The getAll action will be handled by the FileList (and by the FileListBody, particularly) component, so client components don’t have to know about data source implementations. Also, let’s structure the store based on state shape guidelines from the redux documentation, we will see why this works better later:

fileactions.js
export function getAll() {
return {
type: 'files/getAll'
};
}

filereducer.js
const initialState = {};

// Fixed data
const response = {
data: [
{fileId: 1, fileName: 'File1.xlsx', related: 151624},
{fileId: 2, fileName: 'File2.pdf', related: 151623},
{fileId: 3, fileName: 'Some other File3.docx', related: 151625}
]
};

export function filereducer(state = initialState, action) {
switch (action.type) {
case 'files/getAll': {
return customNormalize(response, 'fileId');
}
default:
return state;
}
}

// Our custom normalize logic
function customNormalize(data, idField = 'id') {
// Result object initialization
let result = {byId: {}, allIds: []};
if (data.length > 0) {
data.forEach((item) => {
const id = item[idField];

// Setting the byId and allIds fields accordingly
result.byId[id] = item;
allIds.push(id);
});
}
return result;
}

Since we’ve moved all data logic into the body, we need to tweak our FileList and FileListBody code to avoid breaking the filter functionality:

FileList.js
...

const [filter, setFilter] = useState();

const handleOnChange = (event) => {
setFilter(event.target.value);
};

const searchString = <input type="text" onChange={handleOnChange} />;

const thead = useMemo(() => <FileListHeader columns={columns} />, [columns]);

return (

...

FileListBody.js
import React, {useEffect} from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import FileListItem from './FileListItem';
import * as FileActions from './fileactions';

export const FileListBody = (props) => {
const {files, columns, data, filter} = props;

let results = data.allIds;
if (data && filter) {
results = data.allIds.filter((id) =>
data.byId[id].fileName.toLowerCase().startsWith(filter)
);
}

useEffect(() => {
// Simulate an API call and dispatch an action to update the store
setTimeout(function() {
files.getAll();
}, 5000);
}, []);

// Let's also put a loading indicator for whenever we don't have a defined response
return !results
? 'Loading...'
: results.length > 0
? results.map((id) => (
<FileListItem
key={`file-${id}`}
columns={columns}
row={data.byId[id]}
/>
))
: 'No data!';
};

const mapDispatch = (dispatch) => {
return {
files: bindActionCreators(Object.assign({}, FileActions), dispatch)
};
};

const mapState = (state) => {
return {
data: state.files
};
};

export default connect(mapState, mapDispatch)(FileListBody);

And what about the renders when the filter changes?

Pretty much the same. If the FileList component is going to support 30 or 40 columns, rendering might become expensive, and it is going to run on every keydown event. What if we use a redux action instead?

fileactions.js
...
export function filter(searchString) {
return {
type: 'files/filter',
searchString
};
}
...

filereducer.js

...
case 'files/filter': {
let result = {...state};

// Yeah I'm bad at naming variables =)
if (!action.searchString && result.results) {
delete result.results;
} else {
result.results = state.allIds.filter((id) =>
state.byId[id].fileName.toLowerCase().startsWith(action.searchString)
);
}

return result;
}
...

FileList.js
import React, {useMemo, useState} from 'react';
import styled from 'styled-components';
import FileListBody from './FileListBody';
import FileListHeader from './FileListHeader';
import {filter} from './fileactions';
import {connect} from 'react-redux';

const StyledFileList = styled.section`
padding: 15px;

width: 35%;
table {
width: -webkit-fill-available;
thead {
font-weight: bold;
}
td {
border: 1px solid grey;
padding: 5px 10px;
}
}
`;

// Configurable columns
const columns = [
{displayName: 'File name', source: 'fileName'},
{displayName: 'Related', source: 'related'}
];

export function FileList(props) {
const {setFilter} = props;

const handleOnChange = (event) => {
setFilter(event.target.value);
};

const searchString = <input type="text" onChange={handleOnChange}
/>;

const thead = useMemo(() => <FileListHeader columns={columns} />, [columns]);

return (
<StyledFileList>
{searchString}
<table>
{thead}
<tbody>
<FileListBody columns={columns} />
</tbody>
</table>
</StyledFileList>
);
}

const mapDispatch = (dispatch) => {
return {
setFilter: (searchString) => dispatch(filter(searchString))
};
};

export default connect(undefined, mapDispatch)(FileList);

FileListBody.js
import React, {useEffect} from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import FileListItem from './FileListItem';
import * as FileActions from './fileactions';

export const FileListBody = (props) => {
const {files, columns, data} = props;

const results = data.results || data.allIds;
useEffect(() => {
// Simulate an API call and dispatch an action to update the store
setTimeout(function() {
files.getAll();
}, 5000);
}, []);

// Let's also put a loading indicator for whenever we don't have a defined response
return !results
? 'Loading...'
: results.length > 0
? results.map((id) => (
<FileListItem
key={`file-${id}`}
columns={columns}
row={data.byId[id]}
/>
))
: 'No data!';
};

const mapDispatch = (dispatch) => {
return {
files: bindActionCreators(Object.assign({}, FileActions), dispatch)
};
};

const mapState = (state) => {
return {
data: state.files
};
};

export default connect(mapState, mapDispatch)(FileListBody);

Finally, we’ve avoided the FileList component re-render!

Tips & tricks

Component distribution, state management, and redux’s store design have game-changing results on how your app deals with re-renders and performance. Keep in mind the following tips:

  1. Passing in set state callbacks to children and executing them force parent re-render as well, and this might not be the most effective way to do it, some times an action dispatch is just more efficient.
  2. But be careful when connecting a component to the store, keep in mind that all connected components might have a re-render cycle if references to dependencies change.
  3. Avoid using expensive calculations in the mapState functions, since, as the application scales, this can become a serious issue; all mapState functions are executed with every dispatch. 
  4. User experience starts to decline when clicks or other interface events fire the re-render cycle and it takes a full second to run, always dedicate some time to check on the profiler when writing reusable or heavy-used components.
  5. Keep in mind the redux’s official docs state shape recommendations, as they are extremely useful when updating state to one row, or when filtering. 
  6. It may be a good idea to use a normalization library or write your own generic normalization code based on your needs.

 

Sources

React:
Docs:
https://reactjs.org/docs/getting-started.html

Hooks API reference:
https://reactjs.org/docs/hooks-reference.html

Profiler:
https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html

Redux docs:
https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape

Normalizr:
https://github.com/paularmstrong/normalizr

 

Emiliano Javier González

Emiliano Javier González

Senior Software Engineer
Emiliano Javier González

Latest posts by Emiliano Javier González

Share This Article

Post A Comment