fast-reflexes / better-react-mathjax

MIT License
124 stars 16 forks source link
mathjax react

A simple React component for MathJax

Up-to-date component for using MathJax in latest React (using functional components and hooks API). Focuses on being versatile and making the use of MathJax in React a pleasant experience without flashes of non-typeset content, both with respect to initial rendering as well as dynamic updates. Simple to use but with many configuration options.

Features



Basic workflow

Installation

Add this library manually as a dependency to package.json...

dependencies: {
    "better-react-mathjax": "^2.0.3"
}

... and then run npm install or let npm or yarn do it for you, depending on which package manager you have chosen to use:

# npm
npm install better-react-mathjax

# yarn
yarn add better-react-mathjax

Usage

better-react-mathjax introduces two React components - MathJaxContext and MathJax. For MathJax to work with React:

  1. Wrap your entire app in a MathJaxContext component (only use one in your app).

    const App = () => {
    
    return (
       <MathJaxContext>
           <!-- APP CONTENT -->
       </MathJaxContext>
    )
    }
  2. Then simply use MathJax components at different levels for the actual math.

    const Component = () => {
    
    return (
       <div>
           <MathJax>{ /* math content */ }</MathJax>
           <h3>This is a header</h3>
           <MathJax>
               <div>
                   <h4>This is a subheader</h4>
                   <span>{ /* math content */ }</span>
                   <h4>This is a second subheader</h4>
                   <span>{ /* math content */ }</span>
                   ...
               </div>
           </MathJax>
           <p>
               This is text which involves math <MathJax>{ /* math content */ }</MathJax> inside the paragraph.
           </p>
       </div>
    )
    }

    In the typical case, the content of a MathJax component can be everything from a subtree of the DOM to a portion of text in a long paragraph. If you have a lot of math, try to wrap as much as possible in the same MathJax component. The MathJaxContext is responsible for downloading MathJax and providing it to all wrapped MathJax components that typeset math. By default, MathJaxContext imports MathJax from a CDN which allows for use of Latex, AsciiMath and MathML with MathJax version 2 and Latex and MathML with the default MathJax version 3 with HTML output for both. If you need something else or want to host your own copy of MathJax, read more about the src attribute of the MathJaxContext below.

Display math and inline math

Both Latex, AsciiMath and MathML have the notion of display math and inline math where display math uses a style and font where the math is allowed to take up more space



Inline math should be used when math is typeset in the middle of text, which then puts some restrictions on space and style



These styles can be set on individual instances of math in Latex and MathML by the use of different pre-configured delimiters (Latex) and tag attributes (MathML), but in AsciiMath, all the math in an app has to use the same style which is set in the configuration (display math by default). On top of this, the MathJax component has a property inline which controls whether the wrapper element added by the MathJax component uses inline or block display. Since a MathJax component can contain a lot of other things than just a single portion of math, it is important to understand that the inline prop is NOT synonymous with inline math since the former controls an element which may contain a lot of things whereas the latter always controls the typesetting of a specific string of math. Therefore, always use configuration and delimiters to control whether to use display math or inline math and use the inline prop to coordinate with the previous setting and control the appearance of the MathJax component itself. Study the elaborate examples below for more insights.

Exceptions to the above rule is when the MathJax component prop renderMode has the value pre in which case one MathJax component becomes synonymous with a single piece of math (given as the text prop) whereby the inline property controls the math mode of the output. The impact of the inline prop on the wrapper element will be ignored if a style that overrides display is added to a MathJax component.

Examples

The first 3 are basic examples with zero configuration standard setup using MathJax version 3 with default MathJax config and no extra options. Note that sandboxes tend to be slower than use in a real environment.

Example 1: Basic example with Latex

Standard setup using MathJax version 3 with default MathJax config and no extra options.

export default function App() {

    return (
        <MathJaxContext>
              <h2>Basic MathJax example with Latex</h2>
              <MathJax>{"\\(\\frac{10}{4x} \\approx 2^{12}\\)"}</MathJax>
        </MathJaxContext>
    );

}

Sandbox: https://codesandbox.io/s/better-react-mathjax-basic-example-latex-bj8gd

Example 2: Basic example with AsciiMath

Using AsciiMath with the default version 3 import requires adding an extra loader (see the MathJax documentation for further information). AsciiMath uses the same display mode on the entire page, which is display math by default. It can be changed to inline math by adding asciimath: { displaystyle: false } to the input config.

export default function App() {
    const config = {
        loader: { load: ["input/asciimath"] }
    };

    return (
        <MathJaxContext config={config}>
            <h2>Basic MathJax example with AsciiMath</h2>
            <MathJax>{"`frac(10)(4x) approx 2^(12)`"}</MathJax>
        </MathJaxContext>
    );
}

Sandbox: https://codesandbox.io/s/better-react-mathjax-basic-example-asciimath-ddy4r

Example 3: Basic example with MathML

MathML is supported natively by a few but far from all browsers. It might be problematic to use with Typescript (no types for MathML included in this package).

export default function App() {
    return (
        <MathJaxContext>
            <h2>Basic MathJax example with MathML</h2>
            <MathJax>
                <math>
                    <mrow>
                        <mrow>
                            <mfrac>
                                <mn>10</mn>
                                <mi>4x</mi>
                            </mfrac>
                        </mrow>
                        <mo>&asymp;</mo>
                        <mrow>
                            <msup>
                                <mn>2</mn>
                                <mn>12</mn>
                            </msup>
                        </mrow>
                    </mrow>
                </math>
            </MathJax>
        </MathJaxContext>
    );
}

Sandbox: https://codesandbox.io/s/better-react-mathjax-basic-example-mathml-20vv6

Example 4: Elaborate example with Latex

Sandbox: https://codesandbox.io/s/better-react-mathjax-example-latex-3vsr5

Example 5: Elaborate example with AsciiMath

Sandbox: https://codesandbox.io/s/better-react-mathjax-example-asciimath-p0uf1

Example 6: Elaborate example with MathML

Make sure to study the comments in this file as MathML processing is a little bit different from Latex and AsciiMath.

Sandbox link: https://codesandbox.io/s/better-react-mathjax-example-mathml-nprxz

Example 7: Elaborate example with optimal settings for dynamic updates with Latex

This example shows a configuration that in some particular cases has proven to result in a very smooth experience with no flashes of non-typeset content. It is by no means recommended as a first attempt and can be tried if you experience problems with flashes of non-typeset content, long waiting times or other undesired behaviour. Especially for those using MathJax version 2, some of the configuration options can be used as an inspiration.

Sandbox link: https://codesandbox.io/s/better-react-mathjax-example-latex-optimal-8nn9n

Under the hood

The MathJaxContext component downloads MathJax and provides it to all users of the MathJaxBaseContext, which includes MathJax components. A MathJax component typesets its content only once initially, if the dynamic flag is not set, in which case the content is typeset every time a change might have occurred. To avoid showing the user flashes of non-typeset content, the MathJax component does its work in a layout effect, which runs "before the browser has a chance to paint". Nevertheless, since typesetting operations are asynchronous, both because the MathJax library needs to be downloaded but also because MathJax should typeset asynchronously to not block the UI if it has a lot to typeset, the typesetting taking place before the browser paints the updates cannot be guaranteed. In most situations however, it should.

The MathJax library by default typesets the entire page when it has been downloaded, unless instructed explicitly not to do so (check instructions on how to do this for version 2 here and for version 3 here). However, given React and its dynamic nature, with existing content being rerendered and new content being added, math likely needs to be typeset more often than that; at a minimum when a component is mounted and sometimes also as a result of dynamic updates of an existing component. Since this often doesn't coincide with initial page load, math rerendered or added after this moment (for example when showing a new page or component) would not get typeset. This is where the MathJax component plays an important part by explicitly typesetting its content whenever a change might have occurred. It is recommended to use MathJax components and not only rely on automatic typesetting on startup.

TypeScript types

This project has both its own types and MathJax types included in the package. For MathJax version 2, a refactored and updated version of @types/mathjax is used whereas for MathJax version 3, this package depends on the types from mathjax-full. Nonetheless, none of the logic from these are used in this project so after building production code and tree-shaking, these dependencies will not affect the size of the final bundle. If you would prefer a separate @types package for this project, please make a suggestion about this in an issue on the project Github page. Note also that issues with the MathJax 2 types can be addressed and updated within this project whereas the types from mathjax-full are used unaltered. You can import the configurations and types of the MathJax objects from versions 2 and 3 as MathJax2Config, MathJax2Object, MathJax3Config and MathJax3Object.

