相关文章推荐
悲伤的冰棍  ·  最好用的图表工具 -- ...·  1 年前    · 
失望的鸵鸟  ·  jupyter notebook play ...·  1 年前    · 
风度翩翩的西装  ·  EF Core ...·  1 年前    · 
Collectives™ on Stack Overflow

Find centralized, trusted content and collaborate around the technologies you use most.

Learn more about Collectives

Teams

Q&A for work

Connect and share knowledge within a single location that is structured and easy to search.

Learn more about Teams

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.

is it pending, or is it resolved it's resolved when you inspected it in the console - that doesn't mean it was resolved when you logged it ... the console "lies" :p – Jaromanda X Sep 9, 2019 at 3:54 token.then((data, error) ... the callback function to .then (for an actual Promise/A+ spec Promise) will only ever have a single argument - other "Promise like" promises (e.g. jQuery) break this spec - if data is undefined that implies that response.data.access_token is undefined – Jaromanda X Sep 9, 2019 at 3:55 Can you check if you are getting a proper response from the axios call? And also if return response.data.access_token; is not undefined? – Praneeth Paruchuri Sep 9, 2019 at 4:17

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 (
        ... rest of the functional component's code
                    Two problems to watch out for: 1) The useEffect is missing token and getToken as dependencies. 2) Since getToken is async, it could call setToken after the component is unmounted.
    – Mattias Wallin
                    Mar 15, 2021 at 7:12
                    1) No it's not missing any dependencies as it just needs to be called once, which can only be done with just [] 2) Yes, thats the valid scenario which can be handled with a check of screen being visible or not.
    – Milind Agrawal
                    Mar 15, 2021 at 11:26
                    If it's only called once, then why bother restricting it with if (!token)? If token changes, it would trigger the side-effect again which is why you guard against that but it also means token really is a dependency of the side-effect.
    – Marc Dingena
                    Sep 20, 2021 at 2:14
                    @MarcDingena You are right, we don't need !token if we have no dependency added. Restriction would be required when you use useEffect as componentDidUpdate.
    – Milind Agrawal
                    Sep 21, 2021 at 3:13
    

    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

    This should be the accepted answer since the previous will require useEffect to be async. The simplest way to encapsulate the asynchronous code is inside a async IIFE. – David Aug 6, 2021 at 10:33

    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)
            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;
    

    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

    Then you must be something wrong. I recreated a similar example, take a look: codesandbox.io/embed/quiet-wood-bbygk – Praneeth Paruchuri Sep 9, 2019 at 4:07 You were right. I just accepted your answer. I didn't inspect my response object carefully enough. – Yu Chen Sep 9, 2019 at 4:25

    Thanks for contributing an answer to Stack Overflow!

    • Please be sure to answer the question. Provide details and share your research!

    But avoid

    • Asking for help, clarification, or responding to other answers.
    • Making statements based on opinion; back them up with references or personal experience.

    To learn more, see our tips on writing great answers.