Preventing React Crashes: Handling Google Translate

Introduction

While working at my company, I was reviewing Sentry issues that caused the entire React application to crash. I encountered several similar errors such as Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node and Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node. According to the Sentry logs, these errors occurred for different users on the same pages during similar but fairly ordinary actions. After a little research and performing the same actions as the users while using Google Translator, I was able to reproduce the issue on these pages.

How Google Translate Does Translation

Let’s take a button as an example, which by default renders text and a heart icon. It can also appear as a small button, rendering only the heart icon by removing the text based on the isSmall prop.

// FavoriteButton.tsx const FavoriteButton: React.FC<{ isSmall?: boolean;
}> = ({ isSmall = false }) => { return ( <button> {!isSmall && 'Like it'} <img src="/assets/favorite.svg" alt="favorite-icon" /> </button>
 );
}; 
Enter fullscreen mode
Exit fullscreen mode

Let’s create a simple parent container that passes the desired state to our button. To do this, we’ll add a simple true/false state switch in the form of a checkbox. The component passes the state to the Button.

// App.tsx const App: React.FC = () => { const [isIconOnly, setIsIconOnly] = useState(false); return ( <div> <div> <label> <input type="checkbox" onChange={() => setIsIconOnly(!isIconOnly)} />
 Use small state </label>
 </div>
 <div> <FavoriteButton isSmall={isIconOnly} />
 </div>
 </div>
 );
};
Enter fullscreen mode
Exit fullscreen mode

Application

Now, let’s schematically imagine how our example will look in the tree. We’ll focus only on the right part, namely the Button component.

Application tree

Next, let’s see how the right part will look in the Virtual DOM. Note, this is not an exact representation of the VDOM, but just a rough scheme. We’re interested in what React will check for rendering the text in Button.

Button DOM tree

Now we’ll turn on Google Translate and translate the application into any language. I’ll take Estonian.

Trasnlated application

With the translation enabled, let’s try clicking the checkbox. Voilà, we see a white screen and an error in the console: Failed to execute 'removeChild' on 'Node'.

What Happened?

At first glance, it may seem that the translation plugin only replaced the English text with Estonian. If that were the case, React would continue to work correctly, as it’s only interested in the presence of a text node, not its content. But if you open the browser console and look at the tree, you’ll see that this is not the case. In reality, the translator replaced the text node with font elements and added another text node with the translation, thus changing the tree structure. How it looks now:

DOM tree with Translator
Note, I deliberately hid the left branch in the diagram as it doesn’t interest us. Although the text in the checkbox will also be changed. But from React point of view, all the elements are static there.

Now in the Button component diagram, we see that the text element is no longer a direct child of the button element. A font with translated text has taken its place. However, the element still exists in memory. This will happen by default with all text elements. I’ll describe below when this will not happen.

Even though the DOM tree has changed, React doesn’t know about it. For React, the previous scheme with virtual nodes is still relevant. React still considers the VirtualTextNode a child of the VirtualButtonNode. Now we’ll click the checkbox and trigger React to re-render the component. When rendering the Button component, React will see that isSmall is true and the text needs to be removed. The rendering phase for VDOM will be successful. It will refer to VirtualButtonNode and simply remove the link to the text node. But during the commit phase, React will apply changes to the DOM, where it will necessarily call buttonNode.removeChild(textNode), the references to which it keeps in memory. More about the phases. And at this moment, the crash will happen. Because the textNode is no longer a child of buttonNode.

Let’s imagine that the img tag doesn’t exist, or if it disappears along with the text under the isSmall condition. In that case, the button element will become empty. React will recognize this during the rendering phase, and during the commit phase, it will not execute the removeChild method but will simply overwrite the entire inner part of the button element. As if it called buttonNode.innerHtml = null. In this case, it doesn’t matter what’s inside TextNode or FontNodeeverything will be removed.

Fix Examples

Let’s see how we can fix this. There can be many fix options. I’ll present just a few of them.

Wrap in an Element

If you add a wrapper, such as a span, the problem will go away.

{!isSmall && <span>Like it</span>}
Enter fullscreen mode
Exit fullscreen mode

It’s very simple. The translator will create font elements inside the span element. At the same time, the span itself will remain at the same level in the button descendant tree. If isSmall becomes true, React will remove the span from the tree along with all its children.

The additional element has its downside – it can complicate the layout, especially if there are already CSS selectors for span elements.

translate=”no”

Do you remember I promised to tell you when the translator doesn’t add font by default? You can add the translate="no" HTML attribute to any tag, thus adding an exception for Google Translate, including the html tag, that is, the entire application.

One downside is that users who don’t understand the application language will be disappointed.

Unstable. Replace with an Empty String

I tried replacing && with rendering an empty string with a space, and the problem went away. There is an image below.

{isSmall ? ' ' : 'Like it'}
Enter fullscreen mode
Exit fullscreen mode

I assume that React will stop calling the element removal and will only try to replace the content in the TextNode. Remember, I said that the replaced text is only removed from the DOM tree but is still stored in the application memory. And React refers to this node. It can change the text content.

Cons:

  1. This is very implicit code, which will require explanations for other developers.
  2. Highly dependent on the current layout and may require additional CSS hacks.

Override Element Removal

In issues on GitHub, I found an example from Dan Abramov, where he rewrote the native removeChild and insertBefore methods. He suggests not calling the original method if the child element is not found. Note his comment that this will affect performance. But this is exactly what the React team could do if they decided to fix the problem on their side.

Summary

The errors encountered were related to nodes being altered in the DOM by the translator, which caused React Virtual DOM to become inconsistent with the actual DOM. We explored how Google Translate replaces text nodes with font elements, leading to crashes when React tries to manipulate these nodes.

As you can guess, this issue is not limited to React and Google Translate but applies to other Virtual DOM libraries and any external interference with the rendering mechanism.

Final Thoughts

In our case, we personally used span elements and the translate="no" attribute in specific places to prevent issues caused by Google Translate.

I hope this investigation was interesting and useful, providing insight into handling multilingual support in React applications and preventing crashes due to external changes. If you are interested in reading more about this issue, you should check out this GitHub issue.


Discover more from Coursity

Subscribe to get the latest posts sent to your email.

Leave a Comment

Your email address will not be published. Required fields are marked *

Discover more from Coursity

Subscribe now to keep reading and get access to the full archive.

Continue reading

Scroll to Top