expanding subRows in mantine DataGrid(react-table) - reactjs

since DataGrid follows react-table, I try to show subRows in DataGrid like this exmple but the subRows or getLeafRows array properties array in each row of Table stays empty array[]. I think there is a problem in DataGrid component!
my code is:
const items= useMemo(
() =>
data?.map((item) => ({
id: item.id,
name: item.name,
time: item.time,
subRows: parseStringToArray(data.children).map((child) => ({
id: child.id,
name: child.name,
time: child.time,
})),
})),
[data]
);
const onRow = useCallback<(row: Row<any>) => HTMLAttributes<HTMLTableRowElement>>((row) => {
return row.getCanExpand()
? {
onClick: row.getToggleExpandedHandler(),
style: { cursor: 'pointer' },
}
: {};
}, []);
const getRowCanExpand = useCallback<(row: Row<any>) => boolean>((row) => !!row.original.subRows?.length, []);
return(
<DataGrid
data: items || [],
columns: gridCols || [],
withRowExpanding: true,
getRowCanExpand,
renderSubComponent,
onRow,
withColumnFilters
withSorting
withFixedHeader
withPagination
highlightOnHover
height={1000}
pageSizes={['50', '100', '200']}
/>
)
I'll be happy if you help me in this concept.
I expect to show my subRows in datagrid like another rows in datagrid

Try this
const items = useMemo(
() =>
data?.map((item) => ({
id: item.id,
name: item.name,
time: item.time,
subRows: parseStringToArray(item.children)?.map((child) => ({
id: child.id,
name: child.name,
time: child.time,
})) || [],
})),
[data]
);
const onRow = useCallback<(row: Row<any>) => HTMLAttributes<HTMLTableRowElement>>((row) => {
return row.getCanExpand()
? {
onClick: row.getToggleExpandedHandler(),
style: { cursor: 'pointer' },
}
: {};
}, []);
const getRowCanExpand = useCallback<(row: Row<any>) => boolean>((row) => {
return !!row.original.subRows?.length;
}, []);
return (
<DataGrid
data={items || []}
columns={gridCols || []}
withRowExpanding={true}
getRowCanExpand={getRowCanExpand}
renderSubComponent={renderSubComponent}
onRow={onRow}
withColumnFilters={true}
withSorting={true}
withFixedHeader={true}
withPagination={true}
highlightOnHover={true}
height={1000}
pageSizes={['50', '100', '200']}
/>
);
In the useMemo hook, I changed the data.children property to item.children. This is because the data array does not have a children property, but each item in the array does.
I added a null check to the parseStringToArray function in case it returns null or undefined.
I added a default value of an empty array for the subRows property in case the parseStringToArray function returns null or undefined.
In the getRowCanExpand function, I added a null check for the subRows property using the optional chaining operator (?.). This is because the subRows property may be null or undefined if the parseStringToArray function returns null or undefined.

Related

useCallback not updating array with object

