React doesn't re-render component on state change - reactjs

I have a component with a search field and a list of items, where I am using that value from search field to filter through the array of items and update the list. This is are the parts of the component responsible for that:
this.state = {
tasks: null,
unfilteredTasks: null,
};
componentDidMount() {
this.gateway.loadTasks().then(result => {
this.setState({tasks: result, unfilteredTasks: result})
})
}
onSearchChange(event) {
const value = event.target.value;
const propsToCheck = ['task.text', 'type', 'status', 'topic.name'];
this.setState(prevState => {
const tasks = prevState.unfilteredTasks.filter(obj => checkObjectContainsValue(obj, value)(propsToCheck));
return {tasks: tasks};
})
}
render() {
return <TasksPresentation
<TasksList
tasks={this.state.tasks}
onSearchChange={(event) => this.onSearchChange(event)}>
</TasksList>
</TasksPresentation>
}
With this I can see in the developer tools that the state of the component is being changed and that the tasks list is being filtered, but the lists is not being re-rendered. Why is that, how can I fix this?
Update
On inspecting further down the component chain. I can see that the tas list table component that is responsible for rendering of the list is a class component that uses prop rows which is the tasks list being send down from the parent component, to build state:
constructor(props) {
super(props);
this.state.data = this.transformRows(props.rows)
}

Related

react lifecycle method - correct way to filter

I am new to react. I have a product page component which is a page showing a particular product. What I am trying to do is that this ProductPage find the particular product in the products (which is an array of objects stored as props) and save it as the state before render it out.
I got these errors. I am not able to filter out single product from the array.
1) currentProduct is not defined.
2)Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
Which lifecycle method should I use in order to resolve this? I am confused. Can someone explain to me?
In my ProductPage.js,
import React, { Component } from 'react';
import { connect } from 'react-redux';
class ProductPage extends Component {
constructor(props) {
super(props)
this.state = {
product: [],
productId: this.props.match.params._id,
}
this.productsFiltering = this.productsFiltering.bind(this);
}
// update the state so that a product can be rendered
componentDidUpdate() {
const currentProduct = this.productsFiltering(this.state.productId);
this.setState({
product: currentProduct,
})
}
// find a product based on the Id
// pass the _id from url to find out the single product
productsFiltering = (productId) => {
const productArray = this.props.products;
return productArray.find(product => product._id === productId)
}
render() {
// error
const {name, price, description} = product;
return (
<div>
<span>{product.name}</span>
<span>{product.price}</span>
<span>{product.description}</span>
</div>
)
}
}
const mapStateToProps = (state) => ({
products: state.productsContainer,
});
export default connect(
mapStateToProps,
)(ProductPage);
The root issue is that you're copying props to state, which there is no need to do.
ProductPage displays a product and doesn't change, therefore no state is needed. Simply grab the matching product and store it in a variable then render. No setState() needed.
you can remove both errors if you change your code to this:
class ProductPage extends Component {
constructor(props) {
super(props);
this.state = {
product: []
};
this.productsFiltering = this.productsFiltering.bind(this);
}
// update the state so that a product can be rendered
componentDidUpdate() {
const productId = this.props.match.params._id;
if (productId && this.state.product._id != productId) {
const currentProduct = this.productsFiltering(productId);
if (currentProduct) {
this.setState({
product: currentProduct
});
}
}
}
// find a product based on the Id
// pass the _id from url to find out the single product
productsFiltering = (productId) => {
const productArray = this.props.products;
if(productArray.length){
return productArray.find((product) => products._id === productId);
}
};
render() {
const { name, price, description } = this.state.product || {};
return (
<div>
<span>{product.name}</span>
<span>{product.price}</span>
<span>{product.description}</span>
</div>
);
}
}
const mapStateToProps = (state) => ({
products: state.productsContainer
});
export default connect(mapStateToProps)(ProductPage);
-- always check for undefined or null properties
-- don't use setState in componentDidMount if the new state is constantly changing
-- avoid using a state which is directly drived from props because it's a bad practice and you can get stale props

Unmounting child component throws can't call setstate on an unmounted component in reactjs

