Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance Optimizations (issue #61) #74

Merged
merged 13 commits into from
Jun 9, 2019
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
"react-redux": "^7.1.0-rc.1",
"react-router-dom": "^4.2.2",
"react-scripts": "3.0.1",
"react-virtualized-auto-sizer": "^1.0.2",
"react-waypoint": "^8.0.1",
"react-window": "^1.8.2",
"redux": "^4.0.0",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.2.0",
Expand Down
41 changes: 41 additions & 0 deletions src/components/Filters/DynamicFilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// @flow
import React from "react";
import { selectFilterTypeAtIndex } from "redux-ducks/filters";
import { useSelector } from "react-redux";
import FilterTextField from "components/Filters/FilterTextField";
import FilterSelect from "components/Filters/FilterSelect";
import FilterSort from "components/Filters/FilterSort";
import FilterTristate from "components/Filters/FilterTristate";
import FilterGroup from "components/Filters/FilterGroup";

type Props = { index: number };

const DynamicFilter = ({ index }: Props) => {
const type = useSelector(state => selectFilterTypeAtIndex(state, index));

if (["HEADER", "SEPARATOR", "CHECKBOX"].includes(type)) {
console.error(`Catalogue filters - ${type} is not implemented.`);
}

switch (type) {
case "TEXT":
return <FilterTextField index={index} key={index} />;

case "SELECT":
return <FilterSelect index={index} key={index} />;

case "SORT":
return <FilterSort index={index} key={index} />;

case "TRISTATE":
return <FilterTristate index={index} key={index} />;

case "GROUP":
return <FilterGroup index={index} key={index} />;

default:
return null;
}
};

export default DynamicFilter;
69 changes: 28 additions & 41 deletions src/components/Filters/DynamicSourceFilters.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,15 @@ import React, { useState } from "react";
import Button from "@material-ui/core/Button";
import Drawer from "@material-ui/core/Drawer";
import FormGroup from "@material-ui/core/FormGroup";
import type { FilterAnyType } from "types/filters";
import FilterActions from "components/Filters/FilterActions";
import { filterElements } from "components/Filters/filterUtils";
import { makeStyles } from "@material-ui/styles";
import { fetchCatalogue } from "redux-ducks/catalogue";
import {
selectCurrentFilters,
resetFilters,
updateLastUsedFilters,
updateCurrentFilters
} from "redux-ducks/filters";
import { useSelector, useDispatch } from "react-redux";
import { selectFiltersLength } from "redux-ducks/filters";
import { useSelector } from "react-redux";
import DynamicFilter from "components/Filters/DynamicFilter";
import times from "lodash/times";

// FIXME: Weird blue line when clicking the <FormGroup>

// FIXME: I think using cloneDeep here is getting really laggy.
// Even after switching to non-lodash, still laggy.
// May have to do actual object updates instead.

// Choosing to use a deep copy instead of the standard setState method
// It would be a huge pain to try updating an array of objects (and be less readable)
// https://stackoverflow.com/questions/29537299/react-how-do-i-update-state-item1-on-setstate-with-jsfiddle

const useStyles = makeStyles({
openButton: {
marginBottom: 24,
Expand All @@ -46,30 +32,31 @@ const useStyles = makeStyles({

const DynamicSourceFilters = () => {
const classes = useStyles();
const dispatch = useDispatch();

const filters = useSelector(selectCurrentFilters);
const filtersLength = useSelector(selectFiltersLength);

const [drawerOpen, setDrawerOpen] = useState(false);

const handleResetFilters = () => {
dispatch(resetFilters());
};

const handleUpdateFilters = (newFilters: Array<FilterAnyType>) => {
dispatch(updateCurrentFilters(newFilters));
};

const handleSearchWithFilters = () => {
dispatch(updateLastUsedFilters()); // Must come before fetchCatalogue. This is a synchronous function.
dispatch(fetchCatalogue());
const handleSearchClick = () => {
setDrawerOpen(false);
};

if (!filtersLength) {
return (
<Button
disabled
variant="contained"
color="primary"
className={classes.openButton}
>
Filters
</Button>
);
}

return (
<React.Fragment>
<Button
disabled={!filters.length}
variant="contained"
color="primary"
onClick={() => setDrawerOpen(true)}
Expand All @@ -85,15 +72,15 @@ const DynamicSourceFilters = () => {
>
{/* without this div, FilterGroup components screw up, not sure why though */}
<div>
<FilterActions
onResetClick={handleResetFilters}
onSearchClick={handleSearchWithFilters}
/>
{filters.length && (
<FormGroup className={classes.filters}>
{filterElements(filters, handleUpdateFilters)}
</FormGroup>
)}
<FilterActions onSearchClick={handleSearchClick} />

<FormGroup className={classes.filters}>
{times(filtersLength).map((_, index) => (
// The order of filters is constant, so using index as the key is fine.
// eslint-disable-next-line react/no-array-index-key
<DynamicFilter index={index} key={index} />
))}
</FormGroup>
</div>
</Drawer>
</React.Fragment>
Expand Down
56 changes: 38 additions & 18 deletions src/components/Filters/FilterActions.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
// @flow
import React from "react";
import { withStyles } from "@material-ui/core/styles";
import React, { memo } from "react";
import { makeStyles } from "@material-ui/styles";
import Button from "@material-ui/core/Button";
import Divider from "@material-ui/core/Divider";
import { useDispatch } from "react-redux";
import { resetFilters, updateLastUsedFilters } from "redux-ducks/filters";
import { fetchCatalogue } from "redux-ducks/catalogue";

const styles = {
const useStyles = makeStyles({
// TODO: Position the controls div so that it's always at the top of the viewport
// I tried with position sticky and absolute, but it didn't work as intended
// Try again in the future
Expand All @@ -21,24 +24,41 @@ const styles = {
flexBasis: "40%"
}
}
};
});

type Props = {
classes: Object,
onResetClick: Function,
onSearchClick: Function
onSearchClick: Function // for any additional actions that fire
};

const FilterActions = ({ classes, onResetClick, onSearchClick }: Props) => (
<div className={classes.controls}>
<div className={classes.actionButtons}>
<Button onClick={onResetClick}>Reset</Button>
<Button variant="contained" color="primary" onClick={onSearchClick}>
Search
</Button>
const FilterActions = memo(({ onSearchClick }: Props) => {
const classes = useStyles();
const dispatch = useDispatch();

const handleResetClick = () => {
dispatch(resetFilters());
};

const handleSearchWithFiltersClick = () => {
dispatch(updateLastUsedFilters()); // Must come before fetchCatalogue. This is a synchronous function.
dispatch(fetchCatalogue());
onSearchClick();
};

return (
<div className={classes.controls}>
<div className={classes.actionButtons}>
<Button onClick={handleResetClick}>Reset</Button>
<Button
variant="contained"
color="primary"
onClick={handleSearchWithFiltersClick}
>
Search
</Button>
</div>
<Divider />
</div>
<Divider />
</div>
);
);
});

export default withStyles(styles)(FilterActions);
export default FilterActions;
65 changes: 38 additions & 27 deletions src/components/Filters/FilterGroup.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,52 @@
// @flow
import React from "react";
import React, { memo } from "react";
import ExpansionPanel from "@material-ui/core/ExpansionPanel";
import ExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails";
import ExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary";
import Typography from "@material-ui/core/Typography";
import Icon from "@material-ui/core/Icon";
import FormGroup from "@material-ui/core/FormGroup";
import type { FilterTristate as FilterTristateType } from "types/filters";
import FilterTristate from "components/Filters/FilterTristate";
import { useSelector, useDispatch } from "react-redux";
import { selectFilterAtIndex, updateFilterGroup } from "redux-ducks/filters";
import TristateCheckbox from "components/Filters/TristateCheckbox";

type Props = {
name: string,
state: Array<FilterTristateType>,
onChange: Function
};
// NOTE: This component is unoptimized. A single change will cause the entire list to rerender.
// The list of tristates this generates does tend to get quite long, but I think it's still
// reasonably performant. Not going to optimize this for now.

// NOTE: Assuming that GROUP will only contain TRISTATE children
// NOTE: using name as the key, this shouldn't be a problem

const FilterGroup = ({ name, state, onChange }: Props) => (
<ExpansionPanel>
<ExpansionPanelSummary expandIcon={<Icon>expand_more</Icon>}>
<Typography>{name}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<FormGroup>
{state.map((tristate, nestedIndex) => (
<FilterTristate
name={tristate.name}
state={tristate.state}
onChange={onChange(nestedIndex)}
key={`${name} ${tristate.name}`}
/>
))}
</FormGroup>
</ExpansionPanelDetails>
</ExpansionPanel>
);
type Props = { index: number };

const FilterGroup = memo(({ index }: Props) => {
const dispatch = useDispatch();

const filter = useSelector(state => selectFilterAtIndex(state, index));

const handleChange = (clickedIndex: number) => () => {
dispatch(updateFilterGroup(index, clickedIndex));
};

return (
<ExpansionPanel>
<ExpansionPanelSummary expandIcon={<Icon>expand_more</Icon>}>
<Typography>{filter.name}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<FormGroup>
{filter.state.map((tristate, nestedIndex) => (
<TristateCheckbox
name={tristate.name}
state={tristate.state}
onChange={handleChange(nestedIndex)}
key={`${filter.name} ${tristate.name}`}
/>
))}
</FormGroup>
</ExpansionPanelDetails>
</ExpansionPanel>
);
});

export default FilterGroup;
Loading