I am trying to update array with objects using useCallback but instead of updating object it is adding or appending one too.
const items = [
{
id: 101,
name: 'Mae Jemison',
},
{
id: 201,
name: 'Ellen Ochoa',
},
];
const [headings, setHeadings] = useState(items);
const handleHeadingTextChange = useCallback(
(value, id) => {
let items2 = headings;
items2 = items2.map((item, key) => {
if (items2[key].id == id) {
items2[key].name = value
}
return items2
});
setHeadings((prevState) => [...prevState, items2]) // adding, not updating
//setHeadings((prevState) => [...prevState, ...items2]) // try 2 : still adding
},
[setHeadings],
);
<input type="text" id="101" value="" onChange={handleHeadingTextChange} />
So, on change expected output is
items = [
{
id: 101,
name: 'Johaan',
},
{
id: 201,
name: 'Ellen Ochoa',
},
];
But Instead I am getting
items = [
{
id: 101,
name: 'Johaan',
},
{
id: 201,
name: 'Ellen Ochoa',
},
[{
id: 101,
name: 'Johaan',
},
{
id: 201,
name: 'Ellen Ochoa',
}]
];
I am not sure how to set value in setHeading function so that it only update the value and not appending one too. Is there way to make it update only?
problems
.map iterates over the entire array. that doesn't make sense if you are trying to just update a single item.
onChange takes an event handler, but your handler accepts (value, id)
the value property of your <input> should be set from the object's name property
solution
function MyComponent() {
const [headings, setHeadings] = useState(items);
const updateName = key => event => {
setHeadings(prevState => {
return [
// elements before the key to update
...prevState.slice(0, key),
// the element to update
{ ...prevState[key], name: event.currentTarget.value },
// elements after the key
...prevState.slice(key + 1),
]
})
}
return headings.map((h, key) =>
<input key={key} id={h.id} value={h.name} onChange={updateName(key)} />
)
}
improvement
Imagine having to write that function each time you have a component with array or object state. Extract it to a generic function and use it where necessary -
// generic functions
function arrUpdate(arr, key, func) {
return [...arr.slice(0, key), func(arr[key]), ...arr.slice(key + 1)]
}
function objUpdate(obj, key, func) {
return {...obj, [key]: func(obj[key])}
}
// simplified component
function MyComponent() {
const [headings, setHeadings] = useState(items);
const updateName = key => event => {
setHeadings(prevState =>
// update array at key
updateArr(prevState, key, elem =>
// update elem's name property
updateObj(elem, "name", prevName =>
// new element name
event.currentTarget.value
)
)
)
}
return headings.map((h, key) =>
<input key={key} id={h.id} value={h.name} onChange={updateName(key)} />
)
}
multiple arrow functions?
Curried functions make writing your event handlers easier, but maybe you've never used them before. Here's what the uncurried version would look like -
function MyComponent() {
const [headings, setHeadings] = useState(items);
const updateName = (key, event) => {
setHeadings(prevState =>
// update array at key
updateArr(prevState, key, elem =>
// update elem name property
updateObj(elem, "name", prevName =>
// new element name
event.currentTarget.value
)
)
)
}
return headings.map((h, key) =>
<input key={key} id={h.id} value={h.name} onChange={e => updateName(key, e)} />
)
}

prevent api from being called before state is updated

I have a list of objects. I want to make an api call once the location field of the object is changed. So for that, I have a useEffect that has id, index and location as its dependencies. I have set a null check for the location, if the location isn't empty, I want to make the api call. But the thing is, the api is being called even when the location is empty, and I end up getting a 400. How can I fix this and make the call once location isn't empty?
const [plants, setPlants] = useState([
{
name: 'Plant 1',
id: uuidv4(),
location: '',
coords: {},
country: '',
isOffshore: false,
}
]);
const [locationIDObject, setlocationIDObject] = useState({
id: plants[0].id,
index: 0
});
const handlePlantLocChange = (id, index, value) => {
setPlants(
plants.map(plant =>
plant.id === id
? {...plant, location: value}
: plant
)
)
setlocationIDObject({
id: id,
index: index
})
}
const getCoords = (id, index) => {
axios.get('http://localhost:3002/api/coords', {
params: {
locAddress: plants[index].location
}
}).then((response) => {
if(response.status === 200) {
handlePlantInfoChange(id, PlantInfo.COORD, response.data)
}
})
}
const handler = useCallback(debounce(getCoords, 5000), []);
useDeepCompareEffect(() => {
if(plants[locationIDObject.index].location !== '')
handler(locationIDObject.id, locationIDObject.index);
}, [ locationIDObject, plants[locationIDObject.index].location])
return (
<div className='plant-inps-wrapper'>
{
plants.map((item, idx) => {
return (
<div key={item.id} className="org-input-wrapper">
<input placeholder={`${item.isOffshore ? 'Offshore' : 'Plant ' + (idx+1) + ' location'}`} onChange={(e) => handlePlantLocChange(item.id, idx, e.target.value)} value={item.isOffshore ? 'Offshore' : item.location} className="org-input smaller-input"/>
</div>
)
})
}
</div>
)
I think your useCallback is not updating when value of your variables is changing and that is the issue:
Although the check is correct, but the call is made for older values of the variable. You should update the dependencies of your useCallback:
console.log(plants) inside getCoords might help you investigate.
Try this:
const handler = useCallback(debounce(getCoords, 5000), [plants]);
So it turns out the issue was with my debouncing function. I don't know what exactly the issue was, but everything worked as expected when I replaced the debouncing function with this:
useEffect(() => {
console.log("it changing")
const delayDebounceFn = setTimeout(() => {
getCoords(locationIDObject.id, locationIDObject.index)
}, 4000)
return () => clearTimeout(delayDebounceFn)
},[...plants.map(item => item.location), locationIDObject.id, locationIDObject.index])

