How to test a function that uses prevsProps from componentDidUpdate - reactjs

Consider I have a function that is called from inside the ComponentDidUpdate. I need to unit test the canCheck function.
ComponentDidUpdate(prevProps,prevState){
if(prevState.isFull){
callA();
}
if(canCheck(prevProps)){
callB()
}
}
The canCheck function is as follows
canCheck(prevProps){
const {
isFocused,
getTheStartState,
index,
loading
} = this.props;
if (!isFocused) {
return false;
}
return (
index === 0 &&
prevProps.getTheStartState &&
!getTheStartState &&
!loading
);
}
Things I have tried include
const ShallowRenderer = require('react-test-renderer/shallow');
describe('canCheck', () => {
test('should return correctly value', () => {
const renderer = new ShallowRenderer();
renderer.render(
<TestContainer
isFocused
getTheStart
{...dummy}
/>
);
const instance = renderer.getMountedInstance();
instance.componentDidUpdate(
{
getTheStartState: false
loading: true
},
{ someState: true }
);
expect(renderer.getMountedInstance().canCheck()).toBeFalsy();
});
});
Though I have mentioned getTheStartState in the prevProps of componentDidUpdate I always get the error as cannot read getTheStartState of undefined. Are there any suggestions of how we can unit test such function, I could not find much resource for this online!

Related

How to restrict react Component to Re-rendering,

I am passing functions to my child component. And I am using React.memo to restrict compoenent from re-rendering. But My component rerenders when parent re-renders. I tried to check why this is happening by using useEffect on all the props and I get to this point that my functions are causing compoenent to re-renders.
// my functions
const scrollToView = (index) => {
if (scrollRef && scrollRef.current && scrollRef.current[index]) {
scrollRef.current[index].scrollIntoView({ behavior: 'smooth' });
}
};
const scrollToReportView = (reportIndex) => {
if (scrollToReportRef && scrollToReportRef.current &&
scrollToReportRef.current[reportIndex]) {
scrollToReportRef.current[reportIndex].scrollIntoView({
behavior: 'smooth' });
}
}
.......
function LeftNav({
scrollToView, //function
scrollToReportView, //function
reports, //object
}) {
useEffect(() => {
console.log('scrollToView')
}, [scrollToView])
useEffect(() => {
console.log('scrollToReportView')
}, [scrollToReportView])
useEffect(() => {
console.log('reports')
}, [reports])
return (
<div>{'My Child Component'}</div>
);
}
export default memo(LeftNav);
And this is how my left nav is being called
<LeftNav
scrollToView={(index) => scrollToView(index)}
scrollToReportView={(repIndex)=> scrollToReportView(repIndex)}
reports={reports}
/>
With
<LeftNav
scrollToView={(index) => scrollToView(index)}
scrollToReportView={(repIndex)=> scrollToReportView(repIndex)}
reports={reports}
/>
you're creating new anonymous functions every time you render the LeftNav component, so memoization does absolutely nothing.
Just
<LeftNav
scrollToView={scrollToView}
scrollToReportView={scrollToReportView}
reports={reports}
/>
instead (assuming those functions are stable by identity (e.g. are declared outside the component or are properly React.useCallbacked or React.useMemoed).
In other words, if your component is currently
function Component() {
// ...
const scrollToView = (index) => {
if (scrollRef && scrollRef.current && scrollRef.current[index]) {
scrollRef.current[index].scrollIntoView({ behavior: "smooth" });
}
};
const scrollToReportView = (reportIndex) => {
if (scrollToReportRef && scrollToReportRef.current && scrollToReportRef.current[reportIndex]) {
scrollToReportRef.current[reportIndex].scrollIntoView({
behavior: "smooth",
});
}
};
return (
<LeftNav
scrollToView={(index) => scrollToView(index)}
scrollToReportView={(repIndex) => scrollToReportView(repIndex)}
reports={reports}
/>,
);
}
it needs to be something like
function Component() {
// ...
const scrollToView = React.useCallback((index) => {
if (scrollRef?.current?.[index]) {
scrollRef.current[index].scrollIntoView({ behavior: "smooth" });
}
}, []);
const scrollToReportView = React.useCallback((reportIndex) => {
if (scrollToReportRef?.current?.[reportIndex]) {
scrollToReportRef.current[reportIndex].scrollIntoView({
behavior: "smooth",
});
}
}, []);
return (<LeftNav scrollToView={scrollToView} scrollToReportView={scrollToReportView} reports={reports} />);
}
so the scrollToView and scrollToReportView functions have stable identities.

Is this.state current when the setState updater is called?

I understand that a fully updated state is passed to setState's callback. I would like to know, though, if this.state is also current at that time. The reason being that I have a method which uses this.state, and I'd like to know if it's safe to call from inside the callback. Example:
areWeBigger = () => {
const { theirSize } = this.props;
const { ourSize } = this.state;
return ourSize > theirSize;
};
attemptToEat = () => {
this.setState(({ ourSize }) => {
const bigger = this.areWeBigger(); // Is this safe?
return { ourSize: bigger ? ourSize + 1 : ourSize - 1 };
});
};
EDIT
I realize now that what I've been calling the "callback" is actually called the "updater". Sorry for the confusion.
I think it should work. The syntax is also a bit different from what you have used. You are not using the callback in the above example, but rather using the functional convention of setState method. Please refer React JS Documentation for more information about the same. Also, the following snippet might present a better approach towards what you are trying to achieve.
const areWeBigger = () => {
const { theirSize } = this.props;
const { ourSize } = this.state;
return ourSize > theirSize;
};
const attemptToEat = () => {
this.setState({ ourSize: "whatever value" }, () => {
const bigger = this.areWeBigger(); // it is okay to use this
// it doesn't return anything, that is not supported so remove your return statement
// ideally you can also use componentDidUpdate lifecycle method if you don't want to always trigger a callback, you can refer react docs.
})
});
};
It seems you don't need setState callback in your case.
From here, you can call setState like this:
this.setState((prevState, props) => {
return {counter: prevState.counter + props.step};
})
So you can call your areWeBigger function like this:
areWeBigger = (ourSize , theirSize ) => {
return ourSize > theirSize;
};
attemptToEat = () => {
this.setState(({ ourSize }, {theirSize }) => { //extract ourSize and theiSize from (current)state and props respectively
const bigger = this.areWeBigger(ourSize, theirSize ); // this is before the state is updated
return { ourSize: bigger ? ourSize + 1 : ourSize - 1 };
});
};
To keep it in your way with direct state access in "areWeBigger":
areWeBigger = () => {
const {ourSize} = this.state;
const {theirSize} = this.props;
return ourSize > theirSize;
};
callback = () => {
// ... here the state is up to date
}
attemptToEat = () => {
this.setState(({ ourSize }) => { //extract ourSize and theiSize from (current)state and props respectively
const bigger = this.areWeBigger(); // this is before the state is updated
return { ourSize: bigger ? ourSize + 1 : ourSize - 1 };
}, callback); // to see the use of callback
};

How to refactor componentWillReceiveProps to componentDidUpdate or possibly a Hook?

I have this application that has a deprecated lifecycle method:
componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
Currently, I have used the UNSAFE_ flag:
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
I have left it like this because when I attempted to refactor it to:
componentDidUpdate(prevProps, prevState) {
if (this.state.displayErrors) {
this._validate(prevProps, prevState);
}
}
It created another bug that gave me this error:
Invariant Violation: 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.
It starts to happen when a user clicks on the PAY NOW button that kicks off the _handlePayButtonPress which also checks for validation of credit card information like so:
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
_validate = props => {
const { cardExpireDate, cardNumber, csv, nameOnCard } = props;
const validationErrors = {
date: cardExpireDate.trim() ? "" : "Is Required",
cardNumber: cardNumber.trim() ? "" : "Is Required",
csv: csv.trim() ? "" : "Is Required",
name: nameOnCard.trim() ? "" : "Is Required"
};
if (validationErrors.csv === "" && csv.trim().length < 3) {
validationErrors.csv = "Must be 3 or 4 digits";
}
const fullErrors = {
...validationErrors,
...this.props.validationErrors
};
const isValid = Object.keys(fullErrors).reduce((acc, curr) => {
if (fullErrors[curr]) {
return false;
}
return acc;
}, true);
if (isValid) {
this.setState({ validationErrors: {} });
//register
} else {
this.setState({ validationErrors, displayErrors: true });
}
return isValid;
};
_handlePayButtonPress = () => {
const isValid = this._validate(this.props);
if (isValid) {
console.log("Good to go!");
}
if (isValid) {
this.setState({ processingPayment: true });
this.props
.submitEventRegistration()
.then(() => {
this.setState({ processingPayment: false });
//eslint-disable-next-line
this.props.navigation.navigate("PaymentConfirmation");
})
.catch(({ title, message }) => {
Alert.alert(
title,
message,
[
{
text: "OK",
onPress: () => {
this.setState({ processingPayment: false });
}
}
],
{
cancelable: false
}
);
});
} else {
alert("Please correct the errors before continuing.");
}
};
Unfortunately, I do not have enough experience with Hooks and I have failed at refactoring that deprecated lifecycle method to one that would not create trouble like it was doing with the above error. Any suggestions at a better CDU or any other ideas?
You need another check so you don't get in an infinite loop (every time you call setState you will rerender -> component did update -> update again ...)
You could do something like this:
componentDidUpdate(prevProps, prevState) {
if (this.state.displayErrors && prevProps !== this.props) {
this._validate(prevProps, prevState);
}
}
Also I think that you need to call your validate with new props and state:
this._validate(this.props, this.state);
Hope this helps.
componentDidUpdate shouldn't replace componentWillRecieveProps for this reason. The replacement React gave us was getDerivedStateFromProps which you can read about here https://medium.com/#baphemot/understanding-react-react-16-3-component-life-cycle-23129bc7a705. However, getDerivedStateFromProps is a static function so you'll have to replace all the setState lines in _validate and return an object instead.
This is how you work with prevState and hooks.
Working sample Codesandbox.io
import React, { useState, useEffect } from "react";
import "./styles.css";
const ZeroToTen = ({ value }) => {
const [myValue, setMyValue] = useState(0);
const [isValid, setIsValid] = useState(true);
const validate = value => {
var result = value >= 0 && value <= 10;
setIsValid(result);
return result;
};
useEffect(() => {
setMyValue(prevState => (validate(value) ? value : prevState));
}, [value]);
return (
<>
<span>{myValue}</span>
<p>
{isValid
? `${value} Is Valid`
: `${value} is Invalid, last good value is ${myValue}`}
</p>
</>
);
};
export default function App() {
const [value, setValue] = useState(0);
return (
<div className="App">
<button value={value} onClick={e => setValue(prevState => prevState - 1)}>
Decrement
</button>
<button value={value} onClick={e => setValue(prevState => prevState + 1)}>
Increment
</button>
<p>Current Value: {value}</p>
<ZeroToTen value={value} />
</div>
);
}
We have two components, one to increase/decrease a number and the other one to hold a number between 0 and 10.
The first component is using prevState to increment the value like this:
onClick={e => setValue(prevState => prevState - 1)}
It can increment/decrement as much as you want.
The second component is receiving its input from the first component, but it will validate the value every time it is updated and will allow values between 0 and 10.
useEffect(() => {
setMyValue(prevState => (validate(value) ? value : prevState));
}, [value]);
In this case I'm using two hooks to trigger the validation every time 'value' is updated.
If you are not familiar with hooks yet, this may be confusing, but the main idea is that with hooks you need to focus on a single property/state to validate changes.

how to handle race conditions in class components?

Suppose there is a component where ask server to do some search and response will be rendered. How to ensure most recent request's response is rendered even if server side for any reason answers in different ordering? I'm not asking about cancelling previous request since it's not always possible with reasonable efforts.
onClick = () => {
apiCall(this.state.searchQuery).then(items => this.setState({ items }));
};
Is there elegant way to handle that? By now I know few approaches:
disabling button till request comes(provides bad experiences in large amount of cases - say for searching while typing)
checking inside then() if request's params matches this.props/this.state data(does not handle case when we intentionally forced new search with same query - say by pressing Enter/clicking "Search" button)
onClick = () => {
const searchQuery = this.state.searchQuery;
apiCall(searchQuery)
.then(items =>
this.state.searchQuery === searchQuery
&& this.setState({ items })
);
};
marking requests somehow and checking if it's latest(works, but looks too verboose especially if there are few requests we need to check)
searchQueryIndex = 0;
onClick = () => {
this.searchQueryIndex++;
const index = this.searchQueryIndex;
apiCall(this.state.searchQuery)
.then(items =>
this.searchQueryIndex === searchQueryIndex
&& this.setState({ items })
);
};
I'd call that trio "ugly, broken and messy".
Is there something such clear way as hooks allow:
useEffect(() => {
const isCanceled = false;
apiCall(searchQuery).then(items => !isCanceled && setItems(items));
return () => {isCanceled = true;};
}, [searchQuery])
Your onClick handler suggest a class component since you use this and this.setState:
onClick = () => {
apiCall(this.state.searchQuery).then(items =>
this.setState({ items })
);
};
I adjusted onlyLastRequestedPromise to take a function that will return something (you can return Promise.reject('cancelled') or anything).
const onlyLastRequestedPromise = (promiseIds => {
const whenResolve = (
promise,
id,
promiseID,
resolveValue,
whenCancelled = () => Promise.reject('cancelled')
) => {
if (promise !== undefined) {
//called by user adding a promise
promiseIds[id] = {};
} else {
//called because promise is resolved
return promiseID === promiseIds[id]
? Promise.resolve(resolveValue)
: whenCancelled(resolveValue);
}
return (function(currentPromiseID) {
return promise.then(function(result) {
return whenResolve(
undefined,
id,
currentPromiseID,
result
);
});
})(promiseIds[id]);
};
return (id = 'general', whenCancelled) => promise =>
whenResolve(
promise,
id,
undefined,
undefined,
whenCancelled
);
})({});
A class example on how to use it:
class Component extends React.Component {
CANCELLED = {};
last = onlyLastRequestedPromise(
'search',
() => this.CANCELLED
);
onSearch = () => {
this.last(apiCall(this.state.searchQuery)).then(
items =>
items !== this.CANCELLED && this.setState({ items })
);
};
changeAndSearch = e => {
this.setState(
{}, //state with new value
() => this.onSearch() //onSearch after state update
);
};
render() {
return (
<div>
<SearchButton onClick={this.onSearch} />
<Other onChange={this.changeAndSearch} />
</div>
);
}
}
I agree it's a lot of code but since you put most of the implementation in the lib it should not clutter your components.
If you had a functional component you could create the last function with useRef:
//
function ComponentContainer(props) {
const CANCELLED = useRef({});
const last = useRef(
onlyLastRequestedPromise('search', () => CANCELLED)
);
const [searchQuery,setSearchQuery] = useState({});
const mounted = useIsMounted();
const onSearch = useCallback(
last(apiCall(searchQuery)).then(
items =>
items !== CANCELLED &&
mounted.current &&
//do something with items
)
);
}
Finally figured out how to utilize closure to mimic "just ignore that" approach from hooks' world:
class MyComponent extends React.Component {
const ignorePrevRequest = () => {}; // empty function by default
loadSomeData() {
this.ignorePrevRequest();
let cancelled = false;
this.ignorePrevRequest = () => { cancelled = true; }; // closure comes to play
doSomeCall().then(data => !cancelled && this.setState({ data }))
}
}

