I am working with react table where I am passing the data as prop to react table component which is a mobx state. Also I am using the pagination in for react table data. The issue is when I am updating the state value from the cell edit of react table the state is get updated and when if move to the next pagination and do some changes there and comeback to the first page and it shows only the old state value only. I have included my coding below. Can anyone please give me a solution?
Parent component where I am calling the React Table as child component.
`/** #format */
/* eslint-disable prefer-const */
/* eslint-disable #typescript-eslint/naming-convention */
import { toJS } from "mobx";
import { observer } from "mobx-react-lite";
import React, { useEffect, useState } from "react";
import { Button, Card, CardBody, Row } from "reactstrap";
import styled from "styled-components";
// eslint-disable-next-line import/no-extraneous-dependencies
import { useLocation } from "react-router";
import Rocket from "../../assets/img/raw/rocket-pngrepo-com.png";
import ReactTable from "../ReactTable";
import CategorySelector from "./CategorySelector";
import useStore from "../../mobx/UseStore";
import UserService from "../../services/UserService";
import NotificationManager from "../common/react-notifications/NotificationManager";
import IntlMessages from "../../helpers/IntlMessages";
import { REFACTORDATA } from "../../constants/refactorData";
import "./animation.scss";
import { START } from "../../constants";
import LogService from "../../services/LogService";
const CardLayout = styled(Card)`
/* border: 1px solid red; */
/* width: 50%; */
`;
const SaveButton = styled(Button)`
/* position: absolute; */
justify-content: end;
padding: 0.5rem 2rem 0.5rem 2rem;
span {
font-size: 2rem;
}
`;
// const TextArea = styled.textarea`
// /* background-color: #151a30; */
// color: #ffffff;
// `;
const OutPutViewSection = () => {
const { UIState, UserState } = useStore();
// const processedData = REFACTORDATA(UIState.processedData);
// finding current screen
function getCurrentScreenName() {
// eslint-disable-next-line react-hooks/rules-of-hooks
const location = useLocation();
//
const { pathname } = location;
const urlParts = pathname.split("/");
const lastPart = urlParts[urlParts.length - 1];
return lastPart;
}
const currentScreen = getCurrentScreenName();
// bring metadata id for updating database
const { metaDataId, metaData } = UIState;
// statelocal for controlling the category and set data origin
const [dataOrigin, setDataOrigin] = useState(metaData ? metaData.dataOrigin : "");
const [isSelectModalOpen, setIsSelectModalOpen] = useState(false);
const toggleSelectModal = () => {
setIsSelectModalOpen(!isSelectModalOpen);
};
// handling change in dataorigin
const onChangeDataOrigin = ({ target }) => {
const { value } = target;
setDataOrigin(value);
};
// table columns
const onSaveData = async () => {
if (!dataOrigin) {
NotificationManager.info("plase-add-data-origin", null, null, null, null, null);
return;
}
let response;
let uType;
let reqData;
if (currentScreen === "manageCVStatus" || currentScreen === "cvStatus") {
// eslint-disable-next-line #typescript-eslint/no-shadow
const { orgId, adminId, templateId, docPath, processType, totalPages } = metaData;
// while updating from metadata userId is populated
let { userId, _id } = metaData;
// eslint-disable-next-line no-underscore-dangle
userId = userId._id;
reqData = {
orgId,
adminId,
userId,
templateId,
metaDataId: UIState.metaDataId,
docPath,
processedData: UIState.processedData,
processType,
totalPages,
dataOrigin,
};
response = await UserService.saveProcessedData(reqData);
// updating only data origin and processed data which may change
UIState.cvMetaDataOperations("update", _id, {
dataOrigin,
processedDataId: { _id: UIState.currentProcessedDataId, processedData: UIState.processedData },
});
UIState.setProcessedData(response.processedData);
// update state with latest data
UIState.updateCvData(response.metaDataId, response.updatedData, response.processedData);
}
if (currentScreen === "cvProcessing" || currentScreen === "cvProcess") {
const { userId, orgId, adminId, userType } = UserState.userData;
uType = userType;
reqData = {
orgId,
adminId,
userId,
templateId: UIState.selectedTemplateId,
metaDataId: UIState.metaDataId,
docPath: UIState.currentFolderPath ? UIState.currentFolderPath : UIState.currentFilePath,
processedData: UIState.processedData,
processType: UIState.processType ? UIState.processType : "-",
totalPages: UIState.totalPages,
dataOrigin,
};
response = await UserService.saveProcessedData(reqData);
}
if (response.success) {
UIState.updateCurrentMetaDataId(response.metaDataId);
NotificationManager.success("Data Saved", "Saved", 1500);
// setting is data saved flag true to prevent deletion of image from server
UIState.updateState("isDataSaved", true);
// after saving data fetch new data
// fetching new data only when processed new cv
if (uType) UIState.fetchMetaData(uType);
}
if (response.error) {
let errorMessage: string = response.error;
if (errorMessage.includes("metaData validation failed")) {
errorMessage = "nofication-name-email-required";
} else if (errorMessage.includes("duplicate key error collection")) {
errorMessage = "notifcation-processed-by-other-user";
} else if (errorMessage.includes("Email is already exists")) {
errorMessage = "notification-email-duplication";
} else {
errorMessage = "notification-error-occured";
}
NotificationManager.error(errorMessage, null, 5000);
}
// set graph status
UserState.setUserGraphApiCallStatus = "false";
};
const validate = (e) => {
e.preventDefault();
if (!dataOrigin) {
NotificationManager.error("data-origin-is-required", null, 5000);
} else {
onSaveData();
}
};
// * cleanup
// eslint-disable-next-line
useEffect(() => {
return () => {
setDataOrigin("");
};
}, []);
const action = "extractProcess";
const templateId = undefined;
// multipage
const onMultiImageProcess = async () => {
// const action = "tempProcess";
const markedData: any = { ...UIState.annotatePages };
const reqData = {
socketId: UIState.socketId,
userId: UserState.userData.userId,
orgId: UserState.userData.orgId,
multiPageInvoiceClasses: markedData,
processType: UIState.processType,
action,
totalPages: UIState.totalPages,
folderPath: UIState.currentFolderPath,
...UIState.templateConfigurations,
templateId,
};
await UserService.processImage(reqData);
};
// PROCESSING SINGLE PAGE
const onSingleImageProcess = async () => {
const markedData = UIState.annotatePages[UIState.currentSelectedPage];
// const action = "tempProcess";
const reqData = {
socketId: UIState.socketId,
userId: UserState.userData.userId,
orgId: UserState.userData.orgId,
imagePath: UIState.currentFilePath,
singlePageInvoiceClasses: [...markedData],
processType: UIState.processType,
action,
totalPages: UIState.totalPages,
...UIState.templateConfigurations,
templateId,
};
await UserService.processImage(reqData);
};
const onProcess = async () => {
try {
// set processing only for cv processing not on create edit
// if (isCreateEditScreen() === false)
UIState.setLoadingState("isProcessing", true);
if (UIState.totalPages === 1) {
await onSingleImageProcess();
}
if (UIState.totalPages > 1) {
await onMultiImageProcess();
}
UIState.increamentCVProcessingProgress(START);
LogService.info("CLASS COORDINATES 🔭 ", toJS(UIState.annotatePages));
// if (isCreateEditScreen()) {
// UIState.setLoadingState("isProcessing", false);
// history.push("/user/manageTemplate");
// }
} catch (error) {
LogService.error(error);
}
};
return (
<>
{/* Rocket animation code here */}
{!UIState.processedData && currentScreen === "cvProcess" && (
<Row>
{/* until auto processing is on going don't show extract data button */}
<div className="d-flex flex-column justify-content-center align-items-center m-auto mb-3">
{/* Extract Data */}
{UIState.isAutoProcessDone && !UIState.processedData && (
<Button color="success" className="font-weight-bold" onClick={onProcess}>
<IntlMessages id="extract-data" />
</Button>
)}
</div>
<div className="OutputViewSection__filler_div" />
<div className="d-flex justify-content-center align-items-center p-5">
<div
className={`w-50 ${UIState.isProcessing ? "vibrate-1" : ""} ${
UIState.isProcessedDataEmmited ? "slide-out-top" : ""
}`}
>
<img src={Rocket} alt="rocket" width="100%" style={{ transform: "rotate(-45deg)" }} />
</div>
<div>
<p className="h4 text-dark">{UIState.cvProcessingProgress}%</p>
</div>
</div>
</Row>
)}
{/* save button */}
<CardLayout className="mb-4" style={{ display: UIState.processedData ? "block" : "none" }}>
<CardBody>
<div className="d-flex flex-column justify-content-center align-items-center mt-3">
{!UIState.isDataSaved && currentScreen === "cvProcess" && (
<p className="h5">
<IntlMessages id="extraction-complated-save-data" />
</p>
)}
<SaveButton color="success" pill className="m-1" size="xl" outline onClick={onSaveData}>
<IntlMessages id="Save" />
</SaveButton>
</div>
<Row className="d-flex flex-column pl-4 pr-4 mb-3">
<p className="h6 font-weight-bold">
<IntlMessages id="cvstatus.data-origin" />
</p>
<textarea value={dataOrigin} onChange={onChangeDataOrigin} />
</Row>
{/* categorey and data origin fields show only when metadataid is set */}
{metaDataId && (
<div className="d-flex justify-content-end mb-3">
{/* <textarea className="textinput" value={category} onChange={onChangeCategory} onBlur={onCategoryFocusLoss} /> */}
{isSelectModalOpen && <CategorySelector isOpen={isSelectModalOpen} toggle={toggleSelectModal} />}
{/* <Select options={options} /> */}
<Button onClick={toggleSelectModal}>
<IntlMessages id="select-category" />
</Button>
</div>
)}
{UIState.processedData && <ReactTable data={UIState.processedData} />}
</CardBody>
</CardLayout>
</>
);
};
export default observer(OutPutViewSection);`
`
ReactTable.tsx
`/** #format */
/* eslint-disable react/jsx-props-no-spreading */
import { observer } from "mobx-react-lite";
import React from "react";
import { usePagination, useTable } from "react-table";
import { Pagination, PaginationItem, PaginationLink, Table } from "reactstrap";
// import Pagination from "../../containers/pages/Pagination";
import useStore from "../../mobx/UseStore";
import EditableCell from "./components/EditableCell";
import { ReactTableStyle } from "./Style";
// Set our editable cell renderer as the default Cell renderer
const defaultColumn = {
Cell: EditableCell,
};
// export default function Index({ columns, data, updateMyData, skipPageReset }) {
function Index({ data }) {
const { UIState } = useStore();
// table columns
const columns = React.useMemo(
() => [
{
Header: "Label",
accessor: "label",
},
{
Header: "Data",
accessor: "data",
},
// { Header: "Action", accessor: "action" },
],
[]
);
const [skipPageReset, setSkipPageReset] = React.useState(false);
const updateMyData = (rowIndex, columnId, value, isChecked) => {
// We also turn on the flag to not reset the page
setSkipPageReset(true);
UIState.updateProcessedData({ rowIndex, columnId, value, isChecked });
};
// Use the state and functions returned from useTable to build your UI
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page,
// Instead of using 'rows', we'll use page,
// which has only the rows for the active page
// The rest of these things are super handy, too ;)
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize },
} = useTable(
{
columns,
data,
defaultColumn,
autoResetPage: !skipPageReset,
initialState: { pageIndex: 0, pageSize: 5 },
pageCount: 2,
updateMyData,
},
usePagination
);
// useEffect(() => {
// setPageSize(2);
// }, []);
return (
<ReactTableStyle>
<div className="tableWrap">
<Table {...getTableProps()}>
{/* <thead>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column: any) => (
<th
{...column.getHeaderProps({
className: column.collapse ? "collapse" : "",
})}
>
{column.render("Header")}
</th>
))}
</tr>
))}
</thead> */}
<tbody {...getTableBodyProps()}>
{console.log()}
{page.map((row, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell: any) => (
// <td
// {...cell.getCellProps({
// // className: cell.column.collapse ? "collapse" : "",
// className: "d-flex",
// })}
// >
<div>{cell.render("Cell")}</div>
// </td>
))}
{row.cells.map((cell) => {
console.log(cell);
return "";
})}
</tr>
);
})}
</tbody>
</Table>
</div>
<div className="Footer">
{/* <Pagination
currentPage={pageIndex - 1}
firstIsActive={!canPreviousPage}
lastIsActive={!canNextPage}
numberLimit={3}
totalPage={pageCount}
onChangePage={gotoPage}
/> */}
<Pagination aria-label="Page navigation " listClassName="justify-content-center" size="sm">
<PaginationItem>
<PaginationLink className="first" onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
<i className="simple-icon-control-start" />
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink className="prev" onClick={() => previousPage()} disabled={!canPreviousPage}>
<i className="simple-icon-arrow-left" />
</PaginationLink>
</PaginationItem>
{/* {[...new Array(pageOptions.length).keys()].map((pageNumber) => (
<PaginationItem active={pageIndex === pageNumber}>
<PaginationLink onClick={() => gotoPage(pageNumber)}>{pageNumber + 1}</PaginationLink>
</PaginationItem>
))} */}
<PaginationItem active>
<PaginationLink onClick={() => gotoPage(pageIndex)}>{pageIndex + 1}</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink className="next" onClick={() => nextPage()} disabled={!canNextPage}>
<i className="simple-icon-arrow-right" />
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink className="last" onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
<i className="simple-icon-control-end" />
</PaginationLink>
</PaginationItem>
</Pagination>
</div>
</ReactTableStyle>
);
}
export default observer(Index);
`
The child component that I am using as cell.
`/`** #format */
import { InputGroup, InputGroupAddon, InputGroupText, Input } from "reactstrap";
import React from "react";
import { observer } from "mobx-react-lite";
import styled from "styled-components";
import useStore from "../../../mobx/UseStore";
import IntlMessages from "../../../helpers/IntlMessages";
// Create an editable cell renderer
const EditableCell = ({
value: initialValue,
row: { index, original },
column: { id },
updateMyData, // This is a custom function that we supplied to our table instance
}) => {
const { UIState } = useStore();
// We need to keep and update the state of the cell normally
const [value, setValue] = React.useState(initialValue);
// const [check, setCheck] = React.useState(false);
const onChange = (e) => {
setValue(e.target.value);
};
// We'll only update the external data when the input is blurred
const onBlur = () => {
updateMyData(index, id, value, true);
// setCheck(true);
};
// If the initialValue is changed external, sync it up with our state
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return (
<>
{/* adding editable text only for data not for label */}
{id === "label" && UIState.labelKeyMap[value] && (
<div className="font-weight-bold m-3">
{/* <IntlMessages id={`${value}`} /> */}
<IntlMessages id={`${UIState.labelKeyMap[value]}`} />
</div>
)}
{id === "data" && (
<InputGroup className="mb-3">
<InputGroupAddon addonType="prepend">
<InputGroupText>
<Input
addon
color="secondary"
type="checkbox"
aria-label="Checkbox for following text input"
checked={original.isChecked}
/>
</InputGroupText>
</InputGroupAddon>
<Input type="textarea" className="text-dark" width="100" value={value} onChange={onChange} onBlur={onBlur} />
</InputGroup>
)}
</>
);
};
export default observer(EditableCell);
`
The function that update the mobx state in another file.
`updateProcessedData({ rowIndex, columnId, value, isChecked }) {
console.log(`processed data -> ${toJS(JSON.stringify(this.processedData))}`);
this.processedData.forEach((row: any, index) => {
if (index === rowIndex) {
// updating the index of process data obj as with map not working
this.processedData[index] = {
...this.processedData[rowIndex],
[columnId]: value,
isChecked,
};
}
console.log(`processed after -> ${toJS(JSON.stringify(this.processedData))}`);
return row;
});
}`
I want to persist the data that I made changes in the page even i am navigating to other pages
Related
I have a project, and this project contains several interfaces, and among these interfaces there is an interface for uploading an image, and the problem is in the deletion icon. When you click on it, a modal appears, but the element is deleted before the modal appears.
How can i solve the problem?
this file display a list of instructions that contains upload Image
import '../../../styles/input/index.scss';
import '../../../styles/dropzone/index.scss';
import { Button, Col, message, Modal, Row, Spin, Upload, UploadFile } from 'antd';
import { FunctionComponent, useCallback, useRef, useState } from 'react';
import { motion, useAnimation } from 'framer-motion';
import { defaultTranstion } from '../../../constants/framer';
import { Controller } from 'react-hook-form';
import FromElemnetWrapper from '../form-element-wrapper';
import { Delete, UploadCloud } from 'react-feather';
import { getBase64 } from '../../../utils/get-base64';
import _ from 'lodash';
import config from '../../../api/nuclearMedicineApi/config';
import { FormattedMessage } from 'react-intl';
import BasicModal from '../modal';
import { UploadOutlined } from '#ant-design/icons';
import axios from 'axios';
import { IFormError } from '../general-form-containner';
interface DropzoneProps {
name: string;
control: any;
rules?: any;
label: string;
disabled?: boolean;
multiple?: boolean;
accept?: string;
refType?: number;
defaultFileList?: any;
onRemove?: any;
customRequest?: (option: any) => void;
onProgress?: any;
}
const Dropzone: FunctionComponent<DropzoneProps> = ({
name,
control,
rules,
label,
disabled,
multiple,
accept,
refType,
defaultFileList,
onRemove,
customRequest,
onProgress
}) => {
const focusController = useAnimation();
const errorController = useAnimation();
const [previewVisible, setpreviewVisible] = useState(false);
const [previewImage, setpreviewImage] = useState('');
const handleCancel = () => setpreviewVisible(false);
const handlePreview = async (file: any) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}
setpreviewImage(file?.preview ?? file.url);
setpreviewVisible(true);
};
const [isModalOpen, setIsModalOpen] = useState(false);
const [errors, setErrors] = useState<IFormError[]>([]);
const [visibleModal, setVisibleModal] = useState(false);
const [removePromise, setRemovePromise] = useState();
const [deleteVisible, setdeleteVisible] = useState<boolean>(false);
const onDeleteHandle = () => {
setdeleteVisible(false);
};
const deletehandleCancel = () => {
setdeleteVisible(false);
};
let resolvePromiseRef = useRef<((value: boolean) => void) | undefined>();
// let resolvePromiseRef = useRef<HTMLInputElement | null>(null)
const handleRemove = useCallback(() =>{
const promise = new Promise(resolve => {
resolvePromiseRef.current = resolve
});
setVisibleModal(true);
return promise;
}, [])
const handleOkModalRemove = useCallback(() => {
if (resolvePromiseRef.current) {
resolvePromiseRef.current(true)
}
}, [removePromise]);
const handleCancelModalRemove = useCallback(() => {
if (resolvePromiseRef.current) {
resolvePromiseRef.current(false);
setVisibleModal(false)
}
}, [removePromise]);
return (
<>
<FromElemnetWrapper
focusController={focusController}
errorController={errorController}
label={label}
required={rules.required?.value}
>
<Controller
control={control}
name={name}
rules={rules}
render={({
field: { onChange, onBlur, value, name, ref },
fieldState: { invalid, error },
}) => {
if (invalid) {
errorController.start({ scale: 80 });
} else {
errorController.start(
{ scale: 0 },
{ ease: defaultTranstion.ease.reverse() },
);
}
return (
<div
onBlur={() => {
onBlur();
focusController.start({ scale: 0 });
}}
onFocus={() => {
focusController.start({ scale: 80 });
}}
className='relative'
>
<div className='upload-container'>
<form
className='dropzone needsclick'
id='demo-upload'
action='/upload'
>
{/* <> */}
<Upload
action={`${config.baseUrl}api/services/app/Attachment/Upload`}
headers={config.headers}
ref={ref}
multiple={multiple}
disabled={disabled}
data={{ RefType: refType }}
listType='picture'
fileList={value}
id={name}
accept={accept}
onPreview={handlePreview}
onRemove={handleRemove}
iconRender={
() => {
return <Spin style={{ marginBottom: '12px', paddingBottom: '12px' }}></Spin>
}
}
progress={{
strokeWidth: 3,
strokeColor: {
"0%": "#f0f",
"100%": "#ff0"
},
style: { top: 12 }
}}
beforeUpload={
(file) => {
console.log({ file });
return true
}
}
// onProgress= {(event: any) => (event.loaded / event.total) * 100}
// onChange={(e) =>
// onChange(e.fileList)
// }
// onChange={(response) => {
// console.log('response: ', response);
// if (response.file.status !== 'uploading') {
// console.log(response.file, response.fileList);
// }
// if (response.file.status === 'done') {
// message.success(`${response.file.name}
// file uploaded successfully`);
// } else if (response.file.status === 'error') {
// message.error(`${response.file.name}
// file upload failed.`);
// }
// else if (response.file.status === 'removed') {
// message.error(`${response.file.name}
// file upload removed.`);
// }
// }}
>
<div className='upload-button'>
<div className='wrapper'>
<motion.div
className='fas fa-angle-double-up'
whileHover={{
y: [
0, -2, 2,
0,
],
transition: {
duration: 1.5,
ease: 'easeInOut',
yoyo: Infinity,
},
}}
>
<UploadCloud
style={{
margin: '.2rem',
display:
'inline-block',
}}
color='white'
size={20}
/>
Upload
</motion.div>
</div>
</div>
</Upload>
{/*
<Modal
title="Are you sure?"
visible={visibleModal}
onOk={handleOkModalRemove}
onCancel={handleCancelModalRemove}
/> */}
<BasicModal
header={
<>
<FormattedMessage id={'confirmdeletion'} />
</>
}
headerType='error'
content={
<>
<Row>
<Col span={8} offset={4}>
<Button
type='primary'
className='savebtn'
onClick={onDeleteHandle}
style={{
cursor:
Object.keys(errors).length !==
0
? 'not-allowed'
: 'pointer',
}}
>
<FormattedMessage id={'affirmation'} />
</Button>
</Col>
<Col span={8} offset={4}>
<Button
type='default'
className='savebtn'
onClick={deletehandleCancel}
style={{
cursor:
Object.keys(errors).length !==
0
? 'not-allowed'
: 'pointer',
}}
>
<FormattedMessage id={'cancel'} />
</Button>
</Col>
</Row>
</>
}
isOpen={visibleModal}
footer={false}
width='35vw'
handleCancel={handleCancelModalRemove}
handleOk={handleOkModalRemove}
/>
{/* {_.isEmpty(value) && (
<div className='dz-message needsclick'>
<FormattedMessage id='dropfileshere' />
</div>
)} */}
<BasicModal
isOpen={previewVisible}
header={<FormattedMessage id="Preview image" />}
footer={false}
handleCancel={handleCancel}
content={<img
alt='example'
style={{ width: '100%' }}
src={previewImage}
/>}
/>
{/* </> */}
</form>
</div>
{invalid && (
<p className='form-element-error'>
{error?.message}
</p>
)}
</div>
);
}}
/>
</FromElemnetWrapper>
</>
);
};
export default Dropzone;
Why it doesn't work?
https://ant.design/components/upload
onRemove - A callback function, will be executed when removing file button is clicked, remove event will be prevented when return value is false or a Promise which resolve(false) or reject
You have to return a promise which resolves to false or return false
How to solve it?
You have to return a promise so first of all, you need a ref or a state to store resolve function of that promise so you can call it in modal
const resolvePromiseRef = useRef<((value: boolean) => void) | undefined>();
Then you will need to assign the resolve function to the ref and return the promise
Now onRemove will wait for your promise to resolve
const handleRemove = () =>{
const promise = new Promise(resolve => {
resolvePromiseRef.current = resolve
});
setVisibleModal(true);
return promise;
}
Now your functions handlers
const handleOkModalRemove = useCallback(() => {
if (resolvePromiseRef.current) {
resolvePromiseRef.current(true)
}
}, [removePromise]);
const handleCancelModalRemove = useCallback(() => {
if (resolvePromiseRef.current) {
resolvePromiseRef.current(false)
}
}, [removePromise]);
You can also use the state, but I recommend ref because it doesn't rerender the component when changed.
How in react-table to avoid cell rerender when changing data? In simple terms: each cell is an input, onBlur a patch to the backend is fired and again the data is retrieved from the server. It seems that only the cells with my custom component are being re-rendered, and, of course, the whole table, which is probably intended.
A basic table component built according to the examples from the official react-table documentation. Only added scrolling ScrollArea and styling from Table.
import React from "react";
import {
flexRender,
getCoreRowModel,
useReactTable,
} from "#tanstack/react-table";
import { Table } from "#mantine/core";
import { ScrollArea } from "#mantine/core";
const TableModel = ({ data, columns }) => {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<ScrollArea style={{ height: "50vh" }}>
<Table style={{ position: "relative" }}>
<thead
style={{ position: "sticky", top: "0", backgroundColor: "black" }}
>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</Table>
</ScrollArea>
);
};
export default TableModel;
Here I defined the columns and downloaded the data from the server. IsLoading fires only on the first render.
import React from "react";
import TableModel from "./TableModel";
import { createColumnHelper } from "#tanstack/react-table";
import { v4 as uuidv4 } from "uuid";
import { usePZItemsData } from "../hooks/usePZItemsData";
import { CustomInputHover } from "./CustomInputHover";
export const ItemList = ({ id }) => {
const { data: pzItemsData, isLoading } = usePZItemsData(id);
const columnHelper = createColumnHelper();
const columns = React.useMemo(
() => [
columnHelper.accessor("id"),
columnHelper.accessor("product.name", { header: "Nazwa" }),
columnHelper.accessor("quantity", {
header: "Nazwa",
cell: (i) => <CustomInputHover i={i} />,
}),
],
[]
);
return (
<>
{isLoading ? (
<h1>Loading...</h1>
) : (
<TableModel data={pzItemsData?.data} columns={columns} />
)}
</>
);
};
Custom input cell. Using input or input from the framework slowed down the rendering terribly. I used this solution, which reduces the rerender time drastically, but it is still long. In short, when you click on a div, a Input appears and then its validation.
import { NumberInput, TextInput } from "#mantine/core";
import { usePrevious } from "#mantine/hooks";
import React, { useEffect } from "react";
import { useUpdatePZItem } from "../hooks/usePZItemActions";
export const CustomInputHover = ({ i }) => {
const [clicked, setClicked] = React.useState(false);
const [value, setValue] = React.useState();
const previousValue = usePrevious(value);
const { mutate, isError, isSuccess } = useUpdatePZItem();
useEffect(() => {
setValue(i.row.original.quantity);
}, [i.row.original.quantity]);
const handleBlur = (e) => {
if (previousValue != value) {
mutate({
id: i.row.original.id,
data: { quantity: value },
});
} else {
if (!isError) {
setClicked(false);
}
}
};
React.useEffect(() => {
if (isSuccess) {
setClicked(false);
}
}, [isSuccess]);
React.useEffect(() => {
if (isError) {
setClicked(true);
}
}, [isError]);
const handleOnKeyDown = (e) => {
if (e.key !== "Tab" && e.key !== "Shift" && e.key !== "Alt") {
setClicked(true);
console.log(e);
}
};
return (
<>
{clicked ? (
<NumberInput
style={{ width: "180px" }}
autoFocus
value={value}
size="xs"
radius="xs"
variant="filled"
onBlur={(e) => handleBlur(e)}
async
error={isError && "Podaj poprawną wartość"}
onFocus={(e) => e.target.select()}
onChange={(val) => setValue(val)}
/>
) : (
<div
onClick={() => setClicked(true)}
tabIndex={0}
onKeyDown={(e) => handleOnKeyDown(e)}
>
{i.row.original.quantity}
</div>
)}
</>
);
};
I have been working on refactoring code for a crypto trading platform - changing the class based components to functional components.
The main aim is to display every info of selected coin pair of binance.com using websocket.
If coin pair is changed by selecting option, we have to close old socket and create new socket connection, at first the plan was to implement this in TradeCoinSelection.tsx, but there is a part that is getting coin pair list and calling function of 'parent' (index.tsx) with changed info.
What is happening is the socket thread is not killed, even though there is close() function, the close() function is not finding the old socket thread because ws is resetting in re-rendering.
This means the front end seems to be alternating between new and old pair data as the old pair socket thread is not closed properly.
Any help is appreciated.
Functional component code - TradeCoinSection.tsx
import { useState, useEffect } from 'react';
import Select, { Option } from 'rc-select';
import { Row, Col, Card } from 'reactstrap';
import TradeApi from '#api/TradeApi';
import { IExchangeListAPIResponse } from '#api/types/exchange';
import { connect } from 'react-redux';
import { AppDispatch, RootState } from '#redux/store';
import { fetchExchangeCoinListAction } from '#redux/actions/Exchange';
import { ICoinSymbolResponse } from '#api/types/trade';
import ReconnectingWebSocket from 'reconnecting-websocket';
function TradeCoinSection (props: any) {
const [disabled] = useState<boolean>(false);
const [symbolLoader, setSymbolLoader] = useState<boolean>(false);
const [symbolList, setSymbolList] = useState<Array<ICoinSymbolResponse>>(props.exchangeCoinList);
const [binanceTickerData, setBinanceTickerData] = useState<Record<string, string>>({
h: "",
c: "",
b: "",
p: "",
l: "",
v: "",
q: "",
});
const [didLoadSymbolList] = useState<boolean>(false);
let ws : ReconnectingWebSocket = new ReconnectingWebSocket('wss://stream.binance.com:9443/ws/bnbusdt#ticker');
ws.close();
const closeAndOpenTickerStream = (symbol: string) => {
if(symbol != "") {
ws.close()
ws = new ReconnectingWebSocket('wss://stream.binance.com:9443/ws/'+symbol.toLowerCase()+'#ticker');
ws.addEventListener('open', () => {
// console.log('connected');
});
// eslint-disable-next-line #typescript-eslint/no-explicit-any
ws.onmessage = (evt: any) => {
// listen to data sent from the websocket server
const message = JSON.parse(evt.data);
setBinanceTickerData(message);
};
ws.addEventListener('close', () => {
// console.log('disconnected');
});
}
};
// eslint-disable-next-line #typescript-eslint/no-explicit-any
const onSymbolChangeHandler = (value: string, option: any) => {
let baseAssest = '';
let quoteAssest = '';
if (value) {
const splitArray = option.key.split('-');
if (splitArray.length > 1) {
baseAssest = splitArray[0];
quoteAssest = splitArray[1];
}
}
console.log("1,", value);
props.symbolChangeHandler(value, quoteAssest, baseAssest);
closeAndOpenTickerStream(value);
};
const fetchCoinPairList = async () => {
try {
setSymbolLoader(true);
// setErrorList([]);
console.log("extra-1")
if(props.selectedExchangeAccountSite != '') {
const response = await TradeApi.getCoinPair(props.selectedExchangeAccountSite);
// const response = await TradeApi.getCoinPair();
setSymbolList(response.data);
props.fetchExchangeCoinListAction(response.data);
let symbolObj = response.data[0];
if (props.selectedSymbol) {
response.data.find((obj) => {
if (obj.symbol === props.selectedSymbol) {
symbolObj = obj;
}
});
}
onSymbolChangeHandler(symbolObj.symbol, {
key: symbolObj.symbol_pair,
value: symbolObj.symbol,
});
}
// eslint-disable-next-line #typescript-eslint/no-explicit-any
} catch (err: any) {
} finally {
setSymbolLoader(false);
console.log(symbolLoader)
}
};
useEffect(() => {
if (props.exchangeCoinList && props.exchangeCoinList.length < 1) {
fetchCoinPairList();
}
return () => {
ws.close();
}
}, []);
useEffect(() => {
if(props.selectedExchangeAccountSite !== '' && !didLoadSymbolList) {
fetchCoinPairList();
}
if (
props.exchangeCoinList &&
props.exchangeCoinList.length > 0 &&
!props.selectedSymbol_state
) {
setSymbolList(props.exchangeCoinList);
let symbolObj = props.exchangeCoinList[0];
if (props.selectedSymbol) {
props.exchangeCoinList.find((obj: any) => {
if (obj.symbol === props.selectedSymbol) {
symbolObj = obj;
}
});
}
onSymbolChangeHandler(symbolObj.symbol, {
key: symbolObj.symbol_pair,
value: symbolObj.symbol,
});
}
}, [props.selectedExchangeAccount]);
const temp_disabled = disabled;
return (
<Card className="shadow-xl p-3">
<Row className="less-gutters align-items-start">
<Col xl={2} lg={12} md="12">
<Row className="less-gutters flex-column flex-sm-row flex-xl-column">
{!props.dropdownDisabled && (
<Col sm={6} md={6} lg={6} xl={12}>
<Select
placeholder="Select account"
style={{ width: '100%' }}
value={props.selectedExchangeAccount}
// eslint-disable-next-line #typescript-eslint/no-explicit-any
onChange={(exchangeId: number, options: any) =>
props.exchangeAccountChangeHandler(exchangeId, options.label, options.site)
}
className="mb-2"
>
{props.exchangeList.length > 0
? props.exchangeList.map((obj: IExchangeListAPIResponse) => (
<Option key={obj.id} value={obj.id} label={obj.account_name} site={obj.exchange}>
<b>{obj.account_name}</b>
</Option>
))
: null}
</Select>
</Col>
)}
<Col sm={6} md={6} lg={6} xl={12}>
<Select
showSearch={true}
disabled={
props.dropdownDisabled ? props.dropdownDisabled : temp_disabled
}
placeholder="Select Symbol"
style={{ width: '100%' }}
value={props.selectedSymbol}
onChange={onSymbolChangeHandler}
className="currency-dropdown"
>
{symbolList.length > 0
? symbolList.map((obj: ICoinSymbolResponse) => (
<Option value={obj.symbol} key={obj.symbol_pair}>
<b>{obj.symbol_pair}</b>
</Option>
))
: null}
</Select>
</Col>
</Row>
</Col>
<Col xl={10} lg={12} md="12">
<div className="d-flex w-100 market-data-container mt-md-3 mt-lg-3 mt-xl-0">
<div className="w-100 d-flex align-items-start mt-3 mt-md-0 sm:flex-col">
<div className="last-price-block col-md-4 col-lg-3 pl-0 pl-md-0 pl-lg-3 pr-3">
<div className="market-data-last-price">
{binanceTickerData.h &&
parseFloat(binanceTickerData.c).toFixed(8)}
</div>
<div className="market-data-worth">
{binanceTickerData.b &&
`${parseFloat(binanceTickerData.b).toFixed(8)}`}
</div>
</div>
<div className="market-data d-flex flex-wrap w-100 justify-content-start pl-0 pl-md-3 mt-3 mt-md-0">
<div className="market-data-block w-33 mb-md-3 mb-lg-2">
<div className="market-data-block-title">24h Change</div>
<div className="market-data-block-value">
{binanceTickerData.p &&
parseFloat(binanceTickerData.p).toFixed(2)}{' '}
{binanceTickerData.p}%
</div>
</div>
<div className="market-data-block w-33 mb-md-3 mb-lg-2">
<div className="market-data-block-title">24h High</div>
<div className="market-data-block-value">
{binanceTickerData.h &&
parseFloat(binanceTickerData.h).toFixed(8)}
</div>
</div>
<div className="market-data-block w-33 mb-md-3 mb-lg-2">
<div className="market-data-block-title">24h Low</div>
<div className="market-data-block-value">
{binanceTickerData.l &&
parseFloat(binanceTickerData.l).toFixed(8)}
</div>
</div>
<div className="market-data-block w-33 mb-md-3 mb-lg-0">
<div className="market-data-block-title">
24h Volume({props.quoteAssest})
</div>
<div className="market-data-block-value">
{binanceTickerData.v &&
parseFloat(binanceTickerData.v).toFixed(2)}
</div>
</div>
<div className="market-data-block w-33 mb-md-0 mb-lg-0">
<div className="market-data-block-title">
24h Volume({props.baseAssest})
</div>
<div className="market-data-block-value">
{binanceTickerData.q &&
parseFloat(binanceTickerData.q).toFixed(2)}
</div>
</div>
</div>
</div>
</div>
</Col>
</Row>
</Card>
);
}
const mapStateToProps = (state: RootState, props: any) => ({
exchangeCoinList: state.exchange.exchangeCoinList,
ownProps: props
});
const mapDispatchToProps = (dispatch: AppDispatch) => ({
fetchExchangeCoinListAction: (data: Array<ICoinSymbolResponse>) =>
dispatch(fetchExchangeCoinListAction(data)),
});
// export default TradeCoinSection;
export default connect(mapStateToProps, mapDispatchToProps)(TradeCoinSection);
Parent (Index.tsx)
import { useState, useEffect } from 'react';
import { withRouter } from 'react-router-dom';
import { Row, Col } from 'reactstrap';
import { BarChart2 } from 'react-feather';
import { connect } from 'react-redux';
// Import Api and constants;
import TradeApi from '#api/TradeApi';
import AppConfig from '#config/config';
import { getCatchError } from '#helpers/ErrorHandler';
import { RootState } from '#redux/store';
import TradeCoinSection from '#pages/Admin/Trade/Trade/partials/TradeCoinSection';
import TradeChartSection from '#pages/Admin/Trade/Trade/partials/TradeChartSection';
import OrderHistorySection from '#pages/Admin/Trade/OrderHistory/partials/OrderHistorySection';
import OpenOrderSection from '#pages/Admin/Trade/OpenOrder/partials/OpenOrderSection';
import PageTitle from '#components/ui/PageTitle/PageTitle';
import TradeAddSection from '#pages/Admin/Trade/Trade/partials/TradeAddSection';
import EmptyDataComponent from '#components/EmptyDataComponent';
import LoaderComponent from '#components/LoaderComponent';
import ExchangeModal from '../../Settings/partials/ExchangeModal';
function TradePage (props: any) {
const [ symbolPriceObj, setSymbolPriceObj ] = useState({
symbol: '',
price: '',
status: '',
filter_data: null,
symbol_info: null,
} as any);
const [symbolDetailLoader, setSymbolDetailLoader] = useState<boolean>(false);
const [isAddExchangeModalOpen, setIsAddExchangeModalOpen] = useState<boolean>(false);
const [selectedExchangeAccount, setSelectedExchangeAccount] = useState<number>(0);
const [selectedExchangeAccountLabel, setSelectedExchangeAccountLabel] = useState<string>('');
const [selectedExchangeAccountSite, setSelectedExchangeAccountSite] = useState<string>('');
const [selectedSymbol, setSelectedSymbol] = useState<string>('');
const [baseAssest, setBaseAssest] = useState<string>('');
const [quoteAssest, setQuoteAssest] = useState<string>('');
const [errorList, setErrorList] = useState<Array<string>>([]);
const [lastRecordInsertedAt, setLastRecordInsertedAt] = useState<number>(0);
const postOrderHandler = () => {
setLastRecordInsertedAt(Date.now());
}
const addExchangeModalHandler = (isAdded?: boolean) => {
setIsAddExchangeModalOpen(!isAddExchangeModalOpen);
if (isAdded) {
setTimeout(() => {
window.location.reload();
}, 2000);
}
}
const exchangeAccountChangeHandler = (exchangeId: number, exchangeLabel: string, exchangeSite: string) => {
setSelectedExchangeAccount(exchangeId);
setSelectedExchangeAccountLabel(exchangeLabel);
setSelectedExchangeAccountSite(exchangeSite);
}
const symbolChangeHandler = (symbol: string, quoteAssest: string, baseAssest: string) => {
setSelectedSymbol(symbol);
setQuoteAssest(quoteAssest);
setBaseAssest(baseAssest);
}
const fetchSymbolDetailHandler = async () => {
try {
setSymbolDetailLoader(true);
setErrorList([]);
const response = await TradeApi.getCoinPairPrice(selectedExchangeAccount, selectedSymbol);
const coinObj = {
filter_data: response.data.filter_data,
symbol_info: response.data.symbol_info,
price: response.data.price,
symbol: '',
status: '',
};
setSymbolPriceObj(coinObj);
} catch (error: any) {
console.log(symbolDetailLoader, errorList);
setSymbolDetailLoader(false);
setErrorList(getCatchError(error));
} finally {
setSymbolDetailLoader(false);
}
}
useEffect(() => {
if(selectedSymbol != '')
fetchSymbolDetailHandler();
}, [selectedSymbol])
useEffect(() => {
document.title = `${AppConfig.REACT_APP_NAME} | Trade`;
if (props.exchangeList && props.exchangeList.length > 0) {
const exchangeList = props.exchangeList;
setSelectedExchangeAccount(exchangeList[0].id);
setSelectedExchangeAccountLabel(exchangeList[0].account_name);
setSelectedExchangeAccountSite(exchangeList[0].exchange);
postOrderHandler();
}
}, []);
useEffect(() => {
if (props.exchangeList.length > 0) {
const exchangeList = props.exchangeList;
setSelectedExchangeAccount(exchangeList[0].id);
setSelectedExchangeAccountLabel(exchangeList[0].account_name);
setSelectedExchangeAccountSite(exchangeList[0].exchange);
postOrderHandler();
}
}, [props]);
useEffect(() => {
postOrderHandler();
}, [selectedExchangeAccount])
return (
<>
<PageTitle
titleHeading="Trade"
titleDescription="Buy and sell all of your currencies"
pageTitleIconBox={true}
pageTitleIcon={<BarChart2 className="display-2 text-primary" />}
/>
{props.exchangeLoader && <LoaderComponent isSmallLoader={true} />}
{!props.exchangeLoader &&
props.exchangeList &&
props.exchangeList.length > 0 && (
<>
<Row className="less-gutters buysell">
<Col xl={3} lg={4} md={5} className="mb-4 mb-md-0 pl-2 pr-2">
<TradeAddSection
selectedSymbol={selectedSymbol}
selectedExchangeAccount={selectedExchangeAccount}
selectedExchangeSite={selectedExchangeAccountSite}
symbolPriceObj={symbolPriceObj}
quoteAssest={quoteAssest}
baseAssest={baseAssest}
selectedAccountLabel={selectedExchangeAccountLabel}
postOrderHandler={postOrderHandler}
/>
</Col>
<Col xl={9} lg={8} md={7}>
<TradeCoinSection
selectedSymbol={selectedSymbol}
exchangeList={props.exchangeList}
selectedExchangeAccount={selectedExchangeAccount}
selectedExchangeAccountSite={selectedExchangeAccountSite}
exchangeAccountChangeHandler={exchangeAccountChangeHandler}
symbolChangeHandler={symbolChangeHandler}
quoteAssest={quoteAssest}
baseAssest={baseAssest}
/>
{selectedSymbol && (
<TradeChartSection selectedSymbol={selectedSymbol} />
)}
</Col>
</Row>
{selectedExchangeAccount && (
<OpenOrderSection
selectedExchangeAccount={selectedExchangeAccount}
lastRecordInsertedAt={lastRecordInsertedAt}
selectedSymbol={selectedSymbol}
isTradePage={true}
/>
)}
{selectedExchangeAccount && (
<OrderHistorySection
selectedExchangeAccount={selectedExchangeAccount}
lastRecordInsertedAt={lastRecordInsertedAt}
selectedSymbol={selectedSymbol}
isTradePage={true}
/>
)}
</>
)}
{!props.exchangeLoader &&
props.exchangeList &&
props.exchangeList.length < 1 && (
<Row className="less-gutters buysell">
<Col xl={12} lg={12} md={12}>
<EmptyDataComponent
message="No exchange connected. Please connect an exchange first"
buttonText="Connect Exchange"
buttonClickHandler={addExchangeModalHandler}
showExchangeLinkSection={true}
/>
<ExchangeModal
isOpen={isAddExchangeModalOpen}
toggler={addExchangeModalHandler}
/>
</Col>
</Row>
)}
</>
);
}
const mapStateToProps = (state: RootState, props: any) => ({
exchangeList: state.exchange.exchangeList,
exchangeLoader: state.exchange.exchangeListStatusObj.loading,
ownProps: props
});
export default withRouter(connect(mapStateToProps, {})(TradePage));
I am trying to filter an array of cars whenever any of my 3 filters changes. The way I see fit -for the moment- with React is to: check all filters whenever any of them changes, in this order: handleOnChangeDateFilter (where I set all my cars to the initial state if this filter is not set), handleOnChangeFuelFilter, handleOnChangeSeats. I displayed some console.logs but for some reason (I think it might be something related to rendering and when useEffect runs. It runs after every re-render OR after any of the values in the dependency array changes, right?) (PS: I know I could somehow check to compare the new values with the old values by somehow capturing the prevState, so that I do not run a specific filter again with no reason, but forgot how to capture prevState. Will do that later, as it should work fine without that now, too)
So, any idea why the below code does not work?
import moment from "moment";
import { Link } from "react-router-dom";
import Spinner from "../components/Spinner";
import { Col, Row, DatePicker, Select } from "antd";
import React, { useState, useEffect, useRef } from "react";
import { useSelector, useDispatch } from "react-redux";
import DefaultLayout from "../components/DefaultLayout";
import { getAllCars } from "../redux/actions/carsActions";
import cron from "node-cron";
const { RangePicker } = DatePicker;
function Home() {
const { cars } = useSelector((state) => state.carsReducer); // state aici e de fapt store-ul nostru!
const [timeRange, setTimeRange] = useState([]);
const [fuelTypes, setFuelTypes] = useState([]);
const [seatNumbers, setSeatNumbers] = useState([]);
const { loading } = useSelector((state) => state.alertsReducer);
const [totalCars, setTotalcars] = useState([]);
const dispatch = useDispatch();
useEffect(() => {
console.log("merge!");
dispatch(getAllCars());
}, []);
useEffect(() => {
console.log("cars bro", cars);
setFuelTypes(
[...new Set(cars.map((car) => car.fuelType.toLowerCase()))].sort()
);
setSeatNumbers(
[...new Set(cars.map((car) => car.capacity))].sort((a, b) => a - b)
);
setTotalcars(cars);
}, [cars]);
useEffect(() => {
console.log("refilter is triggered!");
refilter();
}, [timeRange, fuelTypes, seatNumbers]);
function handleOnChangeDateFilter() {
console.log("intra in handleOnChangeDateFilter", timeRange);
console.log("totalCars:", totalCars);
if (timeRange.length === 0) {
console.log(
"No date range filter specified! Will reinitialize and then proceed with other filters!"
);
setTotalcars(cars);
return;
}
var selectedFrom = moment(timeRange[0], "MMM DD yyyy HH:mm");
var selectedTo = moment(timeRange[1], "MMM DD yyyy HH:mm");
console.log("selectedFrom", selectedFrom, "selectedTo", selectedTo);
var temp = [];
for (var car of cars) {
if (car.bookedTimeSlots.length === 0) {
temp.push(car);
} else {
var toAdd = true;
for (var booking of car.bookedTimeSlots) {
if (
selectedFrom.isBetween(booking.from, booking.to) ||
selectedTo.isBetween(booking.from, booking.to) ||
moment(booking.from).isBetween(selectedFrom, selectedTo) ||
moment(booking.to).isBetween(selectedFrom, selectedTo)
) {
console.log(
`${car.name} is booked from ${booking.from} to ${booking.to}! Will NOT be added!`
);
toAdd = false;
break; // we should not add this car to the displayed cars if we found a minimum of one booking that
// intersects non-available time range
}
}
if (toAdd) temp.push(car);
}
}
setTotalcars(temp);
}
function handleOnChangeFuelFilter() {
console.log(`intra in handleOnChangeFuelFilter:`, totalCars);
if (fuelTypes === []) {
console.log("no fuel filter specified! Will leave function!");
return;
}
var temp = [];
for (var car of totalCars) {
if (fuelTypes.includes(car.fuelType.toLowerCase())) {
// console.log(`${car.name} is of type ${car.fuelType}! Will be added!`);
temp.push(car);
}
}
setTotalcars(temp);
}
function handleOnChangeSeatsFilter() {
console.log(`intra in handleOnChangeSeatsFilter:`, totalCars);
if (seatNumbers === []) {
console.log("No seat filter specified! Will leave function!");
return;
}
var temp = [];
for (var car of totalCars) {
if (seatNumbers.includes(car.capacity)) {
// console.log(`${car.name} has ${car.capacity}! Will be added!`);
temp.push(car);
}
}
setTotalcars(temp);
}
function onRangePickerFilterChange(values) {
console.log("============STARTED============");
console.log("onRangePickerFilterChange ->", values);
setTimeRange(values);
}
function onSeatsFilterChange(values) {
console.log("============STARTED============");
console.log("onSeatsFilterChange ->", values);
setSeatNumbers(values);
}
function onFuelFilterChange(values) {
console.log("============STARTED============");
console.log("onFuelFilterChange ->", values);
setFuelTypes(values);
}
function refilter() {
// console.log('values refilter:', values);
// console.log('============STARTED============');
handleOnChangeDateFilter();
console.log("AFTER DATE FILTER:", totalCars);
handleOnChangeFuelFilter();
console.log("AFTER FUEL FILTER:", totalCars);
handleOnChangeSeatsFilter();
console.log("AFTER SEATS FILTER(final):", totalCars);
console.log("============FINISHED============");
}
return (
<DefaultLayout>
<Row className="mt-3" justify="center">
<Col lg={20} sm={24} className="d-flex justify-content-left">
<RangePicker
// ref={refRangePicker}
showTime={{ format: "HH:mm" }}
format="MMM DD yyyy HH:mm"
// onChange={handleOnChangeDateFilter}
// onChange={refilter}
onChange={onRangePickerFilterChange}
/>
<Select
// onChange={handleFuelFilterChange}
// onChange={refilter}
onChange={onFuelFilterChange}
allowClear
mode="multiple"
placeholder="Fuel type"
style={{ width: "10%" }}
>
{fuelTypes.map((fuelType, index) => {
return (
<Select.Option key={index} value={fuelType}>
{fuelType}
</Select.Option>
);
})}
</Select>
<Select
// onChange={refilter}
onChange={onSeatsFilterChange}
allowClear
mode="multiple"
placeholder="Seats"
style={{ width: "10%" }}
>
{seatNumbers.map((seatNumber, index) => {
return (
<Select.Option key={index} value={seatNumber}>
{seatNumber}
</Select.Option>
);
})}
</Select>
</Col>
</Row>
{loading === true && <Spinner />}
<Row justify="center" gutter={16}>
{totalCars.map((car) => {
return (
<Col lg={5} sm={24} xs={24}>
<div className="car p-1 bs1">
<img src={car.image} className="carimg" />
<div className="car-content d-flex align-items-center justify-content-between">
<div className="text-left pl-2">
<p>{car.name}</p>
<p>
<sup>{car.rentPerHour} eur</sup>/<sub>Hour</sub>
</p>
</div>
<div className="text-left pl-2">
<p>Seats: {car.capacity}</p>
</div>
<div className="text-left pl-2">
<p>Fuel: {car.fuelType}</p>
</div>
<div>
<button className="btn1 mr-2">
<Link to={`/booking/${car._id}`}>Book Now</Link>
</button>
</div>
</div>
</div>
</Col>
);
})}
</Row>
</DefaultLayout>
);
}
export default Home;
The problem was with the filtering logic, not the useEffects or anything else strictly related to React. Here is the full working code (could be improved, but this is a working solution):
import moment from "moment";
import { Link } from "react-router-dom";
import Spinner from "../components/Spinner";
import { Col, Row, DatePicker, Select } from "antd";
import React, { useState, useEffect, useRef } from "react";
import { useSelector, useDispatch } from "react-redux";
import DefaultLayout from "../components/DefaultLayout";
import { getAllCars } from "../redux/actions/carsActions";
const { RangePicker } = DatePicker;
function Home() {
const { cars } = useSelector((store) => {
console.log("in callback:", store.carsReducer);
return store.carsReducer;
});
const [timeRange, setTimeRange] = useState([]);
const [fuelTypes, setFuelTypes] = useState([]); // all present fuel types in my DB (initialized in first useEffect)
const [seatNumbers, setSeatNumbers] = useState([]); // all present seat capacity numbers in my DB (initialized in first useEffect)
const [selectedTimeRange, setSelectedTimeRange] = useState([]);
const [selectedFuelTypes, setSelectedFuelTypes] = useState([]); // selected fuel types (selected by the user from the antd Select.Option component)
const [selectedSeatNumbers, setSelectedSeatNumbers] = useState([]); // selected seat numbers (selected by the user from the antd Select.Option component)
const { loading } = useSelector((store) => store.alertsReducer);
const [totalCars, setTotalCars] = useState([]);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getAllCars());
}, []);
useEffect(() => {
setFuelTypes(
[...new Set(cars.map((car) => car.fuelType.toLowerCase()))].sort()
); // all possible fuel types
setSeatNumbers(
[...new Set(cars.map((car) => car.capacity))].sort((a, b) => a - b)
); // all possible seat numbers
setTotalCars(cars); // all cars
}, [cars]);
useEffect(() => {
refilter();
}, [selectedTimeRange, selectedFuelTypes, selectedSeatNumbers]);
function refilter() {
// no need for 3 separate functions (that would mean setting the state of the totalCars variable 3 times
// using the `setTotalCars` function - which -if I am not mistaken- might trigger unexpected behavior
// with the re-rendering of the component)
// temporary array; eventually, totalCars will be assigned `temp` value using the `setTotalCars` method!
var temp = [];
// logica timeRange
if (selectedTimeRange.length === 0) {
console.log("user selected no time range");
temp = [...cars];
} else {
console.log('user selected timeRange: ', selectedTimeRange);
var selectedFrom = moment(selectedTimeRange[0], "MMM DD yyyy HH");
var selectedTo = moment(selectedTimeRange[1], "MMM DD yyyy HH");
for (var car of cars) {
if (car.bookedTimeSlots.length == 0) {
temp.push(car);
} else {
var toBeAdded = true;
for (var booking of car.bookedTimeSlots) {
if (selectedFrom.isBetween(booking.from, booking.to) ||
selectedTo.isBetween(booking.from, booking.to) ||
moment(booking.from).isBetween(selectedFrom, selectedTo) ||
moment(booking.to).isBetween(selectedFrom, selectedTo)) {
toBeAdded = false; // if time range of booking overlaps with selected time range, don't add car to temp!
break;
}
}
if (toBeAdded) {
temp.push(car);
}
}
}
}
// fuelTypes logic
if (selectedFuelTypes.length !== 0){
console.log('User selected fuel types: ', selectedFuelTypes);
temp = temp.filter((car) => selectedFuelTypes.includes(car.fuelType));
console.log(`filtered by fuelTypes: ${temp}`);
}
// seatsNumber logic
if (selectedSeatNumbers.length !== 0){
console.log('User selected seat numbers: ', selectedSeatNumbers);
temp = temp.filter((car) => selectedSeatNumbers.includes(car.capacity));
console.log(`filtered by seatsNumber: ${temp.length}`);
}
// finally, assign filtered values to totalCars!
console.log("temp:", temp);
setTotalCars(temp);
}
return (
<DefaultLayout>
<Row className="mt-3" justify="center">
<Col lg={20} sm={24} className="d-flex justify-content-left">
<RangePicker
showTime={{ format: "HH:mm" }}
format="MMM DD yyyy HH:mm"
onChange={(values) => {
setSelectedTimeRange(values);
}}
/>
<Select
allowClear
mode="multiple"
placeholder="Fuel"
style={{ width: "10%" }}
onChange={(values) => {
setSelectedFuelTypes(values);
}}
>
{fuelTypes.map((fuelType, index) => {
return (
<Select.Option key={index} value={fuelType}>
{fuelType}
</Select.Option>
);
})}
</Select>
<Select
onChange={(values) => {
setSelectedSeatNumbers(values);
}}
allowClear
mode="multiple"
placeholder="Seats"
style={{ width: "10%" }}
>
{seatNumbers.map((seatNumber, index) => {
return (
<Select.Option key={index} value={seatNumber}>
{seatNumber}
</Select.Option>
);
})}
</Select>
</Col>
</Row>
{loading === true && <Spinner />}
<Row justify="center" gutter={16}>
{totalCars.map((car) => {
return (
<Col lg={5} sm={24} xs={24}>
<div className="car p-1 bs1">
<img src={car.image} className="carimg" />
<div className="car-content d-flex align-items-center justify-content-between">
<div className="text-left pl-2">
<p>{car.name}</p>
<p>
<sup>{car.rentPerHour} eur</sup>/<sub>Hour</sub>
</p>
</div>
<div className="text-left pl-2">
<p>Seats: {car.capacity}</p>
</div>
<div className="text-left pl-2">
<p>Fuel: {car.fuelType}</p>
</div>
<div>
<button className="btn1 mr-2">
<Link to={`/booking/${car._id}`}>Book Now</Link>
</button>
</div>
</div>
</div>
</Col>
);
})}
</Row>
</DefaultLayout>
);
}
export default Home;
Hello everyone :D I need your advise/tip. Right now I have a APIDataTable component. It has its rows, columns and etc. This component is responsible to show/build data table on frontend with search bar in it above the table. I have an search bar, which is not functional right now. I want it to search data from data table. What should I start from? How can i make it perform search in Table. Thank you for any advise and tip <3
import React, { useEffect, useState } from "react";
import { plainToClassFromExist } from "class-transformer";
import { Pagination } from "../../models/Pagination";
import {
DataTable,
DataTableHead,
DataTableHeadCell,
DataTableBody,
DataTableRow,
DataTableCell,
} from "../DataTable";
import { request } from "../../api";
import "./index.css";
import { MenuSurface } from "../MenuSurface";
import { IconButton } from "../IconButton";
import { Checkbox } from "../Checkbox";
import { Dialog } from "../Dialog";
import { GridCell, GridRow } from "../Grid";
import { Button } from "../Button";
export class Column<T> {
label: string;
width?: number;
filter?: JSX.Element;
render: (row: T) => JSX.Element | string;
constructor(column: Partial<Column<T>>) {
Object.assign(this, column);
}
}
type APIDataTableProps<T> = {
apiPath?: string;
params?: string;
columns?: Column<T>[];
type: Function;
onRowClick?: (row: T) => void;
};
export const APIDataTable = <T extends object>({
apiPath,
params,
columns,
type,
onRowClick,
}: APIDataTableProps<T>) => {
const [data, setData] = useState<Pagination<T>>(null);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(15);
const [isLoading, setIsLoading] = useState(false);
const [isDialogOpen, setDialogOpen] = useState(false);
const [isMenuSurFaceOpen, setMenuSurfaceOpen] = useState(false);
const [hiddenColumns, setHiddenColumns] = useState<number[]>(
JSON.parse(localStorage.getItem(`hiddenColumns-${apiPath + params}`)) || []
);
const fetchData = async () => {
const urlSearchParams = new URLSearchParams(params);
urlSearchParams.set("page", page.toString());
urlSearchParams.set("page_size", pageSize.toString());
const url = `${apiPath}?${urlSearchParams}`;
const response = await request(url);
const data = plainToClassFromExist(new Pagination<T>(type), response, {
excludeExtraneousValues: true,
});
setData(data);
setIsLoading(false);
};
useEffect(() => {
if (!!apiPath) {
setIsLoading(true);
fetchData();
}
}, [page, pageSize]);
const headCells = columns
.filter((e, i) => !hiddenColumns?.includes(i))
.map((column) => (
<DataTableHeadCell key={column.label} width={column.width}>
{column.label}
</DataTableHeadCell>
));
const rows = data?.results?.map((row, index) => (
<DataTableRow
key={"row-" + index}
onClick={() => !!onRowClick && onRowClick(row)}
>
{columns
.filter((e, i) => !hiddenColumns?.includes(i))
.map((column) => {
return (
<DataTableCell key={column.label} width={column.width}>
<div className="data-table-cell-text">{column.render(row)}</div>
</DataTableCell>
);
})}
</DataTableRow>
));
let uncheckedCheckboxes = hiddenColumns;
const onCheckboxChange = (index: number, value: boolean) => {
if (!value) {
uncheckedCheckboxes.push(index);
//setHiddenColumns(uncheckedCheckboxes);
} else {
const array = [...uncheckedCheckboxes];
array.splice(array.indexOf(index), 1);
uncheckedCheckboxes = array;
}
};
const [isOpen, setIsOpen] = useState(false);
return (
<div className="data-table-container">
<div className="search-test">
<div className="mdc-menu-surface--anchor">
<label
className="mdc-text-field mdc-text-field--filled mdc-text-field--no-label mdc-text-field--with-leading-icon mdc-text-field--with-trailing-icon"
htmlFor="input"
id="search-menu-surface"
>
<IconButton density={-1} icon="search" />
<input
className="mdc-text-field__input "
type="text"
placeholder="Поиск"
id="searchinput"
/>
<IconButton
density={-1}
icon="arrow_drop_down"
onClick={() => {
setMenuSurfaceOpen(true);
}}
/>
</label>
<MenuSurface
isOpen={isMenuSurFaceOpen}
onClose={() => setMenuSurfaceOpen(false)}
fullwidth
>
<div className="data-table-filters-container">
{columns.map(
(column) =>
!!column.filter && (
<div className="data-table-filter">
<div className="data-table-filter-label mdc-typography--subtitle1">
{column.label}
</div>
<div className="data-table-column-filter">
{column.filter}
</div>
</div>
// <GridRow>
// <GridCell span={3}>{column.label}</GridCell>
// <GridCell span={3}>{column.filter}</GridCell>
// </GridRow>
)
)}
{/* <GridCell span={2}> */}
{/* <Button label="Поиск" raised /> */}
{/* <Button
label="Отмена"
raised
onClick={() => {
setIsOpen(false);
}}
/> */}
{/* </GridCell> */}
</div>
</MenuSurface>
</div>
<IconButton
onClick={() => {
setDialogOpen(true);
}}
density={-1}
icon="settings"
/>
<Dialog
isOpen={isDialogOpen}
onOkClick={() => {
localStorage.setItem(
`hiddenColumns-${apiPath + params}`,
JSON.stringify(uncheckedCheckboxes)
);
setDialogOpen(false);
setHiddenColumns(uncheckedCheckboxes);
}}
onCloseClick={() => setDialogOpen(false)}
>
<div
style={{
display: "flex",
flexDirection: "column",
}}
>
{columns.map((column, index) => (
<Checkbox
label={column.label}
onChange={(value) => onCheckboxChange(index, value)}
defaultChecked={!uncheckedCheckboxes.includes(index)}
/>
))}
</div>
</Dialog>
</div>
<DataTable
pagination={true}
count={data?.count}
rowsNumber={data?.results.length}
page={page}
next={data?.next}
previous={data?.previous}
isLoading={isLoading}
onNextClick={() => setPage(page + 1)}
onPreviosClick={() => setPage(page - 1)}
>
<DataTableHead>{headCells}</DataTableHead>
<DataTableBody>{rows}</DataTableBody>
</DataTable>
</div>
);
};
I'm guessing that you want to search bar to effectively filter out rows that don't match. in this case what you want to do is add a filter to the search text (naturally you'll add a state for the search value, but it looks like you'll have that handled.
You'll add your filter here const rows = data?.results?.filter(...).map
You filter function will look something like this
const rows = data?.results.filter((row) => {
// In my own code if I have other filters I just make them return false
// if they don't match
if (
searchText &&
!(
// exact match example
row.field === searchText ||
// case-insensitive example
row.otherField?.toLowerCase().includes(searchText)
// can continue with '||' and matching any other field you want to search by
)
)
return false;
return true;
}).map(...)