AntDesign Cascader: error Not found value in options

I am wanting to use the "Cascader" component of "Ant Design" but I am having trouble filling it with data. This is my code, which I am doing wrong, sorry I am still a newbie and I need your support please.
function CascaderEmpCliUn(props) {
const optionLists = { a: []}
const [state, setState] = useState(optionLists);
useEffect(() => {
async function asyncFunction(){
const empresas = await props.loginReducer.data.empresas;
const options = [
empresas.map(empresa => ({
value: empresa.id,
label: empresa.siglas,
children: [
empresa.cli_perm.map(cliente => ({
value: cliente.id,
label: cliente.siglas,
children: [
cliente.uunn_perm.map(un => ({
value: un.id,
label: un.nombre,
}))
]
}))
]})
)
];
setState({a : options})
}
asyncFunction();
}, [])
return (
<Cascader options={state.a} placeholder="Please select" />
)
}
ERROR
Not found value in options
I was able to reproduce your error with dummy data whenever I had an empty array of children at any level. I'm not sure why this should be a problem, but it is. So you need to modify your mapping function to check the length of the child arrays. It seems to be fine if passing undefined instead of an empty array if there are no children.
General Suggestions
You don't need to store the options in component state when you are getting them from redux. It can just be a derived variable. You can use useMemo to prevent unnecessary recalculation.
You are passing the entire loginReducer state in your props which is not ideal because it could cause useless re-renders if values change that you aren't actually using. So you want to minimize the amount of data that you select from redux. Just select the empresas.
Revised Code
function CascaderEmpCliUn() {
// you could do this with connect instead
const empresas = useSelector(
(state) => state.loginReducer.data?.empresas || []
);
// mapping the data to options
const options = React.useMemo(() => {
return empresas.map((empresa) => ({
value: empresa.id,
label: empresa.siglas,
children:
empresa.cli_perm.length === 0
? undefined
: empresa.cli_perm.map((cliente) => ({
value: cliente.id,
label: cliente.siglas,
children:
cliente.uunn_perm.length === 0
? undefined
: cliente.uunn_perm.map((un) => ({
value: un.id,
label: un.nombre
}))
}))
}));
}, [empresas]);
return <Cascader options={options} placeholder="Please select" />;
}
The final code of "options" object:
const options = useMemo(() => {
return empresas.map((empresa) => ({
value: empresa.id,
label: empresa.siglas,
children:
empresa.cli_perm.length === 0
? console.log("undefined")
:
empresa.cli_perm.map((cliente) => ({
value: cliente.id,
label: cliente.siglas,
children:
cliente.uunn_perm.length === 0
? console.log("undefined")
:
cliente.uunn_perm.map((un) => ({
value: un.id,
label: un.nombre
}))
}))
}));
}, [empresas]);

how to update object using index in react state?

suppose i am having an array of like
[
{ id: 1, name: "John" , selected: true}
{ id: 2, name: "Jim" , selected: false }
{ id: 3, name: "James" , selected: false }
]
I am having function which get id and on the basis of that id I want to change the selected property of object to true and other selected property object to false
here is my function what i have tried but its throwing error
const handleOnClick = (id) => {
const temps = state.findIndex((sub) => sub.id === id)
setState(prevState => ({
...prevState,
[prevState[temps].selected]: !prevState[temps].selected
}))
Use a .map() to iterate everything. You shouldn't change values within the array without recreating the tree down to the changed object. See below example.
Below you will:
map over every element
Spread their current values
Overwrite the selected depending on if the id is the newly "selected" id
I also supplied an additional function to update using the index of the item in the array instead of by id since your title is "how to update object using index in react state?", although your question also mentions changing by id. I would say go by id if you have the choice, array indices can change.
const App = () => {
const [state, setState] = React.useState([
{ id: 1, name: "John" , selected: true},
{ id: 2, name: "Jim" , selected: false },
{ id: 3, name: "James" , selected: false }
]);
// This takes the ID of the item
const handleOnClick = (id) => {
const newState = state.map(item => {
return {
...item,
selected: item.id === id
};
});
setState(newState);
};
// This takes the index of the item in the array
const handleOnClickByIndex = (indexToSelect) => {
const newState = state.map((item, idx) => {
return {
...item,
selected: indexToSelect === idx
};
});
setState(newState);
};
return (
<div>
{state.map((item, mapIndex) => (
<div key={item.id}>
<div>
<span>{item.name} is selected: {item.selected ? "yes" : "no"}</span>
{/* This will update by ID*/}
<button onClick={() => handleOnClick(item.id)}>Select using ID</button>
{/* This will update by the index of the array (supplied by map) */}
<button onClick={() => handleOnClickByIndex(mapIndex)}>Select using index</button>
</div>
</div>
))}
</div>
);
};
ReactDOM.render(
<App/>,
document.getElementById("app")
);
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="app"></div>

react-select AsyncSelect loadOptions through React.cloneElement

I have a config file with some fields for generating input elements inside a component.
I'm trying to generate an AsyncSelect input field and assigning it loadOptions prop through the config. Problem is that the function never gets called.
Here's the object in the configuration for generating the AsyncSelect input:
{
key: 'cityCode',
value: (customer, borrower) => ({
label: borrower.cityName,
value: borrower.cityCode
}),
text: 'city',
disabled: falseFn,
input: REACT_ASYNC_SELECT_INPUT,
props: (borrower, component) => ({
inputValue: component.state.cityName,
loadOptions: () => {
return CitiesService.find(component.state.cityName || '')
.then(records => records.map(record => ({
label: record.name,
value: record.code
})));
},
onInputChange: cityName => {
component.setState({
cityName
});
},
onChange: ({label, value}, {action}) => {
if (action === 'clear') {
component.updateCustomer(component.props.fieldKey + 'cityName', '');
component.updateCustomer(component.props.fieldKey + 'cityCode', -1);
} else {
component.updateCustomer(component.props.fieldKey + 'cityName', label);
component.updateCustomer(component.props.fieldKey + 'cityCode', value);
}
}
}),
render: trueFn
},
Here's the part of the component render utilizing the config file to render different inputs:
<div className="values">
{
BorrowerInfoConfig().map(field => {
if (field.render(borrower)) {
const kebabCaseKey = _.kebabCase(field.key);
const fieldElement = React.cloneElement(field.input, {
className: `${kebabCaseKey}-input`,
value: field.value ? field.value(customer, borrower) : _.get(borrower, field.key),
onChange: e => {
let value = e.target.value;
if (field.options) {
value = Number(value);
}
this.updateCustomer(fieldKey + field.key, value);
},
disabled: field.disabled(borrower),
...field.props(borrower, this)
}, field.options ? Object.keys(field.options).map(option => <option
key={option}
className={`${kebabCaseKey}-option`}
value={option}>
{field.options[option]}
</option>) : null);
return <div key={field.key} className={`value ${kebabCaseKey}`}>
<span>{field.text}</span>
{fieldElement}
</div>;
}
return null;
})
}
</div>
As you can see I use React.cloneElement to clone the input from the config file and assign new properties to it depends on what I get from the config file, in this case 4 custom props:
inputValue
loadOptions
onInputChange
onChange
My problem is that loadOptions is never called, any ideas why? On the other hand inputValue is assign correctly to the cityName state of the component.
My Problem was that REACT_ASYNC_SELECT_INPUT references normal Select and not Select.Async.

Resources