0

I'm very new to JavaScript/React in general and struggling with the concept of Promise and async.

First I have getSimById, an API call in a JS file which returns a Promise:

export function getSimById(simId) {
  return fetch(simsUrl + "/results/" + simId, {
    method: "GET",
    headers: new Headers({
      Authorization: "Basic " + base64.encode(login + ":" + password)
    })
  })
    .then(handleResponse)
    .catch(handleError);
}

And handleResponse is an async function.

export async function handleResponse(response) {
  if (response.ok) {
    let someResponse = response.json();
    return someResponse;
  }

  if (response.status === 400) {
    throw new Error(error);
  }

  const error = await response.text();
  throw new Error("Network response was not ok.");
}

Now I have a functional component which returns a Table:

import React, { useState, useEffect } from "react";
import { getSimById } from "../api/outrightSimulatorApi";

function SimulationReport(props) {

  const location = useLocation();
  const [simResult, setSimResult] = useState([]);

  useEffect(() => {
    getSimById(location.state.simId).then(result => setSimResult(result));
  }, []);

  let reformattedData = getSimById(location.state.simId).then(
    data => reformattedData = data?.markets?.length ? data.markets.reduce(
      (accumulator, market) =>
        market.selections.map(({ name, probability }, index) => ({
          ...accumulator[index],
          "Team name": name,
          [market.name]: probability,
        })),
      [],
    ) : null);

  return (
      <div>
          <Table striped bordered hover size="sm" responsive>
            <thead>
              <tr>{

              }
              </tr>
            </thead>
            <tbody>{

             }
            </tbody>
          </Table>
      </div>
  );

In this piece of code, I would like to map through reformattedData as an array and ultimately map through its values in the returned Table. However, reformattedData is not an array in this instance, and is actually a Promise. Because of this, whenever I try to access something like reformattedData[0] it actually returns undefined, and I am unable to map through its values in the Table. How do I assign the Promise to a variable in this case so I can perform operations on it?

clattenburg cake
  • 1,096
  • 3
  • 19
  • 40
  • Instead of calling `getSimById` for `reformattedData`, you need to map over the `simResult` array. Make sure to handle errors properly inside the `useEffect` by adding a `catch` after `then`. – goto Aug 18 '20 at 17:00
  • Does this answer your question? [How do I return the response from an asynchronous call?](https://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call) – Matt Morgan Aug 18 '20 at 17:00
  • Why are you calling `getSimById` in two different places? You should only be doing it within a `useEffect` callback, not inline in the component function. – T.J. Crowder Aug 18 '20 at 17:03

3 Answers3

1

You shouldn't be calling getSimById in two different places, it should only be in the useEffect callback, which should list location.state.simId as a dependency.

Something along these lines:

function SimulationReport(props) {

  const location = useLocation();
  const [simResult, setSimResult] = useState([]);

  useEffect(() => {
    getSimById(location.state.simId).then(data => {
        const reformattedData = data?.markets?.length ? data.markets.reduce(
          (accumulator, market) =>
            market.selections.map(({ name, probability }, index) => ({
              ...accumulator[index],
              "Team name": name,
              [market.name]: probability,
            })),
          [],
        ) : null;
        setSimResult(reformattedData); // *** Set state here
      })
      .catch(error => {
        // *** Handle/report error
      });
  }, [location.state.simId]); // *** Note the dependency

  return (
      <div>
          <Table striped bordered hover size="sm" responsive>
            <thead>
              <tr>{

              }
              </tr>
            </thead>
            <tbody>{
              // *** Use `simResult` when rendering
              simResult.map(entry => <markup>for entry</markup)
             }
            </tbody>
          </Table>
      </div>
  );
}

There's another wrinkle: You want to disregard the results you get asynchronously if your effect is run again before they arrive. To do that, you return a function from your useEffect callback so React can tell you when it happens, like this:

  useEffect(() => {
    let cancelled = false; // ***
    getSimById(location.state.simId).then(data => {
        if (cancelled) {
            // Don't use it
            return;
        }
        const reformattedData = data?.markets?.length ? data.markets.reduce(
          (accumulator, market) =>
            market.selections.map(({ name, probability }, index) => ({
              ...accumulator[index],
              "Team name": name,
              [market.name]: probability,
            })),
          [],
        ) : null;
        setSimResult(reformattedData);
      })
      .catch(error => {
        // Handle/report error
      });
      return () => {        // *** A callback React will use when the effect runs again
        cancelled = true;   // *** Remember that *this* call has been cancelled
      };
  }, [location.state.simId]);

This article by Dan Abramov provides some excellent information about hooks in general and useEffect in particular.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Sorry, had a bit of a misplaced `}` or something in there, and missed out the `.catch`. Hit refresh if you don't see `.catch`. – T.J. Crowder Aug 18 '20 at 17:10
  • Can you explain what `cancelled = true` does after component is unmounted? In my opinion is useless because component is going to me unmounted and unless you were removing events from DOM or something like this, is wrong, and maybe be drive another one to confussion. – Adolfo Onrubia Aug 18 '20 at 17:16
  • @AdolfoOnrubia - It's not (just) about the component being *unmounted*, it's about the effect running again (for instance, if `location.stat.simId` changes). (Although that said, it's also important for the unmounted case -- otherwise, you get errors about "setting state on an unmounted component.") See the article I linked for details. It's very much not useless. :-) – T.J. Crowder Aug 18 '20 at 17:20
  • Sorry, but completly disagree with it, can't get the point. Pls check this: https://reactjs.org/docs/hooks-effect.html#explanation-why-effects-run-on-each-update returning a function in useEffect will `cleanup`, component is removed from DOM – Adolfo Onrubia Aug 18 '20 at 17:25
  • @AdolfoOnrubia - All due respect, it's not a matter of opinion. :-) Please, again, read the article by Dan Abramov (if you don't know the name, he's a major, major name in React.) Here's an example of the unmount scenario: https://codesandbox.io/s/setting-state-on-unmounted-component-m3h9s?file=/src/App.js Here it is with the fix: https://codesandbox.io/s/setting-state-on-unmounted-component-forked-88poi?file=/src/App.js – T.J. Crowder Aug 18 '20 at 17:33
  • @AdolfoOnrubia *"returning a function in useEffect will cleanup, component is removed from DOM"* That's **one** time it'll happen. Another is if any of the effect's dependencies changes, making the previous async request's results outdated and something to ignore (in favor of the new results). – T.J. Crowder Aug 18 '20 at 17:34
  • Ok, I know who is he, and all his job with the ecosystem, and will check that, now, but can you pls answer this question? what is the value of `cancelled` and where it lives after this component is unmounted? – Adolfo Onrubia Aug 18 '20 at 17:35
  • 1
    Thanks for all the explanations but I'm at the same point, just updated your codesandbox https://codesandbox.io/s/setting-state-on-unmounted-component-forked-o0ym9?file=/src/App.js:184-366. P.D. just trying to figure out if i missed something, thx. – Adolfo Onrubia Aug 18 '20 at 17:41
  • @AdolfoOnrubia - I may not quite understand what you're asking, so apologies if I misunderstand, but: `cancelled` is a variable in the lexical environment created when `useEffect` calls the callback. Since both the async completion handler the function we return close over that environment, `cancelled` lives on until both of those functions are released. Let's assume the component is unmounted before the ajax completion: React will call the function, which sets `cancelled` to `true`, then React drops its reference to the function. Now there's just the ajax completion handler... – T.J. Crowder Aug 18 '20 at 17:43
  • ...that has a reference to the environment containing `cancelled`. When that handler is called, the component is already unmounted, so we must not try to set state on it. Thanks to the variable, we don't, and the completion handler returns. Now that handler is released by the ajax stuff, which means the environment containing `cancelled` is reclaimed, and `cancelled` ceases to exist. – T.J. Crowder Aug 18 '20 at 17:43
  • @AdolfoOnrubia - Re your updated codesandbox: Yes, absolutely, if you have something you can reliably cancel instead, that's another approach you can take. Some ajax libs provide cancellation, and eventually [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) will be mature enough to rely on for `fetch` and the like. So that works too, **if** you're doing something you can cancel that way. – T.J. Crowder Aug 18 '20 at 17:45
  • I think got your point, but is really needed? is it clear to implement or for others to understand? I think it doesn't. Just trying to clarify that this is not, (IMO) the way to do it. – Adolfo Onrubia Aug 18 '20 at 17:46
  • @AdolfoOnrubia - Again: If you can cancel something, great, that's another way to go. If you can't, this is how you handle it. I don't think it's at all unclear, and it's certainly a use case the React team expect and demonstrate. – T.J. Crowder Aug 18 '20 at 17:48
  • 1
    Thanks for your comments T.J. Crowder, very appreciated. – Adolfo Onrubia Aug 18 '20 at 17:49
  • @AdolfoOnrubia - Thank you for pointing out the approach you can take when things can be cancelled. :-) – T.J. Crowder Aug 18 '20 at 17:50
  • 1
    And now I know how to do it when things are not cancelables :-), BIG THANKS – Adolfo Onrubia Aug 18 '20 at 18:04
