0

I have a react hook style component in typescript. I'm using office uifabric as ui framework. I want to get the following pattern to work in a "best practice" manner:

  1. I have a component with an onClick event (in my case a CommandBar)
  2. User clicks on the action
  3. I make an async call to a backend api (the async part is whats causing me trouble here I think, and this is unfortunately a requirement).
  4. When the async call is complete, or fails. I want to show a MessageBar with information.
  5. All in all using as little code as possible, react hooks seems to have the ability to produce nice and condence code together with TypeScript. But I'm new to both things so I may be out on a limb here...

Creating a dummy fetch method that is NOT async causes my example code to work as expected. But when I have the async call, something gets lost and I do not get a re-render and visibility of the MessageBar. The api call is made though!

const UserProfile = () => {
  const [apiStatusCode, setApiResponseCode] = useState(0);
  const [showMessageBar, setShowMessageBar] = useState(false);

  const startAsync = () => {
    // With await here I get a compiler
    // error. Without await, the api call is made but 
    // the messagebar is never rendered. If the call is not async, things
    // work fine.
    myAsyncMethod();
  };
  const toggleMessageBar = (): (() => void) => (): void => setShowMessageBar(!showMessageBar);

  const myAsyncMethod = async () => {
    try {
      const responseData = (await get('some/data'))!.status;
      setApiResponseCode(responseData);
      toggleMessageBar();
    } catch (error) {
      console.error(error);
      setApiResponseCode(-1);
      toggleMessageBar();
    }
  };

  const getItems = () => {
    return [
      {
        key: 'button1',
        name: 'Do something',
        cacheKey: 'button1', //
        onClick: () => startAsync()
      }
    ];
  };

  return (
    <Stack>

      <Stack.Item>
        <CommandBar
          items={getItems()}          
        />
      </Stack.Item>

      {showMessageBar &&
      <MessageBar messageBarType={MessageBarType.success} onDismiss={toggleMessageBar()} dismissButtonAriaLabel="Close">
        Successfully retrieved info with status code: {apiStatusCode}
      </MessageBar>
      }

      // Other ui parts go here        
    </Stack>
  )
};


export default UserProfile;

Assigning an async method to the onClick event gives compiler error: Type 'Promise<void>' is not assignable to type 'void'. TS2322

Not awaiting the async call causes react not re-rendering when call is complete.

2 Answers2

0

A common pattern for this sort of thing it to update your state in the callback as you are doing, and then respond to the new data with useEffect:

useEffect(()=>{
    setShowMessageBar(!showMessageBar)
},[apiStatusCode, showMessageBar])

const myAsyncMethod = async () => {
    try {
      const responseData = (await get('some/data'))!.status;
      setApiResponseCode(responseData);
      // toggleMessageBar(); <-- delete me
    } catch (error) {
      console.error(error);
      setApiResponseCode(-1);
      // toggleMessageBar(); <-- delete me
    }
  };
Will Jenkins
  • 9,507
  • 1
  • 27
  • 46
  • 1
    Voted your answer as the right, you beat me with a minute! But how would you trigger the async method from the onclick event with your solution? The handler doesnt seem to support async methods. Thank you for your help! – André Johansson Jul 02 '19 at 09:27
0

Found the answer eventually, the key was to use reacts useEffect mechanism. This answer led me to the solution:

Executing async code on update of state with react-hooks

Key changes in my code:

Create a state to track execution

const [doAsyncCall, setDoAsyncCall] = useState(false);

Set the state in the onClick event:

  const getItems = () => {
    return [
      {
        key: 'button1',
        name: 'Do something',
        cacheKey: 'button1', //
        onClick: () => setDoAsyncCall(true)
      }
    ];
  };

Wrap the async functionality in a useEffect section that depends on the state:

  useEffect(() => {
    async function myAsyncMethod () {
      try {
        const responseData = (await get('some/data'))!.status;
        setApiResponseCode(responseData);
        setShowMessageBar(true);
      } catch (error) {
        console.error(error);
        setApiResponseCode(-1);
        setShowMessageBar(true);
      }
    }

    if (doAsyncCall) {
      myAsyncMethod();
    }
  }, [doAsyncCall]);