Search code examples
javascriptreactjsjsxoffice-addinsoutlook-web-addins

How to render dropdown based on an API call


I have a contacts variable that is set by calling an API. This is all done in an immediately invoked function expression (async () => {})();. I have to get a token to access the Microsoft Graph API. This lets me call an read the selected email using the Office.js library await getMessage(microsoftGraphAccessToken...).

I call my own API endpoint getContactsByEmail. The contacts variable contains data, but the contactDropdown does not show - it just remains as <></>.

const App = () => {
  let contacts = null;
  let contactDropdown = <></>;

  (async () => {
    let microsoftGraphAccessToken = null;
    let message = null;

    await Office.onReady();

    //Check if Graph is already authorized
    const graphTokenResponse = await graphAuth.getToken(graphLoginRequest);

    microsoftGraphAccessToken = graphTokenResponse.accessToken;

    await getMessage(microsoftGraphAccessToken, Office.context.mailbox.item.itemId, async (data) => {
      message = data;
    });

    const contacts = await getContactsByEmail(message.from.emailAddress.address);

    if (contacts.length > 0) {
      contactDropdown = (
        <select>
          {contacts.map((contact) => (
            <option key={contact.id} value={contact.id}>
              {contact.name}
            </option>
          ))}
        </select>
      );
    }
  })();

  return (
    <div>
      {contactDropdown}
    </div>
  );
}

Solution

  • In whatever React tutorial(s) you're using, you've overlooked the single most fundamental concept in React... state.

    The initial value for the variable contactDropdown is <></>. So that's what gets rendered in the component when it first renders. Then at a later time you update the value for that variable. However, directly updating a variable does not trigger React to re-render the component. Updating state does.

    Put the variable in state:

    const [contactDropdown, setContactDropdown] = useState(<></>);
    

    Then use the state setter when you need to update it at a later time:

    if (contacts.length > 0) {
      setContactDropdown(
        <select>
          {contacts.map((contact) => (
            <option key={contact.id} value={contact.id}>
              {contact.name}
            </option>
          ))}
        </select>
      );
    }
    

    Invoking that state setter function tells React to queue a re-render after updating the state value.


    As an aside... In general it's uncommon to store markup in state. Instead, you should store data in state and generate the markup in the rendering. What you have should work, but could easily run into problems if you ever need to do anything else with that data.

    For example, store the "contacts" in state:

    const [contacts, setContacts] = useState([]);
    

    And update it with your fetched data:

    const newContacts = await getContactsByEmail(message.from.emailAddress.address);
    setContacts(newContacts);
    

    Then use that data to render the markup in your component:

    return (
      <div>
        {contacts.length > 0 ? (
          <select>
            {contacts.map((contact) => (
              <option key={contact.id} value={contact.id}>
                {contact.name}
              </option>
            ))}
          </select>
        ) : <></>}
      </div>
    );
    

    Additionally, and this is important... I just realized your component will infinitely re-render. Don't make API calls like that directly in the component body. They'll be invoked on every render. And if that operation itself triggers a re-render, then every render will trigger a re-render.

    To perform an API call once when the component first renders, wrap it in useEffect with an empty dependency array:

    useEffect(() => {
      // your IIFE goes here
    }, []);