React从API调用设置状态遇异步问题:无法渲染账户列表
我搞不清问题出在哪,感觉碰到了竞态条件但找不到原因。我尝试在useEffect里从API加载一组"accounts"数据,请求完成后调用setAccounts()设置状态。网络面板显示API响应正常,但渲染时用状态变量accounts却显示不出内容。
相关代码片段(idToken来自Auth0):
const [accounts, setAccounts] = useState(); const fetchAccounts = async () => { const url = "http://0.0.0.0:8000/api/v1/account/"; const response = await fetch(url, { method: "get", headers: new Headers({ "Content-Type": "application/json", Authorization: "Bearer " + idToken, }), }).then((response) => response.json()); console.log("debug", response); setAccounts(response); }; useEffect(() => { const getToken = async () => { if (idToken == null) { const id_token = await getIdTokenClaims(); setIdToken(id_token.__raw); } }; getToken(); if (idToken) { console.log("ID token check", idToken); fetchAccounts(); getClientPortfolios(); } console.log("accounts", accounts); // 这里打印不出内容 }, [idToken]); return ( <div> {accounts.map((account) => { <div key={account.id}>{account.name}</div>; })} </div> )
目前的问题:状态变量似乎没被设置,而且map函数抛出错误:Uncaught TypeError: Cannot read properties of undefined (reading 'map')
我的实现是不是有问题?
注:fetchAccounts中的debug语句输出正常,能看到返回的accounts数据
1. 初始状态未设置为空数组
useState()默认初始值是undefined,组件首次渲染时accounts是undefined,调用map必然报错。把初始状态设为空数组即可避免这个问题:
const [accounts, setAccounts] = useState([]);
2. useEffect异步逻辑执行时机错误
getToken是异步函数,但你直接调用后没有等待它完成就判断idToken。第一次执行useEffect时idToken还是null,不会进入请求分支;等getToken完成调用setIdToken后,useEffect才会再次触发,此时idToken有值才会调用接口。优化逻辑确保token获取完成后再请求数据:
useEffect(() => { const loadData = async () => { let currentIdToken = idToken; if (!currentIdToken) { const id_token = await getIdTokenClaims(); currentIdToken = id_token.__raw; setIdToken(currentIdToken); } if (currentIdToken) { console.log("ID token check", currentIdToken); await fetchAccounts(); getClientPortfolios(); } }; loadData(); // 注:这里打印accounts永远是旧值,因为setState是异步的,状态更新不会同步反映在当前闭包中 }, [idToken]);
3. map回调未返回JSX元素
你的map回调用了大括号但没有显式返回元素,导致渲染时无内容。两种修正方式:
// 方式1:用小括号省略return {accounts.map((account) => ( <div key={account.id}>{account.name}</div> ))} // 方式2:显式return {accounts.map((account) => { return <div key={account.id}>{account.name}</div>; })}
4. setState异步性导致console.log无更新
setAccounts是异步操作,调用后不会立刻更新accounts状态,所以useEffect里的console.log("accounts", accounts)打印的是当前闭包中的旧值,这是正常现象,无需纠结。
const [accounts, setAccounts] = useState([]); const fetchAccounts = async () => { const url = "http://0.0.0.0:8000/api/v1/account/"; try { const response = await fetch(url, { method: "get", headers: new Headers({ "Content-Type": "application/json", Authorization: "Bearer " + idToken, }), }); const data = await response.json(); console.log("debug", data); setAccounts(data); } catch (err) { console.error("Failed to fetch accounts:", err); } }; useEffect(() => { const loadData = async () => { let currentIdToken = idToken; if (!currentIdToken) { const id_token = await getIdTokenClaims(); currentIdToken = id_token.__raw; setIdToken(currentIdToken); } if (currentIdToken) { console.log("ID token check", currentIdToken); await fetchAccounts(); getClientPortfolios(); } }; loadData(); }, [idToken]); return ( <div> {accounts.map((account) => ( <div key={account.id}>{account.name}</div> ))} </div> );
内容的提问来源于stack exchange,提问作者Lauren Boland

