I have App component, which loads JSON data from the server.
And after data is loaded, I update state of child component.
Now my function looks like this:
componentDidMount() {
setTimeout(()=> {
if (this.state.users.length !== this.props.users.length) {
this.setState({users: this.props.users});
this.setState({tasks: this.getTasksArray()});
}, 500);
}
I use setTimeout to wait if data is loaded and sent to child. But I'm sure, it is not the best way
May be, it's better to use redux instead of setTimeout.
Parent component loads data:
componentWillMount() {
var _url = "/api/getusers/";
fetch(_url)
.then((response) => response.json())
.then(users => {
this.setState({ users });
console.log("Loaded data:", users);
});
}
Parent sends props with:
<AllTasks users={this.state.users} />
So, my question is: what is the best way to watch changes in child component?
I mean in this particular situation.
Yes, this is not the correct way because api calls will be asynchronous and we don't know how much time it will take.
So instead of using setTimeout, use componentWillReceiveProps lifecycle method in child component, it will get called whenever you change props values (state of parent component).
Like this:
componentWillReceiveProps(newProps){
this.setState({
users: newProps.users,
tasks: this.getTasksArray()
})
}
One more thing, don't call setState multiple times within a function because setState will trigger re-rendering so first do all the calculations then do setState in the last and update all the values in one call.
As per DOC:
componentWillReceiveProps() is invoked before a mounted component
receives new props. If you need to update the state in response to
prop changes (for example, to reset it), you may compare this.props
and nextProps and perform state transitions using this.setState() in
this method.
Update:
You are calling a method from cWRP method and using the props values inside that method, this.props will have the updated values after this lifecycle method only. So you need to pass the newProps values as a parameter in this function and use that instead of this.props.
Like this:
componentWillReceiveProps(newProps){
this.setState({
users: newProps.users,
tasks: this.getTasksArray(newProps)
})
}
getTasksArray(newProps){
//here use newProps instead of this.props
}
Check this answer for more details: componentWillRecieveProps method is not working properly: ReactJS
I found the problem.
I have getTasksArray() function that depends of props and uses this.props.users.
So, when I update state like this:
this.setState({
users: newProps.users,
tasks: this.getTasksArray()
})
getTasksArray() function uses empty array.
but when I split it to 2 lines and add setTimeout(fn, 0) like this:
this.setState({users: newProps.users});
setTimeout(()=> { this.setState({ tasks: this.getTasksArray() }, 0)
getTasksArray() function uses array that is already updated.
setTimeout(fn, 0) makes getTasksArray() to run after all other (even if I set timeout to 0 ms).
Here is the screenshot of console.log's without setTimeout:
And here is the screenshot with setTimeout:
Related
I have a situation that an item outside the component might influence the item in the backend. ex: change the value of one of the properties that are persisted, let's say the item status moves from Pending to Completed.
I know when it happens but since it is outside of a component I need to tell to the component that it is out of sync and re-fetch the data. But from outside. I know you can pass props calling the render method again. But the problem is I have a reducer and the state will pick up the last state and if I use an prop to trigger an effect I get into a loop.
Here is what I did:
useEffect(() => {
if (props.effect && !state.effect) { //this runs when the prop changes
return dispatch({ type: props.effect, });
}
if (state.effect) { // but then I get here and then back up and so on
return ModelEffect[state.effect](state?.data?.item)}, [state.status, state.effect, props.effect,]);
In short since I can't get rid of the prop the I get the first one then the second and so on in an infinite loop.
I render the root component passing the id:
render(html`<${Panel} id=${id}/>`,
document.getElementById('Panel'));
Then the idea was that I could do this to sync it:
render(html`<${Panel} id=${id} effect="RELOAD"/>`,
document.getElementById('Panel'));
any better ways to solve this?
Thanks a lot
I resolved it by passing the initialized dispatch function to a global.
function Panel (props) {
//...
const [state, dispatch,] = useReducer(RequestReducer, initialData);
//...
useEffect(() => {
//...
window.GlobalDispatch = dispatch;
//...
}, [state.status, state.effect,]);
with that I can do:
window.GlobalDispatch({type:'RELOAD'});
I know mutating state can work against PureComponent (or similar)
Is there other reason not to mutate state?
I wonder if the 3rd way is ok to do?
// The Right Way:
// copy the existing items and add a new one
addItemImmutably = () => {
this.setState({
items: [...this.state.items, this.makeItem()]
});
};
// The Wrong Way:
// mutate items and set it back
addItemMutably = () => {
this.state.items.push(this.makeItem());
this.setState({ items: this.state.items });
};
// is this ok? (mutate but set state with new copy)
addItem3rdWay = () => {
this.state.items.push(this.makeItem());
this.setState({items: [...this.state.items]});
}
Here is an example: You have fired an async method that sends a request with your current state data. In meantime you have executed a function that mutates the state. This will cause the async function to send the mutated state despite the fact it intended to send the state before mutation.
You can think state as your database.
You don't directly mutate your database, you modify your database by API.
setState is the API.
Of course, you can directly mutate your database, but other components will have a hard time retrieving those data, because those are inconsistent right now, because somebody doesn't use the API that framework provides for you.
If you really like the way mutating state, you can use Vue, Vue is designed like that.
I have a React component that maintains state for several child components. Via componentDidMount() I am calling this function in the parent component from the child components:
change = (fieldset, field, data) => {
this.setState({
[fieldset]: {
...this.state[fieldset],
[field]: data,
}
})
}
Think form/fieldset/field for the usage pattern, but with fields calling the above function.
The problem I'm having is that I believe I'm confusing React by calling this function so many times in quick succession, because state is not updated for all but one or two items.
I've tried using Object.assign() to avoid mutating state, but for the most part state has not updated correctly even at the point where I begin to read current start.
Is this against React best practices? Is there a better way for child components to call setState in a parent component?
Since the way you update the state depends on the state itself you need to use a function instead of an object.
change = (fieldset, field, data) => {
this.setState(prevState => ({
[fieldset]: {
...prevState[fieldset],
[field]: data,
}
}))
}
Functions will be applied one after another. So you wont override any pending changes.
From docs
this.setState({quantity: this.state.quantity + 1})
this.setState({quantity: this.state.quantity + 1})
Subsequent calls will override values from previous calls in the same
cycle, so the quantity will only be incremented once. If the next
state depends on the previous state, we recommend using the updater
function form, instead.
I'm trying to set up a React app where clicking a map marker in one component re-renders another component on the page with data from the database and changes the URL. It works, sort of, but not well.
I'm having trouble figuring out how getting the state from Redux and getting a response back from the API fit within the React life cycle.
There are two related problems:
FIRST: The commented-out line "//APIManager.get()......" doesn't work, but the hacked-together version on the line below it does.
SECOND: The line where I'm console.log()-ing the response logs infinitely and makes infinite GET requests to my database.
Here's my component below:
class Hike extends Component {
constructor() {
super()
this.state = {
currentHike: {
id: '',
name: '',
review: {},
}
}
}
componentDidUpdate() {
const params = this.props.params
const hack = "/api/hike/" + params
// APIManager.get('/api/hike/', params, (err, response) => { // doesn't work
APIManager.get(hack, null, (err, response) => { // works
if (err) {
console.error(err)
return
}
console.log(JSON.stringify(response.result)) // SECOND
this.setState({
currentHike: response.result
})
})
}
render() {
// Allow for fields to be blank
const name = (this.state.currentHike.name == null) ? null : this.state.currentHike.name
return (
<div>
<p>testing hike component</p>
<p>{this.state.currentHike.name}</p>
</div>
)
}
}
const stateToProps = (state) => {
return {
params: state.hike.selectedHike
}
}
export default connect(stateToProps)(Hike)
Also: When I click a link on the page to go to another url, I get the following error:
"Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op."
Looking at your code, I think I would architect it slightly differently
Few things:
Try to move the API calls and fetch data into a Redux action. Since API fetch is asynchronous, I think it is best to use Redux Thunk
example:
function fetchHikeById(hikeId) {
return dispatch => {
// optional: dispatch an action here to change redux state to loading
dispatch(action.loadingStarted())
const hack = "/api/hike/" + hikeId
APIManager.get(hack, null, (err, response) => {
if (err) {
console.error(err);
// if you want user to know an error happened.
// you can optionally dispatch action to store
// the error in the redux state.
dispatch(action.fetchError(err));
return;
}
dispatch(action.currentHikeReceived(response.result))
});
}
}
You can map dispatch to props for fetchHikeById also, by treating fetchHikeById like any other action creator.
Since you have a path /hike/:hikeId I assume you are also updating the route. So if you want people to book mark and save and url .../hike/2 or go back to it. You can still put the the fetch in the Hike component.
The lifecycle method you put the fetchHikeById action is.
componentDidMount() {
// assume you are using react router to pass the hikeId
// from the url '/hike/:hikeId'
const hikeId = this.props.params.hikeId;
this.props.fetchHikeById(hikeId);
}
componentWillReceiveProps(nextProps) {
// so this is when the props changed.
// so if the hikeId change, you'd have to re-fetch.
if (this.props.params.hikeId !== nextProps.params.hikeId) {
this.props.fetchHikeById(nextProps.params.hikeId)
}
}
I don't see any Redux being used at all in your code. If you plan on using Redux, you should move all that API logic into an action creator and store the API responses in your Redux Store. I understand you're quickly prototyping now. :)
Your infinite loop is caused because you chose the wrong lifecycle method. If you use the componentDidUpdate and setState, it will again cause the componentDidUpdatemethod to be called and so on. You're basically updating whenever the component is updated, if that makes any sense. :D
You could always check, before sending the API call, if the new props.params you have are different than the ones you previously had (which caused the API call). You receive the old props and state as arguments to that function.
https://facebook.github.io/react/docs/react-component.html#componentdidupdate
However, if you've decided to use Redux, I would probably move that logic to an action creator, store that response in your Redux Store and simply use that data in your connect.
The FIRST problem I cannot help with, as I do not know what this APIManager's arguments should be.
The SECOND problem is a result of you doing API requests in "componentDidUpdate()". This is essentially what happens:
Some state changes in redux.
Hike receives new props (or its state changes).
Hike renders according to the new props.
Hike has now been updated and calls your "componentDidUpdate" function.
componentDidUpdate makes the API call, and when the response comes back, it triggers setState().
Inner state of Hike is changed, which triggers an update of the component(!) -> goto step 2.
When you click on a link to another page, the infinite loop is continued and after the last API call triggered by an update of Hike is resolved, you call "setState" again, which now tries to update the state of a no-longer-mounted component, hence the warning.
The docs explain this really well I find, I would give those a thorough read.
Try making the API call in componentDidMount:
componentDidMount() {
// make your API call and then call .setState
}
Do that instead of inside of componentDidUpdate.
There are many ways to architect your API calls inside of your React app. For example, take a look at this article: React AJAX Best Practices. In case the link is broken, it outlines a few ideas:
Root Component
This is the simplest approach so it's great for prototypes and small apps.
With this approach, you build a single root/parent component that issues all your AJAX requests. The root component stores the AJAX response data in it's state, and passes that state (or a portion of it) down to child components as props.
As this is outside the scope of the question, I'll leave you to to a bit of research, but some other methods for managing state and async API calls involved libraries like Redux which is one of the de-facto state managers for React right now.
By the way, your infinite calls come from the fact that when your component updates, it's making an API call and then calling setState which updates the component again, throwing you into an infinite loop.
Still figuring out the flow of Redux because it solved the problem when I moved the API request from the Hike component to the one it was listening to.
Now the Hike component is just listening and re-rendering once the database info catches up with the re-routing and re-rendering.
Hike.js
class Hike extends Component {
constructor() {
super()
this.state = {}
}
componentDidUpdate() {
console.log('dealing with ' + JSON.stringify(this.props.currentHike))
}
render() {
if (this.props.currentHike == null || undefined) { return false }
const currentHike = this.props.currentHike
return (
<div className="sidebar">
<p>{currentHike.name}</p>
</div>
)
}
}
const stateToProps = (state) => {
return {
currentHike: state.hike.currentHike,
}
}
And "this.props.currentHikeReceived()" got moved back to the action doing everything in the other component so I no longer have to worry about the Hikes component infinitely re-rendering itself.
Map.js
onMarkerClick(id) {
const hikeId = id
// Set params to be fetched
this.props.hikeSelected(hikeId)
// GET hike data from database
const hack = "/api/hike/" + hikeId
APIManager.get(hack, null, (err, response) => {
if (err) {
console.error(err)
return
}
this.props.currentHikeReceived(response.result)
})
// Change path to clicked hike
const path = `/hike/${hikeId}`
browserHistory.push(path)
}
const stateToProps = (state) => {
return {
hikes: state.hike.list,
location: state.newHike
}
}
const dispatchToProps = (dispatch) => {
return {
currentHikeReceived: (hike) => dispatch(actions.currentHikeReceived(hike)),
hikesReceived: (hikes) => dispatch(actions.hikesReceived(hikes)),
hikeSelected: (hike) => dispatch(actions.hikeSelected(hike)),
locationAdded: (location) => dispatch(actions.locationAdded(location)),
}
}
Even though there are many questions with the same subject line, I could not get an answer for my problem.
Problem
I have a select dropdown. On click of which, I call an Api which fetches some key values. I consider this set of key value input fields as a component. So each and every time onChange of my select drop-down, I have used lifecycle methods to handle API Calls. Also, I record these input key values and send back their state to parent component.
According to ReactJS lifecycle methods:
I use
componentDidMount
To call the API for the first time after initial render. This works.
componentDidUpdate
To call the API for subsequent API calls on select drop-down change. But here is the problem. When I try to update the state of input fields the program falls into an infinite loop and hence there are infinite API calls. I am pretty sure after debugging that the problem is with setState(), But I couldnt find the best way to handle states in componentDidUpdate method.
This link exactly replicates my problem but i want a standardized solution
Hope this is clear.
Thanks for the help in advance!
This is spelled out pretty clearly in the docs:
componentDidUpdate(prevProps) {
// Typical usage (don't forget to compare props):
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}
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.
You can use setState() within componentDidUpdate(). But you have to use the condition for that. Otherwise, it get infinite loop.
As the example,
componentDidUpdate(){
if(this.props.id !== this.state.id) {
this.setState({
id: this.props.id
});
}
}
This happens because setState triggers a call to componentDidUpdate.
When componentDidUpdate is called, setState does not check whether or not state change has occurred. It simply calls componentDidUpdate again and again, which leads to stackoverflow.
class Component extends React.Component{
constructor(props){
super(props);
this.state = {changeState: false}
}
componentDidMount(){
this.setState({changeState: true});
}
componentDidUpdate(){
this.setState({changeState: false});
}
}
Here, first changeState is set to false in constructor, and then componentDidMount is triggered, which sets state of changeState to true. This state change triggers componentDidUpdate, which sets the state of changeState again to true. This triggers componentDidUpdate again and again.
You have to check the real difference between two state objects.
Below you can find my solution, My state object has movies, which is an array of objects. I edited and movie and then comparing these two arrays.
async componentDidUpdate(prevProps, prevState) {
if (prevState.movies.filter (e => this.state.movies.includes(e))) {
const response = await axios.get("http://localhost:3002/movies");
this.setState({ movies: response.data })
}
}
Yes you cannot setState() inside componentDidUpdate it would lead to infinite loop.Instead you can call a function onChange event and change the state there.