React integration

This is for:

Developer

Qubit React provides a method of controlling the rendering of React components from within a Qubit Experience.

Because the rendering can be controlled from within the experience, you can, as with all Qubit Experiences, create one or more variations, possibly even with a different rendering in each variation, target it with segments and track the impact of rendering changes on key metrics defined in your experience goals.

Website implementation

In this we will look at the changes that you need to make to your website’s codebase.

To expose a component for use in Experiences, wrap the relevant components with qubit-react/wrapper.

import QubitReact from 'qubit-react/wrapper'

<QubitReact id='header' ...props>
  <Header ...props />
</QubitReact>

A unique id is required for each wrapped component. We recommended that all properties passed to the wrapped component are also passed to the wrapper. All the properties passed to the wrapper component will be forwarded to your custom render function.

Note

You can find the source code for the qubit-react module on GitHub.

Usage

In this section we will look at the code that you write inside Qubit’s platform.

Activation

Open your experience and select Settings. Select Edit in the Triggers card and then custom trigger next to Custom JavaScript (default).

This opens an editor that you can use to edit the default code block:

module.exports = function triggers (options, cb) {
  cb(true)
}

Qubit React uses a wrapper ownership concept to ensure that only one experience can control the contents of a wrapper at any given time. This reduces conflicts between experiences attempting to modify the same component.

Ownership will not be taken until the options.react.render() method is called in the experience execution function. It is your job to ensure that multiple experiences do not try to execute at the same time. If they do, only the first experience will execute succesfully. All subsequent attempts will result in an error being thrown.

Before taking ownership of a wrapper, you first need to register it during the experience activation phase, using the options.react.register() method. You can register multiple wrappers by passing in multiple wrapper Ids. This will return a promise that resolves once React is available and all registered wrappers are ready to render on the page.

Example:

module.exports = function triggers (options, cb) {
  options.react.register(['header']).then(cb)
}
Warning

Remember, it is your responsibility to make sure two experiences don’t try and claim the same wrapper at the same time. If they do, one of them will end up throwing an exception during execution.

Execution

Render content

Now that we are finally in execution phase, let’s render some custom content into our wrapped component.

Example:

module.exports = function experienceExecution (options) {
  const React = options.react.getReact()

  class NewHeader extends React.Component {
    render () {
      return <h2>NEW HEADER</h2>
    }
  }

  options.react.render('header', function (props) {
    return <NewHeader />
  })
}

Rendering original content

Sometimes, it might be useful to render the original content temporarily. For example, you may want to show the original content and render something custom again at a later point.

Example:

module.exports = function experienceExecution (options) {
  const React = options.react.getReact()

  setTimeout(() => {
    // renders original content
    options.react.render('header', function (props) {
      return props.children
    })
    setTimeout(() => {
      // renders new content again
      options.react.render('header', function (props) {
        return <NewContent />
      })
    }, 5000)
  }, 5000)
}

Manually releasing ownership

Qubit Experiences will automatically take care of releasing wrapper ownership when experiences restart due to virtual page views. If for some reason you need to release ownership under other circumstances, there is a method available to do so:

module.exports = function experienceExecution (options) {
  options.react.render('header', () => <NewContent />)

  setTimeout(() => {
    options.react.release()
  }, 5000)
}
Tip
Leading-practice

Calling release will render the original content of the wrapped component and release the ownership so that other experiences can claim it.

Real world example

Activation

Example:

module.exports = function experienceActivation (options, cb) {
  const saleEnds = Date.UTC(2017, 0, 1, 0, 0, 0)
  const remaining = saleEnds - Date.now()
  if (remaining > (60 * 60 * 1000)) return
  if (remaining < 0) return

  options.state.set('saleEnds', saleEnds)
  options.react.register(['header-subtitle-text', 'promo-banner-text'], cb)
}

Execution

Example:

