JBrowse 2 · React App examples

Loading configuration

Bundle a config at build time, fetch one at runtime, or add tracks and views after mount.

On this page: Import a config.json · Fetch a config.json · Add tracks programmatically · Launch a view imperatively

Import a config.json

Bundle a config.json at build time and pass it to createViewState.

JBrowse Web auto-loads a config.json from the current directory (or ?config=). The embedded component does not — it makes no assumptions about URLs and leaves how and when to load the config to you.

If the config ships in your bundle, a regular ES import is enough; bundlers handle JSON natively and there is no runtime fetch:

import config from './config.json'

const state = createViewState({ config })

One gotcha: URIs inside a config.json resolve relative to wherever the file was downloaded from. Bundling a config authored for another host means tagging each location with a baseUri, as this example does. The top-level shape is JBrowseRootConfig; to load from a server at runtime, see Fetch a config.json.

View source — 18 lines
import { addRelativeUris } from '@jbrowse/core/util/addRelativeUris'
import { JBrowseApp, useCreateViewState } from '@jbrowse/react-app2'

import config from '../volvox-config.json' with { type: 'json' }

// The config's URIs are relative to where it was downloaded from. addRelativeUris
// tags each with a baseUri so JBrowse resolves them against that directory.
const configUrl =
  'https://jbrowse.org/code/jb2/main/test_data/volvox/config.json'
addRelativeUris(config, new URL(configUrl))

export default function WithImportConfigJson() {
  // `useCreateViewState`, not `useState(() => createViewState(…))`: React
  // double-invokes a state initializer under StrictMode and throws the second
  // result away, which for an engine is a whole orphaned worker pool per mount.
  const state = useCreateViewState({ config })
  return <JBrowseApp viewState={state} />
}

Fetch a config.json

Fetch a config.json at runtime, then build the view state.

When the config lives on a server — or differs per environment, user or route — fetch it before createViewState, and hold the result in state so React renders once it resolves.

As with a bundled config, URIs in the file resolve relative to where it was downloaded from, so tag each location with a baseUri after fetching. Shape: JBrowseRootConfig.

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

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

type ViewState = ReturnType<typeof createViewState>

// The config's URIs are relative to where it lives. addRelativeUris tags each
// with a baseUri so JBrowse resolves them against that directory at load time.
//
// A config.json may also name plugins. Unlike JBrowse Web, the embedded app
// doesn't fetch those for you — createViewState is synchronous and fetching is
// not — so hand config.plugins to loadPlugins and pass the result back in. Its
// baseUri does for plugin urls what addRelativeUris does for data urls: a
// config elsewhere names them relative to itself, not to your app.
const configUrl =
  'https://jbrowse.org/code/jb2/main/test_data/volvox/config.json'

export default function WithFetchConfigJson() {
  const [state, setState] = useState<ViewState>()
  useEffect(() => {
    // The engine is not owned by React, so unmounting alone leaves its RPC
    // worker threads and its autoruns running — see the external-plugin example
    // for why an engine built in an effect has to be destroyed by that effect.
    const mount = {
      unmounted: false,
      engine: undefined as ViewState | undefined,
    }
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    ;(async () => {
      const response = await fetch(configUrl)
      if (!response.ok) {
        throw new Error(`HTTP ${response.status} fetching config ${configUrl}`)
      }
      const config = await response.json()
      addRelativeUris(config, new URL(configUrl))
      const plugins = await loadPlugins(config.plugins ?? [], {
        baseUri: configUrl,
      })
      if (mount.unmounted) {
        return
      }
      mount.engine = createViewState({ config, plugins })
      setState(mount.engine)
    })()
    return () => {
      mount.unmounted = true
      if (mount.engine) {
        destroyViewState(mount.engine)
      }
    }
  }, [])

  return state ? <JBrowseApp viewState={state} /> : null
}

Add tracks programmatically

Add a track config at runtime with addTrackConf + showTrack.

addTrackConf registers a track config on a running app and showTrack opens it — from an event handler, not during render:

state.jbrowse.addTrackConf(trackConf)
state.session.views[0]?.showTrack(trackConf.trackId)

The slots a track config accepts are per type under docs/config, and each adapter type has its own page. To add new track types, adapters or renderers rather than tracks, see plugins.

View source — 59 lines
import { useRef, useState } from 'react'

import { JBrowse } from '@jbrowse/react-app2'

import type { ViewModel } from '@jbrowse/react-app2'

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

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

// a track config you want to add at runtime instead of up front — it could be
// any track config object, e.g. one a user built or fetched
const genesTrackConf = {
  type: 'FeatureTrack',
  trackId: 'volvox_genes',
  name: 'Volvox genes',
  assemblyNames: ['volvox'],
  adapter: { type: 'Gff3TabixAdapter', uri: `${base}/volvox.sort.gff3.gz` },
}

export default function AddTracksProgrammatically() {
  // ref to the live engine, for imperative control after launch
  const ref = useRef<ViewModel>(null)
  const [added, setAdded] = useState(false)

  function addTrack() {
    const state = ref.current
    if (state) {
      state.jbrowse.addTrackConf(genesTrackConf)
      state.session.views[0]?.showTrack('volvox_genes')
      setAdded(true)
    }
  }

  return (
    <div>
      <button
        disabled={added}
        onClick={() => {
          addTrack()
        }}
      >
        {added ? 'Genes track added' : 'Add genes track'}
      </button>
      <JBrowse
        ref={ref}
        assemblies={assemblies}
        tracks={[]}
        sessionName="Programmatic tracks"
        views={[
          {
            type: 'LinearGenomeView',
            init: { assembly: 'volvox', loc: 'ctgA:1..50000' },
          },
        ]}
      />
    </div>
  )
}

Launch a view imperatively

Open a linear genome view after mount via the LaunchView extension point, instead of the declarative views prop.

Most view types are declared up front in views. For one that should appear in response to something at runtime — a button, a search hit, a backend event — use the LaunchView-* extension points. This demo boots an empty session and launches a LinearGenomeView after mount:

await pluginManager.evaluateAsyncExtensionPoint('LaunchView-LinearGenomeView', {
  session: state.session,
  assembly: 'hg38',
  loc: 'chr10:1-100000',
  tracks: ['my_track'],
})
// also: LaunchView-LinearSyntenyView, LaunchView-DotplotView

This is the machinery the import wizard uses, so anything a user can launch from the UI you can launch from code. The shared init model behind every launch surface is described in Automating JBrowse.

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

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

type ViewState = ReturnType<typeof createViewState>

export default function WithLaunchLinearGenomeView() {
  const [viewState, setViewState] = useState<ViewState>()
  const [error, setError] = useState<unknown>()

  useEffect(() => {
    // The engine is not owned by React, so unmounting alone leaves its RPC
    // worker threads and its autoruns running — see the external-plugin example
    // for why an engine built in an effect has to be destroyed by that effect.
    // One box rather than a `let`, because the cleanup below assigns from a
    // separate call and the compiler's narrowing doesn't see through that.
    const mount = { engine: undefined as ViewState | undefined }
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    ;(async () => {
      try {
        const state = createViewState({
          config: {
            assemblies: [
              {
                name: 'GRCh38',
                aliases: ['hg38'],
                uri: 'https://jbrowse.org/genomes/GRCh38/fasta/hg38.prefix.fa.gz',
                refNameAliases: {
                  uri: 'https://s3.amazonaws.com/jbrowse.org/genomes/GRCh38/hg38_aliases.txt',
                },
              },
            ],
            tracks: [
              {
                type: 'QuantitativeTrack',
                trackId: 'hg38.100way.phyloP100way',
                name: 'hg38.100way.phyloP100way',
                category: ['Conservation'],
                assemblyNames: ['hg38'],
                adapter: {
                  type: 'BigWigAdapter',
                  uri: 'https://hgdownload.soe.ucsc.edu/goldenpath/hg38/phyloP100way/hg38.phyloP100way.bw',
                },
              },
            ],
          },
        })
        const { pluginManager } = getEnv(state)

        mount.engine = state
        setViewState(state)
        // Strict so a bad assembly/loc reaches the catch below and renders the
        // error, instead of being swallowed into a silently blank view
        await pluginManager.evaluateAsyncExtensionPointStrict(
          'LaunchView-LinearGenomeView',
          {
            tracks: ['hg38.100way.phyloP100way'],
            loc: 'chr10:1-100000',
            assembly: 'hg38',
            session: state.session,
          },
        )
      } catch (e) {
        console.error(e)
        setError(e)
      }
    })()
    return () => {
      if (mount.engine) {
        destroyViewState(mount.engine)
      }
    }
  }, [])

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