Why componentWillReceiveProps is triggered only if component initially had props? - javascript

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.

Related

ref input value in Enzyme test expected to be empty string but returns undefined

Edit Here is a Code Sandbox of this: https://codesandbox.io/s/react-typescript-bt34v
Unfortunately, I cannot get the Enzyme configuration to work in the Sandbox, but the essential code for this issue is all there, and clicking the button will print the typeof value for the input box.
I'm trying to write a test that tests how an input element's value should change after a Button is clicked.
Here is the function that's called when the Button component is clicked, stopTimer, which calls a useReducer dispatch() to update the useReducer's state store, and below that is what the button looks like.
const stopTimer = (): void => {
dispatch({
payload: state.totalSeconds,
type: 'stop',
});
...
<Button handleClick={ stopTimer } />
In another file, this is what the useReducer looks like, and below that is a useEffect that is called when the store changes. It updates the value of a ref to an empty '' string whenever the store is updated.
const [store, dispatch] = useReducer(reducer, dashboardStore);
...
useEffect((): void => {
ref.current!.value = '';
}, [store]);
...
<input ref={ref} type='text'></input>
To test this with Enzyme, I mount the parent component <Dashboard /> and I give the input an initial value of 'hello'. I then invoke the Button's handleClick prop, which should call the useReducer's dispatch function, updating the store, then causing useEffect to fire, which should set the input's value to an empty string.
I'm expecting the input's value to be an empty string after this, but it ends up being undefined.
I'm also using Enzyme's debug() to try to verify what the DOM looks like at various times. When I do not invoke the Button's handleClick, or if I do not call wrapper.update(), the input's value is 'hello' as expected, but debug() shows the input not having a value prop after update() is called.
it('should contain an empty string when the stop button is clicked', () => {
const wrapper = mount(<Dashboard />);
wrapper.find('input').props().value = 'hello';
act(() => wrapper.find(Button).props().handleClick());
wrapper.update();
console.log(wrapper.find('input').debug());
expect(wrapper.find('input').props().value).toBe('');
});

componentDidUpdate not called after render

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.

Semantic-UI React Checkbox state not working

I've got a parent container that calls a class-based react component child (since the semantic-ui react docs are all written with class-based components). The child component has a Form.Field:
<Form.Field
control={Checkbox}
label={{children: 'cardreader'}}
checked = {this.state.cardReaderChecked}
onChange={this.cardReaderToggleHandler}
/>
I've got a state:
state = {
cardReaderChecked: false,
}
and a cardReaderToggleHandler:
cardReaderToggleHandler = () => {
console.log(this.state.cardReaderChecked);
this.setState((prevState, props) => ({
cardReaderChecked : !prevState.cardReaderChecked
}))
console.log(this.state.cardReaderChecked);
}
I've tried toggling on this.state.cardReaderChecked but I found a lot of references and docs recommending this approach with the prevState to avoid the delay in state updating.
However, there must a logical fault because it doesn't work. If I refresh the page, the checkbox is clear as this.state.cardReaderChecked.
The first time I click the checkbox it renders with the tick, and my this.state.cardReaderChecked updates to true (according to my trusty React tools in Chrome). However, both console.log printouts give me a false and if I pass my state back to the parent form, it also shows that the checkbox is false.
Every subsequent click toggles but a ticked checkbox shows a true state but passes on a false to the parent form (that's where the console.log is currently) and vice versa an unticked checkbox passes back a true.
I'm almost tempted to remove the not from the prev.state in the setState, but I would prefer to understand why this happens.
Bonus Question: How can I query the checkbox state checked in a functional component?
Pass the function to a child as prop
class Parent extends Component {
state = {
cardReaderChecked: false,
}
cardReaderToggleHandler = () => {
this.setState((prevState, props) => ({
cardReaderChecked : !prevState.cardReaderChecked
}), () => console.log(this.state.cardReaderChecked))
}
....
render () {
return (
....
<Form.Field
control={Checkbox}
label={{children: 'cardreader'}}
checked = {this.state.cardReaderChecked}
onChange={this.cardReaderToggleHandler}
/>
)
}

Calling functions after state change occurs in reactjs

My question is this. I have two components. First component is an image cropper. Second component is the one that i should display the cropped image.
The problem i'm facing is i can pass the cropped image to my second component but i have to press the button that crops the image and pass to the second component, twice. On the second click only my image is passing to the second component. But i can display the cropped image in the first component only by one click. I think it is happening because in reactjs state changes are not occurring immediately. So how can i fix this.
My approach was to create a prop function in the 1st component as this.props.croppedImage(this.state.preview.img); here this.state.preview.img is the cropped image. And in the 2nd component i'm getting the cropped image by calling the prop function.
My code
1st component (cropper)
class CropperTest extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "beautiful",
scale: 1,
preview: null,
}
this.handleSave = this.handleSave.bind(this);
}
handleSave = () => {
const img = this.editor.getImageScaledToCanvas().toDataURL();
this.setState({
preview: {
img,
scale: this.state.scale,
}
})
this.props.croppedImage(this.state.preview.img);
}
setEditorRef = (editor) => {
this.editor = editor
}
render() {
return (
<div>
<div className="overlay"></div>
<div className="crop_div">
<AvatarEditor
image={this.props.cropImage}
ref={this.setEditorRef}
width={450}
height={450}
border={50}
color={[255, 255, 255, 0.6]} // RGBA
scale={this.state.scale}
rotate={0}
/>
</div>
<div className="zoom_slider text_align_center">
<input className="crop_btn" type='button' onClick={this.handleSave} value='Save'/>
</div>
</div>
)
}
}
export default CropperTest;
2nd component
Here i'm basically doing the following.
<CropperTest croppedImage = {this.getCroppedImg}/>
getCroppedImg(img){
alert("Perfect Storm");
this.setState({
previewImg:img
})
}
I think it is happening because in reactjs state changes are not occurring immediately. So how can i fix this?
From the React#setState,
setState(updater, [callback])
setState() enqueues changes to the component state.
The setState doesn't immediately update the state. setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall.Instead, use componentDidUpdate or a setState callback (setState(updater, callback))
Call the this.props.croppedImage in the setState callback.You will get updated values of component state. In your case its this.state.preview
this.setState({
preview: {
img,
scale: this.state.scale,
}
}, () => {
this.props.croppedImage(this.state.preview.img);
})
Callbacks are no longer supported in setState.
The best way to go about this is to use a useEffect and set the dependency variable to the state variable.
useEffect(() => {
//do operation on state change
},[stateVariable])
Can confirm useEffect is the way to go now - https://reactjs.org/docs/hooks-effect.html

React - state is inconsistent

What I'm trying to achieve: Pass data from child to parent.
How I'm trying to achieve it: Using this.state as described here
Not sure how to word the title: When I console.log(this.state) in the function in which I modify the state, the correct values in this.state are printed out. However, when I try to read the state in another function, this.state is apparently still at empty values.
constructor(props) {
super(props);
this.state = {
titleInputValue: "",
};
}
<form onSubmit={this.handleSubmit.bind(this)}>
<TextInput
val={this.state.titleInputValue}
changeHandler={this.textInputChangeHandler} />
</form>
// This is the function which can apparently only get the initial state
// title and body are empty strings, they should have values
handleSubmit(event) {
event.preventDefault();
const title = this.state.titleInputValue;
const body = this.state.bodyInputValue;
console.log(this.state);
Meteor.call('stories.insert',title,body);
}
// However, here the state is retrieved just fine.
// This is the function the child calls to update the state.
textInputChangeHandler(event) {
// This console.log call shows the correct state!
console.log(this.state);
this.setState({
titleInputValue: event.target.value,
});
}
TextInput has attribute onChange={this.props.changeHandler.bind(this)}
For illustration:
I wrote asd, and the state was successfully retrieved in textInputChangeHandler, which is the first two console.log calls, but then it's empty in handleSubmit.
This is because the event handler scope is not Component class level. When your component handles the event, it's context is the component (in your case TextInput ) not the parent.
You have to bind that function to this of Component class scope:
<TextInput
val={this.state.titleInputValue}
changeHandler={this.textInputChangeHandler.bind(this) } />
Using JavaScript bind you can specify the function context as well.

Categories

Resources