ReactJS: run function on prop change without using deprecated componentWillReceiveProps - reactjs

I have a container component which gets page number as prop and downloads data for that page. I rely on componentDidUpdate() to trigger the download as componentDidUpdate() fires when pageNumber changes. Is this a reasonable way to do it?
One thing I noticed is that the component gets re-rendered when it receives a new pageNumber even though nothing changes at first and then it re-renders again once data has been downloaded. The first re-render is superfluous. Should I not be bothered by this?
If I was really bothered, I could user shouldComponentUpdate() to only re-render when data changes. (I wonder if this check might even be more costly than the re-render itself?) However, if I used shouldComponentUpdate() and not update on page change, then I couldn't rely on componentDidUpdate() to load my data anymore.
Does this mean that the below is the way to do it or is there a better way?
import React from 'react';
import PropTypes from 'prop-types';
import Table from "../components/Table";
import Pagination from "../components/Pagination";
import {connect} from "react-redux";
import {changePage} from "../js/actions";
const PAGE_COUNT = 10;
const mapStateToProps = state => {
return { currentPage: state.currentPage }
};
const mapDispatchToProps = dispatch => {
return {
changePage: page => dispatch(changePage(page))
};
};
class ConnectedTableContainer extends React.Component {
state = {
data: [],
loaded: false,
};
handlePageChange = page => {
if (page < 1 || page > PAGE_COUNT) return;
this.props.changePage(page);
};
loadData = () => {
this.setState({ loaded: false });
const { currentPage } = this.props;
const pageParam = currentPage ? "?_page=" + currentPage : "";
fetch('https://jsonplaceholder.typicode.com/posts/' + pageParam)
.then(response => {
if (response.status !== 200) {
console.log("Unexpected response: " + response.status);
return;
}
return response.json();
})
.then(data => this.setState({
data: data,
loaded: true,
}))
};
componentDidMount() {
this.loadData(this.props.currentPage);
}
componentDidUpdate(prevProps) {
if (prevProps.currentPage != this.props.currentPage) {
this.loadData();
}
}
render() {
const { loaded } = this.state;
const { currentPage } = this.props;
return (
<div className="container">
<div className="section">
<Pagination onPageChange={ this.handlePageChange } pageCount={ PAGE_COUNT } currentPage={ currentPage }/>
</div>
<div className={ "section " + (loaded ? "" : "loading") }>
<Table data={ this.state.data } />
</div>
</div>
)
}
}
ConnectedTableContainer.propTypes = {
changePage: PropTypes.func.isRequired,
currentPage: PropTypes.number.isRequired,
};
ConnectedTableContainer.defaultProps = {
currentPage: 1,
};
const TableContainer = connect(mapStateToProps, mapDispatchToProps)(ConnectedTableContainer);
export default TableContainer;

It is perfectly fine to use componentDidUpdate() to trigger the download when pageNumber changes.
I would not recommend to implement shouldComponentUpdate, instead inherit from React.PureComponent. This implements shouldComponentUpdate for you by comparing props and state. If any of the props and state change (shallow comparison), it'll re-render, otherwise not.

Related

React - Unable to set state from the response on initial render

This is the response from redux store :
{
"newsletter": true,
"orderConfirmation": true,
"shippingInformation": true,
"orderEnquiryConfirmation": true,
}
This is the jsx file, where am trying to set state. The idea is setting the state from the response and add an onChange handle to each checkboxes.
But currently am receiving a correct response but I tried to set state in didUpdate, DidMount but no luck. I want to know the correct place to set state on initial render of the component.
import React from 'react';
import Component from '../../assets/js/app/component.jsx';
import { connect } from 'react-redux';
import * as actionCreators from '../../assets/js/app/some/actions';
import { bindActionCreators } from 'redux';
import Checkbox from '../checkbox/checkbox.jsx';
const mapStateToProps = (state, ownProps) => {
return {
...state.emailSubscriptions
}
}
const mapDispatchToProps = dispatch => {
return {
actions: bindActionCreators(actionCreators, dispatch)
}
}
#connect(mapStateToProps, mapDispatchToProps)
class EmailSubscriptions extends Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
this.props.actions.getEmailSubscriptions();
this.setState({ // Not setting state
notifications: [
newsletter = this.props.newsletter,
orderConfirmation = this.props.orderConfirmation,
shippingInformation = this.props.shippingInformation,
orderEnquiryConfirmation = this.props.orderEnquiryConfirmation
]
})
}
render() {
return (
<div>
Here I want to use loop through state to create checkboxes
{this.state.notifications&& this.state.notifications.map((item, index) => {
const checkboxProps = {
id: 'subscription' + index,
name: 'subscription',
checked: item.subscription ? true : false,
onChange: (e)=>{ return this.onChange(e, index)},
};
return <div key={index}>
<Checkbox {...checkboxProps} />
</div>
</div>
)
}
}
export default EmailSubscriptions;
I hope getEmailSubscriptions is an async action, so your setState won't update the state as you intended. add componentDidUpdate hook in your class component and your setState statement within an if statement that has an expression checking your props current and prev value.
You can do something like this.
componentDidMount() {
this.props.actions.getEmailSubscriptions();
}
componentDidUpdate(prevProps, prevState, snapshot){
if(this.props.<prop_name> != prevProps.<prop_name>){
this.setState({
notifications: [
newsletter = this.props.newsletter,
orderConfirmation = this.props.orderConfirmation,
shippingInformation = this.props.shippingInformation,
orderEnquiryConfirmation = this.props.orderEnquiryConfirmation
]
})
}
}

How to update prop values in Child class component when Parent class component state is changed? : React Native

I have a parent class component called CardView.js which contains a child class component called Tab.js (which contains a FlatList).
When a button is pressed in CardView.js, a modal appears with various options. A user chooses an option and presses 'OK' on the modal. At this point the onOKHandler method in the parent component updates the parent state (tabOrderBy: orderBy and orderSetByModal: true). NOTE: These two pieces of state are passed to the child component as props.
Here is what I need:
When the onOKHandler is pressed in the parent, I need the child component to re-render with it's props values reflecting the new state values in the parent state. NOTE: I do not want the Parent Component to re-render as well.
At the moment when onOKHandler is pressed, the child component reloads, but it's props are still showing the OLD state from the parent.
Here is what I have tried:
When the onOKHandler is pressed, I use setState to update the parent state and then I use the setState callback to call a method in the child to reload the child. The child reloads but its props are not updated.
I have tried using componentDidUpdate in the child which checks when the prop orderSetByModal is changed. This does not work at all.
I have tried many of the recommendations in other posts like this - nothing works! Where am I going wrong please? Code is below:
Parent Component: CardView.js
import React from "react";
import { View } from "react-native";
import { Windows} from "../stores";
import { TabView, SceneMap } from "react-native-tab-view";
import { Tab, TabBar, Sortby } from "../components";
class CardView extends React.Component {
state = {
level: 0,
tabIndex: 0,
tabRoutes: [],
recordId: null,
renderScene: () => {},
showSortby: false,
orderSetByModal: false,
tabOrderBy: ''
};
tabRefs = {};
componentDidMount = () => {
this.reload(this.props.windowId, null, this.state.level, this.state.tabIndex);
};
reload = (windowId, recordId, level, tabIndex) => {
this.setState({ recordId, level, tabIndex });
const tabRoutes = Windows.getTabRoutes(windowId, level);
this.setState({ tabRoutes });
const sceneMap = {};
this.setState({ renderScene: SceneMap(sceneMap)});
for (let i = 0; i < tabRoutes.length; i++) {
const tabRoute = tabRoutes[i];
sceneMap[tabRoute.key] = () => {
return (
<Tab
onRef={(ref) => (this.child = ref)}
ref={(tab) => (this.tabRefs[i] = tab)}
windowId={windowId}
tabSequence={tabRoute.key}
tabLevel={level}
tabKey={tabRoute.key}
recordId={recordId}
orderSetByModal={this.state.orderSetByModal}
tabOrderBy={this.state.tabOrderBy}
></Tab>
);
};
}
};
startSortByHandler = () => {
this.setState({showSortby: true});
};
endSortByHandler = () => {
this.setState({ showSortby: false});
};
orderByFromModal = () => {
return 'creationDate asc'
}
refreshTab = () => {
this.orderByFromModal();
this.child.refresh()
}
onOKHandler = () => {
this.endSortByHandler();
const orderBy = this.orderByFromModal();
this.setState({
tabOrderBy: orderBy,
orderSetByModal: true}, () => {
this.refreshTab()
});
}
render() {
return (
<View>
<TabView
navigationState={{index: this.state.tabIndex, routes: this.state.tabRoutes}}
renderScene={this.state.renderScene}
onIndexChange={(index) => {
this.setState({ tabIndex: index });
}}
lazy
swipeEnabled={false}
renderTabBar={(props) => <TabBar {...props} />}
/>
<Sortby
visible={this.state.showSortby}
onCancel={this.endSortByHandler}
onOK={this.onOKHandler}
></Sortby>
</View>
);
}
}
export default CardView;
Child Component: Tab.js
import React from "react";
import { FlatList } from "react-native";
import { Windows } from "../stores";
import SwipeableCard from "./SwipeableCard";
class Tab extends React.Component {
constructor(props) {
super(props);
this.state = {
currentTab: null,
records: [],
refreshing: false,
};
this.listRef = null;
}
async componentDidMount() {
this.props.onRef(this);
await this.reload(this.props.recordId, this.props.tabLevel, this.props.tabSequence);
}
componentWillUnmount() {
this.props.onRef(null);
}
//I tried adding componentDidUpdate, but it did not work at all
componentDidUpdate(prevProps) {
if (this.props.orderSetByModal !== prevProps.orderSetByModal) {
this.refresh();
}
}
getOrderBy = () => {
let orderByClause;
if (this.props.orderSetByModal) {
orderByClause = this.props.tabOrderBy;
} else {
orderByClause = "organization desc";
}
return orderByClause;
};
async reload() {
const currentTab = Windows.getTab(this.props.windowId, this.props.tabSequence, this.props.tabLevel);
this.setState({ currentTab });
let response = null;
const orderBy = this.getOrderBy();
response = await this.props.entity.api.obtainRange(orderBy);
this.setState({ records: response.dataList })
}
refresh = () => {
this.setState({ refreshing: true }, () => {
this.reload(this.props.recordId, this.props.tabLevel, this.props.tabSequence)
.then(() => this.setState({ refreshing: false }));
});
};
renderTabItem = ({ item, index }) => (
<SwipeableCard
title={"Card"}
/>
);
render() {
if (!this.state.currentTab) {
return null;
}
return (
<>
<FlatList
ref={(ref) => (this.listRef = ref)}
style={{ paddingTop: 8 }}
refreshing={this.state.refreshing}
onRefresh={this.refresh}
data={this.state.records}
keyExtractor={(item) => (item.isNew ? "new" : item.id)}
/>
</>
);
}
}
export default Tab;

React/TypeScript `Error: Maximum update depth exceeded.` when trying to redirect on timeout

I have a project that I'm trying to get to redirect from page 1 to 2 etc. dynamically. This has worked for me previously, but recently I'm getting this error:
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.
After seeing this message this morning, and multiple SO pages saying NOT to call setState in render, I have moved my setTimeout call into componentDidMount.
So far I've tried
- calling a function that changes this.props.pageWillChange property, then in render I return a object based on that condition
- returning a object pending condition set in an inline if statement in render
- turning pageWillChange into a local prop, rather than one that is inherited by the class (I quite like this option, as the state of this will be the same for every new version of this component)
Many more things, but these felt like they would work. Anyone able to help?
import React, { Component } from "react"
import axios from "axios"
import { GridList, GridListTile } from "#material-ui/core"
import "../assets/scss/tile.scss"
import Request from "../config.json"
import DataTile from "./DiagnosticDataTile"
import { IDiagnosticResultData } from "../interfaces/IDiagnosticResultData"
import { Redirect } from "react-router"
interface IProps {
category: string
redirect: string
}
interface IPageState {
result: IDiagnosticResultData[]
pageWillChange: boolean
}
class Dashboard extends Component<IProps, IPageState> {
_isMounted = false
changeTimeInMinutes = 0.25
willRedirect: NodeJS.Timeout
constructor(props: Readonly<IProps>, state: IPageState) {
super(props)
this.state = state
console.log(window.location)
}
componentDidMount(): void {
this._isMounted = true
this.ChangePageAfter(this.changeTimeInMinutes)
axios
.get(Request.url)
.then(response => {
if (this._isMounted) {
this.setState({ result: response.data })
}
})
.catch(error => {
console.log(error)
})
}
componentWillUnmount(): void {
this._isMounted = false
clearTimeout(this.willRedirect)
}
ChangePageAfter(minutes: number): void {
setTimeout(() => {
this.setState({ pageWillChange: true })
}, minutes * 60000)
}
render() {
var data = this.state.result
//this waits for the state to be loaded
if (!data) {
return null
}
data = data.filter(x => x.categories.includes(this.props.category))
return (
<GridList
cols={this.NoOfColumns(data)}
cellHeight={this.GetCellHeight(data)}
className="tileList"
>
{this.state.pageWillChange ? <Redirect to={this.props.redirect} /> : null}
{data.map((tileObj, i) => (
<GridListTile
key={i}
className="tile"
>
<DataTile data={tileObj} />
</GridListTile>
))}
</GridList>
)
}
}
export default Dashboard
(very new with React and TypeScript, and my first SO post woo!)
Try the code below, also couple of points:
No need for _isMounted field. Code in 'componentDidMount' always runs after it's mounted.
No need to set state in constructor. Actually there is no need for constructor anymore.
I can't see much point of clearTimeout in componentWillUnmount mount. It's never asigned to timeout.
About routing. U can use 'withRouter' high order function to change route programmatically in changePageAfter method.
Hope this helps!
import axios from "axios"
import { GridList, GridListTile } from "#material-ui/core"
import "../assets/scss/tile.scss"
import Request from "../config.json"
import DataTile from "./DiagnosticDataTile"
import { IDiagnosticResultData } from "../interfaces/IDiagnosticResultData"
import { Redirect, RouteComponentProp } from "react-router"
interface PropsPassed {
category: string
redirect: string
}
type Props = PropsPassed & RouteComponentProp
interface IPageState {
result: IDiagnosticResultData[]
pageWillChange: boolean
}
class Dashboard extends Component<Props, IPageState> {
changeTimeInMinutes = 0.25
willRedirect: NodeJS.Timeout
componentDidMount(): void {
this.ChangePageAfter(this.changeTimeInMinutes)
axios
.get(Request.url)
.then(response => {
this.setState({ result: response.data })
})
.catch(error => {
console.log(error)
})
}
changePageAfter(minutes: number): void {
setTimeout(() => {
this.props.history.push({
pathname: '/somepage',
});
}, minutes * 60000)
}
render() {
var data = this.state.result
//this waits for the state to be loaded
if (!data) {
return null
}
data = data.filter(x => x.categories.includes(this.props.category))
return (
<GridList
cols={this.NoOfColumns(data)}
cellHeight={this.GetCellHeight(data)}
className="tileList"
>
{data.map((tileObj, i) => (
<GridListTile
key={i}
className="tile"
>
<DataTile data={tileObj} />
</GridListTile>
))}
</GridList>
)
}
}
export default withRouter(Dashboard)

React shows a stale state in the render

I have an Image class which allows me to change images from the containiner Component and update the image style.
My Class:
import React from "react";
import Radium from 'radium';
class StateImage extends React.Component {
constructor(props) {
super(props);
this.state = {
images: this.props.images.map(image => ({
...image,
loaded: false,
activeStyle: {visibility: 'hidden'}
})),
activeMode: props.activeMode
};
this.state.images.forEach((image, index) => {
const src = image.image;
const primaryImage = new Image();
primaryImage.onload = () => {
const images = [...this.state.images];
images[index].loaded = true;
if (images[index].name === this.state.activeMode) {
images[index].activeStyle = images[index].style;
// is this image the default activated one? if so, activate it now that it's loaded.
images[index].onActivate();
} else
images[index].activeStyle = {visibility: 'hidden'};
this.setState( {
...this.state,
images
});
};
primaryImage.src = src;
});
}
updateImageStyle = (name, style) => {
let images = [...this.state.images].map( (image) => {
if (image.name === name) {
return {
...image,
style: style,
activeStyle: style
}
} else return image;
});
this.setState({
...this.state,
images: images
}, () => {
console.log("updated state");
console.log(this.state);
});
};
onClick = () => {
this.state.images.map( (image) => {
if (image.clickable && image.name === this.state.activeMode)
this.props.eventHandler(this.state.activeMode);
});
};
render () {
console.log("render");
console.log(this.state.images);
let images = this.state.images.map((image, index) => {
return <img
key={ index }
onClick={ this.onClick }
style={ image.activeStyle }
src={ image.image }
alt={ image.alt}/>
});
return (
<div>
{images}
</div>
);
}
}
export default Radium(StateImage);
My problem revolves around updateImageStyle. When this function is called I need to change the style element of the active image and re-render so that users see the change.
updateImageStyle is reached, and I update the images portion of my state. I console.log it once the setState is done and I can verify the change was made correctly!
However, I also console.log from the render and to my amazement, the this.state.images outputted from render is stale and does not reflect my changes.
How can this be? the console.log proves the render that has the stale state is called AFTER I have confirmed my changes have taken place.
My console log:
You are most likely seeing your state being overwritten by a different setState call perhaps the one in primaryImage.onload. Since React batches setState calls together, render() is called only once with the updates.

React authentication HoC

I have a React-Router-Redux application that I built with an expressJS server. Part of this application is authentication using JWT. Aside from protecting Routes, I am trying to create a HoC that will protect it's wrapped component by reaching out to the server and authenticating before displaying the wrapped component. Here is the HoC I have built:
withAuth.js:
import React, { Component } from 'react';
import {connect} from 'react-redux';
import * as actions from '../../store/actions';
export default function (ComposedComponent) {
class Authenticate extends Component {
componentWillMount() {
console.log('will mount');
this.props.authenticate();
}
render() {
const { loading, loaded } = this.props;
return !loading && loaded ? <ComposedComponent {...this.props} /> : null;
}
}
const mapStateToProps = state => {
return {
loading: state.auth.loading,
loaded: state.auth.loaded
};
};
const mapDispatchToProps = dispatch => {
return {
authenticate: () => dispatch(actions.authenticate())
};
};
return connect(mapStateToProps, mapDispatchToProps)(Authenticate)
}
I am using Redux Saga aswell. The authenticate action calls a saga that sets loading to true, loaded to false and reaches out to the server. When the server sends confirmation, loaded is set to true and loading is set to false, aside from a cookie and some data being saved.
It basically works, but the problem is that when I enter a route with this HoC, the authentication process is done twice (HoC's ComponentWillMount is called twice) and I cant figure out why. It happens with a wrapped component that doesnt even reach out to the server or change props on mount/update. What am I missing here?
This is one of the wrapped components that has this problem:
class SealantCustomer extends Component {
state = {
controls: {
...someControls
}
}
shouldComponentUpdate(nextProps) {
if (JSON.stringify(this.props.sealantCustomer) === JSON.stringify(nextProps.sealantCustomer)) return false;
else return true;
}
updateInput = (event, controlName) => {
let updatedControls = inputChangedHandler(event, controlName, this.state.controls);
this.setState({controls: updatedControls});
}
searchCustomer = async (event) => {
event.preventDefault();
this.props.fetchCustomer(this.state.controls.phone.value, this.state.controls.site.value, this.state.controls.name.value);
}
render () {
let sealantCustomer;
if (this.props.loading) {
sealantCustomer = <Loader />;
}
if (!this.props.loading) {
if (!this.props.sealantCustomer) this.props.error ? sealantCustomer = <h3 style={{color: 'salmon'}}>ERROR: {this.props.error}</h3> : sealantCustomer = <h3>Please search for a sealant customer</h3>
else if (this.props.sealantCustomer.length === 0) sealantCustomer = <h3>Found no sealant customers with these details!</h3>
else {
let data = [];
this.props.sealantCustomer.forEach(person => {
...filling data here
})
const columns = [{
...table columns
}]
const keysToSkip = [keys];
sealantCustomer = <ReactTable data={data} columns={columns} defaultPageSize={3} className={['-striped', '-highlight', 'tableDefaults'].join(" ")}
SubComponent={sub component} />
}
}
return (
<div className={classes.sealantCustomerPage}>
<SearchBox controls={this.state.controls} submit={this.searchCustomer} inputUpdate={this.updateInput} name="Sealant Customers" />
<div className={classes.sealantCustomer}>
{sealantCustomer}
</div>
</div>
)
}
};
const mapStateToProps = state => {
return {
loading: state.searches.loading,
error: state.searches.error,
sealantCustomer: state.searches.sealantCustomer
};
};
const mapDispatchToProps = dispatch => {
return {
fetchCustomer: (phone, site, name) => dispatch(actions.searchSealantCustomer(phone, site, name))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(SealantCustomer);

Resources