How to select multiple rows and sub rows in React? - javascript

I'm looking for some help to implement row select functionality in my React app.
I have the following data that I provide to the table(react-data-table-component):
const data = useMemo(
() =>
[...Array(87)].map((_, outer) => ({
id: `o-${outer}`,
name: randFullName(),
age: randNumber({ max: 100 }),
price: randNumber({ max: 5000 }),
description: randProductDescription(),
active: randBoolean(),
date: randFutureDate().toLocaleString(),
items: [...Array(randNumber({ max: 10 }))].map((__, inner) => ({
id: `o-${outer}--i-${inner}`,
name: randFullName(),
age: randNumber({ max: 100 }),
price: randNumber({ max: 5000 }),
description: randProductDescription(),
active: randBoolean(),
date: randFutureDate().toLocaleString(),
})),
})),
[],
);
I display this data in a table.
There is a parent row and each parent row can be expanded which contains the child rows.
Now, I want to be able to select rows. I use a three-state checkbox where three states are:
checked: when all the child rows are checked
empty: when no children row is selected
intermediate: when some of the child rows are selected
I want to render the checkbox states and also be able to access the selected rows.
I am using a custom selector cell like this:
const SelectorCell = ({
rowId,
parentId,
siblingIds,
childrenIds,
}: {
rowId: string;
parentId: string;
siblingIds: string[];
childrenIds: string[];
}) => {
const { toggleRow, rowSelectionStatus } = useContext(
NewTableSelectedRowsContext,
) as NewTableSelectedRowsType;
const handleToggle = useCallback(() => {
// todo
}, []);
const status = rowSelectionStatus(rowId);
return <ThreeStateCheckbox checked={status} onChange={handleToggle} />;
};
I have tried to achieve this using a context provider but haven't got any success:
import { createContext, useCallback, useMemo, useState } from "react";
import {
NewTableSelectedRowsType,
} from "./types";
const statusTypes = {
checked: true,
intermediate: null,
unchecked: false,
};
export const NewTableSelectedRowsContext = createContext<NewTableSelectedRowsType | null>(null);
interface Props {
children: JSX.Element;
}
export const NewTableSelectedRowsContextProvider = ({ children }: Props): JSX.Element => {
const rowSelectionStatus = useCallback((rowId: string) => // todo, []);
const toggleRow = useCallback(() => {
// todo
},
[],
);
const value: NewTableSelectedRowsType = useMemo(
() => ({
toggleRow,
rowSelectionStatus,
}),
[rowSelectionStatus, toggleRow],
);
return (
<NewTableSelectedRowsContext.Provider value={value}>
{children}
</NewTableSelectedRowsContext.Provider>
);
};
How can I achieve that in React?

I'm not sure why you are using context to store the logic of selecting rows or not, you will need to send the data.length to compute if the selected rows are the same amount as the table rows/part of absolutely 0.
You also haven't attached to component code, so my answer will be more general...
You can have state for table status and one for selected rows
enum ETableState {
checked = 'checked',
empty = 'empty',
intermediate = 'intermediate'
}
const [tableState, setTableState] = useState<ETableState>(ETableState.intermediate)
const [selectedRows, setSelectedRows] = useState<string[])([])
on each selecting row, you will call a callback for add/remove it from selectedRows state, that after will trigger an effect
const toggleRow = useCallback((id: string) =>
setSelectedRows(prev =>
prev.includes(id) ? prev.filter(i => i != id) : [...prev, id]
)
, [selectedRows.length])
useEffect(() => {
setTableState(dataLength === selectedRows.length ?
ETableState.checked
: !selectedRows.length ?
ETableState.empty
: ETableState.intermediate
}
,[selectedRows.length, dataLength]
edit
In case you want to be able to use this logic in few separate places, as in child rows and so on, you can wrap all of this code in custom hook and call it in each component with the data.length
function useTableState(dataLength: number) {
...
return { selectedRows, tableState, toggleRow }
}
// usage in a table component
const { selectedRows, tableState, toggleRow } = useTableState(data.length)

Related

An an extra Add Rule button to react-querybuilder

I am using react-querybuilder what I need is another Add Rule button to be add next to the original one and want to add differnt set of fields and operators when using the new button. Here is some part of my code:
import { HBButton, HBIcon } from '#hasty-bazar/core'
import { FC } from 'react'
import { useIntl } from 'react-intl'
import queryBuilderMessages from '../HBQueryBuilder.messages'
interface AddRuleActionProps {
handleOnClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
}
const AddGroupAction: FC<AddRuleActionProps> = ({ handleOnClick }) => {
const { formatMessage } = useIntl()
return (
<>
<HBButton
onClick={handleOnClick}
size="small"
leftIcon={<HBIcon type="plus" />}
sx={{ marginRight: 2, minWidth: 50 }}
>
{formatMessage(queryBuilderMessages.rule)}
</HBButton>
// >>> ANOTHER HBButton with different implementation to be added here
</>
)
}
export default AddGroupAction
Adding a new answer based on your feedback and because this one is very different from the other. I'm about to release v5.0 of react-querybuilder that has the feature I mentioned in the first paragraph of the other answer. This makes achieving the desired result much more straightforward and also eliminates the need for external state management (i.e. Redux).
TL;DR: working codesandbox example here (uses react-querybuilder#5.0.0-alpha.2).
React Query Builder only takes one fields prop, but you can organize the fields into an array of option groups instead of a flat array. I set the operators property on each field to the default operators, filtered appropriately for the type of field (text vs numeric).
import { Field, OptionGroup } from 'react-querybuilder';
import { nameOperators, numberOperators } from './operators';
export const fields: OptionGroup<Field>[] = [
{
label: 'Names',
options: [
{ name: 'firstName', label: 'First Name', operators: nameOperators },
{ name: 'lastName', label: 'Last Name', operators: nameOperators },
],
},
{
label: 'Numbers',
options: [
{ name: 'height', label: 'Height', operators: numberOperators },
{ name: 'weight', label: 'Weight', operators: numberOperators },
],
},
];
Next I set up a custom field selector component to only allow fields that are part of the same option group. So if a "name" field is chosen, the user can only select other "name" fields.
const FilteredFieldSelector = (props: FieldSelectorProps) => {
const filteredFields = fields.find((optGroup) =>
optGroup.options.map((og) => og.name).includes(props.value!)
)!.options;
return <ValueSelector {...{ ...props, options: filteredFields }} />;
};
This custom Add Rule button renders a separate button for each option group that calls the handleOnClick prop with the option group's label as context.
const AddRuleButtons = (props: ActionWithRulesAndAddersProps) => (
<>
{fields
.map((og) => og.label)
.map((lbl) => (
<button onClick={(e) => props.handleOnClick(e, lbl)}>
+Rule ({lbl})
</button>
))}
</>
);
The context is then passed to the onAddRule callback, which determines what field to assign based on the context value.
const onAddRule = (
rule: RuleType,
_pP: number[],
_q: RuleGroupType,
context: string
) => ({
...rule,
context,
field: fields.find((optGroup) => optGroup.label === context)!.options[0].name,
});
Put it all together in the QueryBuilder props, and voilà:
export default function App() {
const [query, setQuery] = useState(initialQuery);
return (
<div>
<QueryBuilder
fields={fields}
query={query}
onQueryChange={(q) => setQuery(q)}
controlElements={{
addRuleAction: AddRuleButtons,
fieldSelector: FilteredFieldSelector,
}}
onAddRule={onAddRule}
/>
<pre>{formatQuery(query, 'json')}</pre>
</div>
);
}
Update: see my other answer
This is a little tricky because the onAddRule callback function only accepts the rule to be added (which is always the default rule), and the parent path. If we could pass custom data into it this question would be much easier to answer.
The best way I can think to do it today is to externalize the query update methods out of the QueryBuilder component and manage them yourself (for the most part). In the example below, I've used Redux Toolkit (overkill for this use case but it's what I'm familiar with) to manage the query and replaced the Add Rule button with a custom component that renders two buttons, one to add a new rule for First Name and one to add a new rule for Last Name.
Working CodeSandbox example.
The redux store:
import { configureStore, createSlice, PayloadAction } from '#reduxjs/toolkit';
import { RuleGroupType } from 'react-querybuilder';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
interface State {
query: RuleGroupType;
}
export const getQuery = (state: State) => state.query;
const initialState: State = {
query: {
combinator: 'and',
rules: [],
},
};
const querySlice = createSlice({
name: 'query',
initialState,
reducers: {
setQuery(state: State, action: PayloadAction<RuleGroupWithAggregation>) {
state.query = action.payload;
},
},
});
const { reducer } = querySlice;
export const { setQuery } = querySlice.actions;
export const store = configureStore({ reducer });
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
The App component:
import {
ActionWithRulesProps,
add,
Field,
formatQuery,
QueryBuilder,
} from 'react-querybuilder';
import 'react-querybuilder/dist/query-builder.scss';
import { getQuery, setQuery, useAppDispatch, useAppSelector } from './store';
const fields: Field[] = [
{ name: 'firstName', label: 'First Name' },
{ name: 'lastName', label: 'Last Name' },
];
const AddRuleButtons = (props: ActionWithRulesProps) => {
const dispatch = useAppDispatch();
const query = useAppSelector(getQuery);
const onClickFirst = () =>
dispatch(
setQuery(
add(
query,
{ field: 'firstName', operator: '=', value: 'First' },
props.path
)
)
);
const onClickLast = () =>
dispatch(
setQuery(
add(
query,
{ field: 'lastName', operator: '=', value: 'Last' },
props.path
)
)
);
return (
<>
<button onClick={onClickFirst}>+Rule (First Name)</button>
<button onClick={onClickLast}>+Rule (Last Name)</button>
</>
);
};
export default function App() {
const dispatch = useAppDispatch();
const query = useAppSelector(getQuery);
return (
<div>
<QueryBuilder
fields={fields}
query={query}
onQueryChange={(q) => dispatch(setQuery(q))}
controlElements={{
addRuleAction: AddRuleButtons,
}}
/>
<pre>{formatQuery(query, 'json')}</pre>
</div>
);
}

Ag-grid editable grid adding new row dynamically

I have an editable AgGrid in my functional component as below.
On the last column, I have buttons to Add/Remove rows.
Now I want the Add row to be displayed only for the last row. I am using cellRenderer for the last column.
With the below change, I get the Add button for the last row (i.e. on 2nd row in my case) on initial render. But if I click on Add for this 2nd row, while I get the Add button for the new 3rd row, but it does not get removed for the 2nd row. not sure if I am implementing this in the wrong way.
const MyCmp = (props) => {
const getData = () => {
return [{
id: 0,
firstName: 'F1',
lastName: 'L1'
}, {
id: 1,
firstName: 'F2',
lastName: 'L2',
}];
}
const [myCols, setMyCols] = useState(null);
const [gridData, setGridData] = useState(getData());
const [gridApi, setGridApi] = useState('');
let cellRules = {
'rag-red': params => {
if (params.data.lastName === 'INVALID_VAL') {
return true;
}
}
};
const handleGridReady = (params) => {
setGridApi(params.api);
setMyCols([{
headerName: 'F Name',
field: 'firstName',
editable: true
}, {
headerName: 'L Name',
field: 'lastName',
cellClassRules: cellRules,
editable: true
}, {
headerName: '',
field: 'buttonCol',
cellRenderer: 'customColRenderer',
cellRendererParams: {
addItems: addItems
}
}]
);
};
const createNewRowData = () => {
const newData = {
id: newCount,
firstName: '',
lastName: ''
};
newCount++;
return newData;
}
let newCount = getData().length;
const addItems = (addIndex, props) => {
const newItems = [createNewRowData()];
const res = props.api.applyTransaction({
add: newItems,
addIndex: addIndex,
});
setGridData(...gridData, res.add[0].data); // IS THIS CORRECT ?
if (props.api.getDisplayedRowCount() > props.api.paginationGetPageSize()) {
props.api.paginationGoToPage(parseInt((props.api.getDisplayedRowCount() / props.api.paginationGetPageSize())) + 1);
}
}
const onCellClicked = (e) => {
}
const frameworkComponents = () => {
return {customColRenderer: customColRenderer}
}
return (
<>
<MyAgGrid
id="myGrid"
columnDefs={myCols}
rowData={gridData}
frameworkComponents={{customColRenderer: customColRenderer}}
{...props}
/>
</>
)
}
My customColRenderer is as below;
export default (props) => {
let isLastRow = (props.rowIndex === (props.api.getDisplayedRowCount() -1)) ? true: false;
const addItems = (addIndex) => {
props.addItems(addIndex, props);
}
return (
<span>
{isLastRow ? <button onClick={() => addItems()}>Add</button> : null}
<span><button onClick={() => props.api.applyTransaction({remove: props.api.getSelectedRows()})}>Remove</button>
</span>
);
};
Within the AgGrid React internals a transaction is generated automatically when your rowData is updated, as such you can choose to apply the transaction either through the api, or by updating your state - you shouldn't need to do both (as you're currently doing). Generally with React I'd suggest updating the state to keep your state true to the data displayed in the grid, however that can depend on use case.
As for the further issue of your cell not updating - that'll be due to the fact AgGrid has detected no change in the 'value' of the cell, as it attempts to reduce the amount of unnecessary cell rendering done.
You could attempt to call:
api.refreshCells({ force: true });
After your api call applying the transaction (I'm not sure where this would need to happen using the setState approach).

Request to api works fine in component but not when using provider with react

When making a request to my API from a component and using react-data-table-component everything works perfectly but if I try to make the request from my Product Provider the pagination is incorrect and no longer works as expected.
With this code I make the request, datatable and pagination from my component working perfectly:
import React, { useState, useEffect, useCallback, useMemo } from "react";
import axiosClient from "../config/axiosClient";
import DataTable from 'react-data-table-component-with-filter'
import { CSVLink } from "react-csv"
import { Link } from 'react-router-dom'
import useProducts from "../hooks/useProducts";
const removeItem = (array, item) => {
const newArray = array.slice();
newArray.splice(newArray.findIndex(a => a === item), 1);
return newArray;
};
const ProductsTest = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [totalRows, setTotalRows] = useState(0);
const [perPage, setPerPage] = useState(10);
const [currentPage, setCurrentPage] = useState(1);
const [searchBox, setSearchBox] = useState('')
const STRING_TRADUCTIONS = { "KILOGRAM" : "KILOGRAMOS", "GRAMS" : "GRAMOS", "BOX" : "CAJA", "PACKAGE" : "PAQUETE", "BOTTLE" : "BOTE", "PIECES" : "PIEZAS", "BAG" : "BOLSA", "LITER" : "LITRO" }
const fetchUsers = async (page, limit = perPage, search = searchBox) => {
setLoading(true)
const dataOnLs = localStorage.getItem('cmtjs')
const config = {
headers: {
"Content-Type": "application/json",
apiKey: dataOnLs
} ,
params: {
limit,
page,
search
}
}
const response = await axiosClient(`/products`, config)
const data = response.data.products.docs.map( doc => (
{
_id: doc._id,
idProduct: doc.idProduct,
barCode: doc.barCode,
name: doc.name,
presentation: STRING_TRADUCTIONS[doc.presentation],
salePrice: doc.salePrice,
purchasePrice: doc.purchasePrice,
stock: doc.stock,
user: doc.user.username,
category: doc.category.name,
provider: doc.provider.name
}
))
setData(data);
setTotalRows(response.data.products.totalDocs);
setLoading(false);
};
useEffect(() => {
fetchUsers(1)
}, []);
const columns = useMemo(
() => [
{
name: "ID",
selector: "idProduct",
sortable: true
},
{
name: "Código de Barras",
selector: "barCode",
sortable: true
},
{
name: "Nombre",
selector: "name",
sortable: true
},
{
name: "Presentación",
selector: "presentation",
sortable: true
},
{
name: "Precio",
selector: "salePrice",
sortable: true
},
{
name: "Stock",
selector: "stock",
sortable: true
},
{ cell: row =>
<Link to={ `/dashboard/product/${row._id}`}>
<button className='btn btn-ghost text-xs'>
Mas
</button>
</Link>}
]
);
const handlePageChange = page => {
fetchUsers(page);
setCurrentPage(page);
};
const handlePerRowsChange = async (newPerPage, page) => {
fetchUsers(page, newPerPage);
setPerPage(newPerPage);
}
const headers = [
{ label: "ID", key: "idProduct" },
{ label: "Código de Barras", key: "barCode" },
{ label: "Nombre", key: "name" },
{ label: "Presentación", key: "presentation" },
{ label: "Precio Venta", key: "salePrice" },
{ label: "Precio Compra", key: "purchasePrice" },
{ label: "Stock", key: "stock" },
{ label: "Creador", key: "user" },
{ label: "Categoría", key: "category" },
{ label: "Proveedor", key: "provider" }
]
const paginationComponentOptions = {
rowsPerPageText: 'Mostrar',
rangeSeparatorText: 'de',
selectAllRowsItem: true,
selectAllRowsItemText: 'Todos',
};
const clear = () => {
setPerPage(10)
setSearchBox('')
fetchUsers(1, 10, '')
}
return (
<div>
<input type="text" onChange={(e)=> setSearchBox(e.target.value)}/>
<button onClick={ ()=> fetchUsers()}>Buscar</button>
<CSVLink data={data} headers={headers} filename={"productos.cdtmx.csv"} className="cursor-pointer">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABmJLR0QA/wD/AP+gvaeTAAACnUlEQVRoge2YXUhUQRiGn7HVRVddc8k0QawQw6hQohKSlsp+oDulQBGCbuxGQ1itDIx+IIWINKK86S4o6ibFEG9ywQzCEJLVSCylorYf1CJaXXe6SPvZ1ePZs3tczfNcfsP3zfvOzHc4M2BgYGAQSYTWxJSrReUg6oHEYHNtuRu1TDkupKx27Tp38+9glJZKv9AmPgQSpYiq9w+GYGBBxU8jrf6RUAwsCgwDkcYwEGlM/gFLVf4JkBcEWJQSvw+/Ua4cJYixWjElxoemcB4CDKgRrwqfZGJ0lImpqYChFe53IZefIeAIhUX8DDJsleZkyffA8jBgNkXTe+ouXy93U2Ev+R3PSE7jU70T96VHZNrSdROphCoDHu8kZ1quAVC1u4x4cxwAdQfLMZuiaei4xevPb/VTqYDqI9Ta56Rj4Ak2SxLHCw6zaU0Wxbl7Gfw4QlPnbT01KjLLZ3RuTj9oxJ61lQp7CYUbdiAQVN5rwOOd1EvfvATVxAMfXtHcdZ+k2ATy127hzrN2nIM9emlTRVA7AJAwff4BhJj/QmfpDuwNT0pCsNPOSVA7ULA+j7Jth3g63EfXUC9H8vazJ3t72MRoQbUBsymaK0UOhBCcbbtBbUsTUkoai2uIi4nVU6Miqg2c3HeM7NWZtPc/xjnYQ8+Ii9Y+JxnJadQUHtVToyKqDOSkrqPSXopP+jj/8M+jQF3bdby+KSrspWxOz9JNpBKqmtj1fojk6oKA+Ev3CCsdO8MuKhiWx7/QYmY2A9/CVt2n/4Vgth6oBS4Cod0FfVKa3Z4fgMd/yPb8i+ay/pma30ZTD+RoXl5rxiqtqbxo7vxH83/ZA0sKw0CkWdYGxsOmQj1j/gHNBoQQDhABBXVkTEocCzifgYGBgQp+AlpMnf09Cu/RAAAAAElFTkSuQmCC"/>
</CSVLink>
<DataTable
columns={columns}
data={data}
progressPending={loading}
pagination
paginationServer
paginationTotalRows={totalRows}
paginationDefaultPage={currentPage}
onChangeRowsPerPage={handlePerRowsChange}
onChangePage={handlePageChange}
selectableRows
//onSelectedRowsChange={({ selectedRows }) => console.log(selectedRows)}
paginationComponentOptions={paginationComponentOptions}
noDataComponent="No hay resultados"
/>
{
searchBox && searchBox !== '' && <button onClick={ () => clear() }>Limpiar</button>
}
</div>
)
}
But I have my product provider where I make a get request to all my products, avoiding making the requests from my component and having the data globally, but if I use "getProducts" from my provider, the first view of the datatable is correct however , when clicking on a page or next, the pager advances but the data displayed does not, for example: it shows me the first 10 records but I ask for the next 10 and the pager advances correctly but the data is still the first 10 records, No I know how to use my provider and make the pager show the following data depending on what the user needs.
This is the code of my provider to obtain the product data
const getProducts = async(page, limit, search) => {
const dataOnLs = localStorage.getItem('cmtjs')
const config = {
headers: {
"Content-Type": "application/json",
apiKey: dataOnLs
} ,
params: {
page,
limit,
search
}
}
try {
const { data } = await axiosClient(`/products`, config)
setProducts(data)
} catch (error) {
So in my component I call "getProducts" from my provider to have the products data in "products" using the useEffect hook
useEffect(() => {
getProducts()
setData(products.products?.docs)
setTotalRows(products.products?.totalDocs)
setLoading(false)
}, []);
In my paginator to obtain the following 10 product records, I click and the text changes that indicates which page is being shown, but the data remains the same as the first page
const handlePageChange = page => {
getProducts(page); // it does not show the next 10 records as it happened in the fetch of my component
setCurrentPage(page); // OK
};
In the same way, my browser no longer works using it in this way, I only changed the function to call my provider now, but it does not work
<input type="text" onChange={(e)=> setSearchBox(e.target.value)}/>
<button onClick={ ()=> getProducts()}>Buscar</button>
I would like to know if you can help me to make my datatable and browser work using my provider. Thanks.
In this code
useEffect(() => {
getProducts()
setData(products.products?.docs)
setTotalRows(products.products?.totalDocs)
setLoading(false)
}, []);
You are calling getProducts(), which is asynchronous. Then you try setData(products...), but the asynchronous call did not finish yet, so products was not updated yet. When eventually the asynchronous code terminates, the useEffect statement is not triggered again, because the dependency array states that the effect is only executed when the component mounts.
Split up your effect in two parts instead, so the second effect gets triggered when new products are available:
useEffect(() => {
getProducts()
}, []);
useEffect(() => {
setData(products.products?.docs)
setTotalRows(products.products?.totalDocs)
setLoading(false)
}, [products]);

In what condition it will re-render while using react custom hooks

I tried a sample in using react hook, make it a custom hook.
The problem is the simple hook useCount() goes fine, but the hook useCarHighlight() intending to switch highlight line would not cause re-render.
I see it is the same of the two, is anything wrong I should attention for about this?
I made a sandbox here: https://codesandbox.io/s/typescript-j2xtf
Some code below:
// index.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import useCarHighlight, { Car } from "./useCarHighlight";
import useCount from "./useCount";
const myCars: Car[] = [
{ model: "C300", brand: "benz", price: 29000, ac: "auto ac" },
{ model: "Qin", brand: "byd", price: 9000 }
];
const App = () => {
const { cars, setHighlight } = useCarHighlight(myCars, "Qin");
const { count, increase, decrease } = useCount(10);
console.log(
`re-render at ${new Date().toLocaleTimeString()},
Current highlight: ${
cars.find(c => c.highlight)?.model
}`
);
return (
<div>
<ul>
{cars.map(car => {
const { model, highlight, brand, price, ac = "no ac" } = car;
return (
<li
key={model}
style={{ color: highlight ? "red" : "grey" }}
>{`[${brand}] ${model}: $ ${price}, ${ac}`}</li>
);
})}
</ul>
<button onClick={() => setHighlight("C300")}>highlight C300</button>
<button onClick={() => setHighlight("Qin")}>highlight Qin</button>
<hr />
<h1>{`Count: ${count}`}</h1>
<button onClick={() => increase()}>+</button>
<button onClick={() => decrease()}>-</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
// useCarHighlight.ts
import { useState } from "react";
export type Car = {
model: string;
brand: string;
price: number;
ac?: "auto ac" | "manual ac";
};
export default function(
initialCars: Car[],
initialSelectedModel: string
): {
cars: Array<Car & { highlight: boolean }>;
setHighlight: (selMod: string) => void;
} {
const carsHighlight = initialCars.map(car => ({
...car,
highlight: initialSelectedModel === car.model
}));
const [cars, setCars] = useState(carsHighlight);
const setHighlight = (selMod: string) => {
cars.forEach(car => {
car.highlight = car.model === selMod;
});
setCars(cars);
};
return {
cars,
setHighlight
};
}
// useCount.ts
import { useState } from "react";
export default function useCount(initialCount: number) {
const [state, setState] = useState(initialCount);
const increase = () => setState(state + 1);
const decrease = () => setState(state - 1);
return {
count: state,
increase,
decrease
};
}
Unlike class components, mutating state of hooks does not queue a re-render, when using hooks you have to update your state in an immutable way.
Also, when calculating the next state based on the previous state it is recommended to use a functional update and read the previous state from the first argument of the function.
const setHighlight = (selMod: string) => {
setCars(prevState =>
prevState.map(car => ({
...car,
highlight: car.model === selMod
}))
);
};
Here is a good resource about immutable update patterns
Dont use forEach in setHighlight, use map instead
const setHighlight = (selMod: string) => {
const newCars = cars.map(car => ({
...car,
highlight: car.model === selMod
}));
setCars(newCars);
};
Use map instead of forEach as the address of car object isn't getting changed when you update highlight property in car.
const setHighlight = (selMod: string) => {
let carsTemp = cars.map(car => ({
...car,
highlight : car.model === selMod
}));
setCars(carsTemp);};

Prevent child's state from reset after parent component state changes also get the values of all child components:ReactJS+ Typescript

I am a bit new to react and I am stuck in this situation where I am implementing custom dropdown filter for a table in react. I have set of dropdown values for each column and there is a Apply button.
I have maintained a child component for this which takes in drop down values and sends the selected one's back to parent. Then I call a back-end API that gives me filtered data which in-turn sets parents state . The problem here is the checkbox values inside dropdown is lost after I get the data and set the parent state.
Each child components has as a set of checkboxes , an Apply and a clear button. So on click of Apply , I have to send the checked one's to the parent or in general whichever the checked one's without losing the previous content.
I am unable to understand why am I losing the checkbox values?
It would be of great help if someone can help me out with this
Sand box: https://codesandbox.io/s/nervous-elgamal-0zztb
I have added the sandbox link with proper comments. Please have a look. I am a bit new to react.
Help would be really appreciated
Parent
import * as React from "react";
import { render } from "react-dom";
import ReactTable from "react-table";
import "./styles.css";
import "react-table/react-table.css";
import Child from "./Child";
interface IState {
data: {}[];
columns: {}[];
selectedValues: {};
optionsForColumns: {};
}
interface IProps {}
export default class App extends React.Component<IProps, IState> {
// Here I have hardcoded the values, but data and optionsForColumns comes from the backend and it is set inside componentDidMount
constructor(props: any) {
super(props);
this.state = {
data: [
{ firstName: "Jack", status: "Submitted", age: "14" },
{ firstName: "Simon", status: "Pending", age: "15" }
],
selectedValues: {},
columns: [],
optionsForColumns: {
firstName: [{ Jack: "4" }, { Simon: "5" }],
status: [{ Submitted: "5" }, { Pending: "7" }]
}
};
}
// Get the values for checkboxes that will be sent to child
getValuesFromKey = (key: any) => {
let data: any = this.state.optionsForColumns[key];
let result = data.map((value: any) => {
let keys = Object.keys(value);
return {
field: keys[0],
checked: false
};
});
return result;
};
// Get the consolidated values from child and then pass it for server side filtering
handleFilter = (fieldName: any, selectedValue: any, modifiedObj: any) =>
{
this.setState(
{
selectedValues: {
...this.state.selectedValues,
[fieldName]: selectedValue
}
},
() => this.handleColumnFilter(this.state.selectedValues)
);
};
// Function that will make server call based on the checked values from child
handleColumnFilter = (values: any) => {
// server side code for filtering
// After this checkbox content is lost
};
// Function where I configure the columns array for the table . (Also data and column fiter values will be set here, in this case I have hardcoded inside constructor)
componentDidMount() {
let columns = [
{
Header: () => (
<div>
<div>
<Child
key="firstName"
name="firstName"
options={this.getValuesFromKey("firstName")}
handleFilter={this.handleFilter}
/>
</div>
<span>First Name</span>
</div>
),
accessor: "firstName"
},
{
Header: () => (
<div>
<div>
<Child
key="status"
name="status"
options={this.getValuesFromKey("status")}
handleFilter={this.handleFilter}
/>
</div>
<span>Status</span>
</div>
),
accessor: "status",
},
{
Header: "Age",
accessor: "age"
}
];
this.setState({ columns });
}
//Rendering the data table
render() {
const { data, columns } = this.state;
return (
<div>
<ReactTable
data={data}
columns={columns}
/>
</div>
);
}
}
const rootElement = document.getElementById("root");
render(<App />, rootElement);
Child
import * as React from "react";
import { Button, Checkbox, Icon } from "semantic-ui-react";
interface IProps {
options: any;
name: string;
handleFilter(val1: any, val2: any, val3: void): void;
}
interface IState {
showList: boolean;
selected: [];
checkboxOptions: any;
}
export default class Child extends React.Component<IProps, IState> {
constructor(props: any) {
super(props);
this.state = {
selected: [],
showList: false,
checkboxOptions: this.props.options.map((option: any) => option.checked)
};
}
// Checkbox change handler
handleValueChange = (event: React.FormEvent<HTMLInputElement>, data: any) => {
const i = this.props.options.findIndex(
(item: any) => item.field === data.name
);
const optionsArr = this.state.checkboxOptions.map(
(prevState: any, si: any) => (si === i ? !prevState : prevState)
);
this.setState({ checkboxOptions: optionsArr });
};
//Passing the checked values back to parent
passSelectionToParent = (event: any) => {
event.preventDefault();
const result = this.props.options.map((item: any, i: any) =>
Object.assign({}, item, {
checked: this.state.checkboxOptions[i]
})
);
const selected = result
.filter((res: any) => res.checked)
.map((ele: any) => ele.field);
console.log(selected);
this.props.handleFilter(this.props.name, selected, result);
};
//Show/Hide filter
toggleList = () => {
this.setState(prevState => ({ showList: !prevState.showList }));
};
//Rendering the checkboxes based on the local state, but still it gets lost after filtering happens
render() {
let { showList } = this.state;
let visibleFlag: string;
if (showList === true) visibleFlag = "visible";
else visibleFlag = "";
return (
<div>
<div style={{ position: "absolute" }}>
<div
className={"ui scrolling dropdown column-settings " + visibleFlag}
>
<Icon className="filter" onClick={this.toggleList} />
<div className={"menu transition " + visibleFlag}>
<div className="menu-item-holder">
{this.props.options.map((item: any, i: number) => (
<div className="menu-item" key={i}>
<Checkbox
name={item.field}
onChange={this.handleValueChange}
label={item.field}
checked={this.state.checkboxOptions[i]}
/>
</div>
))}
</div>
<div className="menu-btn-holder">
<Button size="small" onClick={this.passSelectionToParent}>
Apply
</Button>
</div>
</div>
</div>
</div>
</div>
);
}
}
This appears to be a case of state being managed in an inconvenient way. Currently, the state is managed at the Child level, but it would be easier to manage at the Parent level. This is known as lifting state up in React.
The gist - the shared state is managed in the Parent component, and it's updated by calling a function passed to the Child component. When Apply is clicked, the selected radio value is passed up to the Parent, which merges the new selection into the shared state.
I have created a minimal example of your code, showing how we can lift state up from the Child to the Parent component. I'm also using a few new-ish features of React, like useState to simplify the Child component.
// Child Component
const Child = ({name, options, updateSelections}) => {
const [selected, setSelected] = React.useState([]);
const handleChange = (event) => {
let updated;
if (event.target.checked) {
updated = [...selected, event.target.value];
} else {
updated = selected.filter(v => v !== event.target.value);
}
setSelected(updated);
}
const passSelectionToParent = (event) => {
event.preventDefault();
updateSelections(name, selected);
}
return (
<form>
{options.map(item => (
<label for={name}>
<input
key={name}
type="checkbox"
name={item}
value={item}
onChange={handleChange}
/>
{item}
</label>
))}
<button onClick={passSelectionToParent}>Apply</button>
</form>
)
}
// Parent Component
class Parent extends React.Component {
constructor(props) {
super(props);
this.fields = ["firstName", "status"],
this.state = {
selected: {}
};
}
getValuesFromKey = (data, key) => {
return data.map(item => item[key]);
}
updateSelections = (name, selection) => {
this.setState({
selected: {...this.state.selected, [name]: selection}
}, () => console.log(this.state.selected));
}
render() {
return (
<div>
{this.fields.map(field => (
<Child
key={field}
name={field}
options={this.getValuesFromKey(this.props.data, field)}
updateSelections={this.updateSelections}
/>
))}
</div>
)
}
}
const data = [
{ firstName: "Jack", status: "Submitted" },
{ firstName: "Simon", status: "Pending" },
{ firstName: "Pete", status: "Approved" },
{ firstName: "Lucas", status: "Rejected" }
];
ReactDOM.render(<Parent data={data}/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Your checkbox values are only lost when you hide/show the table, as the table goes out of
DOM the state of it and it's children are lost. When the table is mounted to DOM, Child
component is mounted again initializing a new state taking checkbox values from
getValuesFromKey method of which returns false by default clearing checkbox ticks.
return {
field: keys[0],
checked: false
};
Stackblitz reproducing the issue.
You have to set checkbox values checking the selectedValues object to see if it was selected.
return {
field: keys[0],
checked: this.state.selectedValues[key] && this.state.selectedValues[key].includes(keys[0]),
};

Categories

Resources