module.exports = function experienceExecution (options) {
  const saleEnds = options.state.get('saleEnds')
  const React = options.react.getReact()

  class Countdown extends React.Component {
    componentWillMount () {
      const { endDate } = this.props
      this.state = {
        remaining: endDate - Date.now()
      }
      const interval = setInterval(() => {
        const remaining = endDate - Date.now()
        this.setState({ remaining: endDate - Date.now() })
        if (remaining < 0) {
          clearInterval(interval)
          options.react.release()
        }
      }, 1000)
    }

    render () {
      let secsLeft = Math.floor(this.state.remaining / 1000)
      const minsLeft = Math.floor(secsLeft / 60)
      secsLeft = secsLeft - (minsLeft * 60)
      return <span>{`${minsLeft} mins and ${secsLeft} secs left`}</span>
    }
  }

  options.react.render('header-subtitle-text', function (props) {
    return <span>Great offers somewhere...</span>
  })

  options.react.render('promo-banner-text', function (props) {
    return <Countdown endDate={saleEnds} />
  })
}

Debugging

Qubit React uses driftwood for logging. Enable it via the Developer Console.

window.__qubit.logger.enable({ 'qubit-react:*': null }, { persist: true })
// enable qubit-react logs

window.__qubit.logger.enable({ '*': null }, { persist: true })
// enable all logs

window.__qubit.logger.disable()
// disable logs

See driftwood to see the full API documentation.

FAQs

How and why should I migrate from the legacy qubit-react/experience package in my Experience code?

At the beginning of January 2020, we deprecated the old callback-based wrapper registration method and introduced a new promised-based one in its place. This was done for two main reasons:

  1. The old API was overly-verbose and complex

  2. Under certain scenarios, wrapper ownership would be claimed when an experience wasn’t actually going to fire, preventing all other experiences from claiming that wrapper

While both APIs are currently available, we will be removing support for the old callback-based registration in version 2.0 of the qubit-react/experience package. Here is an example of how to upgrade to the new format:

Old:

module.exports = function triggers (options) {
  const experience = require('qubit-react/experience')(options.meta)
  const release = experience.register(['header'], function (slots, React) {
    options.state.set('slots', slots)
    options.state.set('React', React)
  })
  options.onRemove(release)
  return true
}

module.exports = function variation (options) {
  const React = options.state.get('React')
  const slots = options.state.get('slots')
  options.react.render('header', () => <div>New header!</div>)
  options.onRemove(slots.release)
}

New:

module.exports = function triggers (options) {
  // In the new version, the slots and React instance are accessed via the dedicated
  // API interface, so there is no need to manually pass them through state.
  return options.react.register(['header'])
  // There is also no need to return a remove handler, because it is done for you.
}

module.exports = function variation (options) {
  const React = options.react.getReact()
  options.react.render('header', () => <div>New header!</div>)
}

Is it possible to disable Qubit Experiences in a testing environment?

Yes. If you’re not loading Qubit’s smartserve.js script in your testing environment then this shouldn’t be an issue. Qubit React wrappers are a transparent noop pass through in that case, and should not affect your tests.

If you’re running an e2e testing environment and loading smartserve.js script, you might want to take extra steps to ensure that Qubit Experiences do not alter your wrapped components in unexpected ways. There are two ways to achieve this:

  1. Firstly, you can append the following query parameters to your URL ?qb_opts=remember&qb_experiences=-1. This drops a cookie qb_opts, which persists the setting for the rest of the session

  2. Alternatively, drop a cookie in your server side or client side code. The cookie key is qb_opts and value is %7B%22experiences%22%3A%5B-1%5D%7D. Here’s example JavaScript that drops the cookie:

document.cookie = 'qb_opts=' + encodeURIComponent(JSON.stringify({"experiences":[-1]})) + "; path=/"

The experiences option is typically used to force execution of a specific Qubit Experience. Specifying -1 signals that nothing should be executed, which keeps your testing environment clear of Experiences.