Updating React state after Finished Rendering - javascript

I'm learning about react's lifecycle methods and I'm wondering how to best address this scenario:
I have a component that fetches information about someone's hobbies
componentWillMount() {
this.props.fetchHobbies(this.props.params.id);
}
The data is sent to a reducer which is mapped as a prop at bottom of the component
function mapStateToProps(state) {
return {
hobbies:state.hobbies.user
};
}
I have a component called twitterbootstrap which relies on my component's state to see what rows are selected.
const selectRowProp = {
mode: 'radio',
clickToSelect: true,
bgColor: "rgba(139, 195, 74, 0.71)",
selected: this.state.selected,
};
Here's my question. I want to access this.props.hobbies.selected.
--UPDATE I'm using componentWillRecieveProps as it has a setState option.
componentWillReceiveProps(nextProps){
this.setState({selected:next.props.hobbies })
}
I want to use this.props.hobbies.selected as the next prop to be recieved. How would I incorporate that?

nextProps is the props object that is passed after the first rendering. there is nothing called next.props in the function componentWillReceiveProps. you can do something like that.
componentWillReceiveProps(nextProps){
this.setState({selected:nextProps.hobbies })
}

Related

How do I test the method shouldComponentUpdate?

I would like to test out the method, shouldComponentUpdate but the test I wrote doesn't catch the side effects. How do I test this?
I have tried the solution proposed here but there are issues with it which I will show in the code: How to unit test React Component shouldComponentUpdate method
I have tried using shallow rather than mount but when I did that, my test fails at this point:
expect(shouldComponentUpdate).to.have.property("callCount", 1);
// AssertionError: expected [Function: proxy] to have a property 'callCount' of 1, but got 0
Here's my component's shouldComponentUpdate function:
shouldComponentUpdate(nextProps, nextState) {
if (this.props.storageValue !== nextProps.storageValue) {
localStorage.setItem(constantFile.storageKey, nextProps.storageValue);
localStorage.setItem(constantFile.timestampKey, now());
this.setState({
newStorage: nextProps.storageValue
});
return true;
}
...
return false;
}
Here's my test:
it("Test shouldComponentUpdate", () => {
/* eslint-disable one-var */
const wrapper = mount(
<Provider store={store}>
<MyComponent />
</Provider>),
shouldComponentUpdate = sinon.spy(CapitalHomeAccessPointContainer.prototype, "shouldComponentUpdate");
wrapper.setProps({
storageValue: true
});
expect(shouldComponentUpdate).to.have.property("callCount", 1);
expect(shouldComponentUpdate.returned(true)).to.be.equal(true);
expect(localStorage.getItem(constantFile.timestampKey)).to.equal(someExpectedTimestamp);
});
expect(shouldComponentUpdate).to.have.property("callCount", 1);
fails with
AssertionError: expected false to equal true
Why does this happen? I thought shouldComponentUpdate should have returned true? Later, I'm hoping to test the state and local storage side effects but I do not understand this error.
You're handling of the shouldComponentUpdate lifecycle is wrong. You don't setState within it, instead you only return a boolean (true or false) to let React know when to re-render the DOM. This allows you to block updates from outside props. For example:
shouldComponentUpdate(nextProps, nextState) {
return this.state.newStorage !== nextState.newStorage
}
The above states: if current newStorage state doesn't match next newStorage state, then re-render the component. The above would block prop changes from rerendering the component. If you changed storage props, it WOULD NOT update this component because the state wasn't changed. You want to use shouldComponentUpdate to prevent unnecessary double re-renders and/or to prevent a parent component's re-renders from unnecessarily re-rendering a child component. In this case, you won't need to use this lifecycle method.
Instead, you should either be using static getDerivedStateFromProps for props to update state before a render OR using componentDidUpdate to update state after a render.
In your case, since you only want to update state if this.props.storageValue changes, then you should use componentDidUpdate:
componentDidUpdate(prevProps) {
if (this.props.storageValue !== prevProps.storageValue) {
localStorage.setItem(constantFile.storageKey, nextProps.storageValue);
localStorage.setItem(constantFile.timestampKey, now());
this.setState({ newStorage: nextProps.storageValue });
}
}
The above checks if the current props don't match previous props, then updates the state to reflect the change.
You won't be able to use static getDerivedStateFromProps because there's no comparison between old and new props, but only comparison to incoming props and what's current in state. That said, you COULD use it if you stored something in state that is directly related to the storage props. An example might be if you stored a key from the props and into state. If the props key was to ever change and it didn't match the current key in state, then it would update the state to reflect the key change. In this case, you would return an object to update state or null to not update the state.
For example:
static getDerivedStateFromProps(props, state) {
return props.storage.key !== state.storageKey
? { storageKey: props.storage.key }
: null
}
When it comes to testing, you'll want manually update props to affect state changes. By testing against state changes, you'll indirectly test against componentDidUpdate.
For example (you'll have to mock localStorage, otherwise state won't update -- I'd also recommend exporting the class and importing it, instead of using the Redux connected component):
import { MyComponent } from "../MyComponent.js";
const initialProps = { storageValue: "" };
describe("Test Component", () => {
let wrapper;
beforeEach(() => {
wrapper = mount(<MyComponent { ...initialProps } />);
})
it("updates 'newStorage' state when 'storageValue' props have changed, () => {
wrapper.setProps({ storageValue: "test" });
expect(wrapper.state('NewStorage')).toEqual(...); // this should update NewStorage state
wrapper.setProps({ someOtherProp: "hello" )};
expect(wrapper.state('NewStorage')).toEqual(...); // this should not update NewStorage state
});
});

