Skip to content

Commit

Permalink
Merge pull request #1243 from VEuPathDB/orthogroup-tree-table__useDef…
Browse files Browse the repository at this point in the history
…erredValue

use `useDeferredValue` to improve responsiveness of filter checkboxes
  • Loading branch information
bobular authored Oct 24, 2024
2 parents 083d7e6 + 4638fed commit 8b024bc
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import React, {
useCallback,
MouseEventHandler,
useMemo,
useState,
useEffect,
useDeferredValue,
} from 'react';
import { css } from '@emotion/react';
import { merge } from 'lodash';
Expand Down Expand Up @@ -1100,7 +1099,6 @@ export default CheckboxTree;
function useTreeState<T>(props: CheckboxTreeProps<T>) {
const {
tree,
searchTerm,
searchPredicate,
getNodeId,
getNodeChildren,
Expand All @@ -1112,13 +1110,16 @@ function useTreeState<T>(props: CheckboxTreeProps<T>) {
expandedList,
filteredList,
} = props;

const searchTerm = useDeferredValue(props.searchTerm);

const statefulTree = useMemo(
() => createStatefulTree(tree, getNodeChildren),
[tree, getNodeChildren]
);

// initialize stateful tree; this immutable tree structure will be replaced with each state change
const makeTreeState = useCallback(() => {
const treeState = useMemo(() => {
const isLeafVisible = createIsLeafVisible(
tree,
searchTerm,
Expand Down Expand Up @@ -1163,21 +1164,5 @@ function useTreeState<T>(props: CheckboxTreeProps<T>) {
filteredList,
]);

const [treeState, setTreeState] = useState(makeTreeState);

useEffect(() => {
function performUpdate() {
setTreeState(makeTreeState());
}
if (searchTerm) {
const timerId = setTimeout(performUpdate, 250);
return function cancel() {
clearTimeout(timerId);
};
} else {
performUpdate();
}
}, [makeTreeState, searchTerm]);

return treeState;
}
33 changes: 32 additions & 1 deletion packages/libs/coreui/src/hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { useEffect } from 'react';
import {
Dispatch,
SetStateAction,
useDeferredValue,
useEffect,
useState,
} from 'react';

export const useCoreUIFonts = () =>
useEffect(() => {
Expand All @@ -20,3 +26,28 @@ export const useCoreUIFonts = () =>
linkThree.setAttribute('rel', 'stylesheet');
document.head.appendChild(linkThree);
}, []);

// This hook functions similarly to `useState`, but with the added benefit
// of deferring updates to the state value. The primary state value (first element in the
// returned array) is 'deferred', meaning that updates to this value are handled as low-priority
// and can be interrupted if the state changes rapidly, preventing unnecessary re-renders.
// This is particularly useful for expensive renders or non-urgent updates.
//
// The third return value represents the 'raw' or 'volatile' state, which reflects
// the immediate state changes and should be used in UI elements that require responsive updates
// (e.g., form inputs, user feedback). You can ignore this value if not needed, making it
// a drop-in replacement for `useState`.
//
// Usage:
// const [deferredState, setState, volatileState] = useDeferredState(initialValue);
// - `deferredState`: Use for non-urgent rendering, allowing the UI to remain responsive.
// - `setState`: The state setter function, as in `useState`.
// - `volatileState`: Use when immediate, responsive updates are needed, such as in user interactions.
export function useDeferredState<T>(
initialValue: T
): [T, Dispatch<SetStateAction<T>>, T] {
const [volatileState, setState] = useState(initialValue);
const deferredState = useDeferredValue(volatileState);

return [deferredState, setState, volatileState];
}
3 changes: 3 additions & 0 deletions packages/libs/coreui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ export { default as colors } from './definitions/colors';

// Icons
export * from './components/icons';

// Hooks
export * from './hooks';
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import React, { useMemo, useState } from 'react';
import { orderBy } from 'lodash';

import { Checkbox } from '@veupathdb/wdk-client/lib/Components';
import { LinksPosition } from '@veupathdb/coreui/lib/components/inputs/checkboxes/CheckboxTree/CheckboxTree';
import {
CheckboxTreeStyleSpec,
LinksPosition,
} from '@veupathdb/coreui/lib/components/inputs/checkboxes/CheckboxTree/CheckboxTree';
import { makeClassNameHelper } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils';
import { makeSearchHelpText } from '@veupathdb/wdk-client/lib/Utils/SearchUtils';
import {
Expand All @@ -27,6 +30,18 @@ import { SelectTree } from '@veupathdb/coreui';

const cx = makeClassNameHelper('PhyleticDistributionCheckbox');

const styleOverridesForPopover: CheckboxTreeStyleSpec = {
treeSection: {
container: {
width: 500,
height: 'min(600px, 75vh)',
},
},
searchAndFilterWrapper: {
width: 500,
},
};

interface Props {
selectionConfig: SelectionConfig;
speciesCounts: Record<string, number>;
Expand Down Expand Up @@ -72,6 +87,9 @@ export function PhyleticDistributionCheckbox({

return (
<SelectTree
styleOverrides={
selectionConfig.selectable ? styleOverridesForPopover : undefined
}
hasPopoverButton={selectionConfig.selectable}
buttonDisplayContent="Organism"
tree={prunedPhyleticDistributionUiTree}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { CSSProperties, useMemo, useState } from 'react';
import React, { CSSProperties, useCallback, useMemo, useState } from 'react';
import TreeTable from '@veupathdb/components/lib/components/tidytree/TreeTable';
import { RecordTableProps, WrappedComponentProps } from './Types';
import { useOrthoService } from 'ortho-client/hooks/orthoService';
Expand All @@ -18,7 +18,12 @@ import { extractPfamDomain } from 'ortho-client/records/utils';
import Banner from '@veupathdb/coreui/lib/components/banners/Banner';
import { RowCounter } from '@veupathdb/coreui/lib/components/Mesa';
import { PfamDomain } from 'ortho-client/components/pfam-domains/PfamDomain';
import { FloatingButton, SelectList, Undo } from '@veupathdb/coreui';
import {
FloatingButton,
SelectList,
Undo,
useDeferredState,
} from '@veupathdb/coreui';
import { RecordTable_TaxonCounts_Filter } from './RecordTable_TaxonCounts_Filter';
import { formatAttributeValue } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils';
import { RecordFilter } from '@veupathdb/wdk-client/lib/Views/Records/RecordTable/RecordFilter';
Expand Down Expand Up @@ -46,11 +51,18 @@ export function RecordTable_Sequences(
);

const [resetCounter, setResetCounter] = useState(0); // used for forcing re-render of filter buttons
const [selectedSpecies, setSelectedSpecies] = useState<string[]>([]);
const [pfamFilterIds, setPfamFilterIds] = useState<string[]>([]);
const [corePeripheralFilterValue, setCorePeripheralFilterValue] = useState<
('core' | 'peripheral')[]
>([]);

const [selectedSpecies, setSelectedSpecies, volatileSelectedSpecies] =
useDeferredState<string[]>([]);

const [pfamFilterIds, setPfamFilterIds, volatilePfamFilterIds] =
useDeferredState<string[]>([]);

const [
corePeripheralFilterValue,
setCorePeripheralFilterValue,
volatileCorePeripheralFilterValue,
] = useDeferredState<('core' | 'peripheral')[]>([]);

const groupName = props.record.id.find(
({ name }) => name === 'group_name'
Expand Down Expand Up @@ -277,6 +289,14 @@ export function RecordTable_Sequences(
[mesaColumns]
);

const handleSpeciesSelection = useCallback(
(species: string[]) => {
setSelectedSpecies(species);
setTablePageNumber(1);
},
[setSelectedSpecies, setTablePageNumber]
);

if (
!sortedRows ||
(numSequences >= MIN_SEQUENCES_FOR_TREE &&
Expand Down Expand Up @@ -390,7 +410,7 @@ export function RecordTable_Sequences(
),
value: formatAttributeValue(row.accession),
}))}
value={pfamFilterIds}
value={volatilePfamFilterIds}
onChange={(ids) => {
setPfamFilterIds(ids);
setTablePageNumber(1);
Expand All @@ -413,7 +433,7 @@ export function RecordTable_Sequences(
value: 'peripheral',
},
]}
value={corePeripheralFilterValue}
value={volatileCorePeripheralFilterValue}
onChange={(value) => {
setCorePeripheralFilterValue(value);
setTablePageNumber(1);
Expand All @@ -427,11 +447,8 @@ export function RecordTable_Sequences(
// eslint-disable-next-line react/jsx-pascal-case
<RecordTable_TaxonCounts_Filter
key={`taxonFilter-${resetCounter}`}
selectedSpecies={selectedSpecies}
onSpeciesSelected={(species) => {
setSelectedSpecies(species);
setTablePageNumber(1);
}}
selectedSpecies={volatileSelectedSpecies}
onSpeciesSelected={handleSpeciesSelection}
record={props.record}
recordClass={props.recordClass}
table={props.recordClass.tablesMap.TaxonCounts}
Expand Down

0 comments on commit 8b024bc

Please sign in to comment.