Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@GiacoCorsiglia
Last active March 19, 2024 13:42
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save GiacoCorsiglia/1619828473f4b34d3d914a16fcbf10f3 to your computer and use it in GitHub Desktop.
Save GiacoCorsiglia/1619828473f4b34d3d914a16fcbf10f3 to your computer and use it in GitHub Desktop.
Using MathJax v3 in React

Using MathJax v3 in React

Any improvements or alternative approaches are welcome!

One alternative approach can be found in the CharlieMcVicker/mathjax-react library.

Loading MathJax

It may be possible to bundle MathJax with the rest of your JavaScript, which might have the nice consequence of allowing you to import it instead of using the global MathJax object. But I found it simpler to include the following at the bottom of my html file; this is the common way to load MathJax.

This code also tracks MathJax's loaded state in a globally accessible promise. This allows us to avoid race conditions with React's load and early renders.

<!-- MathJax -->
<script>
  window.__MathJax_State__ = {
    isReady: false,
    promise: new Promise(resolve => {

      window.MathJax = {
        // MathJax can be configured as desired in addition to these options.
        startup: {
          // Don't perform an initial typeset of the page when MathJax loads.
          // Our React components will trigger typsetting as needed.
          typeset: false,
          ready: () => {
            // Do whatever MathJax would normally do at this point.
            MathJax.startup.defaultReady();
            // Set the flag and resolve the promise.
            window.__MathJax_State__.isReady = true;
            resolve();
          }
        }
      };

    })
  };
</script>
<script
  type="text/javascript"
  id="MathJax-script"
  async
  src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"
></script>

I'm using MathJax's TeX-to-SVG capabilities but you could use other input and output formats by loading different scripts, per this document.

The React Component

Next we create a React component for rendering TeX equations. The component has a simple interface. For inline math (normally \(...\)):

<Math tex="e^{i \pi} = -1" />

For display-mode math (normally \[...\]):

<Math display tex="e^{i \theta} = \cos\theta + i\sin\theta" />

The code in this component is based on this official MathJax demo.

function Math({ tex, display = false }) {
  const rootElementRef = useRef(null);

  // Store this in local state so we can make the component re-render when
  // it's updated.
  const [isReady, setIsReady] = useState(__MathJax_State__.isReady);

  useLayoutEffect(() => {
    // Avoid running this script if the MathJax library hasn't loaded yet.
    if (!isReady) {
      // But trigger a re-render of this component once it is loaded.
      __MathJax_State__.promise.then(() => setIsReady(true));
      return;
    }

    // This element won't be null at this point.
    const mathElement = rootElementRef.current;

    // Remove previous typeset children.
    mathElement.innerHTML = "";

    // Reset equation numbers.
    MathJax.texReset();

    // Construct options.
    const options = MathJax.getMetricsFor(mathElement);
    options.display = display;

    // Potentially this could/should be switched to use the synchronous version
    // of this MathJax function, `MathJax.tex2svg()`.
    MathJax.tex2svgPromise(tex, options)
      .then(function (node) {
        // `mathElement.replaceWith(node)` would be nicer markup-wise, but that
        // seems to mess with React's diffing logic.
        mathElement.appendChild(node);
        // Update the global MathJax CSS to account for this new element.
        MathJax.startup.document.clear();
        MathJax.startup.document.updateDocument();
      })
      .catch(function (err) {
        console.error(err);
      });
  }, [tex, display, isReady]);

  return <span ref={rootElementRef}></span>;
}

If you include a lot of equations in your app, it might be worth using this function signature instead:

function M({ t: tex, display = false }) { /* ... */ }

allowing you to write <M t="e^{i \pi} = -1" />.

An Alternative Component (That's Probably Worse)

I suggest you go with the Math component above. Because it associates one React component with one MathJax equation, it's easy to control exactly when the component re-renders. The component described in this section does not have that feature.

It would be nice if you could just include equations in your JSX using the normal MathJax delimiters, such as:

return (
  <ProcessMath>
    Euler's formula, which tells us \( e^{i \pi} = -1 \), has the general form:
    \[
      e^{i \theta} = \cos\theta + i\sin\theta
    \]
  </ProcessMath>
)

The syntax highlighting on the above code block should reveal the problem: { and }, which are so frequently used in LaTeX, already have a meaning in JSX. The correct way to write one of the above equations is \( e^{"{"}}i \pi{"}"} = -1 \)—obviously that's not a good option.

One way around this would be to create a helper function like this:

const math = (tex) => `\\(${tex}\\)`;

which would be used as {math("e^{i \\pi} = -1")} in JSX. That works decently, but the \ characters must be escaped in JavaScript strings, whereas they don't need to be escaped in JSX attribute strings.

For these reasons, in addition to the performance consideration, I recommend the Math component from above. That said, here's the ProcessMath component—if you have a way to improve it, please share!

export default function ProcessMath({ children }) {
  const rootElementRef = useRef(null);

  // Store this in local state so we can make the component re-render when
  // it's updated.
  const [isReady, setIsReady] = useState(__MathJax_State__.isReady);

  useLayoutEffect(() => {
    // Avoid running this script if the MathJax library hasn't loaded yet.
    if (!isReady) {
      // But trigger a re-render of this component once it is loaded.
      __MathJax_State__.promise.then(() => setIsReady(true));
      return;
    }

    // This element won't be null at this point
    const rootEl = rootRef.current;

    // Reset equation numbers.
    MathJax.texReset();

    // Run MathJax typsetting on just this element
    // Potentially this could/should be switched to use the asynchronous version
    // of this MathJax function, `MathJax.typesetPromise()`.
    MathJax.typeset([rootEl]);
  });

  return (
    <div ref={rootElementRef}>
      {children}
    </div>
  );
}
@lukaskesch
Copy link

How do you get access to the MathJax and the __MathJax_State__ object in the react component?

@GiacoCorsiglia
Copy link
Author

Those variables are both globally accessible since they’re on the window object, which you can see in the “Loading MathJax” section above.

Does that answer your question?

@kishoreKlkr
Copy link

How can we make it work if I am trying to run this inside shadow DOM

@GiacoCorsiglia
Copy link
Author

How can we make it work if I am trying to run this inside shadow DOM

Can you clarify what you're trying to do? It sounds like that may be a general React question.

@kishoreKlkr
Copy link

Yes I'm trying to render mathjax within a react component on Shadow Dom. But the variables Mathjax and __MathJax_State__ won't be available to my component because the imported script won't be able to penetrate through shadow dom.

@GiacoCorsiglia
Copy link
Author

I see. I'm not super familiar with the Shadow Dom, but it sounds like you will need to bundle Mathjax along with your other JavaScript.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment