I have a component in which I fetch data based on an item ID that was clicked earlier. The fetch is successful and console.log shows the correct data, but the data gets lost with this.setState. I have componentDidUpdate and componentDidMount in the same component, not sure if this is okay or maybe these two are messing eachother up?
Here is the code:
const teamAPI = 'http://localhost:8080/api/teams/'
const playerAPI = 'http://localhost:8080/api/playersByTeam/'
const matchAPI = 'http://localhost:8080/api/match/'
class View extends Component {
constructor() {
super();
this.state = {
data: [],
playersData: [],
update: [],
team1: [],
team2: [],
matchData: [],
testTeam: [],
};
}
componentDidUpdate(prevProps) {
if (prevProps.matchId !== this.props.matchId) {
fetch(matchAPI + this.props.matchId)
.then((matchResponse) => matchResponse.json())
.then((matchfindresponse) => {
console.log(matchfindresponse);
this.setState({
matchData:matchfindresponse,
testTeam:matchfindresponse.team1.name,
})
})
}
}
componentDidMount() {
fetch(teamAPI)
.then((Response) => Response.json())
.then((findresponse) => {
console.log(findresponse)
this.setState({
data:findresponse,
team1:findresponse[0].name,
team2:findresponse[1].name,
})
})
fetch(playerAPI + 82)
.then(playerResponse => playerResponse.json())
.then(players => {
console.log(players)
this.setState({
playersData:players
})
})
}
The first render also gives this warning:
Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.
Please check the code for the View component.
Everything from ComponentDidMount works fine in render but {this.state.matchData} and {this.state.testTeam} from componentDidUpdate are empty.
Could the problem be that ComponentDidMount re-renders the component which causes the data from ComponentDidUpdate to be lost and if so, how could I fix this?
Tried ComponentWillReceiveProps like this but still no luck
componentWillReceiveProps(newProps) {
if (newProps.matchId !== this.props.matchId) {
fetch(matchAPI + newProps.matchId)
.then((matchResponse) => matchResponse.json())
.then((matchfindresponse) => {
console.log(matchfindresponse.team1.name);
console.log(this.props.matchId + ' ' + newProps.matchId);
this.setState({
matchData:matchfindresponse.team1.name,
})
})
}
}
On your componentDidMount you should be using Promise.all. This isn't really your problem, but it does make more sense.
componentDidMount() {
const promises = [
fetch(teamAPI).then(resp => resp.json()),
fetch(playerAPI + 82).then(resp => resp.json())
];
Promise.all(promises).then(([teamData, playerData]) => {
// you can use this.setState once here
});
}
Looks like your componentDidUpdate should be a getDerivedStateFromProps in combination with componentDidUpdate (this is new to react 16.3 so if you are using an older version use the depreciated componentWillReceiveProps). Please see https://github.com/reactjs/rfcs/issues/26. Notice too that now componentDidUpdate receives a third parameter from getDerivedStateFromProps. Please see https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html for more details.
EDIT: Just to add more details.
Your state object should just include other key like matchIdChanged.
Then
// in your state in your constructor add matchId and matchIdChanged then
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.matchId !== prevState.matchId) {
return { matchIdChanged: true, matchId: nextProps.matchId }
}
return null;
}
componentDidUpdate() {
if (this.state.matchIdChanged) {
fetch(matchAPI + this.props.matchId)
.then((matchResponse) => matchResponse.json())
.then((matchfindresponse) => {
console.log(matchfindresponse);
this.setState({
matchData:matchfindresponse,
testTeam:matchfindresponse.team1.name,
matchIdChanged: false // add this
})
})
}
}
instead of using componentDidUpdate() lifecycle hook of react try using getDerivedStateFromProps() lifecycle function if you are using react 16.3, else try using componentWillReceiveProps() for below versions. In my opinion try to avoid the use of componentDidUpdate().
Plus error you are getting is because, setState() function is called, when your component somehow gets unmounted, there can be multiple reasons for this, most prominent being -
check the render function of this component, are you sending null or something, based on certain condition?
check the parent code of component, and see when is the component getting unmounted.
Or you can share these code, so that we might help you with this.
Plus try to debug using ComponentWillUnmount(), put console.log() in it and test it for more clarity.
Hope this helps, thanks
Related
Being new to react, I saw a similar example but they were not clearly explained and I didn't understand how to solve this problem. I have an API, the new data is posted to the API.
ComponentDidMount() will initiate the data from the API first time.
I went through the documentation and saw that componentDiUpdate() will always re-render the page if the new data is added.
This is my code so far:
constructor(props){
super(props);
this.state = {
newData: [],
dataApi: this.props.getAllData() //method that is using GET_ALL_DATA actions/reducers using fetch(get)
}
}
// it gets the data
componentDidMount() {
this.state.dataApi
}
componentDidUpdate(prevProps, prevState){
this.state.dataApi.then(data => {
if(prevProps.data != this.props.data) {
this.setState({newData: data});
}
}
}
ComponentDidUpdate() errors:
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
How can I fix this problem? by getting the new data from the API when someone makes a post request? Thanks
Solved the problem, thanks everyone:
Solution to the problem will be:
this.state = {
allData: [],
isSubmitted: false
}
async componentDidUpdate(prevProps, prevState){
if(this.mounted && this.state.isSubmitted){
const allData = await this.props.getAllData();
this.setState({isSubmitted: false,
allData: allData.data })
}
componentDidMount(){
this.mounted=true
}
componentWillUnmount() {
this.mounted = false;
}
functionAddDataToApi(){
// some logic
this.setState({isSubmitted: true})
}
The componentDidMount() lifecycle method gets called once only when your component is rendered for the first time.
In your constructor(), you don't need to put a promise on your state. Instead you can remove dataApi and just call the method directly. In componentDidMount(), you'll make your API call. When the API call has finished, you can use this.setState().
The componentDidUpdate method gets called every time one of your prop or state values gets updated. As such, it is a bad idea to update state within it as you risk infinitely looping.
constructor(props){
super(props);
this.state = {
newData: [],
};
}
// it gets the data
componentDidMount() {
this.props.getAllData().then(data => {
this.setState({
newData: data,
});
});
}
if you want to hook to props change before re-render try hooking into componentWillReceiveProps. componentWillReceiveProps will update state synchronously.
also.. code in your componentDidMount will do nothing :)
// it gets the data
componentDidMount(){
this.state.dataApi}
try to replace it with
// it gets the data
componentDidMount() {
this.state.dataApi.then(data => {
this.setState({
newData: data,
});
});
}
When setting the state asynchronously (for example, after a promise is resolved), it's important to make sure that your component is still mounted, otherwise, you risk setting the state on an unmounted component (which triggers the error that you are getting).
To do that, you will need to set some kind of a flag (like this.mounted in the below example):
componentDidMount() {
this.mounted = true;
}
async componentDidUpdate() {
const data = await someAPICall();
if (this.mounted && !_.isEqual(this.state.data, data)) { // See comment below
this.setState({data});
}
}
componentWillUnmount() {
this.mounted = false;
}
Also, when comparing the old data with the new data, you'll need to perform a deep compare (in the example above I'm using lodash isEqual(...)).
A shallow compare (i.e. this.state.data !== data) compares the references of each of the objects, which are always going to be different, regardless of the actual data, and therefore will run into an infinite loop because setState() will trigger another componentDidUpdate() and so on.
Can someone help me solve how do I setState inside componentDidUpdate and not have an infinite loop? Some suggestions said to have a conditional statement, but I am not too familiar with how do I set the conditional for my code.
This is what my code looks like:
I have a dashboard component that gets all the companies and projects data from external functions where the fetch happens and then updates the state. The projects are associated with the company's id.
I am able to get the list of all the projects in JSON, but I can't figure out how to update my projects state inside componentDidUpdate once rendered.
CompanyDashboard.js
import { getCompanys } from "../../actions/companyActions";
import { getProjects } from "../../actions/projectActions";
class CompanyDashboard extends Component {
constructor(props) {
super(props);
this.state = {
companies: [],
projects: []
};
}
componentWillMount() {
// get all companies and update state
getCompanys().then(companies => this.setState({ companies }));
}
componentDidUpdate(prevState) {
this.setState({ projects: this.state.projects });
}
render() {
const { companies, projects } = this.state;
{
companies.map(company => {
// get all the projects
return getProjects(company);
});
}
return <div />;
}
}
export default CompanyDashboard;
companyActions.js
import { getUser, getUserToken } from './cognitoActions';
import config from '../../config';
export function getCompanys() {
let url = config.base_url + '/companys';
return fetch(url, {
method: 'GET',
headers: {'token': getUserToken() }
})
.then(res => res.json())
.then(data => { return data })
.catch(err => console.log(err));
}
projectActions.js
import { getUserToken } from './cognitoActions';
import config from '../../config';
export function getProjects(company) {
let url = config.base_url + `/companys/${company._id['$oid']}/projects`;
return fetch(url, {
method: 'GET',
headers: {'token': getUserToken() }
})
.then(res => res.json())
.then(data => { return data })
.catch(err => console.log(err));
}
The following code is not doing anything meaningful. You are setting your state.projects to be equal to your state.projects.
componentDidUpdate() {
this.setState({ projects: this.state.projects })
}
Also, the following code is not doing anything meaningful because you are not saving the result of companies.map anywhere.
{
companies.map((company) => {
return getProjects(company)
})
}
It's hard to tell what you think your code is doing, but my guess is that you think that simply calling "companies.map(....) " inside your render function is going to TRIGGER the componentDidUpdate function. That is not how render works, you should go back to the drawing board on that one. It also looks like you think that using the curly brackets {} inside your render function will display the objects inside your curly brackets. That's also not true, you need to use those curly brackets inside the components. For instance: {projects}
If I had to guess... the following code is how you actually want to write your component
import { getCompanys } from '../../actions/companyActions';
import { getProjects } from '../../actions/projectActions';
class CompanyDashboard extends Component {
constructor(props) {
super(props);
this.state = {
companies: [],
projects: []
}
}
componentWillMount() {
getCompanys().then(companies => {
const projectPromises = companies.map((company) => {
return getProjects(company)
});
Promise.all(projectPromises).then(projects => {
//possibly a flatten operator on projects would go here.
this.setState({ companies, projects });
});
/*
* Alternatively, you could update the state after each project call is returned, and you wouldn't need Promise.all, sometimes redux can be weird about array mutation in the state, so look into forceUpdate if it isn't rerendering with this approach:
* const projectPromises = companies.map((company) => {
* return getProjects(company).then(project => this.setState({projects: this.state.projects.concat(project)}));
* });
*/
)
}
render() {
const { companies, projects } = this.state;
//Not sure how you want to display companies and projects, but you would
// build the display components, below.
return(
<div>
{projects}
</div>
)
}
}
export default CompanyDashboard;
componentDidUpdate has this signature, componentDidUpdate(prevProps, prevState, snapshot)
This means that every time the method gets called you have access to your prevState which you can use to compare to the new data, and then based on that decide if you should update again. As an example it can look something like this.
componentDidUpdate(prevProps, prevState) {
if (!prevState.length){
this.setState({ projects: this.state.projects })
}
}
Of course this is only an example since I don't know your requirements, but this should give you an idea.
When componentDidUpdate() is called, two arguments are passed:
prevProps and prevState. This is the inverse of
componentWillUpdate(). The passed values are what the values were,
and this.props and this.state are the current values.
`componentDidUpdate(prevProps) {
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}`
You must check the state/props if new state/props different from previous one then you can allow to update your component.
You may call setState() immediately in componentDidUpdate() but
note that it must be wrapped in a condition like in the example above,
or you’ll cause an infinite loop. It would also cause an extra
re-rendering which, while not visible to the user, can affect the
component performance. If you’re trying to “mirror” some state to a
prop coming from above, consider using the prop directly instead.
This is because componentDidUpdate is called just after a component takes up somechanges in the state. so when you change state in that method only then it will move to and from from that method and state change process
I'm creating a hackernews-clone using this API
This is my component structure
-main
|--menubar
|--articles
|--searchbar
Below is the code block which I use to fetch the data from external API.
componentWillReceiveProps({search}){
console.log(search);
}
componentDidMount() {
this.fetchdata('story');
}
fetchdata(type = '', search_tag = ''){
var url = 'https://hn.algolia.com/api/v1/search?tags=';
fetch(`${url}${type}&query=${search_tag}`)
.then(res => res.json())
.then(data => {
this.props.getData(data.hits);
});
}
I'm making the API call in componentDidMount() lifecycle method(as it should be) and getting the data correctly on startup.
But here I need to pass a search value through searchbar component to menubar component to do a custom search. As I'm using only react (not using redux atm) I'm passing it as a prop to the menubar component.
As the mentioned codeblock if I search react and passed it through props, it logs react once (as I'm calling it on componentWillReceiveProps()). But if I run fetchData method inside componentWillReceiveProps with search parameter I receive it goes an infinite loop. And it goes an infinite loop even before I pass the search value as a prop.
So here, how can I call fetchdata() method with updating props ?
I've already read this stackoverflow answers but making an API call in componentWillReceiveProps doesn't work.
So where should I call the fetchdata() in my case ? Is this because of asynchronous ?
Update : codepen for the project
You can do it by
componentWillReceiveProps({search}){
if (search !== this.props.search) {
this.fetchdata(search);
}
}
but I think the right way would be to do it in componentDidUpdate as react docs say
This is also a good place to do network requests as long as you compare the current props to previous props (e.g. a network request may not be necessary if the props have not changed).
componentDidMount() {
this.fetchdata('story');
}
componentDidUpdate(prevProps) {
if (this.props.search !== prevProps.search) {
this.fetchdata(this.props.search);
}
}
Why not just do this by composition and handle the data fetching in the main HoC (higher order component).
For example:
class SearchBar extends React.Component {
handleInput(event) {
const searchValue = event.target.value;
this.props.onChange(searchValue);
}
render() {
return <input type="text" onChange={this.handleInput} />;
}
}
class Main extends React.Component {
constructor() {
this.state = {
hits: []
};
}
componentDidMount() {
this.fetchdata('story');
}
fetchdata(type = '', search_tag = '') {
var url = 'https://hn.algolia.com/api/v1/search?tags=';
fetch(`${url}${type}&query=${search_tag}`)
.then(res => res.json())
.then(data => {
this.setState({ hits: data.hits });
});
}
render() {
return (
<div>
<MenuBar />
<SearchBar onChange={this.fetchdata} />
<Articles data={this.state.hits} />
</div>
);
}
}
Have the fetchdata function in the main component and pass it to the SearchBar component as a onChange function which will be called when the search bar input will change (or a search button get pressed).
What do you think?
Could it be that inside this.props.getData() you change a state value, which is ultimately passed on as a prop? This would then cause the componentWillReceiveProps function to be re-called.
You can probably overcome this issue by checking if the search prop has changed in componentWillReceiveProps:
componentWillReceiveProps ({search}) {
if (search !== this.props.search) {
this.fetchdata(search);
}
}
I am creating a simple chat app where I make an api call to my database via axios which returns an array of message objects. I am able to get the data when I make an axios call in componentWillMount. Then I am trying to setState to display the conversation. Here's the code:
export default class Chat extends Component {
constructor(props){
super(props);
this.state = {
messages : [],
message : '',
};
this.socket = io('/api/');
this.onSubmitMessage = this.onSubmitMessage.bind(this);
this.onInputChange = this.onInputChange.bind(this);
}
componentWillMount() {
axios.get(`api/messages`)
.then((result) => {
const messages = result.data
console.log("COMPONENT WILL Mount messages : ", messages);
this.setState({
messages: [ ...messages.content ]
})
})
};
I have seen some posts concerning lifecycle functions and setting state, and it seems like I'm doing the right thing.
Again to highlight, axios call working fine, setting the state is not working. I am still seeing an empty array. Thanks in advance!
EDIT: Here is the solution to my issue specifically. It was buried in a comment, so I thought I'd leave it here..
"I discovered the issue. It was actually in how I was parsing my data. The spread operator on ...messages.content didn't work because messages.content doesn't exist. messages[i].content exists. So my fix was to spread just ...messages Then in a child component I map over the objects and parse the .content property. Thanks for the help guys!"
In your case, your setState() won't work because you're using setState() inside an async callback
Working Fiddle: https://jsfiddle.net/xytma20g/3/
You're making an API call which is async. So, the setState will be invoke only after receiving the data. It does not do anything with componentWillMount or componentDidMount. You need to handle the empty message in your render. When you receive your data from the API, set that data to the state and component will re-render with the new state which will be reflected in your render.
Pseudo code:
export default class Chat extends Component {
constructor(props){
super(props);
this.state = {
messages : [],
message : '',
};
this.socket = io('/api/');
this.onSubmitMessage = this.onSubmitMessage.bind(this);
this.onInputChange = this.onInputChange.bind(this);
}
componentWillMount() {
axios.get(`api/messages`)
.then((result) => {
const messages = result.data
console.log("COMPONENT WILL Mount messages : ", messages);
this.setState({
messages: [ ...messages.content ]
})
})
render(){
if(this.state.messages.length === 0){
return false //return false or a <Loader/> when you don't have anything in your message[]
}
//rest of your render.
}
};
componentWillMount() is invoked immediately before mounting occurs. It
is called before render(), therefore setting state in this method will
not trigger a re-rendering. Avoid introducing any side-effects or
subscriptions in this method. docs
So, You need to call componentDidMount as-
componentDidMount() {
axios.get(`api/messages`)
.then((result) => {
const messages = result.data
console.log("COMPONENT WILL Mount messages : ", messages);
this.setState({
messages: [ ...messages.content ]
})
})
i'm learning redux along side with react and did a first app to catch a few infos from Destiny and present for the user. The app has a select box where the user can choose one of the many activities and I save that activity to check with the API on the ActivityComponent, the problem is, I do that (get the activity identifier with redux and save on a store) then later I have to retrieve on the ActivityComponent but somehow I had to implement this:
componentWillReceiveProps(nextProps) {
this.props = {};
this.replaceProps(nextProps, cb => this.getAjax());
}
replaceProps(props, callback){
this.props = Object.assign({}, this.props, props);
this.setState(initialState);
callback();
}
Well here's my repository on github if anyone could help me: https://github.com/persocon/destiny-weekly
So the quick answer is no, it's not necessary. Why ? Well, you're not really using redux yet. If you look at that ajax call your are doing in replace props, getAjax, I inspected that in your codebase, and see you're calling setState in the component after receiving a request there.
With redux, you would rather use an action and reducer. The action would be handled, calling the api, and setting the state in the redux "store" with a reducer after receiving this data.
Ok so a full blown example would be something like the following, just first add in redux-thunk, it will definitely help you out going forward, be sure to go read through the example on the README to get a better idea of the how and why.
function startLoading() {
return {
type: 'LOADING_STARTED',
isLoading: true
}
}
function doneLoading(){
return {
type: 'LOADING_ENDED',
isLoading: false
}
}
function setActivity(result) {
let lastGist = result[0];
let activity = {
identifier: result.display.identifier,
title: (result.display.hasOwnProperty('advisorTypeCategory'))? result.display.advisorTypeCategory : '',
name: (result.hasOwnProperty('details') && result.details.hasOwnProperty('activityName')) ? result.details.activityName : '',
desc: (result.hasOwnProperty('details') && result.details.hasOwnProperty('activityDescription')) ? result.details.activityDescription : '',
backgroundImg: (result.display.hasOwnProperty('image')) ? 'http://bungie.net' + result.display.image : '',
modifiers: (result.hasOwnProperty('extended') && result.extended.hasOwnProperty('skullCategories')) ? result.extended.skullCategories : [],
bosses: (result.hasOwnProperty('bosses')) ? result.bosses : [],
items: (result.hasOwnProperty('items') && result.display.identifier == "xur") ? result.items : [],
bounties: (result.hasOwnProperty('bounties')) ? result.bounties : []
}
return {
type: 'SET_ACTIVITY',
activity: activity
}
}
export function findActivity(activity_id) {
return dispatch => {
dispatch(startLoading())
$.get(activity_id, (result)=>{
dispatch(doneLoading())
if(response.status == 200){
dispatch(setActivity(response.json))
}else {
dispatch(errorHere)
}
})
}
}
So it might look a bit intimidating at first, but after a go or two, it will feel more natural doing things this way, instead of in the component.
There shouldn't be any need for replaceProps, as the props will be updated automatically. componentWillReceiveProps is a chance for you to take a peek at what is to come in this lifecycle.
Note: You should never clobber this.props as that is used internally.
I would recommend comparing this.props to nextProps inside componentWillReceiveProps to see if the selected Activity has changed. If so, then fire the ajax call (which I recommend using a redux action passed into the component).
Yeah, I screwed up the comment haha sorry, on the SelectContainer.jsx now I'm doing that to retrieve the activity json after the select change:
const mapDispatchToProps = (dispatch) => {
return {
onSelectChange: (activity) =>{
dispatch(changeApiUrl(activity));
dispatch(findActivity(activity));
}
}
}
UPDATE
import { connect } from 'react-redux';
import { changeApiUrl, findActivity } from '../actions/index.jsx';
import ActivityComponent from '../components/ActivityComponent.jsx';
const mapStateToProps = (state) => {
return state.activity;
}
export class ActivityContainer extends ActivityComponent {
componentDidMount() {
const { dispatch, identifier } = this.props;
dispatch(findActivity(identifier));
}
}
export default connect(mapStateToProps)(ActivityContainer);
Generally speaking on life cycle of methods in react with redux. you should use redux methods. unless you have to use in react life cycle methods.