javascript - 如何在 axios 调用后使用 useReducer 获取反应组件以重新渲染?
问题描述
我正在尝试使用 useReducer 钩子学习状态管理,因此我构建了一个调用 pokeAPI 的简单应用程序。该应用程序应显示一个随机的口袋妖怪,并在按下“捕获另一个”按钮时将更多口袋妖怪添加到屏幕上。
但是,在从 axios 调用填充 Card 之前,它会使用已初始化的空 Card 对象重新渲染组件。我已经根据 stackoverflow 的帖子尝试了至少 3 种不同的解决方案。
在每次尝试中,我都得到了相同的结果:应用程序显示一个未定义的卡片,即使状态已更新而不是未定义,它只是在重新渲染后略微更新。再次单击时,先前的未定义会正确呈现,但现在有一张新卡显示为未定义。
我仍然掌握 React Hooks(不是双关语!)、异步编程和一般的 JS 的窍门。
这是应用程序: https ://stackblitz.com/edit/react-ts-mswxjv?file=index.tsx
这是我第一次尝试的代码:
//index.tsx
const getRandomPokemon = (): Card => {
var randomInt: string;
randomInt = String(Math.floor(898 * Math.random()));
let newCard: Card = {};
PokemonDataService.getCard(randomInt)
.then((response) => {
//omitted for brevity
})
.catch((error) => {
//omitted
});
PokemonDataService.getSpecies(randomInt)
.then((response) => {
//omitted
})
.catch((error) => {
//omitted
});
return newCard;
};
const App = (props: AppProps) => {
const [deck, dispatch] = useReducer(cardReducer, initialState);
function addCard() {
let newCard: Card = getRandomPokemon();
dispatch({
type: ActionKind.Add,
payload: newCard,
});
}
return (
<div>
<Deck deck={deck} />
<CatchButton onClick={addCard}>Catch Another</CatchButton>
</div>
);
};
//cardReducer.tsx
export function cardReducer(state: Card[], action: Action): Card[] {
switch (action.type) {
case ActionKind.Add: {
let clonedState: Card[] = state.map((item) => {
return { ...item };
});
clonedState = [...clonedState, action.payload];
return clonedState;
}
default: {
let clonedState: Card[] = state.map((item) => {
return { ...item };
});
return clonedState;
}
}
}
//Deck.tsx
//PokeDeck and PokeCard are styled-components for a ul and li
export const Deck = ({ deck }: DeckProps) => {
useEffect(() => {
console.log(`useEffect called in Deck`);
}, deck);
return (
<PokeDeck>
{deck.map((card) => (
<PokeCard>
<img src={card.image} alt={`image of ${card.name}`} />
<h2>{card.name}</h2>
</PokeCard>
))}
</PokeDeck>
);
};
我还尝试让调用 Axios 的函数成为一个 Promise,这样我就可以用 .then 链接调度调用。
//index.tsx
function pokemonPromise(): Promise<Card> {
var randomInt: string;
randomInt = String(Math.floor(898 * Math.random()));
let newCard: Card = {};
PokemonDataService.getCard(randomInt)
.then((response) => {
// omitted
})
.catch((error) => {
return new Promise((reject) => {
reject(new Error('pokeAPI call died'));
});
});
PokemonDataService.getSpecies(randomInt)
.then((response) => {
// omitted
})
.catch((error) => {
return new Promise((reject) => {
reject(new Error('pokeAPI call died'));
});
});
return new Promise((resolve) => {
resolve(newCard);
});
}
const App = (props: AppProps) => {
const [deck, dispatch] = useReducer(cardReducer, initialState);
function asyncAdd() {
let newCard: Card;
pokemonPromise()
.then((response) => {
newCard = response;
console.log(newCard);
})
.then(() => {
dispatch({
type: ActionKind.Add,
payload: newCard,
});
})
.catch((err) => {
console.log(`asyncAdd failed with the error \n ${err}`);
});
}
return (
<div>
<Deck deck={deck} />
<CatchButton onClick={asyncAdd}>Catch Another</CatchButton>
</div>
);
};
我还尝试使用 useEffect 钩子让它调用它并产生副作用
//App.tsx
const App = (props: AppProps) => {
const [deck, dispatch] = useReducer(cardReducer, initialState);
const [catchCount, setCatchCount] = useState(0);
useEffect(() => {
let newCard: Card;
pokemonPromise()
.then((response) => {
newCard = response;
})
.then(() => {
dispatch({
type: ActionKind.Add,
payload: newCard,
});
})
.catch((err) => {
console.log(`asyncAdd failed with the error \n ${err}`);
});
}, [catchCount]);
return (
<div>
<Deck deck={deck} />
<CatchButton onClick={()=>{setCatchCount(catchCount + 1)}>Catch Another</CatchButton>
</div>
);
};
解决方案
所以你的代码有几件事,但最后一个版本最接近正确。通常,您希望在 useEffect 中调用 promise。如果您希望它被调用一次,请使用一个空的 [] 依赖数组。https://reactjs.org/docs/hooks-effect.html(ctrl+f “一次”并阅读注释,它不那么可见)。每当 dep 数组更改时,代码都会运行。
注意:您必须更改对 Pokemon 服务的调用,因为您正在运行两个异步调用而不等待其中任何一个。您需要进行getRandomPokemon
异步并等待两个调用,然后返回您想要的结果。(此外,您正在返回 newCard 但未在通话中为其分配任何内容)。首先通过在我的示例代码之类的承诺中返回虚假数据来测试这一点,然后在遇到问题时集成 api。
在您的承诺中,它返回一张卡片,您可以直接在调度中使用它(从响应中,您不需要额外的步骤)。您的 onclick 也错误地用括号写入。这是我编写的一些示例代码,似乎可以工作(使用占位符函数):
type Card = { no: number };
function someDataFetch(): Promise<void> {
return new Promise((resolve) => setTimeout(() => resolve(), 1000));
}
async function pokemonPromise(count: number): Promise<Card> {
await someDataFetch();
console.log("done first fetch");
await someDataFetch();
console.log("done second fetch");
return new Promise((resolve) =>
setTimeout(() => resolve({ no: count }), 1000)
);
}
const initialState = { name: "pikachu" };
const cardReducer = (
state: typeof initialState,
action: { type: string; payload: Card }
) => {
return { ...state, name: `pikachu:${action.payload.no}` };
};
//App.tsx
const App = () => {
const [deck, dispatch] = useReducer(cardReducer, initialState);
const [catchCount, setCatchCount] = useState(0);
useEffect(() => {
pokemonPromise(catchCount)
.then((newCard) => {
dispatch({
type: "ActionKind.Add",
payload: newCard
});
})
.catch((err) => {
console.log(`asyncAdd failed with the error \n ${err}`);
});
}, [catchCount]);
return (
<div>
{deck.name}
<button onClick={() => setCatchCount(catchCount + 1)}>
Catch Another
</button>
</div>
);
};
推荐阅读
- android - 删除短信和阅读通话记录权限后,我仍然收到 Google 警告
- angular - How to unit test 'navigate' with Query Params in Angular?
- rx-java2 - Flowable.merge() 的并发保证是什么
- java - Is this thread safe and efficient in java
- eloquent - 不触及某些字段的时间戳
- javascript - using spread operator in jsx expression
- python - Accessing timestamp part in data
- symfony - How to make LoggerInterface service public in symfony 4
- file - Read and write from txt in Verilog
- r - Left join using R/dplyr with join based on calculation