npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

qubit-react

v1.5.2

Published

A library for enabling Qubit Experiences on React sites

Downloads

2,839

Readme

Qubit React Codeship Status for qubitdigital/qubit-react-wrapper

Smoothly integrate Qubit Experiences on React websites.

Wrap page components using qubit-react/wrapper and change their rendering behaviour from within Experiences to provide segment targeting, personalisation and A/B testing.

Website Implementation

This section refers to the changes that you need to make to your website 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. It is recommended that all props passed to the wrapped component are also passed to the wrapper. All the props passed to the wrapper component will be forwarded to your custom render function.

Usage

This section refers to the code that you write inside Qubit's platform.

In Qubit Experiences you can interact interact with the wrapper using the options.react interface.

Activation

Registering for Wrapper Ownership

Qubit React has a concept of wrapper ownership. This means that only a single 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.

In order to take ownership of a wrapper during the experience activation phase, use the options.react.register() method. You can claim multiple wrappers by passing in multiple wrapper IDs to register. This will return a promise that resolves once React is available and the wrapper is ready to render on the page.

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

Execution

Render Content

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

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, if you show the original content, but then want to render something custom again, you could do this:

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)
}

Real World Example

Activation

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

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

QubitReact uses driftwood for logging. Enable it via Developer Console.

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

Visit driftwood to see the full API documentation.

Behind the Scenes

This section provides technical details on how the wrapper works.

Determining render function

When a wrapper is first mounted, it creates an object under window.__qubit.react.components[id], where the id is unique to the wrapper. There are three things on the object we care about:

{
  renderFunction: (props, React) => {}, // If this is present, it will be used as the render function for the wrapper
  instances: [] // The instances of the mounted Qubit React wrappers
}

Simply put, if the renderFunction exists, it will be used by the wrapper. Since the wrapper will not know when this is attached, the forceUpdate functions should be called after attaching a new render function to ensure the relevant parts of the UI is rerendered.

This is what the qubit-react/experience does behind the scenes:

// First ensure the object path exists and create it if it doesn't
// This will be the case if no instances of the wrapper has been mounted
window.__qubit = window.__qubit || {}
window.__qubit.react = window.__qubit.react || {}
window.__qubit.react.components = window.__qubit.react.components || {}
window.__qubit.react.components.header = window.__qubit.react.components.header || {}

// Add the generic handler to be called by the wrapper
window.__qubit.react.components.header.renderFunction = function (props, React) {
  // If an experience handler is available, use that, otherwise return props.children
}

// Register the specific experience handler
window.__qubit.react.components.header.owner = '12345' // experience ID
window.__qubit.react.components.header.ownerRenderFunction = function (props, React) {
  return <h2>New Site Header!</h2>
}

// Update all the components
window.__qubit.react.components.header.instances.forEach((instance) => instance.forceUpdate())

FAQ

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 the future in version 2.0 of the qubit-react/experience package. Here is an example of how to upgrade to the new format:

Old

function experienceActivation (options, cb) {
  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)
  })
  return {
    remove: release
  }
}

function experienceExecution (options) {
  const React = options.state.get('React')
  const slots = options.state.get('slots')
  options.react.render('header', () => <div>New header!</div>)
  return {
    remove: slots.release
  }
}

New

function experienceActivation (options, cb) {
  // 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.
  options.react.register(['header']).then(cb)
  // There is also no need to return a remove handler, because it is done for you.
}

function experienceExecution (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 already 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 don't alter your wrapped components in unexpected ways. There are 2 ways to achieve this.

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

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.

Development

Setup

This project uses yarn for dependency management, to get up and running

$ npm i -g yarn
$ make bootstrap

Running tests

Tests are implemented with jest:

  • make test to run tests
  • make test-watch to run tests in watch mode

Sandbox

Simulates an environment to play around with the wrapper and experience utility library. Run make sandbox and then go to localhost:8080 to see the wrapper in action.

Want to work on this for your day job?

This project was created by the Engineering team at Qubit. As we use open source libraries, we make our projects public where possible.

We’re currently looking to grow our team, so if you’re a JavaScript engineer and keen on ES2016 React+Redux applications and Node micro services, why not get in touch? Work with like minded engineers in an environment that has fantastic perks, including an annual ski trip, yoga, a competitive foosball league, and copious amounts of yogurt.

Find more details on our Engineering site. Don’t have an up to date CV? Just link us your Github profile! Better yet, send us a pull request that improves this project.