我刚刚开始将 React 用于一个项目,并且正在努力将 async/await 功能整合到我的一个组件中。
我有一个名为 fetchKey
的异步函数,它从我通过 AWS API Gateway 提供服务的 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 的 Material UI 主题,并希望使用它的 Dashboard 模板之一。不幸的是,仪表板模板使用了功能性无状态组件:
const Dashboard = props => {
const classes = useStyles();
const token = fetchKey(props.auth);
console.log(token);
return (
... rest of the functional component's code
https://i.stack.imgur.com/DVCD2.png
其次,如果我改用 token.then((data, error)=> console.log(data, error))
,则两个变量都会得到 undefined
。这似乎表明该函数尚未完成,因此尚未解析 data
或 error
的任何值。然而,如果我尝试放置一个
const Dashboard = async props => {
const classes = useStyles();
const token = await fetchKey(props.auth);
React 强烈抱怨:
> 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
方法设置一些状态,然后继续我的快乐方式。但是,我在此功能组件中没有该选项。
如何将 async/await 逻辑合并到我的功能性 React 组件中?
编辑:所以我只会说我是个白痴。返回的实际响应对象不是 response.data.access_token
。是response.data.Item.access_token
。嗬!这就是为什么结果被返回为未定义的原因,即使实际的承诺已经解决。
is it pending, or is it resolved
当您在控制台中检查它时它已解决 - 这并不意味着它在您记录它时已解决......控制台“撒谎”:p
token.then((data, error)
... .then
的回调函数(对于实际的 Promise/A+ 规范 Promise)将只有一个参数 - 其他“类似 Promise”的承诺(例如 jQuery)打破了这个规范 - 如果 data
是undefined
表示 response.data.access_token
是 undefined
return response.data.access_token;
不是未定义的?
你必须确保两件事
useEffect 类似于 componentDidMount 和 componentDidUpdate,所以如果你在这里使用 setState,那么你需要在用作 componentDidUpdate 时限制代码执行,如下所示:
function Dashboard() {
const [token, setToken] = useState('');
useEffect(() => {
// You need to restrict it at some point
// This is just dummy code and should be replaced by actual
if (!token) {
getToken();
}
}, []);
const getToken = async () => {
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);
};
return (
... rest of the functional component's code
);
}
使用 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
也看看这个:Using Async await in react component
在解决 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
另一种方法是使用 Suspense 和 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;
}
}
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
then(result => ...)
中的 result
是未定义的。
getToken = async () => {
帮助我谢谢useEffect
缺少token
和getToken
作为依赖项。 2) 由于getToken
是异步的,它可以在组件卸载后调用setToken
。[]
来完成 2) 是的,这是可以通过检查屏幕是否可见来处理的有效场景。if (!token)
来限制它呢?如果token
发生变化,它会再次触发副作用,这就是您要防范的原因,但这也意味着token
确实是副作用的依赖项。!token
。将useEffect
用作componentDidUpdate
时需要限制。