React child component not re-rendering on updated parent state - javascript

I've tried to find a solution to this, but nothing seems to be working. What I'm trying to do is create a TreeView with a checkbox. When you select an item in the checkbox it appends a list, when you uncheck it, remove it from the list. This all works, but the problem I have when I collapse and expand a TreeItem, I lose the checked state. I tried solving this by checking my selected list but whenever the useEffect function runs, the child component doesn't have the correct parent state list.
I have the following parent component. This is for a form similar to this (https://www.youtube.com/watch?v=HuJDKp-9HHc)
export const Parent = () => {
const [data,setData] = useState({
name: "",
dataList : [],
// some other states
})
const handleListChange = (newObj) => {
//newObj : { field1 :"somestring",field2:"someotherString" }
setDataList(data => ({
...data,
dataList: data.actionData.concat(newObj)
}));
return (
{steps.current === 0 && <FirstPage //setting props}
....
{step.current == 3 && <TreeForm dataList={data.dataList} updateList={handleListChange}/>
)
}
The Tree component is a Material UI TreeView but customized to include a checkbox
Each Node is dynamically loaded from an API call due to the size of the data that is being passed back and forth. (The roots are loaded, then depending on which node you select, the child nodes are loaded at that time) .
My Tree class is
export default function Tree(props) {
useEffect(() => {
// call backend server to get roots
setRoots(resp)
})
return (
<TreeView >
Object.keys(root).map(key => (
<CustomTreeNode key={root.key} dataList={props.dataList} updateList={props.updateList}
)))}
</TreeView>
)
CustomTreeNode is defined as
export const CustomTreeNode = (props) => {
const [checked,setChecked] = useState(false)
const [childNodes,setChildNodes] = useState([])
async function handleExpand() {
//get children of current node from backend server
childList = []
for( var item in resp) {
childList.push(<CustomTreeNode dataList={props.dataList} updateList={props.updateList} />)
}
setChildNodes(childList)
}
const handleCheckboxClick () => {
if(!checked){
props.updateList(obj)
}
else{
//remove from list
}
setChecked(!checked)
}
// THIS IS THE ISSUE, props.dataList is NOT the updated list. This will work fine
// if I go to the next page/previous page and return here, because then it has the correct dataList.
useEffect(() => {
console.log("Tree Node Updating")
var isInList = props.dataList.find(function (el) {
return el.field === label
}) !== undefined;
if (isInList) {
setChecked(true);
} else {
setChecked(false)
}
}, [props.dataList])
return ( <TreeItem > {label} </TreeItem> )
}

You put props.data in the useEffect dependency array and not props.dataList so it does not update when props.dataList changes.
Edit: Your checked state is a state variable of the CustomTreeNode class. When a Tree is destroyed, that state variable is destroyed. You need to store your checked state in a higher component that is not destroyed, perhaps as a list of checked booleans.

Related

State in child component is not getting updated after button click

I was trying to create a delete operation on the array of objects (videoData).videoData is getting mapped in the child component along with the DELETE button. At the click of the DELETE button in the child component (childComp).
I want to set the current id to the "childData" state in the child component but it's not getting updated with the current id.
When I am consoling log the childData, in the child component, it still says null which means it was not updated.Why is it not updating?
My own explanation -
When the delete button is getting clicked, the testFunc() is getting fired in the parent component which is removing the item with that particular id from videoData array, and as a result, the id is not able to pass to the child component due to which child component is getting rendered with the original state (null). I don't know if the explanation is correct or not, can someone help me in clearing this up?
function ParentComp() {
const [videoData, setvideoData] = useState([{ id: 2 }, { id: 3 }]);
function testFunc(id) {
let hasMatch = false
if (!hasMatch) {
let arr = videoData.filter(item => {
return item.id !== id
})
setvideoData(arr)
}
}
return (
<childComp testFunc={testFunc} videoData={videoData}/>
)
}
function childComp({testFunc, videoData}) {
const [childData, setchildData] = useState(null)
function ChildFunc(itemId) {
testFunc(itemId)
setchildData(itemId)
}
console.log(childData) //null (state not getting updated)
return (
<>
{videoData.map((item) => {
return (
<button onClick={() => ChildFunc(item.id)}>Delete</button>
);
})}
</>
)
}

React | Adding and deleting object in React Hooks (useState)

How to push element inside useState array AND deleting said object in a dynamic matter using React hooks (useState)?
I'm most likely not googling this issue correctly, but after a lot of research I haven't figured out the issue here, so bare with me on this one.
The situation:
I have a wrapper JSX component which holds my React hook (useState). In this WrapperComponent I have the array state which holds the objects I loop over and generate the child components in the JSX code. I pass down my onChangeUpHandler which gets called every time I want to delete a child component from the array.
Wrapper component:
export const WrapperComponent = ({ component }) => {
// ID for component
const { odmParameter } = component;
const [wrappedComponentsArray, setWrappedComponentsArray] = useState([]);
const deleteChildComponent = (uuid) => {
// Logs to array "before" itsself
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5"},
{"uuid":"0ed3cc3-7cd-c647-25db-36ed78b5cbd8"]
*/
setWrappedComponentsArray(prevState => prevState.filter(item => item !== uuid));
// After
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5",{"uuid":"0ed3cc3-
7cd-c647-25db-36ed78b5cbd8"]
*/
};
const onChangeUpHandler = (event) => {
const { value } = event;
const { uuid } = event;
switch (value) {
case 'delete':
// This method gets hit
deleteChildComponent(uuid);
break;
default:
break;
}
};
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
onChangeOut: onChangeUpHandler,
};
setWrappedComponentsArray(wrappedComponentsArray => [...wrappedComponentsArray, objToAdd]);
// Have also tried this solution with no success
// setWrappedComponentsArray(wrappedComponentsArray.concat(objToAdd));
};
return (
<>
<div className='page-content'>
{/*Loop over useState array*/}
{
wrappedComponentsArray.length > 0 &&
<div>
{wrappedComponentsArray.map((props) => {
return <div className={'page-item'}>
<ChildComponent {...props} />
</div>;
})
}
</div>
}
{/*Add component btn*/}
{wrappedComponentsArray.length > 0 &&
<div className='page-button-container'>
<ButtonContainer
variant={'secondary'}
label={'Add new component'}
onClick={() => addOnClick()}
/>
</div>
}
</div>
</>
);
};
Child component:
export const ChildComponent = ({ uuid, onChangeOut }) => {
return (
<>
<div className={'row-box-item-wrapper'}>
<div className='row-box-item-input-container row-box-item-header'>
<Button
props={
type: 'delete',
info: 'Deletes the child component',
value: 'Delete',
uuid: uuid,
callback: onChangeOut
}
/>
</div>
<div>
{/* Displays generated uuid in the UI */}
{uuid}
</div>
</div>
</>
)
}
As you can see in my UI my adding logic works as expected (code not showing that the first element in the UI are not showing the delete button):
Here is my problem though:
Say I hit the add button on my WrapperComponent three times and adds three objects in my wrappedComponentsArray gets rendered in the UI via my mapping in the JSX in the WrapperComponent.
Then I hit the delete button on the third component and hit the deleteChildComponent() funtion in my parent component, where I console.log my wrappedComponentsArray from my useState.
The problem then occurs because I get this log:
(2) [{…}, {…}]
even though I know the array has three elements in it, and does not contain the third (and therefore get an undefined, when I try to filter it out, via the UUID key.
How do I solve this issue? Hope my code and explanation makes sense, and sorry if this question has already been posted, which I suspect it has.
You provided bad filter inside deleteChildComponent, rewrite to this:
setWrappedComponentsArray(prevState => prevState.filter(item => item.uuid !== uuid));
You did item !== uuid, instead of item.uuid !== uuid
Please try this, i hope this works
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item !== uuid));
};
After update
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item.uuid !== uuid)); // item replaced to item.uuid
};
Huge shoutout to #Jay Vaghasiya for the help.
Thanks to his expertise we managed to find the solution.
First of, I wasn't passing the uuid reference properly. The correct was, when making the objects, and pushing them to the array, we passed the uuid like this:
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
parentOdmParameter: odmParameter,
onChangeOut: function(el) { onChangeUpHandler(el, this.uuid)}
};
setWrappedComponentsArray([...wrappedComponentsArray, objToAdd]);
};
When calling to delete function the function that worked for us, was the following:
const deleteChildComponent = (uuid) => {
setWrappedComponentsArray(item => item.filter(__item => __item.uuid !== uuid)); // item replaced to item.uuid
};

