Reputation: 7440
I'm just beginning to use React for a project, and am really struggling with incorporating async/await functionality into one of my components.
I have an asynchronous function called fetchKey
that goes and gets an access key from an API I am serving via AWS API Gateway:
const fetchKey = async authProps => {
try {
const headers = {
Authorization: authProps.idToken // using Cognito authorizer
};
const response = await axios.post(
"https://MY_ENDPOINT.execute-api.us-east-1.amazonaws.com/v1/",
API_GATEWAY_POST_PAYLOAD_TEMPLATE,
{
headers: headers
}
);
return response.data.access_token;
} catch (e) {
console.log(`Axios request failed! : ${e}`);
return e;
}
};
I am using React's Material UI theme, and waned to make use of one of its Dashboard templates. Unfortunately, the Dashboard template uses a functional stateless component:
const Dashboard = props => {
const classes = useStyles();
const token = fetchKey(props.auth);
console.log(token);
return (
... rest of the functional component's code
The result of my console.log(token)
is a Promise, which is expected, but the screenshot in my Google Chrome browser is somewhat contradictory - is it pending, or is it resolved?
Second, if I try instead token.then((data, error)=> console.log(data, error))
, I get undefined
for both variables. This seems to indicate to me that the function has not yet completed, and therefore has not resolved any values for data
or error
. Yet, if I try to place a
const Dashboard = async props => {
const classes = useStyles();
const token = await fetchKey(props.auth);
React complains mightily:
> react-dom.development.js:57 Uncaught Invariant Violation: Objects are
> not valid as a React child (found: [object Promise]). If you meant to
> render a collection of children, use an array instead.
> in Dashboard (at App.js:89)
> in Route (at App.js:86)
> in Switch (at App.js:80)
> in div (at App.js:78)
> in Router (created by BrowserRouter)
> in BrowserRouter (at App.js:77)
> in div (at App.js:76)
> in ThemeProvider (at App.js:75)
Now, I'll be the first to state I don't have enough experience to understand what is going on with this error message. If this was a traditional React class component, I'd use the this.setState
method to set some state, and then go on my merry way. However, I don't have that option in this functional component.
How do I incorporate async/await logic into my functional React component?
Edit: So I will just say I'm an idiot. The actual response object that is returned is not response.data.access_token
. It was response.data.Item.access_token
. Doh! That's why the result was being returned as undefined, even though the actual promise was resolved.
Upvotes: 209
Views: 345475
Reputation: 1064
Using async/await inside a React functional component is a common practice for handling asynchronous operations, such as data fetching. Here’s a simple example demonstrating how to use async/await within a React functional component:
import React, { useState, useEffect } from 'react';
const DataFetchingComponent = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, []); // Empty dependency array means this effect runs once when the
component mounts
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<div>
<h1>Fetched Data</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
export default DataFetchingComponent;
Upvotes: -1
Reputation: 2934
You will have to make sure two things
useEffect
is similar to componentDidMount
and componentDidUpdate
, so if you use setState
here then you need to restrict the code execution at some point when used as componentDidUpdate
as shown below:function Dashboard() {
const [token, setToken] = useState('');
useEffect(() => {
// React advises to declare the async function directly inside useEffect
async function getToken() {
const headers = {
Authorization: authProps.idToken // using Cognito authorizer
};
const response = await axios.post(
"https://MY_ENDPOINT.execute-api.us-east-1.amazonaws.com/v1/",
API_GATEWAY_POST_PAYLOAD_TEMPLATE,
{ headers }
);
const data = await response.json();
setToken(data.access_token);
};
// You need to restrict it at some point
// This is just dummy code and should be replaced by actual
if (!token) {
getToken();
}
}, []);
return <>/*Rendering code*/</>;
}
Upvotes: 186
Reputation: 1057
Component might unmount or re-render with different props.auth
before fetchKey
is resolved:
const Dashboard = props => {
const classes = useStyles();
const [token, setToken] = useState();
const [error, setError] = useState();
const unmountedRef = useRef(false);
useEffect(()=>()=>(unmountedRef.current = true), []);
useEffect(() => {
const effectStale = false; // Don't forget ; on the line before self-invoking functions
(async function() {
const response = await fetchKey(props.auth);
/* Component has been unmounted. Stop to avoid
"Warning: Can't perform a React state update on an unmounted component." */
if(unmountedRef.current) return;
/* Component has re-rendered with different someId value
Stop to avoid updating state with stale response */
if(effectStale) return;
if(response instanceof Error)
setError(response)
else
setToken(response);
})();
return ()=>(effectStale = true);
}, [props.auth]);
if( error )
return <>Error fetching token...{error.toString()}</>
if( ! token )
return <>Fetching token...</>
return //... rest of the functional component's code
An alternative is using Suspense and ErrorBoundary:
// render Dashboard with <DashboardSuspend>
const Dashboard = props => {
const classes = useStyles();
const [token, idToken] = props.tokenRef.current || [];
// Fetch token on first render or when props.auth.idToken has changed
if(token === void 0 || idToken !== props.auth.idToken){
/* The thrown promise will be caught by <React.Suspense> which will render
it's fallback until the promise is resolved, then it will attempt
to render the Dashboard again */
throw (async()=>{
const initRef = props.tokenRef.current;
const response = await fetchKey(props.auth);
/* Stop if tokenRef has been updated by another <Dashboard> render,
example with props.auth changed causing a re-render of
<DashboardSuspend> and the first request is slower than the second */
if(initRef !== props.tokenRef.current) return;
props.tokenRef.current = [response, props.auth.idToken];
})()
}
if(props.tokenRef.current instanceof Error){
/* await fetchKey() resolved to an Error, throwing it will be caught by
<ErrorBoundary> which will render it's fallback */
throw props.tokenRef.current
}
return //... rest of the functional component's code
}
const DashboardSuspend = props => {
/* The tokenRef.current will reset to void 0 each time this component is
mounted/re-mounted. To keep the value move useRef higher up in the
hierarchy and pass it down with props or useContext. An alternative
is using an external storage object such as Redux. */
const tokenRef = useRef();
const errorFallback = (error, handleRetry)=>{
const onRetry = ()=>{
// Clear tokenRef otherwise <Dashboard> will throw same error again
tokenRef.current = void 0;
handleRetry();
}
return <>
Error fetching token...{error.toString()}
<Button onClick={onRetry}>Retry</Button>
</>
}
const suspenseFallback = <>Fetching token...</>
return <ErrorBoundary fallback={errorFallback}>
<React.Suspense fallback={suspenseFallback}>
<Dashboard {...props} tokenRef={tokenRef} />
</React.Suspense>
</ErrorBoundary>
}
// Original ErrorBoundary class: https://reactjs.org/docs/error-boundaries.html
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { error };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
console.log(error, errorInfo);
}
render() {
if (this.state.error) {
// You can render any custom fallback UI
const handleRetry = () => this.setState({ error: null });
return typeof this.props.fallback === 'function' ? this.props.fallback(this.state.error, handleRetry) : this.props.fallback
}
return this.props.children;
}
}
Upvotes: 3
Reputation: 3117
With React Hooks, you can now achieve the same thing as Class component in functional component now.
import { useState, useEffect } from 'react';
const Dashboard = props => {
const classes = useStyles();
const [token, setToken] = useState(null);
useEffect(() => {
async function getToken() {
const token = await fetchKey(props.auth);
setToken(token);
}
getToken();
}, [])
return (
... rest of the functional component's code
// Remember to handle the first render when token is null
Also take a look at this: Using Async await in react component
Upvotes: 117
Reputation: 1032
const token = fetchKey(props.auth);
This returns a promise. To get the data from it, this is one way to do it:
let token = null;
fetchKey(props.auth).then(result => {
console.log(result)
token = result;
}).catch(e => {
console.log(e)
})
Let me know if that works.
I recreated a similar example: https://codesandbox.io/embed/quiet-wood-bbygk
Upvotes: -1