This is a simple question. How do I successfully update state object via react hooks?
I just started using hooks, and I like how it allows to use the simple and pure JavaScript function to create and manage state with the useState() function, and also, make changes that affect components using the useEffect() function, but I can't seem to make update to the state work!
After making a request to an API, it return the data needed, but when I try to update the state for an error in request and for a successful request, it does not update the state. I logged it to the browser console, but no change was made to the state, it returns undefined.
I know that I'm not doing something right in the code.
Here is my App component, Its a single component for fetching and updating:
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
export default function App() {
// Set date state
const [data,setData] = useState({
data: [],
loaded: false,
placeholder: 'Loading'
});
// Fetch and update date
useEffect(() => {
fetch('http://localhost:8000/api/lead/')
.then(response => {
if (response.status !== 200) {
SetData({placeholder: 'Something went wrong'});
}
response.json()
})
.then(result => {
console.log(data);
setData({data: result});
});
},[]);
return (
<h1>{console.log(data)}</h1>
);
}
ReactDOM.render(<App />, document.getElementById('app'));
There are a few things you can improve:
the react-hook useState does not behave like the class counterpart. It does not automatically merge the provided object with the state, you have to do that yourself.
I would recommend if you can work without an object as your state to do so as this can reduce the amount of re-renders by a significant amount and makes it easier to change the shape of the state afterwards as you can just add or remove variables and see all the usages immediately.
With a state object
export default function App() {
// Set date state
const [data,setData] = useState({
data: [],
loaded: false,
placeholder: 'Loading'
});
// Fetch and update date
useEffect(() => {
fetch('http://localhost:8000/api/lead/')
.then(response => {
if (response.status !== 200) {
throw new Error(response.statusText); // Goto catch block
}
return response.json(); // <<- Return the JSON Object
})
.then(result => {
console.log(data);
setData(oldState => ({ ...oldState, data: result})); // <<- Merge previous state with new data
})
.catch(error => { // Use .catch() to catch exceptions. Either in the request or any of your .then() blocks
console.error(error); // Log the error object in the console.
const errorMessage = 'Something went wrong';
setData(oldState=> ({ ...oldState, placeholder: errorMessage }));
});
},[]);
return (
<h1>{console.log(data)}</h1>
);
}
Without a state object
export default function App() {
const [data, setData] = useState([]);
const [loaded, setLoaded] = useState(false);
const [placeholder, setPlaceholder] = useState('Loading');
// Fetch and update date
useEffect(() => {
fetch('http://localhost:8000/api/lead/')
.then(response => {
if (response.status !== 200) {
throw new Error(response.statusText); // Goto catch block
}
return response.json(); // <<- Return the JSON Object
})
.then(result => {
console.log(data);
setData(data);
})
.catch(error => { // Use .catch() to catch exceptions. Either in the request or any of your .then() blocks
console.error(error); // Log the error object in the console.
const errorMessage = 'Something went wrong';
setPlaceholder(errorMessage);
});
},[]);
return (
<h1>{console.log(data)}</h1>
);
}
The correct way to update an Object with hooks it to use function syntax for setState callback:
setData(prevState => {...prevState, placeholder: 'Something went wrong'})
Following method will override your previous object state:
setData({placeholder: 'Something went wrong'}); // <== incorrect
Your final code should look like this:
.then(response => {
if (response.status !== 200) {
setData(prevObj => {...prevObj, placeholder: 'Something went wrong'});
}
return response.json()
})
.then(result => {
setData(prevObj => {...prevObj, data: result});
});
Related
I have a situation in which I have an object with all the books and I want to get the author info which sits in a different collection. I tried fetching the data inside a useMemo, but I get an error since the promise does not get resolved I guess. How to make useMemo wait for the data to come for the author?
async function useAuthor(authorID:string) {
await firestore.collection('users').doc(authorID).get().then(doc => {
return doc.data();
})
};
const normalizedBooks = useMemo(
() =>
books?.map( (book) => ({
...book,
author: useAuthor(book.authorId),
})),
[books]
);
Fetching remote data should be done in an effect:
function useAuthor(authorID:string) {
const [author, setAuthor] = useState(null);
useEffect(() => {
firestore.collection('users').doc(authorID).get().then(
doc => setAuthor(doc.data())
);
}, [authorID]);
return author;
}
Note that author will be null during the first render and until the request completed. You can improve that example by e.g. adding loading and error states. Or you can also use something like react-query which provides those out of the box:
import useQuery from 'react-query';
function getItemById(collectionID:string, itemID:string) {
return firestore.collection(collectionID).doc(itemID).get().then(
doc => doc.data()
);
}
const Author = ({authorID}) => {
const {data: author, isLoading, error} = useQuery(
['users', authorID],
getItemById
);
if (isLoading) return 'Loading...'
if (error) return 'Failed to fetch author :(';
return <p>{author.name}</p>;
}
I keep getting these warnings:
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 a useEffect cleanup
For some of my useEffects that pull data from an API with the help of my useReducer:
export default function HomeBucketsExample(props) {
const {mobileView} = props
const [allDemoBuckets, dispatchAllBuckets] = useReducer(reducer, initialStateAllBuckets)
const ListLoading = LoadingComponent(HomeBucketLists);
useEffect(() =>
{
getAllDemoBuckets(dispatchAllBuckets);
}, [])
return (
<ListLoading mobileView={ mobileView} isLoading={allDemoBuckets.loading} buckets={allDemoBuckets.data} />
);
}
However, Im not sure how to clean up this effect above, I've tried mounting it using True and False, however the error still showed up. How can I fix my function above so the useEffect doesnt throw any warnings
EDIT:
code for my reduer:
export const getAllDemoBuckets = (dispatch) => axiosInstance
.get('demo/all/')
.then(response => {
dispatch({ type: 'FETCH_SUCCESS', payload: response.data })
console.log('fired bucket-data')
})
.catch(error => {
dispatch({ type: 'FETCH_ERROR' })
})
const initialStateAllBuckets = {
loading: true,
error: '',
data: []
}
const reducer = (state, action) =>
{
switch (action.type)
{
case 'FETCH_SUCCESS':
return {
loading: false,
data: action.payload,
error: ''
}
case 'FETCH_ERROR':
return {
loading: false,
data: {},
error: "Something went wrong!"
}
default:
return state
}
}
const [allDemoBuckets, dispatchAllBuckets] = useReducer(reducer, initialStateAllBuckets)
The goal of the warning is to tell you that some action is taking place after the component is unmounted and that the result of that work is going to be thrown away.
The solution isn't to try and work around it with a reducer; the solution is to cancel whatever is happening by returning a callback from useEffect. For example:
useEffect(() => {
const ctrl = new AbortController();
fetchExternalResource(ctrl.signal);
return () => {
ctrl.abort();
}
}, []);
Using flags to determine if a component is mounted (ie using a reducer) to determine whether or not to update state is missing the point of the warning.
It's also okay to leave the warning up if this isn't actually an issue. It's just there to nit pick and tell you that, hey, you may want to clean this up. But it's not an error.
In your case, if you are using fetch, I would modify your code such that the function that dispatches actions can take an AbortSignal to cancel its operations. If you're not using fetch, there's not much you can do, and you should just ignore this warning. It's not a big deal.
It looks like you're using Axios for your requests. Axios supports a mechanism similar to abort signals - This should do the trick.
import { CancelToken } from 'axios';
const getAllDemoBuckets = async (dispatch, cancelToken) => {
try {
const response = await axiosInstance.get('/demo/all', { cancelToken });
dispatch({ type: 'FETCH_SUCCESS', payload: response.data });
} catch (err) {
if ('isCancel' in err && err.isCancel()) {
return;
}
dispatch({ type: 'FETCH_ERROR' });
}
}
const MyComponent = () => {
useEffect(() => {
const source = CancelToken.source();
getAllDemoBuckets(dispatch, source.token);
return () => {
source.cancel();
};
}, []);
}
I have the following component class:
import React, { Component } from "react";
import Form from "react-bootstrap/Form";
import Button from 'react-bootstrap/Button'
import './login.css';
export default class Login extends Component {
handleSubmit = (e) => {
var myRes = null;
fetch(
"/exist/apps/my-app/modules/who-am-i.xq?user=emh&password=emh",
{
}
)
.then((response) => response.json())
.then(
(result) => {
myRes = {
error: null,
loaded: true,
user: result
};
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
myRes = {
error: error,
loaded: true,
user: {}
};
}
);
this.setState(myRes);
}
render() {
return (
<div className="auth-wrapper">
<div className="auth-inner">
<Form onSubmit={this.handleSubmit}>
<h3>Sign In</h3>
.
.
.
<Button variant="primary" type="submit">Submit</Button>
</Form>
</div>
</div>
);
}
}
I have searched for the answer, but what I got was in (result) => {this.setState({error: null, loaded: true, user: result})}. Unfortunately the this is undefined within the fetch.
I want to in the result and error to set the value in state. Unfortunately the this is not defined within the fetch result. How do I set the state in Login from within the fetch?
The problem is you're calling setState too soon. You need to call it only when your promise has settled. The easiest way to do that is with a subsequent then, see *** comments:
handleSubmit = (e) => {
// *** No `let myRes` here
fetch(
"/exist/apps/my-app/modules/who-am-i.xq?user=emh&password=emh",
{
}
)
.then((response) => response.json())
.then(
(result) => {
// *** Transform the resolution value slightly
return {
error: null,
loaded: true,
user: result
};
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
// *** Turn rejection into resolution by returning
// a replacement for `myRes`.
return {
error: error,
loaded: true,
user: {}
};
}
)
.then(myRes => {
// *** You always end up here because both fulfillment and rejecction
// result in an object that's used to fulfill the promise
// created by the first call to `then`
this.setState(myRes);
})
// *** Still use a final handler to catch errors from
// fulfillment handlers
.catch(error => {
// Handle/report error
});
};
first of all fetch should always be async
second put const that = this; in the top of the handler and then you can do setState like this:
handleSubmit = (e) => {
const self = this
var myRes = null;
fetch(
"/exist/apps/my-app/modules/who-am-i.xq?user=emh&password=emh",
{
}
)
.then((response) => response.json())
.then(
result => {
self.setState({error: null, loaded: true, user: result})
myRes = {
error: null,
loaded: true,
user: result
};
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
myRes = {
error: error,
loaded: true,
user: {}
};
}
);
}
and advise you to create a file for the fetch like this:
const nameYouDecided = async () -> {
await fetch("/exist/apps/my-app/modules/who-am-i.xq?user=emh&password=emh" )
.then((response) => response.json())
}
and then when you call it in your code it is shorter
In addition if you have a couple of get request you should do the following
in different file like get.js
const get = async (url) => (
await fetch(url)
.then(response => response.json())
.then(json => json
)
);
const nameYouDecided = get("/exist/apps/my-app/modules/who-am-i.xq?user=emh&password=emh")
You need to set the state inside of .then() fetch function.
Fetch need time to fetch urls, setstate doesnt wait this time so it set a Promise. To turn around this, you need to put your setState inside a .then, the setState line will only be executed when your fetch done the job
fetch(
"/exist/apps/my-app/modules/who-am-i.xq?user=emh&password=emh",
{
}
)
.then((response) => response.json())
.then(
(result) => {
return {
error: null,
loaded: true,
user: result
};
},
(error) => {
return {
error: error,
loaded: true,
user: {}
};
}
).then(myRes => {
this.setState(myRes);
});
}
I am trying to load an array I saved using AsyncStorage back into state but I cant seem to get it working. I am passing the array from AsyncStorage back into context and calling the load_state case.
function loadList() {
try {
const data = AsyncStorage.getItem('data')
console.log(data)
loadState(JSON.parse(data))
}
catch (error) {
console.log(error)
}
}
const loadState = dispatch => {
return (value) => {
dispatch({ type: 'load_state', payload: value})
}}
case 'load_state':
console.log(action.payload.value)
return [...state, ...action.payload.value]
I am fetching data from within componentDidMount as
this.setState({ isLoading: true });
fetch(
`https://api.example.com/location/12345`
)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('Something went wrong ...');
}
})
.then(data => this.setState({ data, isLoading: false }));
and this works absolutely fine. But if I want to replace https://api.example.com/location/12345 with https://api.example.com/location/${this.props.id} to allow the id to change I get back errors that the data does not exist.
This is clearly because the fetch inside componentDidMount is fetching the url before before the this.props.id is read.
How can I delay the fetch until this.props.id is available?
One way is, use componentDidUpdate lifecycle method to get the data whenever component receive new id, but make sure to compare the prev id value with new id value and do the call only when they are not same.
Like this:
componentDidUpdate(prevProps) {
if(this.props.id && (prevProps.id != this.props.id)) {
this._getData();
}
}
_getData(){
this.setState({ isLoading: true });
fetch(
`https://api.example.com/location/${this.props.id}`
)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('Something went wrong ...');
}
})
.then(data => this.setState({ data, isLoading: false }));
}
I use this pattern quite often :
initFromProps(props: MyComponentProps) {
const { } = props;
}
componentWillMount() {
this.initFromProps(this.props);
}
componentWillReceiveProps(nextProps: MyComponentProps) {
this.initFromProps(nextProps);
}
This ensures your component does whatever is necessary when the props change, but also at startup. Then, in initFromProps, you can do something along the lines of this:
initFromProps(props: MyComponentProps) {
const { id } = props;
if (id !== this.props.id) {
this._getData(id);
}
}