React - Trying to remove child component, but there is some issue with the function I pass

I am trying to remove a child component by sending a function and an id and then calling that function
when a button in the child is clicked.
Note: The child is a class component, the parent a functional component
Here is the function defined in the parent:
const removeTable = (tableId) => {
const newArray = tables.filter((el) => (el.id !== tableId)
);
console.log(newArray);
setTables(newArray)
}
This is removing elements, but not the one I want. Instead of removing the element with the id I pass it, it keeps that number, starting from 0.
So when I click on the item with a id of 3, it only keeps 0-2. In this example it should keep 0-2 and 4-6. (The array is supposed to be 7 elements long, but somehow it is shortened (before the filter))
What I've tried and Discovered:
I was completely lost, so I decided to create a mock function without using the child:
const removeTabletest = () => {
const key = 1;
const testArr = [{id:1,op:"adsad"}, {id:2,op:"adsad"},{id:3,op:"adsad"} ];
const fml = testArr.filter( (el) => (el.id !== key));
console.log(fml)
}
This function works as I expect.
Finally I stumbled on the fact that when I console.log(tables) at the beginning of the function, I am not getting the same data as in React Dev Tools. The tables array is not the full array I expect.
But when I create another button that is called by the parent (instead of the child), then tables is logged correctly:
Any idea what is going on? or how else I can achieve this?
This sounds like a difficult approach to use with React. Generally what you want to do is just filter your array in the render method based on props or state. If you need a child element to modify what you filter by, like a button, you would pass a callback to the child. Here is an example:
class Table extends React.Component {
constructor(props) {
super(props);
this.state = { hiddenItems: [] };
}
hideItem = item => {
// to hide a row of the table we add it to the list
// of hidden items.
this.setState({ hiddenItems: [...this.state.hiddenItems, item] });
}
render() {
// Create the table element by filtering out hidden items
const table = this.props.items
.filter(item => !this.state.hiddenItems.includes(item))
.map(item => {
return (
<div key={item.id}>{item.contents}</div>
);
});
return (
<>
{table}
// the onClick function could also be passed to a
// child React object
<button onClick={() => this.hideItem(this.props.item[0])}>
Hide item 0
</button>
</>
);
}
}
Where this.props.items would look something like:
[{ id: 0, contents: 'blah blah'},
{ id: 1, contents: (<span>blah</span>)}]
Of course you can also have a function unhiding an item but hopefully this shows the general approach.

Persisting state across tables on event change

I have a table of schedules that is rendered by a dropdown. Each schedule can then be marked for export via a slider, this will store the schedule id in scheduleIdsToExport and show that schedule in the export table.
But if I change the Select Query dropdown, which renders more schedules specific to that query, the schedules marked for export from the previous query disappear from the table. I want the schedules marked for export to persist in the table no matter what query is selected from the dropdown.
So I'm thinking I need to have something in my slider function to update state with the all the schedule objects marked for export and have them persist in the exported table. I'm not exactly sure how to go about storing all the schedules to keep them in the exported table and have the scheduleIdsToExport array also keep the id's of each schedule
slider = ({ id, isExported }) => {
if (isExported === true) {
this.setState(
{
scheduleIdsToExport: [id, ...this.state.scheduleIdsToExport]
},
() => {
console.log(this.state.scheduleIdsToExport);
}
);
} else {
const newArray = this.state.scheduleIdsToExport.filter(
storedId => storedId !== id
);
this.setState(
{
scheduleIdsToExport: newArray
},
() => {
console.log(this.state.scheduleIdsToExport);
}
);
}
};
The sandbox here will provide further explanation on what is happening.
This is just chaotic!
The problem : Keep track from multiples list of items(schedules) that will eventually be added to another list schedulesToExport
The Solution :
Create a parent component that reflects the previously described state
class Container extends Component{
state ={
querys :[
['foo','lore ipsum','it\'s never lupus'],
['bar','ipsumlorem', 'take the canolli']
],
selectedQuery : 0,
schedulesToExport : []
}
}
Now we have a list of lists, that can be interpreted as a list of querys containing a list of schedules
Render a select element to reflect each query:
render(){
const { querys, selectedQuery } = this.state
const options = querys.map((_, index) => (<option value={index}> Query: {index + 1}</option>))
return(
<div>
<select onChange={this.selectQuery}>
{options}
</select>
{
querys[selectedQuery].map(schedule => (
<button onClick={() => this.selectSchedule(index)}> Schedule: {schedule} </button>
))
}
</div>
)
}
What's happening? We are just rendering the selected query by it's index and showing all it's respective schedules.
Implement the selectQuery and selectSchedule methods which will add the selected schedule in the list to export:
selectQuery = e => this.setState({selectedQuery : e.target.value})
selectSchedule = index => {
const { selectedQuery } = this.state
const selected = this.state.querys[selectedQuery][index]
this.setState({
schedulesToExport: this.state.schedulesToExport.concat(selected)
})
}
That's it, now you a have a list of querys being displayed conditionally rendered selectedQuery props is just a index, but could be a property's name. You only see schedules from the current selected query, so when you click on schedule we just return it's index, and the state.querys[selectedQuery][index] will be your selected schedule, that is securely store in the state on a separated list.
I have updated your sandbox here.
In essence, it does not work in your example because of the following:
schedules
.filter(schedule =>
scheduleIdsToExport.includes(Number(schedule.id))
)
.map(schedule => {
return (
<Table.Row>
...
</Table.Row>
);
})
The value of schedules is always set to the current query, hence you end up showing schedules to export for the current query only.
A solution that changes very little of your code is to ditch scheduleIdsToExport altogether, and use schedulesToExport instead. Initially, we'll set schedulesToExport to an empty
object; we'll add schedules to it (keyed by schedule id) every time a schedule is selected - we'll delete schedules in the same way every time a schedule is unselected.
class App extends React.Component {
// ...
slider = ({ id, isExported }) => {
const obj = this.state.schedules.find(s => Number(s.id) === Number(id));
if (isExported === true) {
this.setState({
schedulesToExport: {
...this.state.schedulesToExport,
[id]: {
...obj
}
}
});
} else {
const newSchedulesToExport = this.state.schedulesToExport;
delete newSchedulesToExport[id];
this.setState({
schedulesToExport: newSchedulesToExport
});
}
};
// ...
}
You would then render the schedules to export as follows:
Object.keys(schedulesToExport).map(key => {
const schedule = schedulesToExport[key];
return (
<Table.Row>
...
</Table.Row>
);
})
Again, see more details in sandbox here.

React DnD - nested component can set props but not parent on drop event

I have two DropTargets, ComponentA and ComponentB arranged in this structure:
<ComponentA
dropEvent={this.handleRootDropEvent}
>
<p>Displayed Categories </p>
{
this.state.categories.map((c, idx) =>
<ComponentB
key={idx}
parentCat={null}
thisCat={c}
level={c.level}
dropEvent={this.handleDropEvent}
/>
)
}
</ComponentA>
And the problem I'm having in particular is with the drop target spec drop function. Both of the DropTargets (for ComponentA and B) have the same drop function defined at the moment:
//ComponentB
const targetSpec = {
drop(props, monitor, component) {
const item = monitor.getItem()
if (props.thisCat.category_id == item.category_id) return
component.setState({ droppedItem: item })
component.setState({ droppedItem: null })
},
canDrop(props, monitor) {
const item = monitor.getItem()
if (props.thisCat.category_id == item.category_id) return false
return true
},
hover(props, monitor, component){
}
}
// Component A
const source = {
drop(props, monitor, component) {
const item = monitor.getItem()
component.setState({ droppedItem: item })
component.setState({ droppedItem: null })
}
}
(I can confirm that both are getting called on drop events, and that item is not null in ComponentA's drop function)
It seems that component.setState({}) actually changes the props of the component it's called on, or at least that's how I'm able to use it in the case of ComponentB:
componentWillReceiveProps = () => {
if (this.props.droppedItem) {
this.props.dropEvent(this.props.droppedItem, this.props.thisCat)
}
}
And that code works, I'm able to trigger a function in the parent component (or this case grandparent) of ComponentB by checking for the presence of those props that are set in the drop function.
However, in the same exact componentWillReceiveProps function definition in ComponentA, this.props.droppedItem is always undefined.
Any idea what I could try to get the props passed successfully? Am I misunderstanding React DnD's component.setState({}) API?

Categories

Resources