React-Apollo - render getting called before getDerivedStateFromProps - reactjs

I have a component that includes a verification form for a user. This component runs a graphql query upon mount. In the Verification component, I need to use the data from the graphql query to set the state so I can use it to update any values, and then submit them with the form. I have since learned about getDerivedStateFromProps and that is working to populate a new state from that data. But, the data isn't available in the DOM. It's as if render gets called before getDerivedStateFromProps.
Here is the component:
class Verification extends React.Component {
constructor(props) {
super(props);
this.state = {
company: {
legalName: ''
},
openCorporatesCompany: {}
};
}
handleLegalNameChange = (legalName) => {
let company = _.cloneDeep(this.state.company);
company.legalName = legalName;
this.setState({
company
})
};
static getDerivedStateFromProps(next, prev) {
let newState = _.cloneDeep(prev);
let {openCorporates: {getEntityAttribute}} = next;
if (getEntityAttribute && getEntityAttribute.value) {
let openCorporatesCompany = JSON.parse(getEntityAttribute.value);
let company = _.cloneDeep(newState.company);
company.legalName = openCorporatesCompany.name;
newState.openCorporatesCompany = openCorporatesCompany;
newState.company = company;
return newState;
}
return null;
}
render() {
console.log(this.state);
return (
<Input
label='Legal Name'
placeholder='Legal entity name...'
type='text'
subtext='Use the name your customers or clients will recognize'
onChange={this.handleLegalNameChange}
value={this.state.legalName}
/>
);
}
}
export const VerificationContainer = compose(
connect(mapStateToProps, mapDispatchToProps)
graphql(GetEntityAttributeQuery, {
name: "openCorporates",
options: (props) => ({
variables: {
entityId: props.currentEntity.id,
type: EntityAttributes.TypeOpenCorporates
}
})
})
)(Verification);
The console output of the console.log(this.state) in render looks like this:
As you can see, the field gets updated in state in company.legalName. But, it never gets populated in the Input box:
Why does the input not get updated with the new state? It's as if the render gets called before getDerivedStateFromProps.

I know your struggle with react and component updating, but I guess there's no bullet proof to get rid of it; 80% I guess you should try any of the different life cycle methods. Back in the day there was componentWillReceiveProps for async calls but since it's been marked unsafe (I guess) you should try getDerivedStateFromProps(props, state)
getDerivedStateFromProps(props, state) {
console.log("*************** PROPS:", props);
let { openCorporates: { getEntityAttribute } } = props;
if (getEntityAttribute && getEntityAttribute.value) {
let openCorporatesCompany = JSON.parse(getEntityAttribute.value);
let company = _.cloneDeep(this.state.company);
company.legalName = openCorporatesCompany.name;
this.setState({
openCorporatesCompany,
company
})
}
}
Take into account I haven't run the snippet.

This ended up doing the trick, thanks to #Charlie's answer:
static getDerivedStateFromProps(props, state) {
let newState = _.cloneDeep(state);
let { openCorporates: { getEntityAttribute } } = props;
if (getEntityAttribute && getEntityAttribute.value) {
let openCorporatesCompany = JSON.parse(getEntityAttribute.value);
let company = _.cloneDeep(newState.company);
company.legalName = openCorporatesCompany.name;
newState.openCorporatesCompany = openCorporatesCompany;
newState.company = company;
}
return newState;
}

Related

React - Class component doesn't receive any params

