如何在 axios 调用后使用 useReducer 使反应组件重新呈现?

How to get react component with useReducer to rerender after axios call?

我正在尝试使用 useReducer 钩子学习状态管理,因此我构建了一个调用 pokeAPI 的简单应用程序。该应用程序应显示一个随机的口袋妖怪,并在按下 'capture another' 按钮时向屏幕添加更多口袋妖怪。

但是,它会在从 axios 调用填充 Card 之前使用初始化的空 Card 对象重新呈现组件。我已经根据 Whosebug 的帖子尝试了至少 3 种不同的解决方案。

在每次尝试中我都得到了相同的结果:应用程序显示未定义的卡片,即使状态已更新而不是未定义,它只是在重新渲染后稍微更新。再次单击时,之前的 undefined 会正确呈现,但现在有一张新卡片显示为 undefined。

我仍然掌握 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 异步并等待两个调用,然后 return 您想要的结果。 (此外,您正在 returning newCard 但未在通话中为其分配任何内容)。首先通过 return 在像我的示例代码这样的承诺中使用虚假数据来测试它,然后如果遇到问题则集成 api。

在您的承诺中,它return是一张您可以直接在派送中使用的卡片(从响应中可以看出,您不需要额外的步骤)。您的 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>
  );
};