Skip to content

Instantly share code, notes, and snippets.

@jerrylow
Created December 20, 2017 18:25
Show Gist options
  • Save jerrylow/41dc58e78bb76e06c22a8fd7f123fdfd to your computer and use it in GitHub Desktop.
Save jerrylow/41dc58e78bb76e06c22a8fd7f123fdfd to your computer and use it in GitHub Desktop.
React: Large Dataset Filtering Performance

React: Large Dataset Filtering Performance

A test React application to test and improve performance filtering/re-rendering large datasets. Start with 5,000 but easily modifiable to see where the bottle neck is.

A Pen by Jerry on CodePen.

License.

const DATA_ENTRIES = 5000;
const { createStore, combineReducers } = Redux;
const { Provider, connect } = ReactRedux;
const entryAttr = {
name: [
"Han Solo",
"Darth Vader",
"Luke Skywalker",
"Princess Leia",
"Chewie",
"Lando Calrissian",
"Boba Fett",
"Yoda",
"R2D2",
"Kylo Ren",
"Rey",
"Fin",
"Poe Dameron",
"Obi-Wan Kenobi"
],
powers: ["Push", "Pull", "Mind Tricks", "Lighning", "Choke", "Drain"],
color: [
"Red",
"Blue",
"Green",
"Purple",
"Yellow",
"Orange",
"White",
"Black"
],
fortune: [100, 200, 300, 400, 500, 600],
home: [
"Crait",
"Tatooine",
"Hoth",
"Endor",
"Jakku",
"Death Star",
"Cloud City"
],
transportation: [
"A-Wing",
"X-Wing",
"Y-Wing",
"Tie Fighter",
"Star Destroyer",
"Millenium Falcon"
]
};
/**
* Actions
**/
const addFilter = (name, value) => {
return {
type: "ADD_FILTER",
name,
value
};
};
const removeFilter = (name, value) => {
return {
type: "REMOVE_FILTER",
name,
value
};
};
const clearFilters = () => {
return {
type: "CLEAR_FILTERS"
};
};
const setContent = entries => {
return {
type: "SET_CONTENT",
entries
};
};
/**
* Reducers
**/
const filters = (state = { filters: {} }, action) => {
switch (action.type) {
case "ADD_FILTER":
let currentAddFilter =
state.filters[action.name] && state.filters[action.name].length
? state.filters[action.name]
: [];
currentAddFilter.push(action.value);
const newAddState = Object.assign({}, state.filters, {
[action.name]: currentAddFilter
});
return Object.assign({}, state, { filters: newAddState });
case "REMOVE_FILTER":
let currentRemoveFilter =
state.filters[action.name] && state.filters[action.name].length
? state.filters[action.name]
: [];
currentRemoveFilter = _.pull(currentRemoveFilter, action.value);
const newRemoveState = Object.assign({}, state.filters, {
[action.name]: currentRemoveFilter
});
return Object.assign({}, state, { filters: newRemoveState });
case "CLEAR_FILTERS":
return Object.assign({}, state, { filters: {} });
default:
return state;
}
};
const content = (state = { entries: [] }, action) => {
switch (action.type) {
case "SET_CONTENT":
return Object.assign({}, state, { entries: action.entries });
default:
return state;
}
};
const reducers = combineReducers({
filters,
content
});
/**
* Components - Filters
**/
const Filters = class Filters extends React.Component {
updateFilter(name, e) {
if (e.target.checked) {
this.props.addFilter(name, e.target.value);
} else {
this.props.removeFilter(name, e.target.value);
}
}
render() {
return (
<div className="filters">
<header className="filters__header">
<h2 className="filters__header__title">Filters</h2>
</header>
<div className="filters__inner">
{Object.keys(entryAttr)
.filter(attr => attr !== "name")
.map((attr, i) => {
return (
<div key={`filter-${i}`} className="filters__filter">
<h3 className="filters__filter__title">{attr}</h3>
<ul>
{entryAttr[attr].map((attrValue, attrValueKey) => {
const inputId = _.snakeCase(attr + attrValue);
const checked = this.props.filters[attr]
? this.props.filters[attr].includes(attrValue)
: false;
return (
<li key={`${attrValue}-${attrValueKey}`}>
<input
type="checkbox"
id={inputId}
value={attrValue}
name={attrValue}
checked={checked}
onChange={e => {
this.updateFilter(attr, e);
}}
/>
<label htmlFor={inputId}>{attrValue}</label>
</li>
);
})}
</ul>
</div>
);
})}
</div>
</div>
);
}
};
const FiltersMapStateToProps = state => {
return {
filters: state.filters.filters
};
};
const FiltersMapDispatchToProps = dispatch => {
return {
addFilter: (name, value) => {
dispatch(addFilter(name, value));
},
removeFilter: (name, value) => {
dispatch(removeFilter(name, value));
}
};
};
const FiltersContainer = connect(
FiltersMapStateToProps,
FiltersMapDispatchToProps
)(Filters);
/**
* Components - Filter Bubbles
**/
const FilterBubbles = class FilterBubbles extends React.Component {
render() {
const allFilters = Object.keys(
this.props.filters
).reduce((filters, filterKey) => {
if (this.props.filters[filterKey]) {
this.props.filters[filterKey].forEach(filter => {
filters.push({
name: filterKey,
value: filter
});
});
}
return filters;
}, []);
return (
<ul className="filter-bubbles">
{allFilters.map(filter => {
return (
<li>
<span>{filter.name}: </span>
{filter.value}
<button
type="button"
onClick={e => {
this.props.removeFilter(filter.name, filter.value);
}}
>
+
</button>
</li>
);
})}
{!!allFilters.length && (
<li className="filters-bubbles__clear-all">
<button type="button" onClick={this.props.clearFilters}>
Clear All
</button>
</li>
)}
</ul>
);
}
};
const FilterBubbleMapStateToProps = state => {
return {
filters: state.filters.filters
};
};
const FilterBubbleMapDispatchToProps = dispatch => {
return {
removeFilter: (name, value) => {
dispatch(removeFilter(name, value));
},
clearFilters: () => {
dispatch(clearFilters());
}
};
};
const FilterBubblesContainer = connect(
FilterBubbleMapStateToProps,
FilterBubbleMapDispatchToProps
)(FilterBubbles);
/**
* Components - Body
**/
const Body = class Body extends React.Component {
filterEntries() {
if (_.isEmpty(this.props.filters)) {
return this.props.entries;
}
return this.props.entries.filter(entry => {
let matchFilters = true;
Object.keys(this.props.filters).forEach(filterKey => {
if (this.props.filters[filterKey].length) {
matchFilters = this.props.filters[filterKey].includes(
entry[filterKey]
)
? matchFilters
: false;
}
});
return matchFilters;
});
}
render() {
const entries = this.filterEntries();
return (
<section className="body">
<header className="body__header">
<h2 className="body__header__title">Entries ({entries.length})</h2>
</header>
<FilterBubblesContainer />
<table>
<thead>
<tr>
{Object.keys(entryAttr).map((attr, i) => (
<th key={`heading-${i}`}>{attr}</th>
))}
</tr>
</thead>
<tbody>
{entries.map(entry => {
return (
<tr key={`row-${entry.id}`}>
{Object.keys(entryAttr).map((attr, k) => (
<td key={`col-${entry.id}-${k}`}>{entry[attr]}</td>
))}
</tr>
);
})}
</tbody>
</table>
</section>
);
}
};
const BodyMapStateToProp = state => {
return {
filters: state.filters.filters,
entries: state.content.entries
};
};
const BodyContainer = connect(BodyMapStateToProp)(Body);
/**
* Components - App
**/
const App = class App extends React.Component {
componentWillMount() {
this.props.setContent(this.getEntries());
}
getEntries() {
const entries = [];
for (let i = 0; i < DATA_ENTRIES; i++) {
const entry = { id: i };
Object.keys(entryAttr).forEach(attr => {
entry[attr] =
entryAttr[attr][Math.floor(Math.random() * entryAttr[attr].length)];
});
entries.push(entry);
}
return entries;
}
render() {
return (
<main className="app">
<FiltersContainer />
<BodyContainer />
</main>
);
}
};
const AppMapStateToProps = state => {
return {
entries: state.content.entries
};
};
const AppMapDispatchToProps = dispatch => {
return {
setContent: entries => {
dispatch(setContent(entries));
}
};
};
const AppContainer = connect(AppMapStateToProps, AppMapDispatchToProps)(App);
const store = createStore(reducers);
ReactDOM.render(
<Provider store={store}>
<AppContainer />
</Provider>,
document.getElementById("content")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.7.2/redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.6/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
html,
body,
#content {
height: 100%;
}
html {
font-size: 62.5%;
}
body {
color: #333;
font-family: 'Roboto', sans-serif;
font-size: 1.6rem;
}
h1, h2, h3, h4 {
margin: 0 0 10px;
}
input[type="checkbox"] {
float: left;
opacity: 0.01;
position: absolute;
+ label {
display: inline-block;
line-height: 1.4;
margin: 0;
padding-left: 25px;
position: relative;
&::before {
background: no-repeat center center;
background-color: rgba(#74828d, 0.25);
border: 1px solid #74828d;
border-radius: 50%;
content: "";
cursor: pointer;
display: block;
height: 15px;
left: 0;
position: absolute;
top: 1px;
width: 15px;
}
}
&:checked + label {
color: #32a9e1;
&::before {
background-color: #32a9e1;
background-image: url(http://www.jerrylow.com/demo/icons/checkmark.svg);
background-size: 13px 10px;
border-color: #32a9e1;
}
}
&:focus + label {
&::before {
outline: auto 5px -webkit-focus-ring-color;
}
}
}
/**
* App
**/
.app {
background: #f7f7f7;
min-height: 100%;
}
.filters {
box-shadow: inset -3px 0 1px rgba(0,0,0,.4);
color: #74828d;
background: #253447;
max-height: 100%;
overflow: auto;
position: fixed;
width: 20%;
}
.filters__header {
background: #1c2939;
box-shadow: inset -3px 0 1px rgba(0,0,0,.4);
padding: 20px 40px;
}
.filters__header__title {
color: white;
font-size: 2rem;
font-weight: 500;
margin: 0;
}
.filters__inner {
padding: 20px 40px;
}
.filters__filter {
ul {
margin: 0 0 30px;
padding: 0;
li {
list-style: none;
margin: 0 0 10px;
}
}
}
.filters__filter__title {
border-bottom: 1px solid rgba(#999, .3);
color: #567893;
font-size: 1.2rem;
font-weight: 100;
margin-bottom: 15px;
padding-bottom: 10px;
text-transform: uppercase;
}
.filter-bubbles {
display: flex;
flex-wrap: wrap;
margin: 0 0 20px;
padding: 0;
&:empty {
margin-bottom: 0;
}
li {
background: #aad4ff;
border-radius: 20px;
color: #1c2939;
font-size: 1.2rem;
list-style: none;
margin: 0 10px 10px 0;
padding: 7px 10px;
span {
text-transform: capitalize;
}
button {
background: #469aef;
border: none;
border-radius: 50%;
color: white;
cursor: pointer;
margin: 0 0 0 5px;
padding: 5px 8px;
transform: rotate(45deg);
}
}
}
li.filters-bubbles__clear-all {
background: none;
padding-left: 0;
padding-right: 0;
button {
background: none;
border-radius: 0;
color: #333;
margin: 0;
padding-left: 0;
padding-right: 0;
transform: none;
}
}
.body {
margin-left: 20%;
padding: 20px 50px;
table {
border-collapse: collapse;
margin: 0 -10px;
width: calc(100% + 20px);
thead th {
border-bottom: 1px solid #ddd;
color: #2eacdf;
font-size: 1.2rem;
padding: 10px 10px;
text-align: left;
text-transform: uppercase;
}
tbody {
tr:nth-of-type(2n-2) td {
background: #ededed;
}
tr {
&:hover td {
background: #dcedff;
}
td {
padding: 12px 10px;
}
}
}
}
}
.body__header__title {
font-weight: 300;
margin: 0 0 20px;
}
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment