App.initialized() not resolving under certain circumstances

We have recently developed and deployed our own Custom App for Freshdesk support_ticket, to help our customer support team in their daily operations.

The app basically connects to our backend systems via http-requests to present order data in a convenient way. Pretty standard stuff.

The first publish and subsequent versions have worked great without any issues. However, on Friday last week (October 11th) we published a new version that started behaving strangely. The sidebar widget does not load when you navigate from the “all tickets” list to a specific ticket and then open the widget in the sidebar. You have to do a hard reload of the ticket page for the widget to actually load. This makes this error particularly hard to debug since the development environment requires you to add “?dev=true” to the URL and do an actual page reload (hence, the app ALWAYS works in development mode AND test mode, but not in production). To maintain a stable version for our customer support team while still allowing us to “test” the custom app in a live environment, we have now published a second, experimental, copy of the app.

Currently there are two versions of the custom app published in our account: “Nestor” and “Nestor - Experimental”.

  • Nestor is an older version of the custom app that still works.
  • Nestor - Experimental is the newer version that has this strange behavior.

The custom app is built in React 18. I have been able to isolate the issue to the fact that when “app.initialized()” is called, in the version with the error, that call never resolves (or throws an error) when you navigate from the all tickets list to a ticket. It just remains idle indefinitely. And the strange thing about this is that we have not changed anything in regards to how we handle the initialization of the custom app between versions. The only thing that I can think of that would affect this is that the app itself has a larger footprint (some more components, added a request…) that might have some timing effects? Please note though that the extra requests and components added in the newer version, are never actually used when the error occurs, since the app never has a chance to load.

The following is the code used to initialize the app (of course just a snippet, client and setClient() are React state):

useLayoutEffect(() => {
  const script = document.createElement('script');
  script.src = '{{{appclient}}}';
  script.addEventListener('load', () => {
    app.initialized().then((returnedClient) => {
      setClient(returnedClient);
    })
  });
  script.defer = true;
  document.head.appendChild(script);
}, []);

useLayoutEffect(() => {
  if (!client) return;

  client.events.on('app.activated', handleAppActivated);
  client.events.on('app.deactivated', handleAppDeactivated);

  return () => {
    client.events.off('app.activated', handleAppActivated);
    client.events.off('app.deactivated', handleAppDeactivated);
  }
}, [client]);

As previously mentioned, both versions of the custom app use the exact same code to initialize the app, but for some reason in the newer version it doesn’t work when you don’t hard reload the ticket page.

I have looked through the community forums and found some similar issues, the most common solution being using “useLayoutEffect” instead of “useEffect” when injecting the script tag and running app.initialized(). That one we already did however, so that did not solve the issue. And in most other cases it seems to be that it doesn’t work at all, which isn’t the case here since it works on hard reload.

Would appreciate some help on this issue. Thank you in advance!

Hi @Wilnersson ,

Greetings!

It looks like the problem happens because things load differently in production compared to your development environment, causing timing issues. This can lead to a situation where the app doesn’t initialize properly unless you reload the page.

Possible Issues:

  1. Embeds the app.initialized() call inside the script.load event. This means that if there’s a timing issue or failure in resolving the promise, the state won’t update, causing the app to remain idle.

  2. No error handling around app.initialized(). If it fails silently, the state won’t update, making the behavior hard to debug.

  3. Depends heavily on client to determine event binding (app.activated/app.deactivated), which introduces more dependencies and complexity.

Suggestions:

  1. Track when the script loads: Use a loaded state to confirm the script has been injected.
  2. Add error handling: Use try-catch or catch() to detect and log any issues during initialization.
  3. Log events: Add logs inside the app.activated event to ensure everything is working when the page loads.

Example snippet:

const [loaded, setLoaded] = useState(false);

useLayoutEffect(() => {
  const script = document.createElement('script');
  script.src = '{{{appclient}}}';
  script.defer = true;

  // Set 'loaded' to true once the script loads
  script.addEventListener('load', () => setLoaded(true));
  document.head.appendChild(script);
}, []);

useLayoutEffect(() => {
  if (!loaded) return;  // Wait until the script is loaded

  app.initialized()
    .then((client) => {
      console.log('App initialized >>', client); // Log successful initialization
      setClient(client);
    })
    .catch((error) => console.error('Initialization error:', error));  // Catch and log any issues
}, [loaded]);