The MathJax types are not always helpful and the user should pay attention even if the compiler does not complain. First of all, several of the types from mathjax-full contain catch-all properties of the form [s: string]: any which effectively allows any props to be passed in. Hence, adding a MathJax 2 configuration to a MathJaxContext using MathJax version 3 will not result in a compile error but instead be accepted even though most of the props won't have the desired effect in MathJax 3.

Also, due to how TypeScript handles excess properties, if a configuration is given in a variable (as opposed to in a literal) where any property matches a property of the required type, the remaining props will be silently ignored. Since MathJax versions share a few configuration properties, it is therefore also possible that a MathJax 3 configuration may be given to a MathJaxContext using MathJax 2 without compiler errors. This can however be avoided by always using literals in which case excess properties are handled differently.

API

The following three properties can be set on both the MathJaxContext and MathJax components. When set on a MathJaxContext component, they apply to all wrapped MathJax components except those on which the property in question is set on the individual MathJax component, which then takes precedence.

Note: MathJax3Object and MathJax3Config are aliases for MathJaxObject and MathJaxConfig as exported by mathjax-full.


hideUntilTypeset: "first" | "every" | undefined

Controls whether the content of the MathJax component should be hidden until after typesetting is finished. The most useful setting here is first since the longest delay in typesetting is likely to occur on page load when MathJax hasn't loaded yet. Nonetheless, with a large amount of math on a page, MathJax might not be able to typeset fast enough in which case non-typeset content might be shown to the user; in this case the setting of every might be handy.

Default: undefined (no content is hidden at any time)

renderMode: "pre" | "post" | undefined

Controls how typesetting by MathJax is done in the DOM. Typically, using the setting of post works well but in rare cases it might be desirable to use pre for performance reasons or to handle very special cases of flashes of non-typeset content.

Default: post

typesettingOptions: { fn: TypesettingFunction, options: OptionList | undefined } | undefined

Used to control typesetting when renderMode is set to pre. Controls which typesetting function to use and an optional object with typesetting details.

Default: undefined (no conversion function is supplied which throws an error when renderMode is pre)

MathJaxContext component


config: MathJax2Config | MathJax3Config | undefined

Controls MathJax and is passed to MathJax as its config.

Default: undefined (default MathJax configuration is used)

MathJax configuration object. Make sure it corresponds to the version used. More information can be found in the docs.

src: string | undefined

The location of MathJax.

Default: undefined (default CDN https://cdnjs.cloudflare.com is used)

Local or remote url to fetch MathJax from. More information about hosting your own copy of MathJax can be found in the MathJax documentation and more in particular on the better-react-mathjax Github page.

A source url may contain both some specific file and some query parameters corresponding to a configuration which, in turn, governs which additional assets MathJax fetches. The default sources used when this property is omitted are the same as those listed in the MathJax instruction (however from a different CDN). This means that for version 2, the fetched resource (MathJax.js?config=TeX-MML-AM_CHTML) includes support for Latex, MML and AsciiMath with HTML output by default, and for version 3, the fetched resource (tex-mml-chtml.js) supports MML and Latex with HTML output. These correspond to some typical and broad use of MathJax in the browser. If you have a use case where you, using standalone MathJax, would have to use a different source url, then you have to manually supply such a url (local or remote) here. This, in analogy to how you would modify the script import to adjust to your needs in a plain HTML environment with direct use of MathJax. Read more about different configurations here (for MathJax 3) and here (for MathJax 2).

version: 2 | 3 | undefined

MathJax version to use. Must be synced with any config passed.

Default: 3

Version of MathJax to use. If set, make sure that any configuration and url to MathJax uses the same version. If src is not specified, setting versionto 2 currently makes use of version 2.7.9 and setting it to 3 uses 3.2.0.

onStartUp: (mathJax: MathJax2Object | MathJax3Object) => void) | undefined

Callback to be called when MathJax has loaded successfully but before the MathJax object has been made available to wrapped MathJax components. The MathJax object is handed as an argument to this callback which is a good place to do any further configuration which cannot be done through the config object.

Default: undefined

onLoad: () => void) | undefined

Callback to be called when MathJax has loaded successfully and after the MathJax object has been made available to the wrapped MathJax components. This marks the last step of the startup phase in the MathJaxContext component when MathJax is loaded. Can be used to sync page loading state along with onInitTypeset callbacks on MathJax components.

Default: undefined

onError: (error: any) => void) | undefined

Callback to handle errors in the startup phase when MathJax is loaded.

Default: undefined

MathJax component


inline: boolean | undefined

Whether the wrapped content should be in an inline or block element. When renderMode is post, this refers to the wrapper component that this MathJax component uses (the user might still have both display and inline math inside). If renderMode is set to pre this property applies to both the wrapper component and the content which will be typeset as inline math if this property is set to true and as display math otherwise.

Note: Currently only MathML and Latex can switch between inline mode and math mode in the same document. This means that AsciiMath will use the document default for content, no matter the setting of this property. The property will still affect the wrapper nonetheless.

Default: false

onInitTypeset: () => void) | undefined

Callback for when the content has been typeset for the first time. Can typically be used for hiding content or showing a loading spinner in a coordinated way across different elements until all are in a representative state.

Default: undefined

onTypeset: () => void) | undefined

Callback for when the content has been typeset (not only initially). Can typically be used for hiding content or showing a loading spinner in a coordinated way across different elements until all are in a representative state. Only used when the dynamic flag is set. Similarly to onInitTypeset, this callback also fires on initial typesetting. If the dynamic is not set, this callback is effectively reduced to having the same effect as onInitTypeset. When the dynamic flag is set, this callback runs after every typesetting, which takes place on every render if renderMode is set to post, and when the text prop changes when renderMode is set to pre.

Default: undefined

text: string | undefined

Required and only used when renderMode is set to pre. Should be the math string to convert without any delimiters. Requires typesettingOptions to be set and version to be 3. If renderMode is post, this property is ignored.

Default: undefined

dynamic: boolean | undefined

Indicates whether the content of the MathJax component may change after initial rendering. When set to true, typesetting should be done repeatedly (every render with renderMode set to post and whenever the text property changes with renderMode set to pre). With this property set to false, only initial typesetting will take place and any changes of the content will not get typeset.

Default: false


Any additional props will be spread to the root element of the MathJax component which is a span with display set to inline when the inline property is set to true, otherwise block. The display can be overridden via style prop if needed (then the inline property does not affect the wrapper). A ref is not possible to set as this functionality is used by the MathJax component itself.

Custom use of MathJax directly

You can use the underlying MathJax object directly (not through the MathJax component) if you want as well. The following snippet illustrates how to use MathJaxBaseContext to accomplish this.

// undefined or MathJaxSubscriberProps with properties version, hideUntilTypeset, renderMode, typesettingOptions and promise
const mjContext = useContext(MathJaxBaseContext)
if(mjContext)
  mjContext.promise.then(mathJaxObject => { /* do work with the MathJax object here */ })

This requires only a MathJaxContext, supplying the MathJaxBaseContext, to be in the hierarchy. The object passed from the promise property is the MathJax object for the version in use.

Sandbox example: https://codesandbox.io/s/better-react-mathjax-custom-example-latex-e5kym

Fighting flashes of non-typeset content

Using MathJax, as is, is as seen from the basic examples above fairly simple, but the real challenge is to use it in a way so that the user doesn't see flashes of non-typeset content. Apart from making MathJax available to React in a simple and straightforward way, this is what this library focuses on.

Static content

Static content does not have the dynamic property set to true and is typeset once only when the component mounts. If the component remounts, the procedure repeats. Before the content is typeset, the user may see the raw content which might be a negative experience. There are several ways to solve this:

Dynamic content

Dynamic content might be harder to work with since it, per definition, updates several times during the time a MathJax component is mounted. With this goal, the dynamic property should be set to true which implies that typesetting will be attempted repeatedly (after every render if renderMode is set to post and when the text property changes if renderMode is set to pre). If not handled correctly, updates might look bad to the user if the content is visible before typesetting. As indicated above in the "Under the hood" section, this should usually not happen since MathJax typesets the content in a layout effect. However, MathJax typesets content asynchronously and there might be occasions where the typesetting takes place after the browsers has already updated. This might happen if you have a lot of math on a page for example. Apart from the general considerations below, there are a few strategies to try in order to solve this problem.

Note: these measures should only be taken to battle flashes of non-typeset content where proven necessary.

General considerations regarding flashes of non-typeset content

General Considerations (don't skip)

Questions and answers

Last but not least ...

MathJax was not written for use in React and React was not written with MathJax in mind so we have to massage them into getting along and working in tandem!

Compatibility

Tested with:

Wish list

MathJax documentation

Github

File problems or contribute on Github: https://github.com/fast-reflexes/better-react-mathjax

Changelog

Migration guides

License

This project is licensed under the terms of the MIT license.