Why am I getting "return _this.state.data" instead of JSON object

I have a parent component doing an AJAX call to get a JSON object. I've done a few console.log's to make sure that the data is correct in the parent component, but then when I pass through props, I get a value of:
ƒ data() {
return _this.state.data;
}
What I've done to this point seems simple enough so I can't find what the issue is.
Parent Component:
class InfoBox extends Component {
state = {
data: []
};
componentDidMount = () => {
this.loadDonationsFromServer();
setInterval(this.loadDonationsFromServer, this.props.pollInterval);
};
loadDonationsFromServer = () => {
$.ajax({
url: "https://jsonplaceholder.typicode.com/comments",
dataType: "json",
cache: false,
success: data => {
this.setState({ data });
},
error: (xhr, status, err) => {
console.error(status, err.toString());
}
});
};
render = () => {
return (
<React.Fragment>
<h1>Information</h1>
<InfoList
data={() => this.state.data}
/>
</React.Fragment>
);
};
}
export default DonationBox;
Child Component:
class InfoList extends Component {
constructor(props) {
super(props);
this.state = {
data: this.props.data
};
}
componentDidMount() {
console.log(this.state.data);
//logs: ƒ data() {
// return _this.state.data;
// }
}
render() {
return <div> Placeholder </div>;
}
}
export default InfoList;
I tried using bind in the child component but still got the same thing:
constructor(props) {
super(props);
this.state = {
data: this.props.data
};
this.checkData = this.checkData.bind(this);
}
componentDidMount() {
this.checkData();
}
checkData = () => {
console.log(this.state.data);
};
First, yes, you should change the data prop that you send to InfoList to be this.state.data rather than an anonymous function. So: <InfoList data={this.state.data} />
But, the main issue is in using componentDidMount in the child component, when really you should be using componentWillReceiveProps instead.
componentDidMount is only called once, and it doesn't wait for your AJAX
The componentDidMount lifecycle hook is invoked one time, before the initial render.
In your child component, at componentDidMount you are trying to log this.state.data - but this state is based on what was set in the constructor which was what was passed in as the data prop when you first mounted InfoList. That was [], because InfoBox had yet to receive back data from its Ajax call. To put it another way:
InfoList.componentDidMount() fired before InfoBox.loadDonationsFromServer() got back its response. And InfoList.componentDidMount() does not get fired again.
componentWillReceiveProps is called whenever props change
Instead, your child component should be using the componentWillReceiveProps lifecycle hook. This is invoked every time a component receives new props. Once the parent's state changes (after load donations returns), then it passes new props to the child. In componentWillReceiveProps, the child can take these new props and updates his state.
I have created a code sandbox that shows you through a bunch of log statements what happens when, along with what your props and state look like at various points in the lifecycle. Instead of actually doing an ajax fetch, I'm just doing a 2-second wait to simulate the fetch. In InfoList.js, the code for componentWillReceiveProps is currently commented out; this way, you can see how things work as they stand. Once you remove the comments and start using componentWillReceiveProps, you'll see how they get fixed.
Additional resources
This is a helpful article that pretty much describes the exact same issue you're facing.
An excellent quick reference for React lifecycle hooks is the React Cheat Sheet)
That is because the data prop that is being passed in is a function.
Change
<InfoList data={() => this.state.data} />
to
<InfoList data={this.state.data} />
Just a nit, you don't really need the constructor in your child component to define the state. Just define it the way you have in your parent component.

Managing React component behavior during update

