skip to Main Content

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

2

Answers


  1. Chosen as BEST ANSWER

    Thanks for the help @David. I moved contacts to a state variable. I converted my IIFE to a function and I am now using that inside useEffect.

    useEffect(() => {
      initialize();
    })();
    
    const initialize = () => {
        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>
          );
        }
    };
    

    The issue I was having is I was rendering an element from a variable: let contactDropdown = <></>; but I am now directly rendering it.

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

    I was wondering if there was a reason for this though? Maybe I am misunderstanding the point in time when contactDropdown is being rendered.


  2. 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
    }, []);
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search