在 React 函数组件中使用 sync/wait

我刚刚开始在一个项目中使用 React,并且正在努力将异步/等待功能整合到我的一个组件中。

我有一个名为 fetchKey的异步函数,它从我通过 AWS API 网关服务的 API 获取访问密钥:

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

我正在使用 React 的资料用户界面主题,并希望利用它的仪表板模板之一。遗憾的是,Dashboard 模板使用了一个功能性的无状态组件:

const Dashboard = props => {
const classes = useStyles();


const token = fetchKey(props.auth);
console.log(token);


return (
... rest of the functional component's code

我的 console.log(token)的结果是一个承诺,这是意料之中的,但我的谷歌浏览器的截图有点矛盾——它是未决的,还是已经解决了? enter image description here

其次,如果我尝试取代 token.then((data, error)=> console.log(data, error)),我得到两个变量的 undefined。在我看来,这似乎表明函数尚未完成,因此还没有解析 dataerror的任何值。然而,如果我试图将一个

const Dashboard = async props => {
const classes = useStyles();


const token = await fetchKey(props.auth);

反应强烈地抱怨道:

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

现在,我将首先声明,我没有足够的经验来理解这个错误消息是怎么回事。如果这是一个传统的 React 类组件,我将使用 this.setState方法设置一些状态,然后继续我的快乐之旅。但是,在这个函数组件中没有这个选项。

如何将异步/等待逻辑合并到功能性 React 组件中?

编辑: 所以我只能说我是个白痴。返回的实际响应对象不是 response.data.access_token。是 response.data.Item.access_token。噢!这就是为什么结果被返回为未定义的,即使实际的承诺得到了解决。

193121 次浏览
const token = fetchKey(props.auth);

这返回了一个承诺。要从中获取数据,这是一种方法:

let token = null;
fetchKey(props.auth).then(result => {
console.log(result)
token = result;
}).catch(e => {
console.log(e)
})

如果成功了告诉我。

我重新创建了一个类似的例子: https://codesandbox.io/embed/quiet-wood-bbygk

使用 React Hooks,您现在可以实现与函数组件中的 Class 组件相同的功能。

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

再看看这个: 在反应组件中使用异步等待

你必须确保两件事

  • useEffect类似于 componentDidMountcomponentDidUpdate,所以如果你在这里使用 setState,那么你需要限制代码执行在某些时候作为 componentDidUpdate使用,如下所示:
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
);
}

组件可能在解析 fetchKey之前用不同的 props.auth卸载或重新渲染:

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

另一种选择是使用 悬念错误边界:

// 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;
}
}