I'm new to ReactJs, and working with the ExtReact framework. I'm displaying a grid, and made a pagination, which is working fine.
I customed the spinner displayed when datas are loading, and it works fine when the "component did mount".
But, when I update the component (like when I switch the page using the ExtReact paginator plugin), there is another native spinner displayed, and I want to custom it too.
My problem is that I don't find a proper way to do it with lifeCycle components methods, since the componentWillUpdate method is deprecated.
I first had only one 'isLoading' state, but I added 'isUpdating' since 'isLoading' was modified after the first render because of the way the store data are loaded.
The isUpdating state seems to stay false, this is what is displayed by the console:
Snapshot
Is updating:false
Updated!
Is updating:false
Saul Goodman!
​
Here is my Component code:
import React, {Component} from 'react';
import {Grid, Column, Toolbar, SearchField} from '#sencha/ext-modern';
import {withRouter} from "react-router-dom";
import Spinner from '../../resources/components/Spinner';
Ext.require('Ext.grid.plugin.PagingToolbar');
class Subscriptions extends Component {
state = {
isLoading: true,
};
store = Ext.create('Ext.data.Store', {
fields: ['Field1', 'Field2', 'Field3'],
autoLoad: false,
pageSize: 30,
proxy: {
type: 'rest', // refers to the alias "proxy.ajax" on Ext.data.proxy.Ajax
url: 'niceadress',
reader: {
type: 'json'
}
}
});
/**
* Loading the datas into the store and then removes the spinner when fetched successfully
*/
componentDidMount() {
this.store.load({
callback: function (records, operation, success) {
this.setState({
isLoading: (!success),
});
console.log("Saul Goodman!");
},
scope: this
});
}
shouldComponentUpdate(nextProps, nextState, nextContext) {
if (nextState.isLoading === true){
return false;
} else {
return true;
}
};
getSnapshotBeforeUpdate(prevProps, prevState) {
this.setState({
isLoading: true,
});
console.log('Snapshot');
console.log('Is loading:' + this.state.isLoading)
return prevState;
}
componentDidUpdate(prevProps, prevState, snapshot) {
this.setState({
isLoading: (!prevState.isUpdating),
});
console.log('Updated!');
console.log('Is loading:' + prevState.isLoading)
}
render() {
if (this.state.isLoading) return <Spinner/>;
return (
<Grid
store={this.store}
plugins={['pagingtoolbar', 'listpaging']}
>
The Grid
</Grid>
)
}
}
export default withRouter(Subscriptions);
Any idea of what I'm doing wrong?
EDIT: Well, I first thought that the store load wasn't going to trigger the componentDidUpdate method, that's why I wrote the isUploading state separately. I'm removing it but I still didn't solved my problem.
How can I do to prevent the virtualDOM to re-render after the setState call in componentDidUpdate?
I'm looking for an elegant way to break this loop.
The isUpdating state seems to stay false, this is what is displayed by
the console
This is because your shouldComponentUpdate is returning false when isUpdating is true.
There is also a typo in your componentDidUpdate's setState
Page changes using ExtReact Paginator strangely aren't triggering the ComponentDidUpdate method. Then, all this doesn't look to be the proper way to change the component behavior. ExtReact documentation looks a bit confusing about this point, I can't see how to override the pagingtoolbar component easily.
Thanks for your answers!

ReactJS ComponentWillMount() after passing Props

