Related
long story short I was asked to create a table component using react-table,
in that table by default it uses component input, which when double-clicked it can immediately type.
and secondly, I want for one of the column editableCell to use dropdown. i have tried it but failed.
depedencies:
{
"#tanstack/react-table": "^8.5.22",
"next": "^12.3.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
component used : SubTable
<SubTable
data={dataTable}
handleAdd={addData}
columns={DataColumns}
tableStyle="h-[250px]"
hiddenColumns={["id"]}
showAddButton={true}
showDeleteButton={true}
skipPageReset={skipPageReset}
updateData={updateMyData}
/>
SubTable.tsx
import { FC, useState } from "react";
import {
useFlexLayout,
usePagination,
useTable,
useGlobalFilter,
useSortBy,
} from "react-table";
import styles from "./styles.module.css";
import EditableCell from "./EditableCell";
import Image from "next/image";
export interface SubTableProps {
setIsForm?: stateData;
data: any[];
columns: any[];
headerComponent?: JSX.Element;
page?: (page: number) => void;
showAddButton?: boolean;
showDeleteButton?: boolean;
onClickRow?: (cell: Cell<any, unknown>, row: Row<any>) => void;
hiddenColumns?: string[];
updateData?: (row: any, columnId: string, value: any) => void;
skipPageReset?: boolean;
tableStyle?: string;
handleAdd?: () => void;
handleConfig?: () => void;
deleteLabel?: string;
showBedButton?: boolean;
showConfigButton?: boolean;
}
const SubTable: FC<SubTableProps> = ({
data,
columns,
hiddenColumns,
skipPageReset,
updateData,
setIsForm,
tableStyle,
showAddButton,
showDeleteButton,
showBedButton = false,
showConfigButton = false,
deleteLabel = "Delete",
handleAdd,
handleConfig,
}) => {
const [editableRowIndex, setEditableRowIndex] = useState(null);
const [active, setActive] = useState("");
const [selectedData, setSelectedData] = useState({});
const { getTableProps, getTableBodyProps, headerGroups, page, prepareRow } =
useTable(
{
columns,
data,
// Set our editable cell renderer as the default Cell renderer
defaultColumn: {
Cell: EditableCell,
},
autoResetPage: !skipPageReset ?? false,
initialState: {
pageIndex: 0,
hiddenColumns: hiddenColumns ?? [],
},
updateData,
editableRowIndex,
setEditableRowIndex,
},
useGlobalFilter,
useSortBy,
usePagination,
useFlexLayout ?? null
);
const clickedRow = (e: React.MouseEvent, result: any) => {
const { index, values } = result;
setActive(index);
const currentIndex = index;
if (e.detail == 2) {
if (editableRowIndex !== currentIndex) {
setEditableRowIndex(index);
} else {
setEditableRowIndex(null);
setSelectedData(values);
// data changed here console.log('update row', values)
}
}
};
return (
<>
<div className={`bg-white p-2 text-xs ${tableStyle ?? ""}`}>
{/* */}
<div className={styles.toolbar}>
{showAddButton && (
<>
<button className={styles.btn} type="button" onClick={handleAdd}>
<Image
src="/images/button/add.png"
className="ml-1"
alt="add_icon"
width={16}
height={16}
/>
Add
</button>
<div className={styles.separate}>|</div>
</>
)}
{showConfigButton && (
<>
<button
className={styles.btn}
type="button"
onClick={handleConfig}
>
<Image
src="/images/button/update.svg"
className="ml-1"
alt="add_icon"
width={16}
height={16}
/>
Configuration
</button>
<div className={styles.separate}>|</div>
</>
)}
{showDeleteButton && (
<button
className={styles.btn}
type="button"
onClick={() => {
console.log("delete");
}}
>
<Image
src="/images/button/delete.svg"
className="ml-1"
alt="delete_icon"
width={16}
height={16}
/>
{deleteLabel}
</button>
)}
{showBedButton && (
<button
className={styles.btn}
type="button"
onClick={() => {
console.log("delete");
}}
>
<Image
src="/images/button/edit-undo.png"
className="ml-1"
alt="delete_icon"
width={16}
height={16}
/>
Empty Bed
</button>
)}
</div>
<div className="overflow-x-auto border-l-[#e7e9ec] border-r-[#e7e9ec] print:block max-h-[200px] overflow-y-auto">
<table
className="table-fixed w-full border-x bg-white relative border-collapse"
{...getTableProps()}
>
<thead className="sticky top-0">
{headerGroups.map((headerGroup, idx) => (
<tr {...headerGroup.getHeaderGroupProps()} key={idx}>
{headerGroup.headers.map((column, idx) => (
<th
className="border border-solid font-normal text-lg text-left p-1 bg-green-100"
{...column.getHeaderProps(column.getSortByToggleProps())}
key={idx}
>
{column.render("Header")}
<span>
{column.isSorted
? column.isSortedDesc
? " 🔻"
: " 🔺"
: " ↕"}
</span>
</th>
))}
</tr>
))}
</thead>
<tbody className="overflow-y-auto" {...getTableBodyProps()}>
{page.map((row, idx) => {
prepareRow(row);
return (
<tr
{...row.getRowProps()}
key={idx}
className={`${
active == row.id ? "bg-bgGrey-1" : ""
} cursor-default`}
onClick={(e) => clickedRow(e, row)}
>
{row.cells.map((cell, idx) => {
return (
<td
className="whitespace-nowrap text-ellipsis overflow-hidden border-b border-r border-y p-1"
{...cell.getCellProps()}
key={idx}
>
{cell.render("Cell")}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</>
);
};
export default SubTable;
useSubTable.tsx
import { useState } from "react";
const useSubTable = () => {
const [dataTable, setDataTable] = useState<any>([]);
const [skipPageReset, setSkipPageReset] = useState(false);
const updateMyData = (rowIndex: number, columnId: any, value: any) => {
// We also turn on the flag to not reset the page
setSkipPageReset(true);
setDataTable((old: any[]) =>
old.map((row: any, index: number) => {
if (index === rowIndex) {
return {
...old[rowIndex],
[columnId]: value,
};
}
return row;
})
);
};
return { dataTable, setDataTable, updateMyData, skipPageReset };
};
export default useSubTable;
EditableCell.tsx
import React, { useState, useEffect } from "react";
// Create an editable cell renderer
const EditableCell = (props: any) => {
const {
value: initialValue,
row: { index },
column: { id },
updateData, // This is a custom function that we supplied to our table instance
editableRowIndex, // index of the row we requested for editing
} = props;
// We need to keep and update the state of the cell normally
const [value, setValue] = useState(initialValue);
const onChange = (e: any) => {
setValue(e.target.value);
};
// We'll only update the external data when the input is blurred
const onBlur = () => {
updateData(index, id, value);
};
// If the initialValue is changed externall, sync it up with our state
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return index === editableRowIndex ? (
<input
style={{ width: "100%" }}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
) : (
<p>{value}</p>
);
};
export default EditableCell;
define a column: column.ts
import TypeDropdown from "./TypeDropdown";
export const dataColumns = [
{
Header: "Name",
accessor: "name",
sticky: "left",
},
{
Header: "Order",
accessor: "order",
sticky: "left",
},
{
Header: "Type",
accessor: "type",
sticky: "left",
Cell: TypeDropdown,
},
];
my custom dropdown component: TypeDropdown.tsx
import React, { useState, useEffect } from "react";
import { Select } from "#/client/components/Inputs";
const TypeDropdown = (props: any) => {
const {
value: initialValue,
row: { index },
column: { id },
updateData, // This is a custom function that we supplied to our table instance
editableRowIndex, // index of the row we requested for editing
} = props;
// We need to keep and update the state of the cell normally
const [value, setValue] = useState(initialValue);
const onChange = (e: any) => {
setValue(e.target.value);
updateData(index, id, e.target.value);
};
// If the initialValue is changed externall, sync it up with our state
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return id === "type" ? (
<Select
value={value}
id="typex"
option={[
{ id: 1, name: "TextField", code: "TextField" },
{ id: 2, name: "Combo", code: "Combo" },
]}
showRed={true}
onChange={(e) => onChange(e)}
required={true}
/>
) : (
<p>{value}</p>
);
};
export default TypeDropdown;
lastly my useSubTableProps.tsx
import React, { useMemo, useState } from "react";
import { dataColumns } from "./column";
interface customObject {
[key: string]: any;
}
const useSubTableProps = () => {
const DataColumns = useMemo(() => dataColumns, []);
const [dataTable, setDataTable] = useState<any>([]);
const [skipPageReset, setSkipPageReset] = useState<boolean>(false);
const updateMyData = (rowIndex: number, columnId: any, value: any) => {
// We also turn on the flag to not reset the page
setSkipPageReset(true);
setDataTable((old: customObject[]) =>
old.map((row: any, index: number) => {
if (index === rowIndex) {
return {
...old[rowIndex],
[columnId]: value,
};
}
return row;
})
);
};
const addData = () => {
setDataTable((prev: any) => [
...prev,
{
name: `test`,
order: 0,
type: "TextField",
},
]);
};
return { DataColumns, dataTable, addData, updateMyData, skipPageReset };
};
export default useSubTableProps;
expected visual, what I want in column type is dropdown component
finally, I found the problem and the solution,
after I re-read the documentation about the column options section, it says to use a valid JSX element, after that, I try to use a simple JSX element and it works. Maybe my previous JSX element was not considered valid by react-table
link: React-table column options
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
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));
Every video I've seen regarding to showing or hiding a div, is quite not effective at all if you're making use of a state that's based on true or false, thus when a button is clicked through the .map() all elements that are hidden would be shown, therefore it wouldn't be in great use of all, I guess that's why the element's index should be in use to determine which element should shown or hidden right?
Scenario
So I'm building a social platform for a learning experience, where I map through all my posts in an array, once I click my comment Icon, the comments should be shown for that post, but unfortunately I'm unable to find a solution regarding to the use of functional components.
this is what I have:
import React, { useState, useEffect, useReducer, useRef, useMemo } from "react";
import axios from "axios";
import Cookies from "universal-cookie";
import "../../styles/private/dashboard.css";
import DashboardHeader from "../../components/private/templates/header";
import DashboardSidebar from "../../components/private/templates/sidebar";
import ImageSearchIcon from "#material-ui/icons/ImageSearch";
import VideoLibraryIcon from "#material-ui/icons/VideoLibrary";
import FavoriteIcon from "#material-ui/icons/Favorite";
import SendIcon from "#material-ui/icons/Send";
import { Avatar } from "#material-ui/core";
import { useSelector, useDispatch } from "react-redux";
import { newPost } from "../../redux/actions/posts/new-post";
import { likePost } from "../../redux/actions/posts/like-post";
import { getPosts } from "../../redux/actions/posts/get-posts";
import { unlikePost } from "../../redux/actions/posts/unlike-post";
import { getPostLikes } from "../../redux/actions/posts/get-likes";
import { likePostComment } from "../../redux/actions/posts/like-comment";
import { unlikePostComment } from "../../redux/actions/posts/unlike-comment";
import { newPostComment } from "../../redux/actions/posts/new-post-comment";
import ChatBubbleOutlineIcon from "#material-ui/icons/ChatBubbleOutline";
import LoopIcon from "#material-ui/icons/Loop";
import FavoriteBorderIcon from "#material-ui/icons/FavoriteBorder";
import MoreHorizIcon from "#material-ui/icons/MoreHoriz";
import Pusher from "pusher-js";
import FlipMove from "react-flip-move";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import io from "socket.io-client";
const socket = io.connect("http://localhost:5000");
function Dashboard({
history,
getPost,
getLike,
getAllPosts,
getAllLikes,
likePosts,
unlikePosts,
}) {
const [participants, setParticipants] = useState({});
const cookies = new Cookies();
const [toggle, setToggle] = useState(false);
const [messages, setMessages] = useState("");
const [media, setMedia] = useState(null);
const [posts, setPosts] = useState([]);
const [error, setError] = useState("");
const [comment, setComment] = useState();
const userLogin = useSelector((state) => state.userLogin);
const { user } = userLogin;
const [uname, setUname] = useState(user.name);
const [upic, setUpic] = useState(user.pic);
const dispatch = useDispatch();
const config = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${user.token}`,
},
};
useEffect(() => {
if (!cookies.get("authToken")) {
history.push("/login");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [history]);
useEffect(() => {
axios.get("/api/post/posts", config).then((response) => {
setPosts(response.data);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const handler = (item) => {
setPosts((oldPosts) => {
const findItem = oldPosts.find((post) => post._id === item._id);
if (findItem) {
return oldPosts.map((post) => (post._id === item._id ? item : post));
} else {
return [item, ...oldPosts];
}
});
};
socket.on("posts", handler);
return () => socket.off("posts", handler);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const postHandler = (e) => {
e.preventDefault();
dispatch(newPost(uname, upic, messages, media));
setMessages("");
};
const LikePost = (postId) => {
likePosts(postId, user._id, user.name, user.pic);
};
const UnlikePost = (postId) => {
unlikePosts(postId);
};
const submitComment = (postId) => {
dispatch(newPostComment(postId, uname, upic, comment));
setComment("");
};
const LikeCommentPost = (postId, commentId) => {
dispatch(likePostComment(postId, commentId, user._id, user.name, user.pic));
};
const UnlikeCommentPost = (postId, commentId) => {
dispatch(unlikePostComment(postId, commentId));
};
return error ? (
<span>{error}</span>
) : (
<div className="dashboard">
<DashboardHeader />
<div className="dashboard__container">
<div className="dashboard__sidebar">
<DashboardSidebar />
</div>
<div className="dashboard__content">
<div className="dashboard__contentLeft">
<div className="dashboard__messenger">
<div className="dashboard__messengerTop">
<Avatar src={user.pic} className="dashboard__messengerAvatar" />
<input
type="text"
placeholder={`What's on your mind, ${user.name}`}
value={messages}
onChange={(e) => setMessages(e.target.value)}
/>
<SendIcon
className="dashboard__messengerPostButton"
onClick={postHandler}
/>
</div>
<div className="dashboard__messengerBottom">
<ImageSearchIcon
className="dashboard__messengerImageIcon"
value={media}
onChange={(e) => setMedia((e) => e.target.value)}
/>
<VideoLibraryIcon className="dashboard__messengerVideoIcon" />
</div>
</div>
<div className="dashboard__postsContainer">
<FlipMove>
{posts.map((post, i) => (
<div className="dashboard__post" key={i}>
<MoreHorizIcon className="dashboard__postOptions" />
<div className="dashboard__postTop">
<Avatar
className="dashboard__postUserPic"
src={post.upic}
/>
<h3>{post.uname}</h3>
</div>
<div className="dashboard__postBottom">
<p>{post.message}</p>
{media === null ? (
""
) : (
<div className="dashboard__postMedia">{media}</div>
)}
</div>
<div className="dashboard__postActions">
{toggle ? (
<ChatBubbleOutlineIcon
onClick={() => setToggle(!toggle)}
className="dashboard__actionComment"
/>
) : (
<ChatBubbleOutlineIcon
onClick={() => setToggle(!toggle)}
className="dashboard__actionComment"
/>
)}
<label
id="totalLikes"
className="dashboard__comments"
style={{ color: "forestgreen" }}
>
{post.commentCount}
</label>
{post.likes.find((like) => like.uid === user._id) ? (
<FavoriteIcon
onClick={() => UnlikePost(post._id)}
className="dashboard__actionUnlike"
/>
) : (
<FavoriteBorderIcon
onClick={() => LikePost(post._id)}
className="dashboard__actionLike"
/>
)}
<label
id="totalLikes"
className="dashboard__likes"
style={{ color: "forestgreen" }}
>
{post.likeCount}
</label>
</div>
<div
className={
toggle
? "dashboard__commentContent toggle"
: "dashboard__commentContent"
}
>
<div className="dashboard__postComments">
{post.comments.map((comment) => (
<div
key={comment.toString()}
className="dashboard__postComment"
>
<div className="dashboard__postCommentTop">
<Avatar src={comment.upic} />
<h4>{comment.uname}</h4>
</div>
<p>{comment.message}</p>
<div className="dashboard__postCommentActions">
{comment.likes.find(
(like) => like.uid === user._id
) ? (
<FavoriteIcon
onClick={() =>
UnlikeCommentPost(post._id, comment._id)
}
className="dashboard__actionUnlike"
/>
) : (
<FavoriteBorderIcon
onClick={() =>
LikeCommentPost(post._id, comment._id)
}
className="dashboard__actionLike"
/>
)}
<label
id="totalLikes"
className="dashboard__likes"
style={{ color: "forestgreen" }}
>
{comment.likeCount}
</label>
</div>
</div>
))}
</div>
<div className="dashboard__commentInput">
<input
type="text"
placeholder="Comment post"
value={comment}
onChange={(e) => setComment(e.target.value)}
/>
<button onClick={() => submitComment(post._id)}>
Send
</button>
</div>
</div>
</div>
))}
</FlipMove>
</div>
</div>
<div className="dashboardContentRight"></div>
</div>
</div>
</div>
);
}
Dashboard.propTypes = {
getLike: PropTypes.arrayOf(PropTypes.string),
getPost: PropTypes.arrayOf(PropTypes.string),
likePost: PropTypes.arrayOf(PropTypes.string),
unlikePost: PropTypes.arrayOf(PropTypes.string),
};
function mapStateToProps(state) {
return {
getPost: getPosts(state),
getLike: getPostLikes(state),
likePosts: likePost(state),
unlikePosts: unlikePost(state),
};
}
function mapDispatchToProps(dispatch) {
return {
getAllPosts: (posts) => dispatch(getPosts(posts)),
getAllLikes: (likes) => dispatch(getPostLikes(likes)),
likePosts: (like) => dispatch(likePost(like)),
unlikePosts: (like) => dispatch(unlikePost(like)),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
Extra
here is a unlisted video from youtube, just for incase that you do not understand.
You have a lot of code going there... You should refactor that i smaller component, more easier to read and maintainability.
You can check there the answer i posted, it's the same core issue:
The axios delete functionality is only deleting last user from table, not the one I click on
your toggle is based on the global state, so each post doesn't have its proper way to see if it's open or not.That's why it will open everything
You need to tell which one is open and which ones aren't.
Here i refactored your code to make it work with multiple boxes open, i didn't test it on codesandbox, i let you try, but it wasn't big changes, so it should works.
Please pay attention to these news changes:
useState property openBoxes
Method toggleCommentBox
Method isCommentBoxOpen
I then replaced in your jsx the way you check if the comment box is open or not.
function Dashboard({
history,
getPost,
getLike,
getAllPosts,
getAllLikes,
likePosts,
unlikePosts,
}) {
const [participants, setParticipants] = useState({});
const cookies = new Cookies();
const [openBoxes, setOpenBoxes] = useState([]);
const [messages, setMessages] = useState("");
const [media, setMedia] = useState(null);
const [posts, setPosts] = useState([]);
const [error, setError] = useState("");
const [comment, setComment] = useState();
const userLogin = useSelector((state) => state.userLogin);
const {user} = userLogin;
const [uname, setUname] = useState(user.name);
const [upic, setUpic] = useState(user.pic);
const dispatch = useDispatch();
const config = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${user.token}`,
},
};
useEffect(() => {
if (!cookies.get("authToken")) {
history.push("/login");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [history]);
useEffect(() => {
axios.get("/api/post/posts", config).then((response) => {
setPosts(response.data);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const handler = (item) => {
setPosts((oldPosts) => {
const findItem = oldPosts.find((post) => post._id === item._id);
if (findItem) {
return oldPosts.map((post) => (post._id === item._id ? item : post));
} else {
return [item, ...oldPosts];
}
});
};
socket.on("posts", handler);
return () => socket.off("posts", handler);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
/*
Toggle state of post boxes
*/
const toggleCommentBox = postId => {
if(openBoxes.includes(postId)){
setOpenBoxes(openBoxes.filter(x => x !== postId))
} else {
setOpenBoxes([...openBoxes, postId])
}
}
/*
Returns boolean if comment box is open on this post
*/
const isCommentBoxOpen = postId => openBoxes.includes(postId)
const postHandler = (e) => {
e.preventDefault();
dispatch(newPost(uname, upic, messages, media));
setMessages("");
};
const LikePost = (postId) => {
likePosts(postId, user._id, user.name, user.pic);
};
const UnlikePost = (postId) => {
unlikePosts(postId);
};
const submitComment = (postId) => {
dispatch(newPostComment(postId, uname, upic, comment));
setComment("");
};
const LikeCommentPost = (postId, commentId) => {
dispatch(likePostComment(postId, commentId, user._id, user.name, user.pic));
};
const UnlikeCommentPost = (postId, commentId) => {
dispatch(unlikePostComment(postId, commentId));
};
return error ? (
<span>{error}</span>
) : (
<div className="dashboard">
<DashboardHeader/>
<div className="dashboard__container">
<div className="dashboard__sidebar">
<DashboardSidebar/>
</div>
<div className="dashboard__content">
<div className="dashboard__contentLeft">
<div className="dashboard__messenger">
<div className="dashboard__messengerTop">
<Avatar src={user.pic} className="dashboard__messengerAvatar"/>
<input
type="text"
placeholder={`What's on your mind, ${user.name}`}
value={messages}
onChange={(e) => setMessages(e.target.value)}
/>
<SendIcon
className="dashboard__messengerPostButton"
onClick={postHandler}
/>
</div>
<div className="dashboard__messengerBottom">
<ImageSearchIcon
className="dashboard__messengerImageIcon"
value={media}
onChange={(e) => setMedia((e) => e.target.value)}
/>
<VideoLibraryIcon className="dashboard__messengerVideoIcon"/>
</div>
</div>
<div className="dashboard__postsContainer">
<FlipMove>
{posts.map((post, i) => (
<div className="dashboard__post" key={i}>
<MoreHorizIcon className="dashboard__postOptions"/>
<div className="dashboard__postTop">
<Avatar
className="dashboard__postUserPic"
src={post.upic}
/>
<h3>{post.uname}</h3>
</div>
<div className="dashboard__postBottom">
<p>{post.message}</p>
{media === null ? (
""
) : (
<div className="dashboard__postMedia">{media}</div>
)}
</div>
<div className="dashboard__postActions">
{isCommentBoxOpen(post.id) ? (
<ChatBubbleOutlineIcon
onClick={() => toggleCommentBox(post._id)}
className="dashboard__actionComment"
/>
) : (
<ChatBubbleOutlineIcon
onClick={() => toggleCommentBox(post._id)}
className="dashboard__actionComment"
/>
)}
<label
id="totalLikes"
className="dashboard__comments"
style={{color: "forestgreen"}}
>
{post.commentCount}
</label>
{post.likes.find((like) => like.uid === user._id) ? (
<FavoriteIcon
onClick={() => UnlikePost(post._id)}
className="dashboard__actionUnlike"
/>
) : (
<FavoriteBorderIcon
onClick={() => LikePost(post._id)}
className="dashboard__actionLike"
/>
)}
<label
id="totalLikes"
className="dashboard__likes"
style={{color: "forestgreen"}}
>
{post.likeCount}
</label>
</div>
<div
className={
toggle
? "dashboard__commentContent toggle"
: "dashboard__commentContent"
}
>
<div className="dashboard__postComments">
{post.comments.map((comment) => (
<div
key={comment.toString()}
className="dashboard__postComment"
>
<div className="dashboard__postCommentTop">
<Avatar src={comment.upic}/>
<h4>{comment.uname}</h4>
</div>
<p>{comment.message}</p>
<div className="dashboard__postCommentActions">
{comment.likes.find(
(like) => like.uid === user._id
) ? (
<FavoriteIcon
onClick={() =>
UnlikeCommentPost(post._id, comment._id)
}
className="dashboard__actionUnlike"
/>
) : (
<FavoriteBorderIcon
onClick={() =>
LikeCommentPost(post._id, comment._id)
}
className="dashboard__actionLike"
/>
)}
<label
id="totalLikes"
className="dashboard__likes"
style={{color: "forestgreen"}}
>
{comment.likeCount}
</label>
</div>
</div>
))}
</div>
<div className="dashboard__commentInput">
<input
type="text"
placeholder="Comment post"
value={comment}
onChange={(e) => setComment(e.target.value)}
/>
<button onClick={() => submitComment(post._id)}>
Send
</button>
</div>
</div>
</div>
))}
</FlipMove>
</div>
</div>
<div className="dashboardContentRight"></div>
</div>
</div>
</div>
);
}
Let me know if it works or you need more explanation
i'm quite new using React, or programming actually haha.
I am practicing making an online store and I already have almost everything working correctly. My problem is that I want the customer to be able to select the size and quantity and set it on a useState to pass that information to the cart.
This is my code:
import { Image, Grid, Icon, Button, Form } from "semantic-ui-react";
import { size } from "lodash";
import classNames from "classnames";
import useAuth from "../../../hooks/useAuth";
import useCart from "../../../hooks/useCart";
import {
isFavoriteApi,
addFavoriteApi,
deleteFavoriteApi,
} from "../../../Api/favorite";
import CarouselScreenshots from "../CarouselScreenshots";
import TabsItem from "../TabsItem"
export default function HeaderItem(props) {
const { item } = props;
const { poster, title, screenshots } = item;
return (
<div>
<Grid className="header-item">
<Grid.Column mobile={16} tablet={6} computer={8}>
<Image src={poster.url} alt={title} fluid />
<CarouselScreenshots title={title} screenshots={screenshots} />
</Grid.Column>
<Grid.Column mobile={16} tablet={10} computer={8}>
<Info item={item} />
</Grid.Column>
</Grid>
</div>
);
}
function Info(props) {
const { item } = props;
const { title, summary, price, discount, url } = item;
const [isFavorites, setIsFavorites] = useState(false);
const [reloadFavorite, setReloadFavorite] = useState(false);
const { auth, logout } = useAuth();
const { addProductCart } = useCart();
const [sizeItem, setSizeItem] = useState(null);
const [quantity, setQuantity] = useState(null);
useEffect(() => {
(async () => {
const response = await isFavoriteApi(auth.idUser, item.id, logout);
if (size(response) > 0) setIsFavorites(true);
else setIsFavorites(false);
})();
setReloadFavorite(false);
}, [item, reloadFavorite]);
const addFavorite = async () => {
if (auth) {
await addFavoriteApi(auth.idUser, item.id, logout);
setReloadFavorite(true);
}
};
const deleteFavorite = async () => {
if (auth) {
await deleteFavoriteApi(auth.idUser, item.id, logout);
setReloadFavorite(true);
}
};
const sizes = [
{
key: 'Small',
text: 'Small',
value: 'Small',
name: 'size'
},
{
key: 'Medium',
text: 'Medium',
value: 'Medium',
name: 'size'
},
{
key: 'Large',
text: 'Large',
value: 'Large',
name: 'size'
},
]
return (
<>
<div className="header-item__title">
{title}
</div>
<div className="header-item__buy">
<div className="header-item__buy-price">
{/* <p>Public price: ${price} </p> */}
<div className="header-item__buy-price-actions">
{discount && <div className="header-item__buy-price-actions"> <p>-{discount}% </p>
<p>${(price - Math.floor(price * discount) / 100).toFixed(2)}</p></div>}
{!discount && <p>${price}</p>}
</div>
<p className="subtitle">Size</p>
<Form>
<Form.Dropdown
placeholder='Select size'
fluid
selection
options={sizes}
/>
<p>Quantity</p>
<Form.Input placeholder='1' />
</Form>
</div>
<div className="header-item__buy-btn-container">
<Button
className="header-item__buy-btn-container__buy-btn"
type="submit"
onClick={() => addProductCart(url)}
>
Buy Now
</Button>
<div className="heart-container" >
<Icon
name={isFavorites ? "heart" : "heart outline"}
className={classNames({ like: isFavorites })}
link
onClick={isFavorites ? deleteFavorite : addFavorite}
/>
</div>
</div>
</div>
<div
className="header-item__summary"
dangerouslySetInnerHTML={{ __html: summary }}
/>
<div className="tabs" >
<TabsItem />
</div>
</>
);
}
This is what you looking for.
With a functional component you will need to import useState hook, and set 2 states like this:
const [size, setSize] = useState("small");
const [quantity, setQuantity] = useState(0);
Then follow the documentation in the link above. You should ultimatly have something like
<select value={size} onChange={(e) => changeSize(e)}>. And in your changeSize function, you should use setSize function to set the state.