Cannot read property function of null

Hello I'm trying to test a function from a function but tells me this error.
TypeError: Cannot read property 'getNextServiceIconStyle' of null
Code
function IssueNextServiceIcon ({ nextService, intl }) {
return (
<div styles[getNextServiceIconStyle(nextService.approaching, nextService.overDue)])}>
<NextServiceIcon className={styles['icon']} />
</div>
)
function getNextServiceIconStyle (approaching, overDue) {
if (overDue) {
return 'next-service-overdue'
}
else if (approaching) {
return 'next-service-approaching'
}
return ''
}
}
Test
test('should', () => {
const wrapper = shallow(<IssueNextServiceIcon {...mockPropsForComponent} />)
const instance = wrapper.instance()
const expectedResult = 'next-service-overdue'
expect(instance.getNextServiceIconStyle(true, false)).toEqual(expectedResult)
})
Any suggestion for the test?
There a few syntax errors and unclosed braces, but if I understood your intent correctly, you'd do smth like this:
function IssueNextServiceIcon({ nextService, intl }) {
function getNextServiceIconStyle(approaching, overDue) {
if (overDue) {
return "next-service-overdue";
} else if (approaching) {
return "next-service-approaching";
}
return "";
}
const styleKey = getNextServiceIconStyle(
nextService.approaching,
nextService.overDue
);
return (
// Or if you need to pass className: className={styles[styleKey]}
<div styles={styles[styleKey]}>
<NextServiceIcon className={styles["icon"]} />
</div>
);
}
Regarding the test, you cannot use wrapper.instance() because this is not a class component. What you could do is to render your component and check that it has proper styles applied:
test('it should have correct styling', () => {
const wrapper = shallow(<IssueNextServiceIcon {...mockPropsForComponent} />)
expect(component.find('NextServiceIcon').prop('style')).toHaveProperty('color', 'red') // test for the actual css you have
})

Resources