I want to ask why the child ( ComponentWillMount() ) component is only once rendered, once I am passing props to it everytime on onClick.
Once I click some button that is passing props to the child, the ComponentWillMount() of child is not triggering again, only in the first click only.
Parent Component:
render(){
return(
<div>
<AppModuleForm
editID = {this.state.editID}
editURL = {this.state.editURL}
editConf = {this.state.editConf}
editDesc = {this.state.editDesc}
editIcon = {this.state.editIcon}
editParent = {this.state.editParent}
editOrder= {this.state.editOrder}
status={this.state.status}
moduleList={this.state.moduleList}
updateAppModuleTree={this.updateAppModuleTree.bind(this)}/>
</div>
)
}
Child Component:
constructor(props){
super(props)
console.log(this.props.editDesc)
this.state={
url:'',
description:'',
parentID:'',
order:'',
configuration:'',
icon:'',
parentIDList:[],
urlDuplicate: false,
isSuccess: false,
errorMessage: '',
}
}
componentWillMount(){
if(this.props.status==='edit'){
let {
editURL,
editDesc,
editParent,
editConf,
editIcon,
editOrder} = this.props
this.setState({
url:editURL,
description:editDesc,
parentID:editParent,
order:editOrder,
configuration:editConf,
icon:editIcon,
})
}
}
componentWillReceiveProps(nextProps){
if(nextProps.status != this.props.status){
if(this.props.status==='edit'){
let {
editURL,
editDesc,
editParent,
editConf,
editIcon,
editOrder} = this.props
this.setState({
url:editURL,
description:editDesc,
parentID:editParent,
order:editOrder,
configuration:editConf,
icon:editIcon,
})
}
}
}
ComponentWillMount is mounting lifecycle method which will be called before mounting your component hence initialisation can be done in that while ComponentWillReceiveProps will be called once props are changed and you will get changes in nextProps parameter.
First you need to read https://reactjs.org/docs/state-and-lifecycle.html
and understand where to use props and why you need to pass something into component state.
From http://lucybain.com/blog/2016/react-state-vs-pros/
So when would you use state?
When a component needs to keep track of information between renderings
the component itself can create, update, and use state.
So you shouldn't transfer to state anything that will not change internally during component live cycle. As I can see all props those you pass to component are most likely will not be changed from within the component, all callbacks and icons you should take from props in component jsx.
If you have some editable data that you pass into its props from parent, on component mount (use componentWillMount()) you can copy that data to component state.That means all data will be stored in component internally and will not being overwritten on every render() call from passed props.
If you need to check if new props contains changes you can use componentWillReceiveProps(newProps) and there you can compare newProps with this.props and and process changes if needed.
Also i can suggest you to rename component callbacks handlers with respect to naming best practices:
<div>
<AppModuleForm
handleEditID = {this.onEditID}
handleEditURL = {this.onEditURL}
handleEditConf = {this.onEditConf}
handleEditDesc = {this.onEditDesc}
handleEditIcon = {this.onEditIcon}
handleEditParent = {this.onEditParent}
handleEditOrder= {this.onEditOrder}
status={this.state.status}
moduleList={this.state.moduleList}
updateAppModuleTree={this.updateAppModuleTree.bind(this)}/>
</div>
And I dont see any reasonable purpose to declare or to store functions in components state. So you can consider to move your handlers this.state.editID
etc. to parent component this scope. Like that
onEditId = () => { /* function code */ }
If you use arrow function = () => it automatically binds to component this and you don't need to bind them manually like you do in
{this.updateAppModuleTree.bind(this)}
After all that may be you will understand more clearly how you should manage your components life cycle and your problem will no longer be relevant.

React router + redux navigating back doesn't call componentWillMount

Currently I pre-load data from api in container component's lifecycle method componentWillMount:
componentWillMount() {
const { dept, course } = this.props.routeParams;
this.props.fetchTimetable(dept, course);
}
It is called when user navigates to route /:dept/:course, and it works fine, until you navigate from let's say: /mif/31 to /mif/33 and then press back button. The component is not actually reinitialized, so the lifecycle method is not called, and the data isn't reloaded.
Is there some sort of way to reload data in this case? Should I maybe use another method of preloading data? I see react router emits LOCATION_CHANGE event on any location change, including navigating back, so maybe I can somehow use that?
If it matters, here's is how I implement data loading:
import { getTimetable } from '../api/timetable';
export const REQUEST_TIMETABLE = 'REQUEST_TIMETABLE';
export const RECEIVE_TIMETABLE = 'RECEIVE_TIMETABLE';
const requestTimetable = () => ({ type: REQUEST_TIMETABLE, loading: true });
const receiveTimetable = (timetable) => ({ type: RECEIVE_TIMETABLE, loading: false, timetable });
export function fetchTimetable(departmentId, courseId) {
return dispatch => {
dispatch(requestTimetable());
getTimetable(departmentId, courseId)
.then(timetable => dispatch(receiveTimetable(timetable)))
.catch(console.log);
};
}
You need to use componentWillReceiveProps to check if new props (nextProps) are same as existing props (this.props). Here's relevant code in Redux example: https://github.com/reactjs/redux/blob/e5e608eb87f84d4c6ec22b3b4e59338d234904d5/examples/async/src/containers/App.js#L13-L18
componentWillReceiveProps(nextProps) {
if (nextProps.dept !== this.props.dept || nextProps.course !== this.props.course) {
dispatch(fetchTimetable(nextProps.dept, nextProps.course))
}
}
I might be wrong here, but I believe the function you are looking for is not componentWillMount but componentWillReceiveProps,
assuming you are passing down variables (like :courseId) from redux router to your component, using setState in componentWillReceiveProps should repaint your component.
Otherwise, you can subscribe to changes in your store: http://redux.js.org/docs/api/Store.html
Disclaimer: I probably know less about redux then you.

Categories

Resources