简体   繁体   中英

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

I am trying to learn state management with the useReducer hook so I have built a simple app that calls the pokeAPI. The app should display a random pokemon, and add more pokemons to the screen as the 'capture another' button is pressed.

However, it rerenders the component with the initialized and empty Card object before populating the Card from the axios call. I've tried at least 3 different solutions based on posts from stackoverflow.

In each attempt I have gotten the same result: the app displays an undefined card on, even though the state is updated and not undefined, it just was updated slightly after the rerendering. When clicked again that prior undefined gets properly rendered but there is now a new card displayed as undefined.

I am still getting the hang of react hooks (no pun intended!), async programming, and JS in general.

Here is the app: https://stackblitz.com/edit/react-ts-mswxjv?file=index.tsx

Here is the code from my first try:

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

I also experimented with making the function that calls Axios a promise so I could chain the dispatch call with a .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>
  );
};

I also tried to have it call it with a side effect using useEffect hook

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

So there are a couple of things with your code, but the last version is closest to being correct. Generally you want promise calls inside useEffect. If you want it to be called once, use an empty [] dependency array. https://reactjs.org/docs/hooks-effect.html (ctrl+f "once" and read the note, it's not that visible). Anytime the dep array changes, the code will be run.

Note: you'll have to change the calls to the Pokemon service as you're running two async calls without awaiting either of them. You need to make getRandomPokemon async and await both calls, then return the result you want. (Also you're returning newCard but not assigning anything to it in the call). First test this by returning a fake data in a promise like my sample code then integrate the api if you're having issues.

In your promise, it returns a Card which you can use directly in the dispatch (from the response, you don't need the extra step). Your onclick is also incorrectly written with the brackets. Here's some sample code that I've written and seems to work (with placeholder functions):

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM