I would like to update the data partially/dynamically using the pagination click.
codesandbox
On the initial load, I would like to render 100 records(DB has more data) and the pagination has to work normally without calling the API.
Once it user reaches the last page, at that time I would like to hit an API call to get another set of 100 data from the database to replace the existing.
From the above example, I can do either. Either by displaying the Static data with pagination on the initial loading of the application nor calling API on either of the pagination clicks.
Table.js
import React, { useState, useEffect } from "react";
import { BootstrapTable, TableHeaderColumn } from "react-bootstrap-table";
import Axios from "axios";
// import { products } from "./products";
import { PHOTOS } from "./constants";
import BSTable from "./BSTable";
function Table() {
/* Varibale Declaration */
const [userData, setUserData] = useState([]);
const [childData, setChildData] = useState();
const [currentPage, setCurrentPage] = useState();
const [sizePerPage, setSizePerPage] = useState();
/* Fetch User Data */
const fetchUserData = async (start = 0, limit = 50) => {
console.log("fetchUserData", start, limit);
var res = await Axios.get(
`https://jsonplaceholder.typicode.com/photos?_start=${start}&_limit=${limit}`
);
console.log("res", res);
setUserData(res.data);
};
/* React Hooks */
useEffect(() => {
fetchUserData();
}, []);
/* Functional Declaration */
let expandedRows = {};
const renderShowsTotal = (start, to, total) => {
return (
<p style={{ color: "blue" }}>
From {start} to {to}, totals is {total} (its a customize
text)
</p>
);
};
const handleExpand = async (rowKey, isExpand) => {
expandedRows = Object.assign(
expandedRows,
(expandedRows[rowKey] = isExpand)
);
var res = await Axios.get(
`https://jsonplaceholder.typicode.com/photos?_start=${0}&_limit=${1}`
);
console.log(res);
setChildData(res.data);
// expandedRows[rowKey] = isExpand;
// console.log(expandedRows[rowKey], Object.keys(expandedRows).length);
// if (!(Object.keys(expandedRows).length === 0)) {
// expandedRows = Object.assign({}, expandedRows[rowKey] = isExpand);
// } else {
// expandedRows[rowKey] = isExpand;
// }
console.log(rowKey, isExpand, expandedRows);
console.log("handleClick", isExpandableRow({ id: 4 }));
};
const onPageChange = (page, sizePerPage) => {
// setCurrentPage(page);
console.log("onPageChange", page, sizePerPage, userData);
// return false;
// fetchUserData((page - 1) * 10, sizePerPage);
// fetchUserData();
};
// const onSizePerPageList = sizePerPage => {
// console.log("onSizePerPageList", currentPage, sizePerPage);
// // const currentIndex = (currentPage - 1) * sizePerPage;
// // setUserData(userData.slice(currentIndex, currentIndex + sizePerPage));
// // setSizePerPage(sizePerPage);
// };
const onSortChange = (sortName, sortOrder) => {
console.log(sortName, sortOrder);
if (sortOrder === "asc") {
userData.sort((a, b) => {
if (parseInt(a[sortName], 10) > parseInt(b[sortName], 10)) {
return 1;
} else if (parseInt(b[sortName], 10) > parseInt(a[sortName], 10)) {
return -1;
}
return 0;
});
} else {
userData.sort((a, b) => {
if (parseInt(a[sortName], 10) > parseInt(b[sortName], 10)) {
return -1;
} else if (parseInt(b[sortName], 10) > parseInt(a[sortName], 10)) {
return 1;
}
return 0;
});
}
};
const options = {
onlyOneExpanding: true,
// onSizePerPageList: onSizePerPageList,
//hideSizePerPage: true,
paginationSize: 1,
onExpand: handleExpand,
sizePerPageList: [15, 10],
// sizePerPageList: [
// {
// text: "5",
// value: 5
// },
// {
// text: "10",
// value: 10
// },
// {
// text: "All",
// value: userData.length
// }
// ],
onPageChange: onPageChange,
sizePerPage: 15, // which size per page you want to locate as default
// pageStartIndex: 0, // where to start counting the pages
// paginationSize: 3, // the pagination bar size.
prePage: "Prev", // Previous page button text
nextPage: "Next", // Next page button text
firstPage: "First", // First page button text
lastPage: "Last", // Last page button text
totalSize: 100,
paginationShowsTotal: renderShowsTotal, // Accept bool or function
paginationPosition: "top", // default is bottom, top and both is all available
//hideSizePerPage: true, // You can hide the dropdown for sizePerPage
alwaysShowAllBtns: true, // Always show next and previous button
withFirstAndLast: currentPage === 4 ? true : false, // Hide the going to First and Last page button
expandRowBgColor: "rgb(242, 255, 163)",
expandBy: "column", // Currently, available value is row and column, default is row
expandBodyClass: function(row, rowIndex, isExpanding) {
if (!isExpanding) {
return "current-is-hidden";
} else {
if (rowIndex > 1) {
return "custom-expand-body-1";
} else {
return "custom-expand-body-0";
}
}
},
expandParentClass: function(row, rowIndex, isExpanding) {
return isExpandableRow(row);
},
onSortChange: onSortChange
};
const isExpandableRow = row => {
// console.log("isExpandableRow", row);
if (row.id < 3) return true;
else return false;
};
const expandComponent = (row, e) => {
console.log("expandComponent", row, e);
if (row.id === true) {
return <BSTable data={childData} />;
}
};
const expandColumnComponent = ({ isExpandableRow, isExpanded }) => {
let content = "";
console.log("expandColumnComponent", isExpandableRow, isExpanded);
if (isExpandableRow) {
content = isExpanded ? "(-)" : "(+)";
} else {
content = " ";
}
return <div> {content} </div>;
};
const expandedColumnHeaderComponent = ({ anyExpand }) => {
const content = anyExpand ? "(-)" : "(+)";
return <div>{content}</div>;
};
const trClassFormat = (rowData, rIndex) => {
return !isExpandableRow(rowData) ? "disable-row-cell" : "";
};
return (
<div className="react-bootstrap-table container mt-4">
<h2>React Bootstrap Table with pagination</h2>
{userData.length > 0 ? (
<BootstrapTable
dataSort={true}
hover
// remote={true}
fetchInfo={{ dataTotalSize: 1000 }}
trClassName={trClassFormat}
data={userData}
pagination
options={options}
expandableRow={isExpandableRow}
expandComponent={expandComponent}
expandColumnOptions={{
expandColumnVisible: true,
expandColumnComponent: expandColumnComponent,
columnWidth: 50,
expandedColumnHeaderComponent: expandedColumnHeaderComponent
}}
autoCollapse={{ sort: true, search: true, filter: true }}
search
>
<TableHeaderColumn expandable={false} dataField="id" isKey={true}>
User ID
</TableHeaderColumn>
<TableHeaderColumn expandable={false} dataField="title">
User Title
</TableHeaderColumn>
<TableHeaderColumn expandable={false} dataField="url">
User URL
</TableHeaderColumn>
</BootstrapTable>
) : (
<i className="fa fa-spinner fa-3x fa-spin" />
)}
</div>
);
}
export default Table;
Thanks in advance.
You can use react-bootstrap-table2:
learn about react-bootstrap-table2 Pagination from this Documentation
Related
How can I make other filter button disappear when picked 1 (or multiple) value in same filter block.
Here is my code base:
const FilterBlock = props => {
const {
filterApi,
filterState,
filterFrontendInput,
group,
items,
name,
onApply,
initialOpen
} = props;
const { formatMessage } = useIntl();
const talonProps = useFilterBlock({
filterState,
items,
initialOpen
});
const { handleClick, isExpanded } = talonProps;
const classStyle = useStyle(defaultClasses, props.classes);
const ref = useRef(null);
useEffect(() => {
const handleClickOutside = event => {
if (ref.current && !ref.current.contains(event.target)) {
isExpanded && handleClick();
}
};
document.addEventListener('click', handleClickOutside, true);
return () => {
document.removeEventListener('click', handleClickOutside, true);
};
}, [isExpanded]);
const list = isExpanded ? (
<Form>
<FilterList
filterApi={filterApi}
filterState={filterState}
name={name}
filterFrontendInput={filterFrontendInput}
group={group}
items={items}
onApply={onApply}
/>
</Form>
) : null;
return (
<div
data-cy="FilterBlock-root"
aria-label={itemAriaLabel}
ref={ref}
>
<Menu.Button
data-cy="FilterBlock-triggerButton"
type="button"
onClick={handleClick}
aria-label={toggleItemOptionsAriaLabel}
>
<div>
<span>
{name}
</span>
<svg
width="8"
height="5"
viewBox="0 0 8 5"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.97291 0.193232C7.20854"
fill="currentColor"
/>
</svg>
</div>
</Menu.Button>
<div>
<div>
{list}
</div>
</div>
</div>
);
};
I am trying to achieve when I chose 1 value or more than 1 value inside filter block the other block will disappear but right now I achieved that when I chose 1 value the other filter block disappear but when I chose another value in the same block the other filter block appear again. Anyone have idea how can I work on this?
I am using React and Redux for this project
Thank you for helping me on this!!!!
Update:
Added parent component for FilterBlock.ks:
const FilterSidebar = props => {
const { filters, filterCountToOpen } = props;
const [selectedGroup, setSelectedGroup] = useState(null);
const talonProps = useFilterSidebar({ filters });
const {
filterApi,
filterItems,
filterNames,
filterFrontendInput,
filterState,
handleApply,
handleReset
} = talonProps;
const filterRef = useRef();
const classStyle = useStyle(defaultClasses, props.classes);
const handleApplyFilter = useCallback(
(...args) => {
const filterElement = filterRef.current;
if (
filterElement &&
typeof filterElement.getBoundingClientRect === 'function'
) {
const filterTop = filterElement.getBoundingClientRect().top;
const windowScrollY =
window.scrollY + filterTop - SCROLL_OFFSET;
window.scrollTo(0, windowScrollY);
}
handleApply(...args);
},
[handleApply, filterRef]
);
const result = Array.from(filterItems)
.filter(
([group, items]) =>
selectedGroup === null ||
selectedGroup === filterNames.get(group)
)
.map(([group, items], iteration) => {
const blockState = filterState.get(group);
const groupName = filterNames.get(group);
const frontendInput = filterFrontendInput.get(group);
return (
<FilterBlock
key={group}
filterApi={filterApi}
filterState={blockState}
filterFrontendInput={frontendInput}
group={group}
items={items}
name={groupName}
onApply={(...args) => {
console.log('args: ', ...args);
setSelectedGroup(prev =>
prev !== null ? null : groupName
);
return handleApplyFilter(...args);
}}
initialOpen={iteration < filterCountToOpen}
iteration={iteration}
/>
);
});
return (
<div className="container px-4 mx-auto">
<Menu
as="div"
className="my-16 justify-center flex flex-wrap py-5 border-y border-black border-opacity-5"
>
{result}
</Menu>
</div>
);
};
Updated added useFilterSideBar.js:
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from '#apollo/client';
import { useHistory, useLocation } from 'react-router-dom';
import { useAppContext } from '#magento/peregrine/lib/context/app';
import mergeOperations from '../../util/shallowMerge';
import { useFilterState } from '../FilterModal';
import {
getSearchFromState,
getStateFromSearch,
sortFiltersArray,
stripHtml
} from '../FilterModal/helpers';
import DEFAULT_OPERATIONS from '../FilterModal/filterModal.gql';
const DRAWER_NAME = 'filter';
export const useFilterSidebar = props => {
const { filters } = props;
const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
const { getFilterInputsQuery } = operations;
const [isApplying, setIsApplying] = useState(false);
const [{ drawer }, { toggleDrawer, closeDrawer }] = useAppContext();
const [filterState, filterApi] = useFilterState();
const prevDrawer = useRef(null);
const isOpen = drawer === DRAWER_NAME;
const history = useHistory();
const { pathname, search } = useLocation();
const { data: introspectionData } = useQuery(getFilterInputsQuery);
const attributeCodes = useMemo(
() => filters.map(({ attribute_code }) => attribute_code),
[filters]
);
// Create a set of disabled filters.
const DISABLED_FILTERS = useMemo(() => {
const disabled = new Set();
// Disable category filtering when not on a search page.
if (pathname !== '/search.html') {
disabled.add('category_id');
disabled.add('category_uid');
}
return disabled;
}, [pathname]);
// Get "allowed" filters by intersection of filter attribute codes and
// schema input field types. This restricts the displayed filters to those
// that the api will understand.
const possibleFilters = useMemo(() => {
const nextFilters = new Set();
const inputFields = introspectionData
? introspectionData.__type.inputFields
: [];
// perform mapping and filtering in the same cycle
for (const { name } of inputFields) {
const isValid = attributeCodes.includes(name);
const isEnabled = !DISABLED_FILTERS.has(name);
if (isValid && isEnabled) {
nextFilters.add(name);
}
}
return nextFilters;
}, [DISABLED_FILTERS, attributeCodes, introspectionData]);
const isBooleanFilter = options => {
const optionsString = JSON.stringify(options);
return (
options.length <= 2 &&
(optionsString.includes(
JSON.stringify({
__typename: 'AggregationOption',
label: '0',
value: '0'
})
) ||
optionsString.includes(
JSON.stringify({
__typename: 'AggregationOption',
label: '1',
value: '1'
})
))
);
};
// iterate over filters once to set up all the collections we need
const [
filterNames,
filterKeys,
filterItems,
filterFrontendInput
] = useMemo(() => {
const names = new Map();
const keys = new Set();
const frontendInput = new Map();
const itemsByGroup = new Map();
const sortedFilters = sortFiltersArray([...filters]);
for (const filter of sortedFilters) {
const { options, label: name, attribute_code: group } = filter;
// If this aggregation is not a possible filter, just back out.
if (possibleFilters.has(group)) {
const items = [];
// add filter name
names.set(group, name);
// add filter key permutations
keys.add(`${group}[filter]`);
// TODO: Get all frontend input type from gql if other filter input types are needed
// See https://github.com/magento-commerce/magento2-pwa/pull/26
if (isBooleanFilter(options)) {
frontendInput.set(group, 'boolean');
// add items
items.push({
title: 'No',
value: '0',
label: name + ':' + 'No'
});
items.push({
title: 'Yes',
value: '1',
label: name + ':' + 'Yes'
});
} else {
// Add frontend input type
frontendInput.set(group, null);
// add items
for (const { label, value } of options) {
items.push({ title: stripHtml(label), value });
}
}
itemsByGroup.set(group, items);
}
}
return [names, keys, itemsByGroup, frontendInput];
}, [filters, possibleFilters]);
// on apply, write filter state to location
useEffect(() => {
if (isApplying) {
const nextSearch = getSearchFromState(
search,
filterKeys,
filterState
);
// write filter state to history
history.push({ pathname, search: nextSearch });
// mark the operation as complete
setIsApplying(false);
}
}, [filterKeys, filterState, history, isApplying, pathname, search]);
const handleOpen = useCallback(() => {
toggleDrawer(DRAWER_NAME);
}, [toggleDrawer]);
const handleClose = useCallback(() => {
closeDrawer();
}, [closeDrawer]);
const handleApply = useCallback(() => {
setIsApplying(true);
handleClose();
}, [handleClose]);
const handleReset = useCallback(() => {
filterApi.clear();
setIsApplying(true);
}, [filterApi, setIsApplying]);
const handleKeyDownActions = useCallback(
event => {
// do not handle keyboard actions when the modal is closed
if (!isOpen) {
return;
}
switch (event.keyCode) {
// when "Esc" key fired -> close the modal
case 27:
handleClose();
break;
}
},
[isOpen, handleClose]
);
useEffect(() => {
const justOpened =
prevDrawer.current === null && drawer === DRAWER_NAME;
const justClosed =
prevDrawer.current === DRAWER_NAME && drawer === null;
// on drawer toggle, read filter state from location
if (justOpened || justClosed) {
const nextState = getStateFromSearch(
search,
filterKeys,
filterItems
);
filterApi.setItems(nextState);
}
// on drawer close, update the modal visibility state
if (justClosed) {
handleClose();
}
prevDrawer.current = drawer;
}, [drawer, filterApi, filterItems, filterKeys, search, handleClose]);
useEffect(() => {
const nextState = getStateFromSearch(search, filterKeys, filterItems);
filterApi.setItems(nextState);
}, [filterApi, filterItems, filterKeys, search]);
return {
filterApi,
filterItems,
filterKeys,
filterNames,
filterFrontendInput,
filterState,
handleApply,
handleClose,
handleKeyDownActions,
handleOpen,
handleReset,
isApplying,
isOpen
};
};
Update FilterList component:
const FilterList = props => {
const {
filterApi,
filterState,
filterFrontendInput,
name,
group,
itemCountToShow,
items,
onApply,
toggleItemOptionsAriaLabel
} = props;
const classes = useStyle(defaultClasses, props.classes);
const talonProps = useFilterList({ filterState, items, itemCountToShow });
const { isListExpanded, handleListToggle } = talonProps;
const { formatMessage } = useIntl();
// memoize item creation
// search value is not referenced, so this array is stable
const itemElements = useMemo(() => {
if (filterFrontendInput === 'boolean') {
const key = `item-${group}`;
return (
<li
key={key}
className={classes.item}
data-cy="FilterList-item"
>
<FilterItemRadioGroup
filterApi={filterApi}
filterState={filterState}
group={group}
name={name}
items={items}
onApply={onApply}
labels={labels}
/>
</li>
);
}
return items.map((item, index) => {
const { title, value } = item;
const key = `item-${group}-${value}`;
if (!isListExpanded && index >= itemCountToShow) {
return null;
}
// create an element for each item
const element = (
<li
key={key}
className={classes.item}
data-cy="FilterList-item"
>
<FilterItem
filterApi={filterApi}
filterState={filterState}
group={group}
item={item}
onApply={onApply}
/>
</li>
);
// associate each element with its normalized title
// titles are not unique, so use the element as the key
labels.set(element, title.toUpperCase());
return element;
});
}, [
classes,
filterApi,
filterState,
filterFrontendInput,
name,
group,
items,
isListExpanded,
itemCountToShow,
onApply
]);
const showMoreLessItem = useMemo(() => {
if (items.length <= itemCountToShow) {
return null;
}
const label = isListExpanded
? formatMessage({
id: 'filterList.showLess',
defaultMessage: 'Show Less'
})
: formatMessage({
id: 'filterList.showMore',
defaultMessage: 'Show More'
});
return (
<li className={classes.showMoreLessItem}>
<button
onClick={handleListToggle}
className="text-sm hover_text-indigo-500 transition-colors duration-sm"
data-cy="FilterList-showMoreLessButton"
>
{label}
</button>
</li>
);
}, [
isListExpanded,
handleListToggle,
items,
itemCountToShow,
formatMessage,
classes
]);
return (
<Fragment>
<ul className={classes.items}>
{itemElements}
{showMoreLessItem}
</ul>
</Fragment>
);
};
FilterList.defaultProps = {
onApply: null,
itemCountToShow: 5
};
Update FilterRadioGroup:
const FilterItemRadioGroup = props => {
const { filterApi, filterState, group, items, onApply, labels } = props;
const radioItems = useMemo(() => {
return items.map(item => {
const code = `item-${group}-${item.value}`;
return (
<FilterItemRadio
key={code}
filterApi={filterApi}
filterState={filterState}
group={group}
item={item}
onApply={onApply}
labels={labels}
/>
);
});
}, [filterApi, filterState, group, items, labels, onApply]);
const fieldValue = useMemo(() => {
if (filterState) {
for (const item of items) {
if (filterState.has(item)) {
return item.value;
}
}
}
return null;
}, [filterState, items]);
const field = `item-${group}`;
const fieldApi = useFieldApi(field);
const fieldState = useFieldState(field);
useEffect(() => {
if (field && fieldValue === null) {
fieldApi.reset();
} else if (field && fieldValue !== fieldState.value) {
fieldApi.setValue(fieldValue);
}
}, [field, fieldApi, fieldState.value, fieldValue]);
return (
<RadioGroup field={field} data-cy="FilterDefault-radioGroup">
{radioItems}
</RadioGroup>
);
};
FilterItemRadioGroup.defaultProps = {
onApply: null
};
The code seems overly complicated for what it does.
Could you perhaps make the filter block a state object like this:
filterGroup = {
title: "",
hasSelectedItems: boolean,
filterItems: []
}
Then it would be a simple matter of checking hasSelectedItems to conditionally render only one filterGroup in the UI.
I apologize I don't have the time to go in depth and try out your code, but I would put the filterBlock rendering inside the return statement and do something like this:
return(
{ (!!filterValue && (filterGroup === selectedGroup))
&&
<FilterGroup />
}
);
I find the best results come from using my state values as conditions inside my return statement.
That way, I don't have to manage complex logic in the .map() functions or with useEffect.
It looks like FilterSideBar is where you build the the array of FilterBlocks.
According to your question, I think you want to display all filter blocks if NO filters are selected, but once a filter is selected, you want the other FilterBlocks to not render, correct?
So, in your FilterSidebar module, I'd modify your "result" variable to include a check to see if anything has already been selected.
If something has been selected, then only render the FilterBlock that corresponds to that selected list item.
const result = Array.from(filterItems)
.filter(
([group, items]) =>
selectedGroup === null ||
selectedGroup === filterNames.get(group)
)
.map(([group, items], iteration) => {
const blockState = filterState.get(group);
const groupName = filterNames.get(group);
const frontendInput = filterFrontendInput.get(group);
return (
// This is the conditional rendering
(!!blockState.filterGroupSelected && blockState.filterGroupSelected === group) ?
<FilterBlock
key={group}
...
/>
: null
);
});
Then you'll have to add an item to your state object (unless you already have it) to track the group that was selected (e.g. "Printer Type"), and only allow that one to render.
If no group has a selected filter item, they should all render.
Also, you'll have to be sure to clear the selectedGroup whenever there are no selected items.
If this does not work, it may be that your state changes are not triggering a re-render. In which case it can be as simple as adding a reference to your state object in your component's return method like this:
{blockState.filterGroupSelected && " " }
It's a hack but it works, and keeps you from adding calls to useEffect.
I am using a custom pagination component that paginates the data from an array. When a user goes to a certain page and refreshes, it takes the user back to the first page. I am thinking I can use Local Storage to handle this. See below:
export interface PublicProps {
ID: number;
}
const PAGE_SIZE: number = 10;
export const TestPage: React.FC<PublicProps> = ({ ID }) => {
const [currentPage, setCurrentPage] = useState<number>(1);
const suitesForProject = SOME DATA FROM AN ARRAY
const totalPages = suitesForProject.length
? Math.ceil(suitesForProject.length / PAGE_SIZE)
: 0;
const handleClickNext = () => {
if (!suitesForProject.length) {
return;
}
setCurrentPage((currentPage) => Math.min(currentPage + 1));
};
const handleClickPrev = () => {
if (!suitesForProject.length || currentPage === 1) {
return;
}
setCurrentPage((currentPage) => currentPage - 1);
};
return (
<>
{suitesForProject
.slice((currentPage - 1) * PAGE_SIZE, PAGE_SIZE * currentPage)
.map((suitesForProject) => (
//doing stuff with the data here
))}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onClickPrevious={handleClickPrev}
onClickNext={handleClickNext}
previousPageButtonAriaLabel="To previous page"
nextPageButtonAriaLabel="To next page"
/>
</>
);
};
Is there a way to handle this?
useState has lazy loading. It is looks like
useState(() => 1); function as first arg is lazy function which called once. Inside this function read localStorage by key, parse value and if it is number return it, else return 1.
You can do something like this..
export interface PublicProps {
ID: number;
}
const PAGE_SIZE: number = 10;
const PAGE_KEY = "MY_PAGINATION_KEY";
const getPageNumber = () => {
if(localStorage && parseInt(localStorage.getItem(PAGE_KEY)) > 0) {
return parseInt(localStorage.getItem(PAGE_KEY));
}
return 1;
}
export const TestPage: React.FC<PublicProps> = ({ ID }) => {
const [currentPage, setCurrentPage] = useState<number>(getPageNumber());
const suitesForProject = SOME DATA FROM AN ARRAY
const totalPages = suitesForProject.length
? Math.ceil(suitesForProject.length / PAGE_SIZE)
: 0;
const handleClickNext = () => {
if (!suitesForProject.length) {
return;
}
localStorage.setItem(PAGE_KEY, currentPage +1)
setCurrentPage((currentPage) => Math.min(currentPage + 1));
};
const handleClickPrev = () => {
if (!suitesForProject.length || currentPage === 1) {
return;
}
localStorage.setItem(PAGE_KEY, currentPage - 1)
setCurrentPage((currentPage) => currentPage - 1);
};
return (
<>
{suitesForProject
.slice((currentPage - 1) * PAGE_SIZE, PAGE_SIZE * currentPage)
.map((suitesForProject) => (
//doing stuff with the data here
))}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onClickPrevious={handleClickPrev}
onClickNext={handleClickNext}
previousPageButtonAriaLabel="To previous page"
nextPageButtonAriaLabel="To next page"
/>
</>
);
};
On dataView true dataViewModel is rendered
{popUps.dataView && popupsFactory.dataView}
Rendering of model is very slow, which have and table with dataSource around 10k-20k or more.
/* eslint-disable react/prop-types */
/* eslint-disable indent */
/* eslint-disable linebreak-style */
import React, { useState, useEffect, useRef } from 'react'
import { Modal, Select, Table, Input, Button, Icon, Tooltip } from 'antd'
//import Highlighter from 'react-highlight-words'
import { connect } from 'react-redux'
import actionDispatcher from 'client/app-actions'
import { isFiltered } from 'client/routes/app/constants/deck-layers'
//import TableLayout from './Table'
import styled from './styled-component'
//import './style.css'
const { Option } = Select
const dataViewModal = (props) => {
const {
visibility
} = props
const [layerId, setLayerId] = useState('')
const [tableColumns, setTableColumns] = useState(null)
const [tableData, setTableData] = useState(null)
const [searchText, setSearchText] = useState('')
const [searchedColumn, setSearchedColumn] = useState('')
const [filteredInfo, setFilteredInfo] = useState(null)
const [sortedInfo, setSortedInfo] = useState(null)
const [pagination, setPagination] = useState({ pageSize: 20, current: 1 })
const [currentDataSource, setCurrentDataSource] = useState(null)
const searchInput = useRef(null)
useEffect(() => {
// console.log(currentDataSource)
}, [currentDataSource])
/**
* initialize layer
*/
useEffect(() => {
setLayerId(props.layers.length ? props.layers[0].uniqueKey : null)
}, [props.layers])
/**
* whenerver layerId changes clear the sort and filter for the columns
*/
useEffect(() => {
clearAll()
}, [layerId])
const tempLayer = props.layers.find(item => item.uniqueKey === layerId)
useEffect(() => {
const layer = props.layers.find(item => item.uniqueKey === layerId)
if (layer?.props?.coordinates) {
showMapDataToUser(layer, false)
} else {
setTableColumns(null)
setTableData(null)
}
}, [tempLayer, sortedInfo, filteredInfo])
const showMapDataToUser = (layer, exportToCsv) => {
const filteredColumnSet = layer.columnSet.filter(
item => item.variableAdded
)
const showDataToUser = []
const showColumns = []
filteredColumnSet.forEach((item, index) => {
if (item.showToUser) {
showColumns.push(`${item.displayName}`)
}
})
showDataToUser.push([showColumns])
layer.props.coordinates.forEach((it) => {
const showData = []
filteredColumnSet.forEach((item, index) => {
if (item.showToUser) {
if (it.geometry &&
it.geometry.data[layer.props.columnSet + isFiltered]) {
if (showColumns.includes('Network GMP')) {
if (item.columnName.toUpperCase() === 'OLYMPUS_ID') {
const splitLatLong = it.geometry.data[index].split('_')
if (splitLatLong.length > 2
&& !isNaN(splitLatLong[splitLatLong.length - 1])
&& !isNaN(splitLatLong[splitLatLong.length - 2])) {
splitLatLong.splice(-2, 2)
}
const splitLatLongString = splitLatLong.join('_')
showData.push(`${splitLatLongString}`.replace(/,/g, ';'))
} else if (item.columnName.toUpperCase() === 'RETAINABILITY_KPI' && !isNaN(it.geometry.data[index])) {
const calculativeDCR = 100 - (it.geometry.data[index])
showData.push(`${calculativeDCR}`.replace(/,/g, ';'))
} else {
showData.push(`${it.geometry.data[index]}`.replace(/,/g, ';'))
}
} else {
showData.push(`${it.geometry.data[index]}`.replace(/,/g, ';'))
}
} else if (it[layer.props.columnSet + isFiltered]) {
if (showColumns.includes('Network GMP')) {
if (item.columnName.toUpperCase() === 'OLYMPUS_ID') {
const splitLatLong = it[index].split('_')
if (splitLatLong.length > 2
&& !isNaN(splitLatLong[splitLatLong.length - 1])
&& !isNaN(splitLatLong[splitLatLong.length - 2])) {
splitLatLong.splice(-2, 2)
}
const splitLatLongString = splitLatLong.join('_')
showData.push(`${splitLatLongString}`.replace(/,/g, ';'))
} else if (item.columnName.toUpperCase() === 'RETAINABILITY_KPI' && !isNaN(item[index])) {
const calculativeDCR = 100 - (item[index])
showData.push(`${calculativeDCR}`.replace(/,/g, ';'))
} else {
showData.push(`${it[index]}`.replace(/,/g, ';'))
}
} else {
showData.push(`${it[index]}`.replace(/,/g, ';'))
}
}
}
})
if (showData.length) {
showDataToUser.push([showData])
}
})
//-------------------------------------------
if (!exportToCsv) {
//create table data
const columnNames = filteredColumnSet.filter(col => col.showToUser).map(col => col.columnName)
const data = showDataToUser.slice(1).map((dataSet, index) => {
//items[0]: array converted to obj
const itemsObj = { key: index + 1 }
dataSet[0].forEach((item, i) => {
itemsObj[columnNames[i]] = item
})
return itemsObj
})
// console.log(data)
setTableData(data)
//
//create table columns
const sortedInfoNew = sortedInfo || {}
const filteredInfoNew = filteredInfo || {}
const columns = []
columns.push({
title: 'No.',
dataIndex: 'key',
width: 70,
// fixed: 'left',
key: 'key',
})
const len = filteredColumnSet.filter(col => col.showToUser).length
filteredColumnSet.filter(col => col.showToUser).forEach((item, index) => {
if (item.showToUser) {
// console.log(item)
const columnObj = index === 0 ? ({
title: item.displayName,
width: 150,
// fixed: 'left',
displayName: item.displayName,
dataIndex: item.columnName,
key: item.columnName,
sorter: item.columnName ? (a, b) => sortByColumnType(a, b, item) : null,
sortDirections: ['descend', 'ascend'],
sortOrder: sortedInfoNew.columnKey === item.columnName && sortedInfoNew.order,
filteredValue: filteredInfoNew[item.columnName] || null, // to clear search
...getColumnSearchProps(item.columnName)
})
: index === len - 1 ?
({
title: () => columnTitle(item, data),
width: 150,
//fixed: 'right',
displayName: item.displayName,
dataIndex: item.columnName,
key: item.columnName,
filters: getFilters(data, item.columnName),
sorter: item.columnName ? (a, b) => sortByColumnType(a, b, item) : null,
onFilter: (value, record) => record[item.columnName].indexOf(value) === 0,
sortDirections: ['descend', 'ascend'],
sortOrder: sortedInfoNew.columnKey === item.columnName && sortedInfoNew.order,
filteredValue: filteredInfoNew[item.columnName] || null,
}) :
({
title: () => columnTitle(item, data),
width: 150,
displayName: item.displayName,
dataIndex: item.columnName,
key: item.columnName,
filters: getFilters(data, item.columnName),
sorter: item.columnName ? (a, b) => sortByColumnType(a, b, item) : null,
onFilter: (value, record) => record[item.columnName].indexOf(value) === 0,
sortDirections: ['descend', 'ascend'],
sortOrder: sortedInfoNew.columnKey === item.columnName && sortedInfoNew.order,
filteredValue: filteredInfoNew[item.columnName] || null,
})
columns.push(columnObj)
}
})
// console.log(columns)
setTableColumns(columns)
//
}
//-------------------------------------------
// console.log(showDataToUser)
return showDataToUser
}
const getFilters = (data, columnName) => {
const uniqueValuesSet = new Set(data.map(dataObj => dataObj[columnName]))
const filters = Array.from(uniqueValuesSet).map(value => ({ text: value, value }))
return filters
}
const sortByColumnType = (a, b, item) => {
if (item.columnType === 'NUMERIC') {
return a[item.columnName] - b[item.columnName]
} else if (item.columnType === 'TEXT') {
if (!a[item.columnName]) {
// console.log(item, a)
}
return a[item.columnName]?.localeCompare(b[item.columnName])
}
// console.log(item)
return null
}
/**
* set to initial values
*/
const clearAll = () => {
setFilteredInfo(null)
setSortedInfo(null)
setPagination({
pageSize: 20,
current: 1
})
setCurrentDataSource(null)
}
function getColumnSearchProps(dataIndex) {
return {
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
<div style={{ padding: 8 }}>
<Input
ref={searchInput}
placeholder={`Search ${dataIndex}`}
value={selectedKeys && selectedKeys[0]}
onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => handleSearch(selectedKeys, confirm, dataIndex)}
style={{ width: 188, marginBottom: 8, display: 'block' }}
/>
<Button
type="primary"
onClick={() => handleSearch(selectedKeys, confirm, dataIndex)}
icon="search"
size="small"
style={{ width: 90 }}
>
Search
</Button>
<Button onClick={() => handleReset(clearFilters)} size="small" style={{ width: 90 }}>
Reset
</Button>
</div>
),
filterIcon: filtered => <Icon type="search" style={{ color: filtered ? '#1890ff' : undefined }} />,
onFilter: (value, record) =>
record[dataIndex].toString().toLowerCase().includes(value.toLowerCase()),
onFilterDropdownVisibleChange: (visible) => {
if (visible) {
setTimeout(() => searchInput.current.select())
}
},
// render: text =>
// searchedColumn === dataIndex ? (
// <Highlighter
// highlightStyle={{ backgroundColor: '#ffc069', padding: 0 }}
// searchWords={[searchText]}
// autoEscape
// textToHighlight={text.toString()}
// />
// ) : (
// text
// ),
}
}
function handleSearch(selectedKeys, confirm, dataIndex) {
confirm()
setSearchText(selectedKeys[0])
setSearchedColumn(dataIndex)
}
function handleReset(clearFilters) {
clearFilters()
setSearchText('')
}
const handleCancel = () => {
props.updatePopUpMode('dataView')
}
const handleChange = (value) => {
// console.log(`selected ${value}`)
setLayerId(value)
}
function onChange(pager, filters, sorter, extra) {
// console.log(pager, filters, sorter, extra)
setFilteredInfo(filters)
setSortedInfo(sorter)
setPagination(pager)
setCurrentDataSource(extra.currentDataSource)
}
const handleDownloadLayer = () => {
//const layer = props.layers.find(item => item.uniqueKey === layerId)
if (tableColumns?.length) {
//export in csv
const showDataToUser = []
showDataToUser.push(tableColumns.map(({ displayName }) => displayName).slice(1)) // don't include key column ref: line 138
const dataSource = currentDataSource || tableData
showDataToUser.push(...dataSource.map(item => tableColumns.map(({ key }) => (item[key])).slice(1))) // don't include key column values ref: line 138
let csvContent = ''
showDataToUser.forEach((row) => {
csvContent += row.join(',')
csvContent += '\n'
})
const encodedUri = URL.createObjectURL(new Blob([csvContent], { type: 'text/plain' }))
const a = document.createElement('a')
a.href = encodedUri
a.target = '_Blank'
a.download = 'mapdata.csv'
a.click()
//export in csv
}
}
const selectedLayer = props.layers.find(layer => layer.uniqueKey === layerId)
const columnTitle = (item, data) => {
const columnData = data.map(dataObj => dataObj[item.columnName])
// console.log(columnData)
const nullColumnData = columnData.filter(value => value === 'null')
const nullPercent = (nullColumnData.length / columnData.length) * 100
return (
<div>
<Tooltip placement="top" title={item.displayName}>
<styled.columnDisplayName>
{item.displayName}
</styled.columnDisplayName>
</Tooltip>
<div style={{ fontSize: '10px', color: '#ff5c00a1' }}>
{Math.round(nullPercent)}% null rows
</div>
</div>
)
}
const LayerNames = () => (
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Select defaultValue={selectedLayer?.uniqueKey} style={{ width: 120 }} onChange={handleChange}>
{
props.layers.map(item =>
<Option value={item.uniqueKey}>{item.displayName}</Option>
)
}
</Select>
</div>
)
return (
<styled.modelWrapper
width="98%"
style={{ top: '20px' }}
title={<LayerNames/>}
visible={visibility}
onCancel={handleCancel}
footer={null}
>
<div className="Model Content" style={{ height: '590px' }}>
<styled.modalContentHeader>
<div style={{ float: 'right' }}>
<Button onClick={handleDownloadLayer}>Export to CSV</Button>
</div>
</styled.modalContentHeader>
<styled.tableWrapper style={{
overflow: 'auto',
height: '88%'
}}
>
{!tableData?.length ?
'NO DATA'
:
<styled.table
className="table-striped-rows"
columns={tableColumns}
dataSource={tableData}
pagination={pagination}
size="small"
onChange={onChange}
scroll={{ y: 410, x: 1340 }}
rowClassName={(record, index) => (index % 2 === 0 ? 'table-row-light' : 'table-row-dark')}
/>
}
</styled.tableWrapper>
</div>
</styled.modelWrapper>
)
}
const mapStateToProps = (store, ownProps) => {
const appViewState = store.appStore.appView
const layers = store.layerStore.layers
return {
selectedLayer: layers.length && layers.find(it => it.id === appViewState.variableSelectLayerId),
visibility: appViewState.popups.dataView,
layers
}
}
const mapDispatchToProps = {
updatePopUpMode: actionDispatcher.updatePopUpMode,
updateCoordinateDataFilter: actionDispatcher.updateCoordinateDataFilter
}
const DataView = connect(
mapStateToProps,
mapDispatchToProps
)(dataViewModal)
export default DataView
All operations like sorting, filtering, and pagination change are very slow. Filters take too long to render. Is there something I can do to optimize code. I tried to do calculation part of table dataSource before rendering and then passing it as a prop from Redux store, but that didn't work either.
I created a suggestions search and its built to break up the fetch based on the current page. The state is console.loged correctly, but the render is one page click event behind. This is obviously not the behavior we want. It seems like the state is being updated fine. I have tried to refactor the code difference ways, and even tried this.forceUpdate()
Here is the code
SearchOrderBar.js
import React, { Component } from "react";
import {Input, Label, Table, Icon, Header, Menu} from 'semantic-ui-react';
import "./SearchOrderBar.css";
// import { resolve } from "dns";
// import PropTypes from 'prop-types';
import Pagination from '../Search/Pagination';
class SearchOrderBar extends Component {
constructor(props) {
super(props);
this.text = "";
this.state = {
suggestions: [],
addToQuery: false,
Query: [],
pagesNeeded: 0,
page: 1
};
let searchTerm = null;
const {pageLimit = null, keyTimer = null, } = props;
this.pageLimit = typeof pageLimit === 'number' ? pageLimit : 10;
this.handlePageClick = this.handlePageClick.bind(this);
this.fetchCallBack = this.fetchCallBack.bind(this);
// this.addToQuery = this.addToQuery.bind(this);
this.keyUpHandler = this.keyUpHandler.bind(this);
this.keyDownHandler = this.keyDownHandler.bind(this);
}
handlePageClick(page){
this.forceUpdate();
this.setState({
page: page
})
this.fetchCallBack();
}
//This fetch should be called in a dynamic switch case
fetchCallBack() {
let y = this.pageLimit;
let x = this.state.page > 1 ? (this.pageLimit*this.state.page) - this.pageLimit : 0;
// Return a promise
return new Promise((resolve, reject) => {
let searchTerm = this.searchTerm;
return fetch(`http://localhost:5000/api/searchorders/${searchTerm}/${x}/${y}`)
.then(res => {
if (!res.ok) {
throw res;
}
// Convert serialized response into json
return res.json()
}).then(data => {
//Use data
let searchTerm = data.map(data => {
let rData = {};
rData = data;
return rData;
})
this.item = searchTerm;
//console.log('here from callback')
this.setState({
suggestions: []
})
return searchTerm;
}).then( data => {
// console.log(this.totalRecords)sd
//console.log(data)
if (searchTerm.length === 0) {
this.setState({
suggestions: [],
rangeCount_URL: `http://localhost:5000/api/searchorderscount/${searchTerm}`
});
} else {
const suggestions = data.filter(function(v){
if(Object.values(v).includes(searchTerm.toLowerCase()) !== -1 || Object.values(v).includes(searchTerm.toUpperCase()) !== -1){
return v
}
})
console.log(suggestions)
this.text = searchTerm;
this.setState({ suggestions: suggestions.sort()});
}
})
})
}
pageCountCallBack(){
return new Promise((resolve, reject) => {
let searchTerm = this.searchTerm;
return fetch(`http://localhost:5000/api/searchorderscount/${searchTerm}/`)
.then(res => {
if (!res.ok) {
throw res;
}
// Convert serialized response into json
return res.json()
}).then(data => {
//Use data
let searchTerm = data.map(data => {
let rData = {};
rData = data;
return rData;
})
this.item = searchTerm;
// console.log('here from Page Count callback')
this.renderSuggestions();
resolve(searchTerm)
})
})
}
keyUpHandler = (e) => {
if(e.target.value.length >= 3){
this.keyTimer = setTimeout(this.countFetch(e), 1500);
} else {
this.setState(() => {
return {
suggestions : [],
pagesNeeded : 0
}
})
clearTimeout(this.keyTimer);
}
}
keyDownHandler = (e) => {
clearTimeout(this.keyTimer);
}
//Any time text is changed in the text field
countFetch = (e) => {
const value = e.target.value;
this.searchTerm = value;
this.pageCountCallBack().then(data => {
const totalRecords = data[0].rows;
this.setState(() => {
return {pagesNeeded : Math.ceil(totalRecords / this.pageLimit)}
})
//console.log("total" + totalRecords);
//console.log("page limit"+this.pageLimit);
//console.log("Needed" + this.state.pagesNeeded );
})
this.fetchCallBack();
}
renderSuggestions() {
//const { suggestions } = this.state;
const tableStyle = {
'tableLayout': 'fixed',
'overflowWrap': 'break-word'
}
return (
<Table style={tableStyle} celled>
{this.state.suggestions.length === 0 ?
(<Table.Body>
<Table.Cell colSpan="7">
<div className="ui fluid warning icon message">
<Icon name="exclamation triangle" size="huge" color="orange"/>
<div className="content">
<Header>No Records Found</Header>
<p>Try Seaching by one of the following:</p>
<ul>
<dt>Name</dt>
<dt>Order Number</dt>
<dt>Address (Shipping or Billing )</dt>
<dt>Phone Number</dt>
<dt>Email</dt>
</ul>
</div>
</div>
</Table.Cell>
</Table.Body>)
: (
<>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Order#</Table.HeaderCell>
<Table.HeaderCell>Billing Address</Table.HeaderCell>
<Table.HeaderCell>Shipping Address</Table.HeaderCell>
<Table.HeaderCell>Email</Table.HeaderCell>
<Table.HeaderCell>Phone Number</Table.HeaderCell>
<Table.HeaderCell>Sales Channel</Table.HeaderCell>
<Table.HeaderCell>Order Date</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{this.state.suggestions.map((item, index) => (
<Table.Row className="hoverRow">
<Table.Cell key={index} onClick={() => this.addToQuery(item)}>
{item.customerPO}
</Table.Cell>
<Table.Cell>
{item.billToAddress}
</Table.Cell>
<Table.Cell>{item.shipToAddress}</Table.Cell>
<Table.Cell>{item.email}</Table.Cell>
<Table.Cell>{item.phone}</Table.Cell>
<Table.Cell>{item.customerContact}</Table.Cell>
<Table.Cell>{item.dateCreated}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</>
)
}
<Pagination key={this.state.pagesNeeded} tableCols="7" pagesNeeded={this.state.pagesNeeded} btnLimit={5} pageClick={this.handlePageClick} currPage={this.state.page} pageLimit={this.pageLimit}/>
</Table>
);
}
handleIconClick(){
console.log('icon clicked ' + this.state.Query )
}
render() {
const {text} = this.state
//console.log(this.state)
return (
<>
<div className="App-Component">
<div className="App-Search">
<Input icon={{ name: 'search', circular: true, link: true, onClick: () => this.handleIconClick() }} placeholder="Search" value={text} type="text" onKeyUp={this.keyUpHandler} onKeyDown={this.keyDownHandler} className="App-Search"/>
{this.renderSuggestions()}
</div>
</div>
</>
);
}
}
export default SearchOrderBar;
Here is the pagination but I don't think this matters as much for the solution. It is relevant for the page button click.
import React, {Component} from 'react';
import {Input, Label, Table, Icon, Header, Menu} from 'semantic-ui-react'
/**
* Helper Method for creating a range of Numbers
* Range )( )
*/
const range = (from, to, step = 1) => {
let i = from;
const range = [];
while (i<=to) {
range.push(i);
i+=step;
}
}
export default class Pagination extends Component {
constructor(props){
super(props)
const { totalRecords = null, pageNeighbours = 0, rangeCount_URL = this.props.rangeCount_URL, pageArray = [] } = props;
this.pageArray = typeof pageArray === 'array' ? pageArray : [];
}
renderPagination = () => {
//console.log("hello from pagination");
let n = this.props.pagesNeeded;
let pArray = [];
let page = this.props.currPage;
//console.log(n)
if (page > 1){
pArray.push(<Menu.Item as='a' icon onClick={() => this.props.pageClick(page-1)}>
<Icon name='chevron left' />
</Menu.Item>)
}
for(let i = (page >1 ? page-1: page); pArray.length < (page > this.props.btnLimit ? this.props.btnLimit+1 : this.props.btnLimit); i++){
//console.log(i);
pArray.push(<Menu.Item index={i} className={i == page ? 'active' : ''} onClick={() => this.props.pageClick(i)} as='a'>{i}</Menu.Item>)
}
if (page < n){
pArray.push(<Menu.Item as='a' icon onClick={() => this.props.pageClick(page+1)}>
<Icon name='chevron right' />
</Menu.Item>)
}
this.pageArray = pArray;
return pArray;
}
render(){
const pageCount = (() => {
const totalRecords = this.totalRecords;
if(totalRecords > 0){
return (this.totalPages = Math.ceil(this.totalRecords / this.props.pageLimit))
}
})();
//console.log(this.pageArray);
return(
<Table.Footer>
{ this.props.pagesNeeded > 1 &&
<Table.Row>
<Table.HeaderCell colSpan={this.props.tableCols}>
<Menu floated='right' pagination>
{this.renderPagination()}
</Menu>
</Table.HeaderCell>
</Table.Row>
}
</Table.Footer>
)
}
}
setState is batched and invoked asynchronously, meaning when you call to this.setState({page}) then read this.state.page in fetchCallBack you probably get the "old" page and not the new page.
Either pass the page directly to fetchCallBack
this.fetchCallBack(page)
And read the page from it and not directly from the state
Or call it as the second argument of setState which is a callback that react will invoke right after the state has been updated.
this.setState({ page }, this.fetchCallBack);
At the point fetchCallBack is called, this.state.page is not updated yet because setState is called asynchronously, that's why it's using the old value. Try this:
handlePageClick(page) {
this.setState({ page }, this.fetchCallBack);
}
The callback syntax allows you to run the function in the next iteration.
I have written the following infinite scroll component in React:
import React from 'react'
import { uniqueId, isUndefined, hasVerticalScrollbar, hasHorizontalScrollbar, isInt, throttle } from '../../../js/utils';
export default class BlSimpleInfiniteScroll extends React.Component {
constructor(props) {
super(props)
this.handleScroll = this.handleScroll.bind(this)
this.itemsIdsRefsMap = {}
this.isLoading = false
this.node = React.createRef()
}
componentDidMount() {
const {
initialId
} = this.props
let id
if (initialId) {
if (typeof initialId === "function") {
id = initialId()
}
else {
id = initialId
}
this.scrollToId(id)
}
}
componentDidUpdate(prevProps) {
if (
this.isLoading
&&
prevProps.isInfiniteLoading
&&
!this.props.isInfiniteLoading
) {
const axis = this.axis()
const scrollProperty = this.scrollProperty(axis)
const offsetProperty = this.offsetProperty(axis)
this.scrollTo(scrollProperty, this.node.current[offsetProperty])
this.isLoading = false
}
}
itemsRenderer(items) {
const length = items.length
let i = 0
const renderedItems = []
for (const item of items) {
renderedItems[i] = this.itemRenderer(item.id, i, length)
i++
}
return renderedItems
}
itemRenderer(id, i, length) {
const {
itemRenderer,
isInfiniteLoading,
displayInverse
} = this.props
let renderedItem = itemRenderer(id, i)
if (isInfiniteLoading) {
if (!displayInverse && (i == length - 1)) {
renderedItem = this.standardLoadingComponentWrapperRenderer(id, renderedItem)
}
else if (i == 0) {
renderedItem = this.inverseLoadingComponentWrapperRenderer(id, renderedItem)
}
}
const ref = this.itemsIdsRefsMap[id] || (this.itemsIdsRefsMap[id] = React.createRef())
return (
<div className="bl-simple-infinite-scroll-item"
key={id}
ref={ref}>
{renderedItem}
</div>
)
}
loadingComponentRenderer() {
const {
loadingComponent
} = this.props
return (
<div className="bl-simple-infinite-scroll-loading-component"
key={uniqueId()}>
{loadingComponent}
</div>
)
}
loadingComponentWrapperRenderer(id, children) {
return (
<div className="bl-simple-infinite-scroll-loading-component-wrapper"
key={id}>
{children}
</div>
)
}
standardLoadingComponentWrapperRenderer(id, renderedItem) {
return this.loadingComponentWrapperRenderer(id, [
renderedItem,
this.loadingComponentRenderer()
])
}
inverseLoadingComponentWrapperRenderer(id, renderedItem) {
return this.loadingComponentWrapperRenderer(id, [
this.loadingComponentRenderer(),
renderedItem
])
}
axis() {
return this.props.axis === 'x' ? 'x' : 'y'
}
scrollProperty(axis) {
return axis == 'y' ? 'scrollTop' : 'scrollLeft'
}
offsetProperty(axis) {
return axis == 'y' ? 'offsetHeight' : 'offsetWidth'
}
scrollDimProperty(axis) {
return axis == 'y' ? 'scrollHeight' : 'scrollWidth'
}
hasScrollbarFunction(axis) {
return axis == 'y' ? hasVerticalScrollbar : hasHorizontalScrollbar
}
scrollToStart() {
const axis = this.axis()
this.scrollTo(
this.scrollProperty(axis),
!this.props.displayInverse ?
0
:
this.scrollDimProperty(axis)
)
}
scrollToEnd() {
const axis = this.axis()
this.scrollTo(
this.scrollProperty(axis),
!this.props.displayInverse ?
this.scrollDimProperty(axis)
:
0
)
}
scrollTo(scrollProperty, scrollPositionOrPropertyOfScrollable) {
const scrollableContentNode = this.node.current
if (scrollableContentNode) {
scrollableContentNode[scrollProperty] = isInt(scrollPositionOrPropertyOfScrollable) ?
scrollPositionOrPropertyOfScrollable
:
scrollableContentNode[scrollPositionOrPropertyOfScrollable]
}
}
scrollToId(id) {
if (this.itemsIdsRefsMap[id] && this.itemsIdsRefsMap[id].current) {
this.itemsIdsRefsMap[id].current.scrollIntoView()
}
}
handleScroll() {
const {
isInfiniteLoading,
infiniteLoadBeginEdgeOffset,
displayInverse
} = this.props
if (
this.props.onInfiniteLoad
&&
!isInfiniteLoading
&&
this.node.current
&&
!this.isLoading
) {
const axis = this.axis()
const scrollableContentNode = this.node.current
const scrollProperty = this.scrollProperty(axis)
const offsetProperty = this.offsetProperty(axis)
const scrollDimProperty = this.scrollDimProperty(axis)
const currentScroll = scrollableContentNode[scrollProperty]
const currentDim = scrollableContentNode[offsetProperty]
const scrollDim = scrollableContentNode[scrollDimProperty]
const finalInfiniteLoadBeginEdgeOffset = !isUndefined(infiniteLoadBeginEdgeOffset) ?
infiniteLoadBeginEdgeOffset
:
currentDim / 2
let thresoldWasReached = false
let memorizeLastElementBeforeInfiniteLoad = () => { }
if (!displayInverse) {
thresoldWasReached = currentScroll >= (scrollDim - finalInfiniteLoadBeginEdgeOffset)
}
else {
memorizeLastElementBeforeInfiniteLoad = () => {
// TODO
}
thresoldWasReached = currentScroll <= finalInfiniteLoadBeginEdgeOffset
}
if (thresoldWasReached) {
this.isLoading = true
memorizeLastElementBeforeInfiniteLoad()
this.props.onInfiniteLoad()
}
}
}
render() {
const {
items
} = this.props
return (
<div className="bl-simple-infinite-scroll"
ref={this.node}
onScroll={this.handleScroll}
onMouseOver={this.props.onInfiniteScrollMouseOver}
onMouseOut={this.props.onInfiniteScrollMouseOut}
onMouseEnter={this.props.onInfiniteScrollMouseEnter}
onMouseLeave={this.props.onInfiniteScrollMouseLeave}>
{this.itemsRenderer(items)}
</div>
)
}
}
And I use it like this in a chat app I am writing:
...
<BlSimpleInfiniteScroll items={chat.messages}
ref={this.infiniteScrollComponentRef}
initialId={() => lastOfArray(chat.messages).id}
itemRenderer={(id, i) => this.messageRenderer(id, i, chat.messages)}
loadingComponent={<BlLoadingSpinnerContainer />}
isInfiniteLoading={isChatLoading}
displayInverse
infiniteLoadBeginEdgeOffset={void 0}
infiniteLoadingBeginBottomOffset={void 0}
onInfiniteLoad={() => this.props.onLoadPreviousChatMessages(chat.id)}
onInfiniteScrollMouseEnter={this.handleInfiniteScrollMouseEnter}
onInfiniteScrollMouseLeave={this.handleInfiniteScrollMouseLeave} />
...
The problem is that as soon as I scroll until the thresold and onInfiniteLoad is called, before the loading spinner is showed and after the data has been loaded the scroll freezes and the component becomes unresponsive.
How can I resolve this issue?
When I render the spinner container and after the previous loaded messages, shouldn't React just append the new divs retaining the previously added items in order to maintain the component performant?
If not, what key concepts of React I am missing?
Thank you for your attention!
UPDATE: Here are the additional components:
BlOrderChat represents a chat window and renders BlSimpleInfiniteScroll:
import React from 'react'
import BlOrderChatMessage from './BlOrderChatMessage';
import { isEmpty, uniqueId } from '../../../js/utils';
import { chatSelector } from '../selectors';
import BlLoadingSpinnerContainer from '../../core/animation/loading/BlLoadingSpinnerContainer';
import BlSimpleInfiniteScroll from '../../core/scroll/BlSimpleInfiniteScroll';
export default class BlOrderChat extends React.Component {
static BL_ORDER_CHAT_MESSAGE_ID_ATTR_PREFIX = 'blOrderChatMessage'
constructor(props) {
super(props)
this.messageRenderer = this.messageRenderer.bind(this)
this.infiniteScrollComponentRef = React.createRef()
}
scrollToBottom() {
this.infiniteScrollComponentRef.current && this.infiniteScrollComponentRef.current.scrollToStart()
}
messageRenderer(messageId, index, messages) {
const {
currentUser, chat
} = this.props
const message = messages[index]
const length = messages.length
const fromUser = chat.users.items[message.from_user_id]
const itemComponentRender = (children) => (
<div key={messageId}>
{children}
</div>
)
const messageIdAttr = `${BlOrderChat.BL_ORDER_CHAT_MESSAGE_ID_ATTR_PREFIX}${message.id}`
const renderMessageComponent = () => (
<BlOrderChatMessage id={messageIdAttr}
key={uniqueId() + message.id}
message={message.message}
sentUnixTs={message.sent_unix_ts}
currentUser={currentUser}
fromUser={fromUser}
usersInvolvedInChatLength={chat.users.order.length} />
)
let children = []
if (index === 0) {
// First message.
children = [
<div key={uniqueId()} className="bl-padding"></div>,
renderMessageComponent()
]
}
else if (index === length - 1) {
// Last message.
children = [
renderMessageComponent(onComponentDidMount),
<div key={uniqueId()} className="bl-padding"></div>
]
}
else {
// Message in the middle.
children = [
renderMessageComponent()
]
}
return itemComponentRender(children)
}
render() {
const {
chat: propsChat, isChatLoading,
currentUser
} = this.props
const chat = chatSelector(propsChat, currentUser)
const chatHasMessages = chat && !isEmpty(chat.messages)
return (
<div className="bl-order-chat">
<div className="bl-order-chat-header">
// ...
</div>
<div className="bl-order-chat-content">
{
(chatHasMessages &&
<div className="bl-order-chat-content-inner">
<div className="bl-order-chat-infinite-scroll">
<BlSimpleInfiniteScroll items={chat.messages}
ref={this.infiniteScrollComponentRef}
initialId={() => lastOfArray(chat.messages).id}
itemRenderer={(id, i) => this.messageRenderer(id, i, chat.messages)}
loadingComponent={<BlLoadingSpinnerContainer />}
isInfiniteLoading={isChatLoading}
displayInverse
infiniteLoadBeginEdgeOffset={void 0}
infiniteLoadingBeginBottomOffset={void 0}
onInfiniteLoad={() => this.props.onLoadPreviousChatMessages(chat.id)}
onInfiniteScrollMouseEnter={this.handleInfiniteScrollMouseEnter}
onInfiniteScrollMouseLeave={this.handleInfiniteScrollMouseLeave} />
</div>
</div>
)
||
(isChatLoading &&
<BlLoadingSpinnerContainer />
)
}
</div>
<div className="bl-order-chat-footer">
// ...
</div>
</div>
)
}
}
BlOrderChatBox, contains BlOrderChat:
import React from 'react'
import BlOrderChat from './BlOrderChat';
import BlAlert from '../../core/alert/BlAlert';
import BlLoadingSpinnerContainer from '../../core/animation/loading/BlLoadingSpinnerContainer';
export default class BlOrderChatBox extends React.Component {
constructor(props) {
super(props)
this.node = React.createRef()
}
render() {
const {
ordId, currentChat,
isCurrentChatLoading, currentUser,
err
} = this.props
return (
<div className="bl-order-chat-box" ref={this.node}>
<div className="bl-order-chat-box-inner">
{
(err &&
<BlAlert type="error" message={err} />)
||
(currentChat && (
// ...
<div className="bl-order-chat-box-inner-chat-content">
<BlOrderChat ordId={ordId}
chat={currentChat}
isChatLoading={isCurrentChatLoading}
onLoadPreviousChatMessages={this.props.onLoadPreviousChatMessages}
currentUser={currentUser} />
</div>
))
||
<BlLoadingSpinnerContainer />
}
</div>
</div>
)
}
}
And here is the component which renders BlOrderChatBox (it is the topmost stateful component):
import React from 'react'
import { POSTJSON } from '../../../js/ajax';
import config from '../../../config/config';
import { newEmptyArrayAble, arrayToArrayAbleItemsOrder, arrayAbleItemsOrderToArray, mergeArrayAbles, newArrayAble, firstOfArrayAble, isArrayAble } from '../../../js/data_structures/arrayable';
export default class BlOrderChatApp extends React.Component {
static NEW_CHAT_ID = 0
static MAX_NUMBER_OF_MESSAGES_TO_LOAD_PER_AJAX = 30
constructor(props) {
super(props)
this.currentUser = globals.USER
this.lastHandleSendMessagePromise = Promise.resolve()
this.newMessagesMap = {}
this.typingUsersDebouncedMap = {}
// Imagine this comes from a database.
const chat = {
// ...
}
const initialState = {
chats: newArrayAble(this.newChat(chat)),
currentChatId: null,
shouldSelectUserForNewChat: false,
newChatReceivingUsers: newEmptyArrayAble(),
isChatListLoading: false,
isCurrentChatLoading: false,
popoverIsOpen: false,
popoverHasOpened: false,
err: void 0,
focusSendMessageTextarea: false,
newChatsIdsMap: {},
currentChatAuthActs: {},
BlOrderChatComponent: null,
}
this.state = initialState
this.handleLoadPreviousChatMessages = this.handleLoadPreviousChatMessages.bind(this)
}
POST(jsonData, callback) {
let requestJSONData
if (typeof jsonData === "string") {
requestJSONData = {
action: jsonData
}
}
else {
requestJSONData = jsonData
}
return POSTJSON(config.ORDER_CHAT_ENDPOINT_URI, {
...requestJSONData,
order_chat_type: this.props.orderChatType,
}).then(response => response.json()).then(json => {
this.POSTResponseData(json, callback)
})
}
POSTResponseData(data, callback) {
if (data.err) {
this.setState({
err: data.err
})
}
else {
callback && callback(data)
}
}
newChat(chat) {
const newChat = {
id: (chat && chat.id) || BlOrderChatApp.NEW_CHAT_ID,
ord_id: this.props.ordId,
users: (chat && chat.users && (isArrayAble(chat.users) ? chat.users : arrayToArrayAbleItemsOrder(chat.users))) || newEmptyArrayAble(),
messages: (chat && chat.messages && (isArrayAble(chat.messages) ? chat.messages : arrayToArrayAbleItemsOrder(chat.messages))) || newEmptyArrayAble(),
first_message_id: (chat && chat.first_message_id) || null,
typing_users_ids_map: (chat && chat.typing_users_ids_map) || {},
}
return newChat
}
isChatNew(chat) {
return (
chat
&&
(chat.id == BlOrderChatApp.NEW_CHAT_ID || this.state.newChatsIdsMap[chat.id])
)
}
loadPreviousChatMessages(chatId, lowestMessageIdOrNull, makeChatIdCurrent) {
this.POST({
act: 'loadPreviousChatMessages',
chat_id: chatId,
lowest_message_id: lowestMessageIdOrNull,
max_number_of_messages_to_load: BlOrderChatApp.MAX_NUMBER_OF_MESSAGES_TO_LOAD_PER_AJAX
}, json => {
this.setState(prevState => {
const chat = prevState.chats.items[chatId]
const messages = arrayToArrayAbleItemsOrder(json.messages)
const newChat = {
...chat,
messages: mergeArrayAbles(messages, chat.messages)
}
const chats = mergeArrayAbles(prevState.chats, newArrayAble(newChat))
return {
...(makeChatIdCurrent ?
{
currentChatId: chatId,
focusSendMessageTextarea: true,
}
:
{
currentChatId: prevState.currentChatId,
}
),
chats,
isCurrentChatLoading: false,
}
})
})
}
loadPreviousChatMessagesIfNotAllLoaded(chatId) {
let lowestMessageIdOrNull
const chat = this.state.chats.items[chatId]
if (
!this.isChatNew(chat)
&&
(lowestMessageIdOrNull = (chat.messages.order.length && firstOfArrayAble(chat.messages).id) || null)
&&
lowestMessageIdOrNull != chat.first_message_id
) {
this.setState({
isCurrentChatLoading: true
}, () => {
this.loadPreviousChatMessages(chat.id, lowestMessageIdOrNull)
})
}
}
handleLoadPreviousChatMessages(chatId) {
this.loadPreviousChatMessagesIfNotAllLoaded(chatId)
}
// ...
render() {
const currentChat = this.state.chats.items[this.state.currentChatId] || null
const err = this.state.err
return (
<div className="bl-order-chat-app">
<BlOrderChatBox currentUser={this.currentUser}
chats={arrayAbleItemsOrderToArray(this.state.chats)}
currentChat={currentChat}
isCurrentChatLoading={this.state.isCurrentChatLoading}
onLoadPreviousChatMessages={this.handleLoadPreviousChatMessages}
err={err} />
</div>
)
}
}
I tried to remove all the irrelevant code to simplify the reading. Also here is the file which contains the chatSelector function (normalizes the chat array-able object) and the *ArrayAble* functions (an array-able object to me is basically an object which maps objects through their ids in items and has an order property which keeps the sort):
import { isUndefined, unshiftArray, findIndex } from "../utils";
export function chatSelector(chat, currentUser) {
const newChat = { ...chat }
newChat.messages = arrayAbleItemsOrderToArray(chat.messages).sort((a, b) => {
const sortByUnixTs = a.sent_unix_ts - b.sent_unix_ts
if (sortByUnixTs == 0) {
return a.id - b.id
}
return sortByUnixTs
})
newChat.users = arrayAbleItemsOrderToArray(chat.users).filter(user => user.id != currentUser.id)
return newChat
}
/**
* Given an array-able object, returns its array representation using an order property.
* This function acts as a selector function.
*
* The array-able object MUST have the following shape:
*
* {
* items: {},
* order: []
* }
*
* Where "items" is the object containing the elements of the array mapped by values found in "order"
* in order.
*
* #see https://medium.com/javascript-in-plain-english/https-medium-com-javascript-in-plain-english-why-you-should-use-an-object-not-an-array-for-lists-bee4a1fbc8bd
* #see https://medium.com/#antonytuft/maybe-you-would-do-something-like-this-a1ab7f436808
*
* #param {Object} obj An object.
* #param {Object} obj.items The items of the object mapped by keys.
* #param {Array} obj.order The ordered keys.
* #return {Array} The ordered array representation of the given object.
*/
export function arrayAbleItemsOrderToArray(obj) {
const ret = []
for (const key of obj.order) {
if (!isUndefined(obj.items[key])) {
ret[ret.length] = obj.items[key]
}
}
return ret
}
export function arrayToArrayAbleItemsOrder(array, keyProp = "id") {
const obj = newEmptyArrayAble()
for (const elem of array) {
const key = elem[keyProp]
obj.items[key] = elem
obj.order[obj.order.length] = key
}
return obj
}
export function newEmptyArrayAble() {
return {
items: {},
order: []
}
}
export function isEmptyArrayAble(arrayAbleObj) {
return !arrayAbleObj.order.length
}
export function mergeArrayAbles(arrayAbleObj1, arrayAbleObj2, prependObj2 = false) {
const obj = newEmptyArrayAble()
for (const key of arrayAbleObj1.order) {
if (isUndefined(arrayAbleObj1.items[key])) {
continue
}
obj.items[key] = arrayAbleObj1.items[key]
obj.order[obj.order.length] = key
}
for (const key of arrayAbleObj2.order) {
if (isUndefined(arrayAbleObj2.items[key])) {
continue
}
if (!(key in obj.items)) {
if (!prependObj2) {
// Default.
obj.order[obj.order.length] = key
}
else {
unshiftArray(obj.order, key)
}
}
obj.items[key] = arrayAbleObj2.items[key]
}
return obj
}
export function newArrayAble(initialItem = void 0, keyProp = "id") {
const arrayAble = newEmptyArrayAble()
if (initialItem) {
arrayAble.items[initialItem[keyProp]] = initialItem
arrayAble.order[arrayAble.order.length] = initialItem[keyProp]
}
return arrayAble
}
export function lastOfArrayAble(obj) {
return (
(
obj.order.length
&&
obj.items[obj.order[obj.order.length - 1]]
)
||
void 0
)
}
Thank you for your help. If there's something missing which I should have included, please, let me know!
UPDATE: Thanks to Sultan H. it has improved, though the scroll still blocks as soon as I get the reply from the server. See it here: https://streamable.com/3nzu0
Any idea on how to improve this behaviour further?
Thanks!
Here is an attempt to resolve the performance issue, it's not preferrable to do tasks inside the Arrow Function that calculates the new state, in this case, at loadPreviousChatMessages you are calculating stuff in the callback, which may yeild to a load while setting the state on that context.
Preferrable Changes, replace this.setState inside your function with this code, all I've done here is clear the context by moving all the tasks out:
const chat = this.state.chats.items[chatId];
const messages = arrayToArrayAbleItemsOrder(json.messages);
const newChat = {
...chat,
messages: mergeArrayAbles(messages, chat.messages);
}
const chats = mergeArrayAbles(prevState.chats, newArrayAble(newChat));
const newState = {
...(
makeChatIdCurrent ?
{
currentChatId: chatId,
focusSendMessageTextarea: true,
}
:
{
currentChatId: this.state.currentChatId,
}
),
chats,
isCurrentChatLoading: false,
};
this.setState(() => newState);
If that doesn't entirely solve the issue, can you tell if there was at least an improvment?