Multiple value in reactjs drag and drop - reactjs

i have a drag and drop with multiple array in that card, but when i added new value inside the card the value added in all card, someone can help me how to the fix this?
the code like this:
import * as Style from '#/components/Form/FormStyle'
import { confirm } from '#/components/Modals/Confirmation'
import dynamic from 'next/dynamic'
import { useCallback, useEffect, useState } from 'react'
import { Trash2 } from 'react-feather'
import { useList } from 'react-use'
import { QuestionCard } from './QuestionCard'
import update from 'immutability-helper'
import { DragVertical, Plus } from '#/components/Icons'
type TermType = {
id: number
name: string
correct: boolean
}
const DefaultQuestion: { id: number; question?: string; placeholder?: string; term: any[] } = {
id: 1,
question: '',
placeholder: '',
term: []
}
const MDEditor = dynamic<any>(() => import('#/components/Form/MDEditor/withNoValue').then((fn) => fn.MDEditor), {
ssr: false
})
export const MultipleAnswer = ({ data }: { data?: any }) => {
const [term, { set: setTerm, updateAt: updateTermAt, removeAt: removeTermAt }] = useList<TermType>()
const [cards, { set: setCards, updateAt: updateCardsAt, removeAt: removeCardsAt }] = useList<typeof DefaultQuestion>()
const handleAddTerm = () => {
const newId = term?.slice(-1)[0]?.id ? term?.slice(-1)[0]?.id + 1 : 1
setTerm([...term, { id: newId, name: `Kolom jawaban ${newId}`, correct: false }])
}
const handleChangeTerm = (index: number, value: string) => {
updateTermAt(index, { ...term[index], name: value })
}
const handleDeleteTerm = async (id: number) => {
const confirmed = await confirm('Apakah anda yakin ingin menghapus pertanyaan ini?', 'Batal', 'Hapus')
if (confirmed) {
removeTermAt(id)
}
}
const [titles, setTitles] = useState<{ [id: string]: string }>({
1: 'Opsi 1',
2: 'Opsi 2',
3: 'Opsi 3',
4: 'Opsi 4'
})
const moveCard = useCallback(
(dragIndex: number, hoverIndex: number) => {
if (!cards) return
const dragCard = cards[dragIndex]
setCards(
update(cards, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragCard]
]
})
)
},
[cards]
)
const handleAdd = () => {
const newId = cards?.slice(-1)[0]?.id ? cards?.slice(-1)[0]?.id + 1 : 1
const newTermId = cards?.slice(-1)[0]?.term?.slice(-1)[0]?.id || 0
const collectIdxBefore = DefaultQuestion.term.map((term, i) => {
const index = i === 0 ? 1 : i + 1
return { ...term, id: newTermId + index }
})
const createTitles = {
[newTermId + 1]: 'Opsi 1',
[newTermId + 2]: 'Opsi 2',
[newTermId + 3]: 'Opsi 3',
[newTermId + 4]: 'Opsi 4'
}
setCards([...cards, { id: newId, term: [...collectIdxBefore] }])
setTitles({ ...titles, ...createTitles })
}
useEffect(() => {
if (data) {
setCards(data?.question)
}
}, [data])
const [isDesktop, setIsDesktop] = useState(false)
useEffect(() => {
const handleResize = () => {
if (window.innerWidth > 768) {
setIsDesktop(true)
} else {
setIsDesktop(false)
}
}
handleResize()
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
const renderCard = (card: typeof DefaultQuestion, index: number) => {
const handleDelete = async (id: number) => {
const confirmed = await confirm('Apakah anda yakin ingin menghapus pertanyaan ini?', 'Batal', 'Hapus')
if (confirmed) {
removeCardsAt(id)
// remove also titles
cards[id].term.forEach((term) => {
delete titles[term.id]
})
}
}
const handleChange = (idx: number, idTerm: number, checked: boolean, termIndex: number) => {
const changedTerm = { ...cards[idx], term: [...cards[idx].term] }
changedTerm.term[termIndex] = { id: idTerm, name: titles?.[idTerm], correct: checked }
updateCardsAt(idx, changedTerm)
}
return (
<QuestionCard key={card.id} index={index} id={card.id} moveCard={moveCard} data-questions>
<div tw="flex justify-center mb-8">
<button tw="flex items-center space-x-3 text-sm text-gray-600 focus:outline-none">
<DragVertical />
<span>Urutkan</span>
</button>
</div>
<MDEditor label="Pertanyaan" defaultValue={data?.question?.[0]?.question} data-question />
<input type="hidden" value="radio" data-type />
<Style.InputGroup mobile={isDesktop ? false : true} tw="mt-5 !flex md:!grid">
<label tw="self-start md:text-right w-28">Jawaban</label>
<div tw="flex flex-col space-y-3">
{term?.map((term, i) => (
<div
key={Math.random()}
tw="relative flex items-center justify-between px-5 py-1 space-x-5 bg-gray-100 rounded-lg"
data-term>
<div tw="flex items-center">
<input
type="text"
defaultValue={term?.name}
tw="max-w-xs bg-transparent border-none focus:outline-none"
onBlur={(evt) => handleChangeTerm(i, evt.target.value)}
style={{ boxShadow: 'none' }}
/>
</div>
<button type="button" onClick={() => handleDeleteTerm(i)}>
<Trash2 width={18} tw="text-gray-600" />
</button>
</div>
))}
<div>
<button
type="button"
onClick={() => handleAddTerm()}
tw="inline-flex text-sm text-left text-secondary hover:underline">
Tambah Opsi
</button>
</div>
</div>
</Style.InputGroup>
<div tw="flex justify-end mt-5">
<button
type="button"
onClick={() => handleDelete(index)}
tw="flex items-center space-x-1 text-gray-400 hover:text-primary">
<Trash2 strokeWidth={1} width={20} />
<span tw="text-sm font-light">Hapus</span>
</button>
</div>
</QuestionCard>
)
}
return (
<div tw="flex flex-col space-y-5">
{cards?.map((card, i) => renderCard(card, i))}
<button
type="button"
tw="flex items-center justify-center w-full py-3 space-x-3 bg-white border border-dashed text-primary border-primary hover:bg-gray-100"
onClick={() => handleAdd()}>
<Plus />
<span tw="font-semibold">Tambah Pertanyaan</span>
</button>
</div>
)
}
the result with error like this:
https://streamable.com/f8l8qs
the firts of error is when i added the new opsi the value added to more card, same when i delete it, there detele all value in card

Related

How to Override Component from Default Column in React-Table

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

how to refresh list of data changed by component

Description - I'm pulling a list of Material data from firestore and displaying it as a table. I've added a modal to add a Material to the database.
Problem - The refresh method I'm passing to the modal, refreshList() isn't refreshing the list of Materials on the Materials page. I'm quite certain the refresh method worksβ€”it's the same one I use to pull all Materials from firestore. what did I do wrong?
Here's the AddMaterialModal.
import { FC, useContext, useState } from "react";
import { ProjectContext } from "../../context/ProjectContext";
import MaterialService from "../../services/materialService";
import { Material } from "../../services/orgTypes";
import { randomString } from "../../utils/utils";
import AddEditTextModal from "./AddEditTextModal";
interface AddMaterialsModalProps {
editting?: boolean;
setOpenModal: (bool: boolean) => void;
selectedMaterial?: Material;
refreshList?: () => void;
}
const AddMaterialModal: FC<AddMaterialsModalProps> = ({
editting,
setOpenModal,
selectedMaterial,
refreshList,
}) => {
const idTemp = randomString(20);
const projectContext = useContext(ProjectContext);
const [newMaterial] = useState<Material>({
material: "material",
actualquantity: "actualquantity",
size: "size",
id: idTemp,
description: "description",
type: "type",
unit: "unit",
quantity: "quantity",
});
const materialService = new MaterialService(
projectContext.selectedProject.id
);
const createNewMaterial = () => {
materialService.updateCreateGlobalMaterial(newMaterial);
if (refreshList != undefined) {
refreshList();
setOpenModal(false);
}
};
return (
<div>
<div className="flex flex-col gap- 2 w-full h-fit ">
{//input some material data here.}
</div>
<div className="flex justify-center align-center">
<button
onClick={createNewMaterial}
className=" w-fit px-6 py-2.5 mt-6 bg-flowius-blue text-white font-medium text-lg leading-tight uppercase rounded shadow-md hover:bg-blue-400 hover:shadow-lg focus:bg-cyan-400 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-500 active:shadow-lg
transition duration-150 ease-in-out"
>
{editting ? "UPDATE" : "ADD +"}
</button>
</div>
</div>
);
};
export default AddMaterialModal;
And here's the main Materials page.
import { FC, useCallback, useContext, useEffect, useState } from "react";
import DataTable from "react-data-table-component";
import { ProjectContext } from "../context/ProjectContext";
import { Material } from "../services/orgTypes";
import Loader from "../components/Loader";
import MaterialIcon from "../components/MaterialIcon";
import { toast } from "react-toastify";
import MaterialService from "../services/materialService";
import Modal from "../components/modal/Modal";
import AddMaterialModal from "../components/modal/AddMaterialModal";
interface Selected {
allSelected: boolean;
selectedCount: number;
selectedRows: Material[];
}
export const formatDate = (date: Date) => {
const month = date.toLocaleString("en-us", { month: "long" });
const year = date.getFullYear();
const day = date.getDate();
return `${day} ${month} ${year}`;
};
const Materials: FC = () => {
const [materials, setMaterials] = useState<Material[]>([]);
const [loading, setLoading] = useState(true);
const [selected, setSelected] = useState<Selected>();
const projectContext = useContext(ProjectContext);
const { id } = projectContext.selectedProject;
const materialService = new MaterialService(id);
const pullMaterials = useCallback(async () => {
if (!id) return;
const materials = await materialService.getProjectMaterials();
setMaterials(materials);
setLoading(false);
}, [id]);
const [openAddMaterialModal, setOpenAddMaterialModal] = useState(false);
useEffect(() => {
pullMaterials();
}, [pullMaterials]);
const columns = [
{
name: "Material",
selector: (row: Material) => row.material,
sortable: true,
},
{
name: "Description",
selector: (row: Material) => row.description,
sortable: true,
},
{
name: "Size",
selector: (row: Material) => row.size,
sortable: true,
},
{
name: "Unit",
selector: (row: Material) => row.unit,
sortable: true,
},
{
name: "Actual Quantity",
selector: (row: Material) => row.actualquantity,
sortable: true,
},
];
const deleteSelected = async () => {
if (!selected) return;
const { selectedRows } = selected;
materialService.deleteGlobalMaterial(selectedRows);
toast.success(`${selectedRows.length} Items deleted`);
pullMaterials();
setSelected(undefined);
};
return (
<div className="m-2 w-full">
<div className="flex text-flowius-blue text-xl flex-row justify-between my-1">
<button
className="bg-flowius-blue text-base text-white p-2 rounded-md"
onClick={() => {
setOpenAddMaterialModal(true);
}}
>
Add Material
</button>
</div>
{(selected?.selectedCount ?? 0) > 0 && (
<MaterialIcon
className="cursor-pointer "
onClick={deleteSelected}
icon="delete"
/>
)}
{loading ? (
<Loader />
) : (
<DataTable
selectableRows={true}
paginationRowsPerPageOptions={[50, 100, 150]}
paginationPerPage={50}
onSelectedRowsChange={setSelected}
pagination={true}
columns={columns}
data={materials}
defaultSortAsc={true}
defaultSortFieldId="Material"
/>
)}
{setOpenAddMaterialModal && (
<Modal
open={openAddMaterialModal}
close={() => {
setOpenAddMaterialModal(false);
}}
title={"Add Material"}
className={""}
>
<AddMaterialModal
editting={false}
setOpenModal={setOpenAddMaterialModal}
refreshList={pullMaterials}
/>
</Modal>
)}
</div>
);
};
export default Materials;

React Functional Component Rendering Old Data

I'm trying to setup a piece of code into its own component, however, by doing so the data doesn't show up after doing it:
Code StackBlitz Example
As you see from the picture, both of the inputs from the component setup are blank. I'm assuming that it might have something to do with closures, but I'm not sure, and even more confused as how to resolve the issue.
App.tsx
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { SimpleTable } from './components/SimpleTable/SimpleTable';
import ITableCol from './components/SimpleTable/ITableCol';
interface IItem {
userId: number;
id: number;
title: string;
body: string;
}
interface IIdItem {
[key: string]: number;
}
export const App = () => {
const getItems = async () => {
const res = await axios.get('https://jsonplaceholder.typicode.com/posts');
const twoItems = [res.data[0], res.data[1]];
setItems(twoItems);
};
const getTableCols = (): ITableCol[] => {
return [
{
title: 'title',
renderFn: renderTitle,
},
{
title: 'input',
renderFn: renderInput,
},
];
};
const processData = () => {
let _idValues = { ...idValues };
for (const item of items) {
if (!_idValues[item.title]) {
_idValues[item.title] = item.id * 5;
}
}
return _idValues;
};
const renderTitle = (item: IItem) => {
return <div className="">{item.title}</div>;
};
const handleChange = (e: ChangeEvent<any>) => {};
const renderInput = (item: IItem) => {
const valueItem = idValues[item.title];
return (
<div className="">
<div>Id: {item.id}</div>
<div>
<input type="number" value={valueItem} onChange={handleChange} />
</div>
</div>
);
};
useEffect(() => {
getItems();
}, []);
const [items, setItems] = useState<IItem[]>([]);
const [idValues, setIdValues] = useState<IIdItem>({});
const [tableCols, setTableCols] = useState<ITableCol[]>([]);
useEffect(() => {
setTableCols(getTableCols());
setIdValues(processData());
}, [items]);
return (
<div className="p-2">
<h1>State Issue Example</h1>
<div className="mb-5">
<div>Simple Table</div>
<SimpleTable data={items} cols={tableCols} />
</div>
<div>
<div>Non Componentized Table</div>
<div className="d-flex">
<div className="w-50">
<b>title</b>
</div>
<div className="w-50">
<b>input</b>
</div>
</div>
</div>
<div>
{items.map((item) => {
return (
<div className="d-flex">
<div className="w-50">{renderTitle(item)}</div>
<div className="w-50">{renderInput(item)}</div>
</div>
);
})}
</div>
</div>
);
};
SimpleCode.tsx
import React, { useState } from 'react';
import ITableCol from '../SimpleTable/ITableCol';
type Props = {
cols: ITableCol[];
data: any[];
};
export const SimpleTable: React.FC<Props> = (props) => {
return (
<div>
<div className="d-flex">
{props.cols.map((col) => {
return (
<div className="w-50">
<b>{col.title}</b>
</div>
);
})}
</div>
{props.data.map((data, index) => {
return (
<div className="d-flex" key={'data-' + index}>
{props.cols.map((col, index) => {
return (
<div key={data.id} className="w-50">
{col.renderFn(data, index)}
</div>
);
})}
</div>
);
})}
<div>Data Size: {props.data.length}</div>
</div>
);
};
ITableCol.tsx
export default interface ITableCol {
renderFn: (item: any, index: number) => void;
title: string;
}
Problem is getTableCols gets render even before API response. Using two useEffect would have solved your issue.Screenshot of the Solution
useEffect(() => {
setIdValues(processData());
}, [items]);
useEffect(() => {
setTableCols(getTableCols());
}, [idValues]);
Given below is the full code of App.tsx
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { SimpleTable } from './components/SimpleTable/SimpleTable';
import ITableCol from './components/SimpleTable/ITableCol';
interface IItem {
userId: number;
id: number;
title: string;
body: string;
}
interface IIdItem {
[key: string]: number;
}
export const App = () => {
const getItems = async () => {
const res = await axios.get('https://jsonplaceholder.typicode.com/posts');
const twoItems = [res.data[0], res.data[1]];
setItems(twoItems);
};
const getTableCols = (): ITableCol[] => {
return [
{
title: 'title',
renderFn: renderTitle,
},
{
title: 'input',
renderFn: renderInput,
},
];
};
const processData = () => {
let _idValues = { ...idValues };
for (const item of items) {
if (!_idValues[item.title]) {
_idValues[item.title] = item.id * 5;
}
}
return _idValues;
};
const renderTitle = (item: IItem) => {
return <div className=""> {item.title}</div>;
};
const handleChange = (e: ChangeEvent<any>) => {};
const renderInput = (item: IItem) => {
const valueItem = idValues[item.title];
return (
<div className="">
<div>Id: {item.id}</div>
<div>valueItem: {valueItem}</div>
<div>
<input type="number" value={valueItem} onChange={handleChange} />
</div>
</div>
);
};
useEffect(() => {
getItems();
}, []);
const [items, setItems] = useState<IItem[]>([]);
const [idValues, setIdValues] = useState<IIdItem>({});
const [tableCols, setTableCols] = useState<ITableCol[]>([]);
useEffect(() => {
setIdValues(processData());
}, [items]);
useEffect(() => {
setTableCols(getTableCols());
}, [idValues]);
return (
<div className="p-2">
<h1>State Issue Example</h1>
<div className="mb-5">
<div>Simple Table</div>
<SimpleTable data={items} cols={tableCols} />
</div>
<div>
<div>Non Componentized Table</div>
<div className="d-flex">
<div className="w-50">
<b>title</b>
</div>
<div className="w-50">
<b>input</b>
</div>
</div>
</div>
<div>
{items.map((item, i) => {
return (
<div key={i} className="d-flex">
<div className="w-50">{renderTitle(item)}</div>
<div className="w-50">{renderInput(item)}</div>
</div>
);
})}
</div>
</div>
);
};

Local storage is not keeping the data after page reload

I'm making in react a list of episodes that user would like to watch later (similar to todo app) , but after reloading the page data is not keeping in local storage.
I'm new to react, so, please help me to understand the issue.
This is my code
import React, { useState, useEffect } from "react";
import { RiCloseCircleLine } from "react-icons/ri";
import { TiEdit } from "react-icons/ti";
import { MyWatchListForm } from "./MyWatchListForm";
export const MyWatchListItem = ({
watchLists,
completeWatchList,
removeWatchList,
updateWatchList,
}) => {
const [edit, setEdit] = useState({
id: null,
value: "",
});
const submitUpdate = (value) => {
updateWatchList(edit.id, value);
setEdit({
id: null,
value: "",
});
};
useEffect(() => {
const data = localStorage.getItem("my-watchList");
const savedData = JSON.parse(data);
setEdit(savedData);
}, []);
useEffect(() => {
localStorage.setItem("my-watchList", JSON.stringify(watchLists));
});
if (edit.id) {
return <MyWatchListForm edit={edit} onSubmit={submitUpdate} />;
}
return watchLists.map((watchList, index) => (
<div className={watchList.isComplete ? "checked" : ""} key={index}>
<div key={watchList.id} onClick={() => completeWatchList(watchList.id)}>
{watchList.text}
</div>
<div>
<RiCloseCircleLine onClick={() => removeWatchList(watchList.id)} />
<TiEdit
onClick={() => setEdit({ id: watchList.id, value: watchList.text })}
/>
</div>
</div>
));
};
Form that is used to get the data from:
export const MyWatchListForm = (props) => {
const [input, setInput] = useState(props.edit ? props.edit.value : "");
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus();
});
const handleSubmit = (e) => {
e.preventDefault();
props.onSubmit({
id: Math.floor(Math.random() * 10000),
text: input,
});
setInput("");
};
const handleChange = (e) => {
setInput(e.target.value);
};
return (
<form
className="w-full max-w-sm flex items-center border-b border-teal-500 py-2"
onSubmit={handleSubmit}
>
{props.edit ? (
<>
<input
className="appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none"
type="text"
value={input}
placeholder="Update the episode"
onChange={handleChange}
ref={inputRef}
></input>
<button>Update</button>
</>
) : (
<>
<input
className="appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none"
type="text"
value={input}
placeholder="Add the episode"
onChange={handleChange}
ref={inputRef}
></input>
<button>Add</button>
</>
)}
</form>
);
};
And the WatchList.js file
import React, { useState } from "react";
import { MyWatchListForm } from "./MyWatchListForm";
import { MyWatchListItem } from "./MyWatchListItem";
export const MyWatchList = () => {
const [watchLists, setWatchLists] = useState([]);
const addWatchList = (watchList) => {
if (!watchList.text || /^\s*$/.test(watchList.text)) {
return;
}
const newWatchList = [watchList, ...watchLists];
setWatchLists(newWatchList);
console.log(...watchLists);
};
const updateWatchList = (watchListId, newValue) => {
if (!newValue.text || /^\s*$/.test(newValue.text)) {
return;
}
setWatchLists((prev) =>
prev.map((item) => (item.id === watchListId ? newValue : item))
);
};
const removeWatchList = (id) => {
const removeArr = [...watchLists].filter(
(watchList) => watchList.id !== id
);
setWatchLists(removeArr);
};
const completeWatchList = (id) => {
const updatedWatchList = watchLists.map((watchList) => {
if (watchList.id === id) {
watchList.isComplete = !watchList.isComplete;
}
return watchList;
});
setWatchLists(updatedWatchList);
};
return (
<div>
<h1>Watch later</h1>
<MyWatchListForm onSubmit={addWatchList} />
<MyWatchListItem
watchLists={watchLists}
completeWatchList={completeWatchList}
removeWatchList={removeWatchList}
updateWatchList={updateWatchList}
/>
</div>
);
};
I fixed the issue by changing the initial state in MyWatchList as below:
export const MyWatchList = () => {
const [watchLists, setWatchLists] = useState(() => {
const data = localStorage.getItem("my-watchList");
return data ? JSON.parse(data) : [];
});
useEffect(() => {
localStorage.setItem("my-watchList", JSON.stringify(watchLists));
}, [watchLists]);
I think the issue with your code is this snippet:
useEffect(() => {
localStorage.setItem("my-watchList", JSON.stringify(watchLists));
});
Every time the state get's updated, this useEffect runs and it replaces the content of localStorage to the values which you are getting in watchlists since you are not providing a dependancy array to the useEffect.
Using the useEffect hook without a dependency array is never a good idea. It basically runs on every render. Actually, goal of the useEffect is to solve this problem. Remove your useEffect and try like this;
const submitUpdate = (value) => {
updateWatchList(edit.id, value);
setEdit({
id: null,
value: "",
});
localStorage.setItem("my-watchList", JSON.stringify(watchLists));
};

componentWillUnmount works after switching to another page

I have two pages and two components LibraryPageFilters.tsx (url: /courses) and UserVideoCreatePage.tsx (url: /ugc/courses/${course.id}).
In component LibraryPageFilters.tsx
useEffect(() => {
console.log(course.id)
if (course.id) {
console.log(544)
dispatch(push(`/ugc/courses/${course.id}`));
}
}, [course]);
i have a check that if course.id present in the store, then we make a redirect.
In component UserVideoCreatePage.tsx
useEffect(() => {
return () => {
console.log(333344444)
dispatch(courseDelete());
};
}, []);
i am deleting a course from the store when componentUnmount.
why does unmount happen after a redirect? as a result, I am redirected back. Because the course is not removed from the store at the moment of unmount, and the check (if (course.id)) shows that the course is in the store and a redirect occurs back (dispatch(push(/ugc/courses/${course.id})))
UserVideoCreatePage.tsx
import React, { useEffect, useRef, useState } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router';
import { Link } from 'react-router-dom';
import { Container } from 'Core/components/Container/Container';
import { Svg } from 'Core/components/Svg';
import { Button } from 'Core/Molecules/Button';
import { Select } from 'Core/Molecules/Select';
import {
agreementCourse, categoriesSelector,
courseDelete,
courseEditorCourseSelector,
courseUpdateApi, getCategories,
getCourse,
updateCourseApi,
} from 'Learnings/store/courseEdit';
import { CourseComments } from 'Learnings/screens/CoursePlayPage/CourseBottom/CourseComments/CourseComments';
import { AddItem } from './AddItem';
import s from 'Admin/Pages/Course/Description/index.scss';
import './UserVideoCreatePage.scss';
export const UserVideoCreatePage: React.FC = () => {
const dispatch = useDispatch();
const { id: idCourse } = useParams();
const course = useSelector(courseEditorCourseSelector, shallowEqual);
const categories = useSelector(categoriesSelector, shallowEqual);
const [value, valueSet] = useState({ name: '', description: '', categories: [], lectures: [], materials: [] });
const [tab, tabSet] = useState('program');
const inputFileRef = useRef<HTMLInputElement>(null);
const img = course.gallery_items && course.gallery_items[0];
console.log(categories);
const handleBtnClick = () => {
if (inputFileRef && inputFileRef.current) {
inputFileRef.current.click();
}
};
useEffect(() => {
dispatch(getCourse(idCourse));
dispatch(getCategories());
}, [idCourse]);
useEffect(() => {
valueSet({
name: course.name,
description: course.description,
categories: course.categories && course.categories[0] && course.categories[0].id,
lectures: course.lectures,
materials: course.materials,
});
}, [course]);
useEffect(() => {
return () => {
console.log(333344444)
dispatch(courseDelete());
};
}, []);
return (
<Container className="createCourse">
<Link to="/" className="gallery__back">
<Svg name="arrow_back" width={26} height={20} className="gallery__svg"/>
<span>Назад</span>
</Link>
<div className="createCourse__twoColumn">
<div className="createCourse__twoColumn-left">
<div className="inputBlock">
<label className="inputBlock__label" htmlFor="video">
НазваниС Π²ΠΈΠ΄Π΅ΠΎ-курса
</label>
<input
id="video"
type="text"
placeholder="Π’Π²Π΅Π΄ΠΈΡ‚Π΅ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ вашСго Π²ΠΈΠ΄Π΅ΠΎ"
className="inputBlock__input"
value={value.name || ''}
onChange={e =>
valueSet({
...value,
name: e.target.value,
})
}
onBlur={e => {
if (e.target.value && course.name !== e.target.value) {
dispatch(updateCourseApi(idCourse, { name: e.target.value }));
}
}}
/>
</div>
<div className="inputBlock">
<label className="inputBlock__label" htmlFor="opisanie">
ОписаниС Π²ΠΈΠ΄Π΅ΠΎ-курса
</label>
<textarea
id="opisanie"
placeholder="Π’Π²Π΅Π΄ΠΈΡ‚Π΅ ΠΊΡ€Π°Ρ‚ΠΊΠΎΠ΅ описаниС вашСго Π²ΠΈΠ΄Π΅ΠΎ"
className="inputBlock__input"
value={value.description || ''}
onChange={e =>
valueSet({
...value,
description: e.target.value,
})
}
onBlur={e => {
if (e.target.value && course.description !== e.target.value) {
dispatch(updateCourseApi(idCourse, { description: e.target.value }));
}
}}
/>
</div>
<Select
title="ΠšΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ Π²ΠΈΠ΄Π΅ΠΎ-курса"
placeholder="ΠšΠ°Ρ‚Π΅Π³ΠΎΡ€ΠΈΠΈ Π²ΠΈΠ΄Π΅ΠΎ-курса"
value={value.categories}
options={categories.map(category => ({ value: category.id, label: category.name }))}
onChange={val => {
valueSet({
...value,
categories: val,
});
dispatch(
updateCourseApi(idCourse, {
category_ids: val,
courses_curators: {
'': {
user_id: val,
},
},
}),
);
}}
search
/>
</div>
<div className="createCourse__twoColumn-right">
<div className="loadVideo">
<div className="loadVideo__field">
<div className="loadVideo__field--block">
{!img && (
<>
<Svg className="loadVideo__field--block-icon" name="icn-load" width={104} height={69}/>
<p className="loadVideo__field--block-text">Π—Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚Π΅ ΠΎΠ±Π»ΠΎΠΆΠΊΡƒ ΠΊ Π²ΠΈΠ΄Π΅ΠΎ</p>
</>
)}
{img && <img src={img && img.image_url} alt=""/>}
</div>
</div>
<div className="loadVideo__under">
<div className="loadVideo__under--left">
<div className="loadVideo__under--text">
<span className="loadVideo__under--text-grey">*Π Π΅ΠΊΠΎΠΌΠ΅Π½Π΄ΡƒΠ΅ΠΌΡ‹ΠΉ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚</span>
<span className="loadVideo__under--text-bold"> 356Ρ…100</span>
</div>
<div className="loadVideo__under--text">
<span className="loadVideo__under--text-grey">*ВСс Π½Π΅ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΏΡ€Π΅Π²Ρ‹ΡˆΠ°Ρ‚ΡŒ</span>
<span className="loadVideo__under--text-bold"> 10 Мб</span>
</div>
</div>
<div className="loadVideo__under--right">
<input
onChange={val => {
if (val.target.files[0]) {
if (img) {
dispatch(
updateCourseApi(idCourse, {
gallery_items: {
'': {
image: val.target.files[0],
id: img.id,
},
},
}),
);
} else {
dispatch(
updateCourseApi(idCourse, {
gallery_items: {
'': {
image: val.target.files[0],
},
},
}),
);
}
}
}}
type="file"
ref={inputFileRef}
className="Library__btn"
/>
<Button
onClick={() => {
handleBtnClick();
}}
>
Π‘ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° ΠΎΠ±Π»ΠΎΠΆΠ΅ΠΊ
</Button>
</div>
</div>
</div>
</div>
</div>
<div className={`block-switcher block-switcher--courseCreate`}>
<div
className={`block-switcher__item ${tab === 'program' && 'block-switcher__item_active'}`}
onClick={() => tabSet('program')}
>
ΠŸΡ€ΠΎΠ³Ρ€Π°ΠΌΠΌΡ‹
</div>
<div
className={`block-switcher__item ${tab === 'comments' && 'block-switcher__item_active'}`}
onClick={() => tabSet('comments')}
>
ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΈ экспСрта
</div>
</div>
{tab === 'program' && (
<>
<AddItem
accept="video/mp4,video/x-m4v,video/*"
fieldName="name"
addType="lecture_type"
title="Π’ΠΈΠ΄Π΅ΠΎ-курсы"
addBtn="Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Π²ΠΈΠ΄Π΅ΠΎ"
type="lectures"
file="video"
lecturesArg={course.lectures}
value={value}
onChangeInput={lecturesNew => {
valueSet({
...value,
lectures: lecturesNew,
});
}}
onVideoUpdate={(params: any) => {
dispatch(updateCourseApi(idCourse, params));
}}
posMove={(lectures: any) => {
dispatch(courseUpdateApi({ id: idCourse, lectures: lectures }, true));
}}
/>
<AddItem
accept=""
fieldName="title"
addType="material_type"
title="ΠœΠ°Ρ‚Π΅Ρ€ΠΈΠ°Π»Ρ‹ ΠΊ Π²ΠΈΠ΄Π΅ΠΎ-курсам"
addBtn="Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Ρ„Π°ΠΉΠ»"
type="materials"
file="document"
lecturesArg={course.materials}
value={value}
onChangeInput={lecturesNew => {
valueSet({
...value,
materials: lecturesNew,
});
}}
onVideoUpdate={(params: any) => {
dispatch(updateCourseApi(idCourse, params));
}}
posMove={(lectures: any) => {
dispatch(courseUpdateApi({ id: idCourse, materials: lectures }, true));
}}
/>
</>
)}
{tab === 'comments' && <CourseComments title="ΠžΠ±ΡΡƒΠΆΠ΄Π΅Π½ΠΈΠ΅"/>}
<Button
className={`${s.button} agreement__btn`}
size="big"
onClick={() =>
dispatch(
agreementCourse(idCourse, {
visibility_all_users: true,
}),
)
}
>
ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ Π½Π° согласованиС
</Button>
</Container>
);
};
LibraryPageFilters.tsx
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
import { push } from 'connected-react-router';
import { getSettingsGlobalSelector } from 'Core/store/settings';
import { Svg } from 'Core/components/Svg';
import { NavBar } from 'Core/Organisms/NavBar';
import { Button } from 'Core/Molecules/Button';
import { CategoriesFilter } from 'Core/Organisms/Filters/components/CategoriesFilter';
import { courseDelete, courseEditorCourseSelector, createNewCourse } from 'Learnings/store/courseEdit';
import { FILTERS, LINKS } from '../../libraryPageConstants';
import { setLibraryPageQuery } from '../../actions/libraryPageActions';
import { getLibraryPageQuerySelector } from '../../libraryPageSelectors';
import s from './index.scss';
import { languageTranslateSelector } from 'Core/store/language';
import { LanguageType } from 'Core/models/LanguageSchema';
import { Status, Tabs } from 'Core/components/Tabs/Tabs';
const statuses: Array<Status> = [
{
key: 'Filter/all',
link: '/courses' || '/courses',
type: 'all' || '',
},
{
key: 'Filter/online',
link: '/courses/online',
type: 'online',
},
{
key: 'Filter/offline',
link: '/courses/offline',
type: 'offline',
},
{
key: 'Filter/complete',
link: '/courses/complete',
type: 'complete',
},
];
export const LibraryPageFilters = () => {
const dispatch = useDispatch();
const [searchTerm, setSearchTerm] = useState('');
const [isBtnDisabled, setIsBtnDisabled] = useState(false);
const course = useSelector(courseEditorCourseSelector, shallowEqual);
const global = useSelector(getSettingsGlobalSelector);
const query = useSelector(getLibraryPageQuerySelector, shallowEqual);
const courseCreateButtonText = useSelector(
languageTranslateSelector('CoursePage/courseCreateButton'),
) as LanguageType;
const { category_id: categoryID } = query;
console.log(course)
useEffect(() => {
console.log(course.id)
if (course.id) {
console.log(544)
dispatch(push(`/ugc/courses/${course.id}`));
}
}, [course]);
useEffect(() => {
return () => {
setIsBtnDisabled(false);
dispatch(courseDelete());
};
}, []);
const onFilter = (values: any) => {
return false;
};
const handleActiveCategory = (id: number) => {
const categoryParam = {
...query,
offset: 0,
category_id: id,
};
if (id === categoryID) {
delete categoryParam.category_id;
}
dispatch(setLibraryPageQuery(categoryParam));
};
const handleSearch = () => {
dispatch(setLibraryPageQuery({ query: searchTerm }));
};
return (
<React.Fragment>
<div className={s.filters}>
{global.coursesPage?.filters.length ? (
<NavBar
className={s.navBar}
links={global.coursesPage.filtersLinks.map(linkType => LINKS[linkType])}
filters={global.coursesPage.filters.map(filterType => FILTERS[filterType])}
onFilter={onFilter}
postfix={
global.coursesPage.courseCreateButton && global.coursesPage.courseCreateButton.enable ? (
<Button
className="coursePageCreateButton"
onClick={() => {
dispatch(createNewCourse());
setIsBtnDisabled(true);
}}
disabled={isBtnDisabled}
>
{courseCreateButtonText['CoursePage/courseCreateButton']}
</Button>
) : null
}
/>
) : (
<div className="track-page__header" data-tut="track-header">
<Tabs statuses={statuses} />
<div className={s.filtersSearch}>
<Svg className={s.filtersSearchIcon} name="search_alternative" width={18} height={18} />
<input
type="text"
placeholder="Поиск"
className={s.filtersSearchInput}
value={searchTerm}
onChange={event => setSearchTerm(event.target.value)}
/>
<button type="button" className={s.filtersButton} onClick={handleSearch}>
Найти
</button>
</div>
</div>
)}
</div>
<CategoriesFilter onChange={handleActiveCategory} selectedID={categoryID} />
</React.Fragment>
);
};
Although react suggests to use Functional Component, try Class Component, I faced similar issues, this was resolved easily in Class Component :
componentDidMount();
componentDidUpdate(prevProps, prevState, snapshot);
These two will solve your problem. Ask me if anything you need.

Resources