Sean
Sean

Reputation: 33

Reload react component after updating state inside useEffect

I have a react component, I am updating a state in Mobx, and would like when this state is updated to reload the same component. I have a list of agencies, and I get the agency by type using query parameters within one component and based on the query parameter I want to get filtered data from DB.

My component code looks like this:

export interface IListAgenciesProps {
  agencyType: string;
  agencyTypeTitle: string;
}

const ListAgencies = (props: any) => {
  let ctx = useContext(imsStore);
  let agencies: IAgency[] = [];
  useEffect(() => {
    const agencyType = props.match.params["AgencyType"];

    ctx.setAgency(agencyType);
    async function agencyLoad(){
      await ctx.loadAgencies();
      agencies = ctx.agencies;
    }
    agencyLoad();
  }, 
  [agencies]);

  let html: JSX.Element;
  if (agencies.length > 0) {
    html = ( <Container>
        <Table celled selectable>
          <Table.Header>
            <Table.Row>
              <Table.HeaderCell>Agency Name</Table.HeaderCell>
              <Table.HeaderCell>Telephone</Table.HeaderCell>
              <Table.HeaderCell>Email</Table.HeaderCell>
              <Table.HeaderCell>Is Active?</Table.HeaderCell>
              <Table.HeaderCell>Hourly Rate</Table.HeaderCell>
            </Table.Row>
          </Table.Header>
          <Table.Body>
            {agencies.map(a => {
              return (
                <Table.Row>
                  <Table.Cell>{a.name}</Table.Cell>
                  <Table.Cell>{a.telephone}</Table.Cell>
                  <Table.Cell>{a.email}</Table.Cell>
                  <Table.Cell>{a.isActive}</Table.Cell>
                  <Table.Cell>{a.pricePerHour}</Table.Cell>
                </Table.Row>
              );
            })}
          </Table.Body>
        </Table>
      </Container>
    );
  } else {
    html = ( <Container>
        <Header as='h1'>
          Sorry, we couldn't find the agency type you're looking for.
        </Header>
      </Container>
    );
  }

  return <>{html}</>;
};

export default observer(ListAgencies);

In my IMSStore.ts, it looks like this:

@observable agencies: IAgency[] = [];
@action loadAgencies = async () => {
    this.agencies = [];
    try {
      const agencies = await agent.Agencies.list(this.agencyType);
      agencies.forEach(agency => {
        this.agencies.push(agency);
      });
    } catch (err) {
      console.log(err);
    } finally {
    }
  };

However, the agencies array in my first code snippet, is always empty when the return statement is executed. So everytime the useEffect runs, it calls the loadAgencies, and in loadAgencies it resets the value of the agencies to an empty array. Then the return statement is executed. How do I make the return statement to wait until the agencies array are actually filled?

Upvotes: 0

Views: 1259

Answers (1)

ChronoLink
ChronoLink

Reputation: 388

Ah, ok if you wanna keep it relatively similar, let me suggest a possible simplification for ya:

export interface IListAgenciesProps {
  agencyType: string;
  agencyTypeTitle: string;
}

const ListAgencies = (props: any) => {
  const agencyType = props.match.params["AgencyType"];
  const ctx = useContext(imsStore);
  const { agencies, loadAgencies } = ctx;

  useEffect(() => {
    loadAgencies(agencyType);
  }, [agencyType]);

  let html: JSX.Element;
  if (agencies.length > 0) {
    html = ( <Container>
        <Table celled selectable>
          <Table.Header>
            <Table.Row>
              <Table.HeaderCell>Agency Name</Table.HeaderCell>
              <Table.HeaderCell>Telephone</Table.HeaderCell>
              <Table.HeaderCell>Email</Table.HeaderCell>
              <Table.HeaderCell>Is Active?</Table.HeaderCell>
              <Table.HeaderCell>Hourly Rate</Table.HeaderCell>
            </Table.Row>
          </Table.Header>
          <Table.Body>
            {agencies.map(a => {
              return (
                <Table.Row>
                  <Table.Cell>{a.name}</Table.Cell>
                  <Table.Cell>{a.telephone}</Table.Cell>
                  <Table.Cell>{a.email}</Table.Cell>
                  <Table.Cell>{a.isActive}</Table.Cell>
                  <Table.Cell>{a.pricePerHour}</Table.Cell>
                </Table.Row>
              );
            })}
          </Table.Body>
        </Table>
      </Container>
    );
  } else {
    html = ( <Container>
        <Header as='h1'>
          Sorry, we couldn't find the agency type you're looking for.
        </Header>
      </Container>
    );
  }

  return <>{html}</>;
};

export default observer(ListAgencies);

Store:

@observable agencies: IAgency[] = [];
@action loadAgencies = async (agencyType) => {
    try {
      this.agencies = await agent.Agencies.list(agencyType);
    } catch (err) {
      console.log(err);
    } finally {
    }
  };

So what's happening now is we're simplifying the component so it saves the agencyType on mount. Then specifying it in the deps array for the effect means that we call loadAgencies everytime the params change. Notice we're now calling the ctx method with a parameter. So loadAgencies is gonna take in that type, go fetch the data, and then set it to the context variable all inside the same @action.

Then, since we've dereferenced agencies from ctx inside the component, it will re-render when the agencies @observable is updated. We don't need to make the call inside the effect async - the component will re-render whenever the data comes back.

Upvotes: 1

Related Questions