1

Ok so your api call is working as expect and you receive in

useEffect(() => {
    getSimById(location.state.simId).then(result => setSimResult(result));
  }, []);

which can be simplified like this parsing data at same time

getSimById(location.state.simId).then(parseSimResult);

But your problem is with let here.

A possible solution could be:

Out of the component (maybe utils) ?

export const parseSimResults = (simResults) => {
  return simResults.markets.reduce(
      (accumulator, market) =>
        market.selections.map(({ name, probability }, index) => ({
          ...accumulator[index],
          "Team name": name,
          [market.name]: probability,
        })),
      [],
    )
}; 

Then just in render map throw simResults in your component render

<thead>
  {simResult && simResults.map(r => {
    <tr key="someKEY">
         {
          ...
         }
    </tr>
  })}
</thead>

Resulting full code

const parseSimResults = (simResults) => {
  return simResults.markets.reduce(
      (accumulator, market) =>
        market.selections.map(({ name, probability }, index) => ({
          ...accumulator[index],
          "Team name": name,
          [market.name]: probability,
        })),
      [],
    )
}; 

const MyComponent.... {
  const [simResults, setSimResults] = useState([]);

  useEffect(() => {
    getSimById(location.state.simId).then(parseSimResults);
  }, []);

  return simResults.map(r => <your JSX>)
}
Adolfo Onrubia
  • 1,781
  • 13
  • 22
0

In your useEffect, you are already calling getSimById() and storing the result, so there's no need to call it again right after.

Instead, try iterating over the simResult array. That should have the value that you're wanting to reference.

Kevin Hoopes
  • 477
  • 2
  • 8
  • Thanks Kevin, but won’t this still return a Promise? – clattenburg cake Aug 18 '20 at 17:07
  • 1
    @clattenburgcake - No, not given where you're using `setSimResult`. But I think you probably do want the reformatting logic you wrote; you just have to relocate it. – T.J. Crowder Aug 18 '20 at 17:11
  • @clattenburgcake It shouldn't, but another way to clarify this code would be to stick to either async/await syntax or .then syntax.They are generally interchangeable, and I believe it would be much easier to keep track of where Promises are coming from. – Kevin Hoopes Aug 18 '20 at 17:16