I have a simple class Component:
class SearchedUserWrapper extends Component {
constructor(props) {
super(props);
this.state = {
searchedPhrase: "",
pageNumber: 1
};
this.increment = this.increment.bind(this);
}
GetUserForSearchResult = (postAmount, pageNumber) => {
const list = [];
for (let index = 0; index < postAmount; index++) {
list.push(<SearchResultUser CurrentPage={pageNumber}></SearchResultUser>);
}
return list;
}
increment = () => {
this.setState({ pageNumber: this.state.pageNumber + 1 })
console.log(this.state.pageNumber + 0);
}
render() {
return (<div>
{this.GetUserForSearchResult(5, this.props.pageNumber)}
<Button onClick={this.increment}> Current page {this.state.pageNumber}</Button>
</div>);
}
}
and function GetUserForSearchResult receives a state from SearchUserWrapper class. My SearchResultUser looks like this:
class SearchResultUser extends Component {
render() {
{console.log(this.props.CurrentPage)}
return (
<div className="user-searchresult">
{this.props.CurrentPage}
</div>);
}
}
export default SearchResultUser;
And console log says that this props are undefined, and the div is empty.
My goal is to have the effect that everytime I click "Current page" button, to refresh all the SearchResultUser component so that it displays a state passed as parameter. What am I doing here wrong? Why does it says to be undefined?
EDIT:
I tried couple of things and discovered something.
If I send the state in the params directly, for example:
render() {
return (<div>
<SearchResultUser CurrentPage={this.state.pageNumber}></SearchResultUser>
</div>
It seems to work, but the order of sending the state to the function, which passes it to params of component doesn't work.
GetUserForSearchResult = (postAmount, pageNumber) => {
const list = [];
for (let index = 0; index < postAmount; index++) {
list.push(<SearchResultUser CurrentPage={pageNumber}></SearchResultUser>);
}
return list;
}
render() {
return (<div>
<SearchResultUser CurrentPage={this.state.pageNumber}></SearchResultUser>
</div>);
Can somebody explain why is it happening like this?
Edit:
In this place(below) I think you have to pass state instead of props, because the main component(SearchedUserWrapper) doesn't receive any props, so this is undefined.
{this.GetUserForSearchResult(5, this.props.pageNumber)}
https://codesandbox.io/s/stackoverflow-71594634-omvqpw
First message:
Did you check if the page number was updated?
If the next state depends on the current state, the doc of react recommend using the updater function form, instead:
this.setState((state) => {
return {quantity: state.quantity + 1};
});
You should call super(props) before any other statement. Otherwise, this.props will be undefined in the constructor at SearchResultUser

Render an array as list with onClick buttons

I'm new at ReactJs development, and I'm trying to render a list below the buttons I created with mapping my BE of graphQl query. I don't know what I'm doing wrong (the code has a lot of testing on it that I tried to solve the issue, but no success.)
The buttons rendered at getCategories() need to do the render below them using their ID as filter, which I use another function to filter buildFilteredCategoryProducts(categoryParam).
I tried to look on some others questions to solve this but no success. Code below, if need some more info, please let me know!
FYK: I need to do using Class component.
import React, { Fragment } from "react";
import { getProductsId } from "../services/product";
import { getCategoriesList } from "../services/categories";
//import styled from "styled-components";
class ProductListing extends React.Component {
constructor(props) {
super(props);
this.state = {
category: { data: { categories: [] } },
product: { data: { categories: [] } },
filteredProduct: { data: { categories: [] } },
};
this.handleEvent = this.handleEvent.bind(this);
}
async handleEvent(event) {
var prodArr = [];
const testName = event.target.id;
const testTwo = this.buildFilteredCategoryProducts(testName);
await this.setState({ filteredProduct: { data: testTwo } });
this.state.filteredProduct.data.map((item) => {
prodArr.push(item.key);
});
console.log(prodArr);
return prodArr;
}
async componentDidMount() {
const categoriesResponse = await getCategoriesList();
const productsResponse = await getProductsId();
this.setState({ category: { data: categoriesResponse } });
this.setState({ product: { data: productsResponse } });
}
getCategories() {
return this.state.category.data.categories.map((element) => {
const elName = element.name;
return (
<button id={elName} key={elName} onClick={this.handleEvent}>
{elName.toUpperCase()}
</button>
);
});
}
buildFilteredCategoryProducts(categoryParam) {
const filteredCategories = this.state.product.data.categories.filter(
(fil) => fil.name === categoryParam
);
let categoryProducts = [];
filteredCategories.forEach((category) => {
category.products.forEach((product) => {
const categoryProduct = (
<div key={product.id}>{`${category.name} ${product.id}`}</div>
);
categoryProducts.push(categoryProduct);
});
});
return categoryProducts;
}
buildCategoryProducts() {
const filteredCategories = this.state.product.data.categories;
let categoryProducts = [];
filteredCategories.forEach((category) => {
category.products.forEach((product) => {
const categoryProduct = (
<div key={product.id}>{`${category.name} ${product.id}`}</div>
);
categoryProducts.push(categoryProduct);
});
});
return categoryProducts;
}
buildProductArr() {
for (let i = 0; i <= this.state.filteredProduct.data.length; i++) {
return this.state.filteredProduct.data[i];
}
}
render() {
return (
<Fragment>
<div>{this.getCategories()}</div>
<div>{this.buildProductArr()}</div>
</Fragment>
);
}
}
export default ProductListing;
Ok, so this won't necessarily directly solve your problem,
but I will give you some pointers that would definitely improve some of your code and hopefully will strengthen your knowledge regarding how state works in React.
So first of all, I see that you tried to use await before a certain setState.
I understand the confusion, as setting the state in React works like an async function, but it operates differently and using await won't really do anything here.
So basically, what we want to do in-order to act upon a change of a certain piece of state, is to use the componentDidUpdate function, which automatically runs every time the component re-renders (i.e. - whenever there is a change in the value of the state or props of the component).
Note: this is different for function components, but that's a different topic.
It should look like this:
componentDidUpdate() {
// Whatever we want to happen when the component re-renders.
}
Secondly, and this is implied from the previous point.
Since setState acts like an async function, doing setState and console.log(this.state) right after it, will likely print the value of the previous state snapshot, as the state actually hasn't finished setting by the time the console.log runs.
Next up, and this is an important one.
Whenever you set the state, you should spread the current state value into it.
Becuase what you're doing right now, is overwriting the value of the state everytime you set it.
Example:
this.setState({
...this.state, // adds the entire current value of the state.
filteredProduct: { // changes only filteredProduct.
...filteredProduct, // adds the current value of filteredProduct.
data: testTwo
},
});
Now obviously if filteredProduct doesn't contain any more keys besides data then you don't really have to spread it, as the result would be the same.
But IMO it's a good practice to spread it anyway, in-case you add more keys to that object structure at some point, because then you would have to refactor your entire code and fix it accordingly.
Final tip, and this one is purely aesthetic becuase React implements a technique called "batching", in-which it tries to combine multiple setState calls into one.
But still, instead of this:
this.setState({ category: { data: categoriesResponse } });
this.setState({ product: { data: productsResponse } });
You can do this:
this.setState({
...this.state,
category: {
...this.state.category,
data: categoriesResponse,
}
product: {
...this.state.product,
data: productsResponse,
},
})
Edit:
Forgot to mention two important things.
The first is that componentDidUpdate actually has built-in params, which could be useful in many cases.
The params are prevProps (props before re-render) and prevState (state before re-render).
Can be used like so:
componentDidUpdate(prevProps, prevState) {
if (prevState.text !== this.state.text) {
// Write logic here.
}
}
Secondly, you don't actually have to use componentDidUpdate in cases like these, because setState actually accepts a second param that is a callback that runs specifically after the state finished updating.
Example:
this.setState({
...this.state,
filteredProduct: {
...this.state.filteredProduct,
data: testTwo
}
}, () => {
// Whatever we want to do after this setState has finished.
});

How to render updated state in react Js?

I am working on React Js in class component I am declaring some states and then getting data from API and changing that state to new value but React is not rendering that new value of state. but if I console.log() that state it gives me new value on console.
My code
class Home extends Component {
constructor(props) {
super(props);
this.state = {
unread: 0,
}
this.getUnread()
}
getUnread = async () => {
let data = await Chatapi.get(`count/${this.props.auth.user.id}/`).then(({ data }) => data);
this.setState({ unread: data.count });
console.log(this.state.unread)
}
render() {
const { auth } = this.props;
return (
<div>
{this.state.unread}
</div>
)
}
This is printing 2 on console but rendering 0 on screen. How can I get updated state(2) to render on screen.
and if I visit another page and then return to this page then it is rendering new value of state (2).
Please call getUnread() function in componentDidMount, something like this
componentDidMount() {
this.getUnread()
}
This is because in React class components, while calling setState you it is safer to not directly pass a value to set the state (and hence, re-render the component). This is because what happens that the state is set as commanded, but when the component is rerendered, once again the state is set back to initial value and that is what gets rendered
You can read this issue and its solution as given in react docs
You pass a function that sets the value.
So, code for setState would be
this.setState((state) => { unread: data.count });
Hence, your updated code would be :
class Home extends Component {
constructor(props) {
super(props);
this.state = {
unread: 0,
}
this.getUnread()
}
getUnread = async () => {
let data = await Chatapi.get(`count/${this.props.auth.user.id}/`).then(({ data }) => data);
this.setState((state) => { unread: data.count });
console.log(this.state.unread)
}
render() {
const { auth } = this.props;
return (
<div>
{this.state.unread}
</div>
)
}

react lifecycle method - correct way to filter

I am new to react. I have a product page component which is a page showing a particular product. What I am trying to do is that this ProductPage find the particular product in the products (which is an array of objects stored as props) and save it as the state before render it out.
I got these errors. I am not able to filter out single product from the array.
1) currentProduct is not defined.
2)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.
Which lifecycle method should I use in order to resolve this? I am confused. Can someone explain to me?
In my ProductPage.js,
import React, { Component } from 'react';
import { connect } from 'react-redux';
class ProductPage extends Component {
constructor(props) {
super(props)
this.state = {
product: [],
productId: this.props.match.params._id,
}
this.productsFiltering = this.productsFiltering.bind(this);
}
// update the state so that a product can be rendered
componentDidUpdate() {
const currentProduct = this.productsFiltering(this.state.productId);
this.setState({
product: currentProduct,
})
}
// find a product based on the Id
// pass the _id from url to find out the single product
productsFiltering = (productId) => {
const productArray = this.props.products;
return productArray.find(product => product._id === productId)
}
render() {
// error
const {name, price, description} = product;
return (
<div>
<span>{product.name}</span>
<span>{product.price}</span>
<span>{product.description}</span>
</div>
)
}
}
const mapStateToProps = (state) => ({
products: state.productsContainer,
});
export default connect(
mapStateToProps,
)(ProductPage);
The root issue is that you're copying props to state, which there is no need to do.
ProductPage displays a product and doesn't change, therefore no state is needed. Simply grab the matching product and store it in a variable then render. No setState() needed.
you can remove both errors if you change your code to this:
class ProductPage extends Component {
constructor(props) {
super(props);
this.state = {
product: []
};
this.productsFiltering = this.productsFiltering.bind(this);
}
// update the state so that a product can be rendered
componentDidUpdate() {
const productId = this.props.match.params._id;
if (productId && this.state.product._id != productId) {
const currentProduct = this.productsFiltering(productId);
if (currentProduct) {
this.setState({
product: currentProduct
});
}
}
}
// find a product based on the Id
// pass the _id from url to find out the single product
productsFiltering = (productId) => {
const productArray = this.props.products;
if(productArray.length){
return productArray.find((product) => products._id === productId);
}
};
render() {
const { name, price, description } = this.state.product || {};
return (
<div>
<span>{product.name}</span>
<span>{product.price}</span>
<span>{product.description}</span>
</div>
);
}
}
const mapStateToProps = (state) => ({
products: state.productsContainer
});
export default connect(mapStateToProps)(ProductPage);
-- always check for undefined or null properties
-- don't use setState in componentDidMount if the new state is constantly changing
-- avoid using a state which is directly drived from props because it's a bad practice and you can get stale props

function returning data but not showing

I have this component
const SummaryBar = props => {
const { MainObject} = props;
const localGetUserFromID = userID => {
getEmailFromId(userID).then(results => {
return results.data.Title; //Comment: This one returning friendly name
});
};
return (<span>Hello {localGetUserFromID(MainObject.AuthorId)}</span>)
}
but when I render it somehow the its only showing Hello and not the output I am getting from my localGetUserFromID function. Am I doing wrong? Note the AuthorId is being pass to an API and the MainObject came from the App Level,
FYI when I try to debug it using dev tools the function is retuning the text I am look for.
localGetUserFromID() returns nothing, that is, undefined, and that's why you see Hello only.
And because localGetUserFromID() makes an asynchronous call to get an email from user ID, it doesn't have to be in render() method. Now this component is defined as a state-less component, but you can re-define it as a stateful component, call the getEmailFromId() in componentDidMount() life-cycle method, and use a return value as an internal state.
Then you can show a value of the internal state after Hello.
class SummaryBar extends Component {
// Skipping prop type definition.
constructor(props) {
super(props)
this.state = {
username: '',
}
}
componentDidMount() {
const { MainObject: { AuthorId } } = this.props
getEmailFromId(AuthorId).then((results) => {
this.setState({
username: results.data.title,
})
})
}
render() {
const { username } = this.state
return (
<span>
Hello { username }
</span>
)
}
}
When things run when debugging but not when running and you are using promises as you are, the 99% of the times is because promises hasn't been resolved when you print.
localGetUserFromID indeed returns a promise that resolves to the friendly name.
You can just prepend await to localGetUserFromID(MainObject.AuthorId) and rewrite you return as this:
return (<span>Hello {await localGetUserFromID(MainObject.AuthorId)}</span>)

Resources