1

Background

I have recently upgraded a fairly sizeable React app to React 18 and for the most part it has been great. One of the key changes is the new double mount in development causing useEffect hooks to all run twice, this is clearly documented in their docs.

I have read their new effect documentation https://beta.reactjs.org/learn/lifecycle-of-reactive-effects and although it is quite detailed there is a use case I believe I have found which is not very well covered.

The issue

Essentially the issue I have run into is I am implementing OAuth integration with a third-party product.
The flow:
-> User clicks create integration
-> Redirect to product login
-> Gets redirected back to our app with authorisation code
-> We hit our API to finalise the integration (HTTP POST request)

The problem comes now that the useEffect hook runs twice it means that we would hit this last POST request twice, first one would succeed and the second would fail because the integration is already setup.

This is not potentially a major issue but the user would see an error message even though the request worked and just feels like a bad pattern.

Considered solutions

Refactoring to use a button

I could potentially get the user to click a button on the redirect URL after they have logged into the third-party product. This would work and seems to be what the React guides recommend (Although different use case they suggested - https://beta.reactjs.org/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers).

The problem with this is that the user has already clicked a button to create the integration so it feels like a worse user experience.

Ignore the duplicate API call

This issue is only a problem in development however it is still a bit annoying and feels like an issue I want to explore further

Code setup

I have simplified the code for this example but hopefully this gives a rough idea of how the intended code is meant to function.

const IntegrationRedirect: React.FC = () => {
  const navigate = useNavigate();
  const organisationIntegrationsService = useOrganisationIntegrationsService();

  // Make call on the mount of this component
  useEffect(() => {
    // Call the method
    handleCreateIntegration();
  }, []);

  const handleCreateIntegration = async (): Promise<void> => {
    // Setup request
    const request: ICreateIntegration = {
      authorisationCode: ''
    };

    try {
      // Make service call
      const setupIntegrationResponse = await organisationIntegrationsService.createIntegration(request);

      // Handle error
      if (setupIntegrationResponse.data.errors) {
        throw 'Failed to setup integrations';
      }

      // Navigate away on success
      routes.organisation.integrations.navigate(navigate);
    }
    catch (error) {
      // Handle error
    }
  };

  return ();
};

What I am after

I am after suggestions based on the React 18 changes that would handle this situation, I feel that although this is a little specific/niche it is still a viable use case. It would be good to have a clean way to handle this as OAuth integration is quite a common flow for integration between products.

Vuk
  • 693
  • 4
  • 14
  • It could be due to StrictMode. Please check this question https://stackoverflow.com/questions/72489140/react-18-strict-mode-causing-component-to-render-twice – Ozgur Sar Sep 15 '22 at 14:14
  • Yeah so it is I understand why it is happening, what I am looking for is a pattern of handling it that does not require removing strict mode. I want to create a pattern for handling this without just avoiding the safety of strictmode – Vuk Sep 15 '22 at 14:15
  • Does this answer your question? [React 18, useEffect is getting called two times on mount](https://stackoverflow.com/questions/72238175/react-18-useeffect-is-getting-called-two-times-on-mount) – Youssouf Oumar Sep 17 '22 at 11:47
  • I had a look, the answer mostly describes the issue. They did talk about canceling API calls however that would mostly only work for GET requests you could end up with some weird situations canceling a post request half way through from the frontend only. I can't imagine how you would know if it worked or didn't before you cancel it. – Vuk Sep 18 '22 at 05:52
  • Did you give it a try? Do you have a way to stop the request in your case? If so I would give it a shot, since the mount/unmount is very quick, I think it would work. – Youssouf Oumar Sep 18 '22 at 07:40
  • Another way, this `useOrganisationIntegrationsService` doesn't give you a way to know if the integration is already setup? If so you can add a `if` statement as guard to avoid the second call if the first succeeded. – Youssouf Oumar Sep 18 '22 at 07:43
  • The problem is even if it cancels on the frontend the backend service doesn't right? That is really the one that needs to be cancelled in which case I need to setup a pattern for that which I may look into. – Vuk Sep 18 '22 at 07:52
  • I see. I think in this case you should review your logic. See [this section](https://beta.reactjs.org/learn/synchronizing-with-effects#not-an-effect-buying-a-product) of [Synchronizing with Effects](https://beta.reactjs.org/learn/synchronizing-with-effects). *"... if remounting breaks the logic of your application, this usually uncovers existing bugs."* – Youssouf Oumar Sep 18 '22 at 09:10
  • Yeah I spent a lot of time reading that section, and I agree with them you should normally avoid situations like mine but OAuth is a standard pattern that requires this, which is why my question was focused on it as a use-case – Vuk Sep 18 '22 at 11:48

1 Answers1

1

You can use the useRef() together with useEffect() for a workaround

const effectRan = useRef(false)
            
useEffect(() => {
  if (effectRan.current === false) {
  // do the async data fetch here
  handleCreateIntegration();
  }

  //cleanup function
  return () => {
  effectRan.current = true  // this will be set to true on the initial unmount
  }

}, []);

This is a workaround suggested by Dave Gray on his youtube channel https://www.youtube.com/watch?v=81faZzp18NM

Ozgur Sar
  • 2,067
  • 2
  • 11
  • 23
  • So I did get this to work, but I also do not love having a ref every time you in theory need to do this. It does seem like there aren't many alternatives. – Vuk Sep 18 '22 at 05:51
  • In the docs, it is specified that this feature is implemented in React 18 to help developers write proper cleanup functions. https://beta.reactjs.org/learn/synchronizing-with-effects – Ozgur Sar Sep 18 '22 at 09:38