I need to hook-up to the component update event, to fetch fresh data. Hence, I wrote something like:
...
constructor(props) {
super(props);
const values = queryString.parse(this.props.location.search),
type = values['type'];
this.props.loadChallengeDetails(type);
}
shouldComponentUpdate() {
console.log('shouldComponentUpdate');
/* if (this.props.location.search !== nextProps.location.search) {
console.log('should reload report');
const values = queryString.parse(nextProps.location.search),
type = values['type'];
this.props.loadChallengeDetails(type);
} */
return true;
}
componentDidUpdate(prevProps) {
console.log('componentDidUpdate');
if (this.props.location.search !== prevProps.location.search) {
console.log('should reload report');
const values = queryString.parse(this.props.location.search),
type = values['type'];
this.props.loadChallengeDetails(type);
}
}
render() {
console.log('details:', this.props.details);
...
This page/view is a "detail" for a "master" page, if you will. Depending on the value of type passed in the query, it fetches some data.
On first render it happens fine. Both shouldComponentUpdate or componentDidUpdate lifecycle methods are invoked. When I go back then view details for another record, however, it shows stale data. Console log prints details inside render, but does not call either shouldComponentUpdate or componentDidUpdate.
What am I doing wrong? Please advise.
Related
I am trying to change the state in a class component by using setState.
More specific I have a table, and I want to edit/update one of its elements. For this case, I am passing the indeces to the handleTableFieldOnChange function for the position of the value in the array.
Since I know that I should not mutate the state, I used an external library to deep copy the tables array/list.
The deep copy and the new value assignment works. The deep copy worked also with the JSON.parse(JSON.stringify(this.state.tables)); alternative.
Problem: For some reason the this.setState(...) does not change the tables value.
I do know the setState is asynchronous, this is why I used the callback and within it, the console.log(...) to check the updated value.
console.log(...) still emits the old value.
private handleTableFieldOnChange(val: boolean | string | number | [number, string], tblRowIndex: number, tblIndex: number, tblColINdex: number) {
const cloneDeep = require('lodash.clonedeep');
const newTables = cloneDeep(this.state.tables);
if (newTables && newTables[tblIndex] && newTables[tblIndex].items ) {
newTables[tblIndex].items![tblRowIndex][tblColINdex].value = val;
}
this.setState( {tables: newTables}, () => {
console.log(this.state.tables)
})
}
state: State = {
tables: [],
report: this.props.report,
};
constructor(props: DetailProp, state: State) {
super(props, state);
this.initFieldsAndTabels();
}
private initFieldsAndTabels() {
if (this.state.report && this.state.report.extraction_items) {
this.state.tables = [];
this.state.report.extraction_items.forEach((extractionItems) => {
this.state.tables.push(extractionItems);
});
}
}
The code in handleTableFieldOnChange looks fine to me.
However in initFieldsAndTabels you are applying push on state directly instead of calling setState which may probably cause the issues:
this.state.report.extraction_items.forEach((extractionItems) => {
this.state.tables.push(extractionItems); //#HERE
});
Also as React.Component docs state you should not call setState in constructor (you are calling initFieldsAndTabels in constructor. Instead you could use componentDidMount.
P.S. If you want to add those extraction items in the constructor then you need something like this:
constructor(props) {
super(props);
// method should return a new array/object, but not modify state
const tables = this.initFieldsAndTabels();
this.state = {
tables,
}
}
I have two component
Read Mode and Pagination
Read Mode Component
state = {
currentPdf:[],
currentPage: null,
totalPages: null,
intialState:1,
};
constructor(props) {
super(props);
this.onChangePage = this.onChangePage.bind(this);
this.onCurrentPageNo = this.onCurrentPageNo.bind(this);
this.onTotalPage = this.onTotalPage.bind(this);
}
componentDidMount() {
this.props.fetchPdfContent();
}
onCurrentPageNo(currentPageNo){
this.setState({ currentPage: currentPageNo });
}
onChangePage(currentPdf) {
this.setState({ currentPdf: currentPdf });
}
onTotalPage(totalpages){
this.setState({ totalPages: totalpages });
}
gotoPrevPage = (currentTarget) => {
if (this.state.currentPage > 1) {
let prevPage = this.state.currentPage - 1;
this.setState({ intialState: prevPage });
}
}
render() {
return (
<button className="btn btn-primary prev-btn pageBtn" onClick={this.gotoPrevPage.bind(this)}>
<span aria-hidden="true" className="icon-ico_arrow-right icomoon"> Left </span>
</button>
<Pagination initialPage={intialState} items={pdfcontnet} onTotalPage={this.onTotalPage} onChangePage={this.onChangePage} onChangePageNo={this.onCurrentPageNo} />
)
}
Pagination Component
constructor(props) {
super(props);
this.state = { pager: {} };
}
componentDidMount() {
// set page if items array isn't empty
if (this.props.items && this.props.items.length) {
this.setPage(this.props.initialPage);
}
}
componentDidUpdate(prevProps, prevState) {
// reset page if items array has changed
this.setPage(this.props.initialPage);
}
setPage(page) {
console.log(page + 'pages');
var items = this.props.items;
var pager = this.state.pager;
if (page < 1 || page > pager.totalPages) {
return;
}
// get new pager object for specified page
pager = this.getPager(items.length, page);
// get new page of items from items array
var pageOfItems = items.slice(pager.startIndex, pager.endIndex + 1);
// update state
this.setState({ pager: pager });
// call change page function in parent component
this.props.onChangePage(pageOfItems);
}
when i click the gotoPrevPage () initalState value need to pass
this.setPage(this.props.initialPage);
if i assign componentDidUpdate() state I got
You need to change initialPage={intialState} to initialPage={this.state.intialState} in your Read Mode Component render method.
PS: You should actually spell it initial, not intial.
try this:
onClick={()=>this.gotoPrevPage.bind(this)}
Your state object should be inside your constructor function in ReadMode. You should also be using this.setState({}) to update state. You also shouldn't be trying to reload the entire DOM when a new item is added the way you are in the Pagination component via commponentDidUpdate. React uses a virtual DOM and doesn't need to reload every element on the page every time one element is updated. It looks like part of your problem is your App continuously updates and you're getting stuck in a never ending loop updating the DOM.
Also, you might not need Pagination to have local state. You could store the state/data in the parent container and just pass it to Pagination via props. React uses one-way data flow and you should familiarize yourself with passing props from a parent component to a child component.
Brush up on state and lifecycle functions by reading the documentation, get an understanding of how React uses the virutal DOM to update elements, and rewrite your App so that state is stored mainly in the parent component and is passed via props to the child component.
Overlooking basic React concepts and structuring your project in a less than ideal way is the source of your problems.
I have a react project and redux for state management. These are my actions.
const mapDispatchToProps = dispatch => ({
handleChange: (name, value) => { dispatch(handleChange(name, value)) },
filterRooms: (rooms) => { dispatch(filterRooms(rooms)) }
});
I have to use these 2 method one by one.
this.props.handleChange(pets, true); // handle changes
this.filterRooms(); // filtering with changing value
filterRooms = () => {
let {rooms,pets} = this.props; // Here I should get pets === true but it's false.
// filter by pets
if (pets) {
tempRooms = tempRooms.filter(room => room.pets === true);
}
this.props.filterRooms(tempRooms);
}
If I use setTimeout for second method thats ok but I think that's not a correct way.
this.props.handleChange(name, value);
setTimeout(() => {
this.filterRooms();
}, 500);
Seems that below two function run in sequence, one after another
this.props.handleChange(pets, true); // handle changes
this.filterRooms(); // filtering with changing value
First dispatch changed value to Redux. And it is updated there (as sample with setTimeout works). But don't expect updated value from Redux will be immediately available to this.filterRooms().
You have some Reactjs component. Reactjs component is essentially class or function. Then you wrap in in connect. So your code may look like this
class Component1 extends React.Component {
onChange: () => {
this.props.handleChange(pets, true); // handle changes
this.filterRooms(); // filtering with changing value
}
// other staff
}
export default connect(mapStateToProps, mapDispatchToProps)(Component1)
Whats happens in React. It instantiate class Component1, then calls connect which in turn calls some methods of your class (i.e. render() or something else). connect also pass some values from Redux store to your component as props. Method of Component1 is executed and may change Redux state (as it do in your sample). But updated props will not be immediately available. props are just arguments, that have been passed by connect function. To change arguments of any function, you should call it once again. So after receiving updated pets, connect will call your component again with updated pets. But it will be later.
connect() -> calls Component1 and passes props.pets = false -> Compoentn1 sets pets in Redux to true -> connect() receives updated pets and calls Component1 with props.pets = true
That's why trick with setTimeout works. Ste timeout just await for second call of Component1
To solve your exact issue, don't read pets from props if you know that you've updated it.
this.props.handleChange(pets, true); // handle changes
this.filterRooms(true); // filtering with changing value
filterRooms = (pets) => {
let {rooms} = this.props;
// filter by pets
if (pets) {
tempRooms = tempRooms.filter(room => room.pets === true);
}
this.props.filterRooms(tempRooms);
}
Currently I get my data from an API in a JSON-format when running my saga. The fetching process begins, when the component did mount. That means the component renders two times.
Now, when the data is available as props. I can use it in order to render it.
My approach to this is like following, I have got a:
Constructor with the initial state
I fetch data in "componentDidMount"
I got a function that takes the JSON properties from props and puts it into new variables
I run this function in my render() function, when the props contain the fetched data
The Problem in this approach: Once the component runs the function where the data becomes "structured", the render-function loops and then after some time, the values of the properties get displayed with a warning message in the console.
My Questions:
How to prevent the looping when render() runs once?
How can I design this, so that particular properties of the fetched object merge into a new object and how to
I hope I described the most important things about my issue. Here is the code:
class Dashboard extends React.Component {
constructor(props) {
super(props);
this.state = {
deviceInfo: {
name: "Initial Name",
batLevel: "78%",
}
}
}
componentDidMount() {
this.props.requestApiData();
}
updateDeviceInfoWithState (){
const devices = (this.props.data.data);
if(devices){
const newDeviceInfo = this.state.deviceInfo;
newDeviceInfo.name = devices[0].shadow.desired.payload.refAppData.name;
newDeviceInfo.batLevel = devices[0].shadow.reported.payload.refAppData.batteryState.level;
this.setState({
deviceInfo: newDeviceInfo,
});
}
}
render() {
this.updateDeviceInfoWithState()
return (
<div className='container'>
<p> {this.state.deviceInfo.name} </p>
<p> {this.state.deviceInfo.batLevel} </p>
</div>
)
}...
Updating the state in the render method is not a good practice, since it might cause an infinite loop.
In your case state is redundant, since you only take the data from props, or replace it with defaults. Instead of using the state return the name and batLevel in the updateDeviceInfoWithState method, and use it in the render method.
Example (not tested):
class Dashboard extends React.Component {
componentDidMount() {
this.props.requestApiData();
}
updateDeviceInfoWithState (){
const devices = this.props.data.data;
if(devices){
const device = devices[0].shadow;
return {
name: device.desired.payload.refAppData.name,
batLevel: device.reported.payload.refAppData.batteryState.level
};
}
return {
name: "Initial Name",
batLevel: "78%",
};
}
render() {
const { name, batLevel } = this.updateDeviceInfoWithState();
return (
<div className='container'>
<p> {name} </p>
<p> {batLevel} </p>
</div>
);
}...
Note 1: If you want to decouple your component from the state, it's better to enforce simple properties as input for the data. For example, this component needs as properties the name and batLevel. It doesn't need to be aware of the array of devices, shadow, payload, etc... You can prepare the data when you receive it in the saga, or use a redux selector in mapStateToProps.
Note 2: If you really need the data in your state, you can use the getDerivedStateFromProps life-cycle method (React 16.3), or update the state in the componentWillReceiveProps if you use an older version.
For this case you can use ComponentWillRecieveProps method like this
componentWillRecieveProps(nextProps) {
// Condition as per ur requirement.
If(this.props.data != nextProps.data) {
this.updateDeviceInfoWithState(nextProps)
}
}
This method will only run whenever ur component props are changed.
When writing tests, I've noticed inexplicable behavior of componentWillReceiveProps.
This hook seems to be triggered only if component was initially mounted with props.
It looks like when I do component.setProps() on a "empty" component, componentWillReceiveProps is either not fired, or has no effect.
// TEST fails:
test('tracking URL preview input field has URL by default', () => {
let component = mount(<Tracking />);
expect(component.find('input').props().value.length).toBe(0);
/* triggers componentWillReceiveProps and thus renderTrackingUrlPreview,
which changes input's props: */
component.setProps();
expect(component.find('input').props().value.length).toBeGreaterThan(0); // false, input is still empty
});
// TEST passes:
test('tracking URL preview input field has URL by default', () => {
let component = mount(<Tracking tokenArr={urlTokens} />);
expect(component.find('input').props().value.length).toBe(0);
/* triggers componentWillReceiveProps and thus renderTrackingUrlPreview,
which changes input's props: */
component.setProps();
expect(component.find('input').props().value.length).toBeGreaterThan(0); // true, input has value
});
Why the first test fails?
Component's constructor and componentWillReceiveProps:
constructor(props) {
super(props);
this.state = {
tokenArr: [],
trackingUrl: ''
};
this.renderTrackingUrlPreview = this.renderTrackingUrlPreview.bind(this);
}
componentWillReceiveProps(nextProps) {
this.setState({
tokenArr: nextProps.tokenArr,
trackingUrl: this.renderTrackingUrlPreview(nextProps.tokenArr) // returns string (which is never empty)
});
}
Solved - it has nothing to do with componentWillReceiveProps, it works as expected.
First test failed because renderTrackingUrlPreview was not prepared for empty tokenArr and in effect returned undefined error which prevented further execution. I somehow missed that in the midst of printed stack trace.