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. In the article I'll show why it happens and suggest some ways to fix it.
TLDR;
Google Translator wraps each text node with 2 font
elements that makes DOM and Virtual DOM incompatible.
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.
Typescript
// FavoriteButton.tsx
const FavoriteButton: React.FC<{
isSmall?: boolean;
}> = ({ isSmall = false }) => {
return (
<button>
{!isSmall && 'Like it'}
<img
src="/assets/favorite.svg"
alt="favorite-icon"
/>
</button>
);
};
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.
Typescript
// 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>
);
};
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.
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.
Now we'll turn on Google Translate and translate the application into any language. I'll take Estonian.
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:
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 theisSmall
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 theremoveChild
method but will simply overwrite the entire inner part of the button element. As if it calledbuttonNode.innerHtml = null
. In this case, it doesn't matter what's insideTextNode
orFontNode
- everything 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.
Typescript
{!isSmall && <span>Like it</span>}
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.
Typescript
{isSmall ? ' ' : 'Like it'}
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:
- This is very implicit code, which will require explanations for other developers.
- 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.
Top comments (0)