This version ensures that the app only runs after the script loads, with proper error handling and logs to help track issues more easily.

Kindly let us know if this works.

Hope this helps.

Thanks,
Anish

Hi @Anish !

Thanks for all the input. I have now implemented your suggestions and the initialization of the app now looks like this:

const [loaded, setLoaded] = useState(false);
const [appActivated, setAppActivated] = useState(false);
const [fwClient, setFwClient] = useState(null);

useLayoutEffect(() => {
  const script = document.createElement('script');
  script.src = '{{{appclient}}}';
  script.defer = true;
  script.addEventListener('load', () => {
    console.log('Nestor app-client script load event fired.');
    setLoaded(true);
  });
  document.head.appendChild(script);
}, []);

useLayoutEffect(() => {
  if (!loaded) return;

  console.log('Nestor app: ', app);

  app.initialized().then((client) => {
    console.log('Nestor app initialized!', client);
    setFwClient(client);
  }).catch((err) => {
    console.error('Nestor initialization error: ', err);
  })
}, [loaded]);

useLayoutEffect(() => {
  if (!fwClient) return;

  fwClient.events.on('app.activated', handleAppActivated);
  fwClient.events.on('app.deactivated', handleAppDeactivated);

  return () => {
    fwClient.events.off('app.activated', handleAppActivated);
    fwClient.events.off('app.deactivated', handleAppDeactivated);
  }
}, [fwClient]);

Looking at the logs, when navigating from “All tickets” to a specific ticket, this is what is logged:
image
In other words, the load-event is fired correctly, it sets the loaded state to true, it triggers the second useLayoutEffect, logs the app-variable (which seems to be assigned correctly), but then it just stops. It doesn’t log the success-message or the error in the catch-statement.

On the other hand, if I hard reload the ticket-page, I get this in the log:

In other words, the app.initialized()-call resolves correctly and I have my client.

So basically I am still in the same situation where the app.initialized()-call never resolves.

I did notice some strange behaviour in dev-mode when moving app.initialized() from the first useLayoutEffect to its own useLayoutEffect. At first, the app didn’t work in dev-mode at all. After a while I figured out that removing React.StrictMode solved this issue, so it appeared to be caused by the component being added, removed and re-added to the DOM (since that is what StrictMode does), hinting on some timing issue. This however, had no effect at all when running it in production (which really isn’t surprising since StrictMode is for dev-mode only). I don’t know if this is related to my issues at all, but thought it was worth mentioning.

Any other ideas? :slight_smile:

Regards,
Henrik

Adding this to my answer above, I managed to solve the issue with React StrictMode. Seems to be related to injecting the script-tag when the App is mounted. StrictMode causes the script-tag to be injected twice, I solved it by adding a ref with the script tag as value indicating whether or not the script already was injected. Don’t know if there is a better solution but it seems to work.

This does NOT solve my initial issue, but still wanted to update.

const [loaded, setLoaded] = useState(false);
const fwScriptElement = useRef(null);

useLayoutEffect(() => {
  // To support StrictMode, if the script-element has already been mounted to the DOM, don't do it again.
  if (fwScriptElement.current !== null) return;

  const script = document.createElement('script');
  script.src = '{{{appclient}}}';
  script.defer = true;
  script.addEventListener('load', () => {
    console.log('[in-dev] Nestor app-client script load event fired.');
    setLoaded(true);
  });
  document.head.appendChild(script);

  fwScriptElement.current = script;
}, []);

I finally found the issue.

While the upgrade from React 17 to 18 went fine, in a later update I changed how the App is rendered in the index.js file to go from the deprecated ReactDOM.render() to the new createRoot() instead.

That is what broke the app. I did not realize that change had been done at the point where the app broke, that is on me.

This is the old version of index.js that works:

export const nestorContainerElement = document.getElementById('nestorRoot');

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  nestorContainerElement
);

This is the new version of index.js that breaks the App in Freshdesk:

export const nestorContainerElement = document.getElementById('nestorRoot');

const root = createRoot(nestorContainerElement);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

ReactDOM.render() is deprecated and, as I understand it, will not be supported in React 19 and forward. React recommends that you use createRoot() instead.

@Anish do you know if createRoot() is handled correctly in the Freshworks environment? Of course there might be some way to solve this from the App development side, and I might have missed something, but to me it seems like this would be something that needs to be looked at from Freshworks side?