Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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>
  );
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment