JBrowse 2 · React App examples

Plugins

Extend the app with plugins — defined inline in your bundle, or loaded at runtime from a URL.

On this page: Embedded (inline) plugin · External plugin

Embedded (inline) plugin

Register a plugin defined inline in your code — here adding a rubber-band menu item.

Plugins extend the app with new track types, adapters, renderers, view types and menu items. The simplest form: define a Plugin subclass in your own source and pass the class in plugins. An npm-published plugin looks identical — you import the class and pass it the same way.

This one adds a “console.log the selected region” item to the linear genome view’s rubber-band menu; click and drag on the ruler to see it.

Plugins can also be loaded from a URL at runtime. If you enable the web worker RPC, a plugin has to be registered in the worker as well as the main thread. Authoring: plugin development guide.

View source — 77 lines
import Plugin from '@jbrowse/core/Plugin'
import { extendViewType } from '@jbrowse/core/pluggableElementTypes'
import { JBrowse } from '@jbrowse/react-app2'

import type PluginManager from '@jbrowse/core/PluginManager'

const base = 'https://jbrowse.org/code/jb2/main/test_data/volvox'

const assemblies = [{ name: 'volvox', uri: `${base}/volvox.2bit` }]

const tracks = [
  {
    type: 'AlignmentsTrack',
    trackId: 'volvox_cram',
    name: 'volvox-sorted.cram',
    assemblyNames: ['volvox'],
    adapter: { type: 'CramAdapter', uri: `${base}/volvox-sorted.cram` },
  },
]

class HighlightRegionPlugin extends Plugin {
  name = 'HighlightRegionPlugin'

  install(pluginManager: PluginManager) {
    extendViewType(pluginManager, 'LinearGenomeView', stateModel =>
      stateModel.extend(self => {
        const superItems = self.rubberBandMenuItems
        return {
          views: {
            rubberBandMenuItems() {
              return [
                ...superItems(),
                {
                  label: 'Console log selected region',
                  onClick: () => {
                    const { leftOffset, rightOffset } = self
                    console.log(
                      JSON.stringify(
                        self.getSelectedRegions(leftOffset, rightOffset),
                      ),
                    )
                  },
                },
              ]
            },
          },
        }
      }),
    )
  }

  configure() {}
}

// #region usePlugin
export default function EmbeddedPlugin() {
  return (
    <JBrowse
      assemblies={assemblies}
      tracks={tracks}
      // the class itself, not a definition to fetch — an embedded app has no
      // config.json to list plugins in
      plugins={[HighlightRegionPlugin]}
      views={[
        {
          type: 'LinearGenomeView',
          init: {
            assembly: 'volvox',
            loc: 'ctgA:1..50000',
            tracks: ['volvox_cram'],
          },
        },
      ]}
    />
  )
}
// #endregion

External plugin

Load a plugin at runtime from a URL with loadPlugins.

Plugins load at runtime from a URL — the model JBrowse Web uses for community-published plugins. loadPlugins fetches the bundles and returns one { plugin, definition } record each, which go to createViewState as plugins. This demo loads the UCSC plugin from unpkg and shows a UCSCAdapter track on hg19.

Pass the records through unchanged rather than mapping to p.plugin: the definition is the plugin’s URL, and that is what the RPC worker uses to load its own copy.

loadPlugins takes the same entries a JBrowse Web config.json lists under plugins, so a fetched config is just loadPlugins(config.plugins ?? [], { baseUri: configUrl }) — pass baseUri so a relative plugin URL resolves against the config rather than your app. Unlike JBrowse Web, the embedded app never fetches them for you: createViewState is synchronous and loading a plugin is not, so run loadPlugins in an effect and render once it resolves.

For plugins you author or npm install, pass the class directly — see embedded plugins. The plugin store lists what’s published.

View source — 107 lines
import { useEffect, useState } from 'react'

import { ErrorMessage } from '@jbrowse/core/ui'
import {
  JBrowseApp,
  createViewState,
  destroyViewState,
  loadPlugins,
} from '@jbrowse/react-app2'

type ViewState = ReturnType<typeof createViewState>

// loadPlugins fetches plugins at runtime from a URL (here the UCSC plugin from
// unpkg), so you don't have to bundle them. Pass the records it returns to
// createViewState unchanged — each one pairs the plugin class with the
// definition it was loaded from, and that definition is what lets the RPC
// worker load the same plugin on its side.
//
// Building the engine yourself means owning its lifetime: React unmounting this
// component does not stop the engine's RPC worker threads or its autoruns, so
// the effect below destroys whatever it built. That is not just tidiness —
// React StrictMode mounts, unmounts and mounts again in development, so without
// it every page visit leaves a whole worker pool behind.
export default function WithExternalPlugin() {
  const [viewState, setViewState] = useState<ViewState>()
  const [error, setError] = useState<unknown>()

  useEffect(() => {
    // one box rather than two `let`s, because the cleanup below assigns from a
    // separate call and the compiler's narrowing doesn't see through that
    const mount = {
      unmounted: false,
      engine: undefined as ViewState | undefined,
    }
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    ;(async () => {
      try {
        const plugins = await loadPlugins([
          {
            name: 'UCSC',
            url: 'https://unpkg.com/jbrowse-plugin-ucsc@^1/dist/jbrowse-plugin-ucsc.umd.production.min.js',
          },
        ])
        // the fetch can land after this effect was already torn down (in
        // StrictMode it usually does); building an engine now would leave one
        // that nothing destroys
        if (mount.unmounted) {
          return
        }
        mount.engine = createViewState({
          config: {
            assemblies: [
              {
                name: 'hg19',
                aliases: ['GRCh37'],
                uri: 'https://jbrowse.org/genomes/hg19/fasta/hg19.fa.gz',
                refNameAliases: {
                  uri: 'https://s3.amazonaws.com/jbrowse.org/genomes/hg19/hg19_aliases.txt',
                },
              },
            ],
            tracks: [
              {
                type: 'FeatureTrack',
                trackId: 'segdups_ucsc_hg19',
                name: 'UCSC SegDups',
                assemblyNames: ['hg19'],
                adapter: { type: 'UCSCAdapter', track: 'genomicSuperDups' },
              },
            ],
            defaultSession: {
              name: 'External plugin example',
              views: [
                {
                  id: 'view1',
                  type: 'LinearGenomeView',
                  init: {
                    assembly: 'hg19',
                    loc: '1:2,467,681..2,667,681',
                    tracks: ['segdups_ucsc_hg19'],
                  },
                },
              ],
            },
          },
          plugins,
        })
        setViewState(mount.engine)
      } catch (e) {
        console.error(e)
        setError(e)
      }
    })()
    return () => {
      mount.unmounted = true
      if (mount.engine) {
        destroyViewState(mount.engine)
      }
    }
  }, [])

  return error ? (
    <ErrorMessage error={error} />
  ) : viewState ? (
    <JBrowseApp viewState={viewState} />
  ) : null
}