when i unmount child component i get can't setstate on unmounted component warning.
What i am trying to do?
I have a parent component ViewItems and child component ItemsList.
In the ViewItems component i retrieve the items list from the server using load_items method which in turn uses client.get_file method. and store those items list in state named "items". I call this load_items method in componenDidMount method. However this will not show the new details for another item.
To give a clear picture of the problem. Consider i render items in one page. When i click on one item it takes me to other page (in this case items component in mounted) and when i click a button click to view item details it lists the details related to that item. When i click the button to get back to list of items (page where we were before) and click another item. It should display details related to the new item clicked.
However, in this case when i click the new item it shows previous item details unless page refresh.
To overcome this, on componentDidUpdate i call this load_items method. This works. But, when i dont close the child component meaning the layout where details of item is shown...i get the can't call setstate on unmounted component warning. This error is shown after child component is unmounted.
Below is the code,
class ViewItems extends React.PureComponent {
constructor(props) {
super(props);
this.default_root_item = {
name: 'Items',
index: 0,
children: [],
};
this.state = {
root_items: this.default_root_item,
items: [],
};
}
componentDidMount() {
this.load_items();
}
componentDidUpdate() {
this.load_items();
}
componentWillUnmount() {
this.unlisten_path_change();
}
load_items = () => {
const file_name = 'file_name.json';
client.get_file(this.props.item_id, file_name, 'json')
.then((request) => {
const items = request.response;
this.setState({items: [this.default_root_item]});}
this.handle_path_change(this.props.location.pathname);})};
return (
<ChildComponent
on_close={this.handle_item_close}
root_item={this.state.root_item}/>)}
export default class ChildComponent extends React.PureComponent {
<Items
items={root_item}/>
function Items(props) {
return (
<ul className="Items_list">
<div className="items">
{props.items.map((item, index) => {
return (
<Item
key={index}
item={item}
/>
);
})}
</div>
</ul>
);}
}
First of all, do not make an API call in componentDidUpdate without making a check whether the props changed or not.
Second: While setting state from API request response, check if the component is still mounted or not
class ViewItems extends React.PureComponent {
constructor(props) {
super(props);
this.default_root_item = {
name: 'Items',
index: 0,
children: [],
};
this.state = {
root_items: this.default_root_item,
items: [],
};
this._isMounted = true;
}
componentDidMount() {
this.load_items();
}
componentDidUpdate(prevProps) {
if (prevProps.item_id !== this.props.item_id) {
this.load_items();
}
}
componentWillUnmount() {
this._isMounted = false;
this.unlisten_path_change();
}
load_items = () => {
const file_name = 'file_name.json';
client.get_file(this.props.item_id, file_name, 'json')
.then((request) => {
const items = request.response;
if (this._isMounted) {
this.setState({items: [this.default_root_item]});
this.handle_path_change(this.props.location.pathname);
}
})
};

React.js setState does not re-render child component

I am passing a list from the parent component's state to a child component's <CheckboxList /> props. The list should then show in the child component. After fetching the list elements from the database I use setState to update the list but the child component is not re-rendered with the new list.
class ParentComponent extends Component {
state = {
users: [{ username: 'before' }]
}
componentDidMount() {
const result = [];
db.collection("users").get().then((querySnapshot) => {
querySnapshot.forEach(function (doc) {
let user = doc.data();
result.push(user);
});
}).then(() => {
this.setState({
users: result
})
})
}
render() {
return (
<div>
<h1>List of users</h1>
<CheckboxList list={this.state.users} />
</div>
)
}
}
The list shows before and it does not update with content from the database. I checked and the values are fetched from the database, they are just not passed to the <CheckboxList /> after the setState. Can anybody help me?
The problem was that I was retrieving the list from the CheckboxList status and then using it through that. Now I have fixed it. I am retrieving it in the CheckboxList render(), storing it into a variable and then using it from there.

React- How to update child prop based on parent state

I'm running into a problem getting a child react component to update when its parent stage changes. I have an Editor parent component that sets its state and then updates the state if the component receives an updated schedule (from a graphQL mutation component).
The problem is that componentDidUpdate triggers which does trigger the Modefield to update, but it is before the setState in componentDidUpdate can update the state. This means the child doesn't update. (Note- I know a more idiomatic way is to get rid of state all together, but this way allows a field to both edit and create a new one.)
How can I cause the child to update based on the parent's state change?
export const updateScheduleMutation = gql`
mutation updateScheduleMutation(
$id: ID!
$mode: String
) {
updateSchedule(
id: $id
mode: $mode
) {
id
mode
}
}
`;
class EditorWrapper extends React.Component {
constructor(props) {
super(props);
this.state = { scheduleId: props.scheduleId || '' };
}
render() {
return (
<Mutation mutation={updateScheduleMutation}>
{(updateSchedule, { mutationData }) => <Editor {...data} updateSchedule={updateSchedule} />}
</Mutation>
)
}
}
class Editor extends React.Component {
constructor(props) {
super(props);
const { schedule } = props;
if(schedule === null){
this.state = {
schedule: { mode: schedule.mode || "" }
};
}
}
componentDidUpdate(prevProps) {
if (prevProps.schedule !== this.props.schedule) {
this.setState({ ...this.props.schedule });
}
}
changeInput = (path, input) => {
const { updateSchedule, schedule } = this.props;
const field = path.split('.')[1];
updateSchedule({ variables: { id: schedule.id, [field]: input } });
this.setState({ [path]: input });
};
render() {
return (
<ModeField input={this.state.schedule.input} />
);
}
}
const ModeField = ({input}) => FormControl value={input} />
EDIT: I updated the component to show the higher level graphQL wrapper. The reason why I wanted state in the Editor component is that in the event the graphQL query comes back as null, I set this.state.mode to an empty string, which I then update on change. Then, I would create a new schedule item on submit.
LIFT THE STATE UP! Try to manage the base state of your data in parent component and use the data as props in your component:
You also can try getDerivedStateFromProps, but before check the react blog advices:
https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html

Re-render array of child components in React after editing the array

I have data in parent's component state like this:
data=[
{key:0,name="abc",value="123"},
{key:1,name="def",value="456"},
{key:2,name="ghi",value="789"}
]
In my react component I am calling child components as list in the return part of the parent component as shown below.
class MyApp extends React.Component{
constructor(props) {
super(props);
this.state = {
record={
name="",
timestamp="",
data =[
{key:0,name="abc",value="123"},
{key:1,name="def",value="456"},
{key:2,name="ghi",value="789"}
]
}
this.deleteFromStateArray=this.deleteFromStateArray.bind(this);
}
deleteFromStateArray(key){
let oldData = [...this.state.record.data];
let newData= [];
oldData.map(function (record, i) {
if (record.key != key) {
newData.push(record);
}
})
newData.map(function (record, i) {
newData[i].key = i + 1
})
this.setState({ record: Object.assign({}, this.state.record, { data: newData }), });
}
render() {
return(
{
this.state.data.map((record,index) =>
<Child key={record.key}
id={record.key}
name={record.name}
value={record.value}
onDelete={this.deleteFromStateArray} />
}
)
}
I am calling onDelete() in the child component like this
<button onClick={this.props.onDelete(this.state.id)} />
\\ I am initializing id as key in state inside constructor in child
My problem is when I am calling onDelete in the child class I am able to remove the obj with key=1 properly in the function but rerendering is not happening properly.
What I mean is state is getting set properly with only 2 items in data 1 with key=0 and other with key=2. But what i see in GUI is 2 child components 1 with key 0 and second one with key=1 which is cxurrently not present in the state data.
Can anybody help me with re-rendering the data properly?
I also want to change the key ordering after deleting from array in setState
React uses key to work out if elements in the collection need to be re-rendered. Keys should be unique and constant. In your method you are changing key property of your records and this probably leads to described error.
Also, you can replace all your code with simple filter call like this:
this.setState(oldState => ({
record: {
...oldState.record,
{
data: oldState.record.data.filter(item => item.key !== key)
}
}
});
It is also worth mentioning that you should keep your state as flat as possible to simplify required logic. In your case removing record and and leaving it as below would be a good idea:
this.state = {
name: "",
timestamp: "",
data: [
{key:0,name: "abc",value: "123"},
{key:1,name: "def",value: "456"},
{key:2,name: "ghi",value: "789"}
]
}
I'm not certain if this actually worked for you but the function declaration for deleteFromStateArray is inside the render function.
I think your component should look like this:
class MyApp extends React.Component{
constructor(props) {
super(props);
this.state = {
record={
name="",
timestamp="",
data =[
{key:0,name="abc",value="123"},
{key:1,name="def",value="456"},
{key:2,name="ghi",value="789"}
]
}
this.deleteFromStateArray=this.deleteFromStateArray.bind(this);
}
deleteFromStateArray(key){
let oldData = [...this.state.record.data];
let newData= [];
oldData.map(function (record, i) {
if (record.key != key) {
newData.push(record);
}
})
newData.map(function (record, i) {
newData[i].key = i + 1
})
this.setState({ record: Object.assign({}, this.state.record, { data: newData }), });
}
render() {
return(
{
this.state.record.data.map((record,index) =>
<Child key={record.key}
id={record.key}
onDelete={this.deleteFromStateArray} />
}
)
}
}
You can use a built-in set function provided by react with state. See below for an example:
import { useState } from 'react';
const [data, setData ] = useState([]);
fetch('https://localhost:5001/getdata')
.then((response) => response.json())
.then((data) => setData(data)) // Example setData usage
.catch((error) => alert)

Resources