Implementing feature flags in single page apps, using React and Redux Saga

By Justin Baker   •   February 14, 2017

Built on frameworks like React, Angular, and Ember, single page apps (SPA) are becoming the new norm for modern applications. They center around dynamically updating a single HTML page via AJAX and HTML5 to deliver a more fluid and faster user experience. This introduces some new complexity when it comes to controlling access to front-end features, specifically via feature flags.

Feature flags (toggles) are used to gate access to particular code snippets, allowing you to control a feature's rollout, target specific users, and kill a feature in production. The challenge with feature flagging single page apps is handling the state transformations (the changes in a webpage's DOM) in a way that maintains performance and a fluid user experience.

This article discusses how to feature flag in a React single page app to show best practices. More specifically, we demonstrate feature flagging using LaunchDarkly's JavaScript SDK.

Feature Flagging in Single Page Apps

With the help of React Router, creating a single page application can be a very quick and easy process, allowing developers to minimize risk and test features without degrading the user experience. We use Redux Sagas to handle asynchronous actions, which is perfect for integrating our app with the LaunchDarkly JavaScript SDK.

Feature Flags

In our app, we use two feature flags, user-type and header-bar-color. The user-type feature flag controls the content that is displaying depending on the group of current logged in user, while header-bar-color returns a hex code to toggle the color of the navigation bar.

An implementation of header-bar-color might look something like this:

Integrating Feature Flags with Redux Sagas

We handle the asynchronous process of requesting a LaunchDarkly feature flag by using Redux Sagas. The workflow begins in the App component of our SPA, where we dispatch an action to initialize the LaunchDarkly client.

/app/components/App.jsclass App extends Component {  componentDidMount () {    this.props.dispatch(ldInitRequest())  }  render () {  …  }}/app/actions/index.jsexport function ldInitRequest() {  return {type: LD_INIT_REQUEST}}

Then, we can use a saga as middleware to wait for an initialization request to be received, using redux-saga's takeEvery module, which will call an effect, initLD.

/app/sagas/index.jsexport function * watchInitLD () {  yield takeEvery(LD_INIT_REQUEST, initLD)}

The effect will call getLD, a function which creates a promise, waiting for the LaunchDarkly client to be ready for flag requests. Since the user is anonymous, we can generate a random token to be used as their key.

/app/sagas/index.jsexport function * initLD () {  let user = {key:Math.random().toString(36).substring(7), anonymous:true}  ld = ldClient.initialize(YOUR_ENVIRONMENT_KEY, user)  const flag = yield call(getLD, user)  yield put({type: LD_INIT, ld: ld, flag: flag})}function getLD () {  var ldPromise = Promise.promisify(ld.on)  return ldPromise('ready').then(function () {    return ld.allFlags()  })}

Once the client is ready, the effect will send the action off to our reducer, to store the client and current feature flag state in props.

/app/reducers/index.jsfunction reducer (state = initialState, action) {  switch (action.type) {    case LD_INIT:      return {...state, ld: action.ld, flag: action.flag, headerColor: action.headerColor}    …    default:      return state  }

Now that the LD client is initialized, and stored in our props, we can define a process to update our flag whenever the current user changes (logging in or out, for example). We take advantage of the fact that we're already using sagas to asynchronously handle user authentication. When logging in, the authorize effect is called to ensure the login data is correct. When authorization is complete, we'll make a call to idLD. This will identify a new user in our LaunchDarkly client, and send the correct flag to the reducer for our newly logged in user. Logging out works in a similar fashion. (Note: We make use of some utility functions defined in auth, which return user information from our local storage.)

/app/sagas/index.jsexport function * authorize ({username, password, isRegistering}) {  try {    ...  } catch (error) {    ...  } finally {    // When done, we tell Redux we're not in the middle of a request any more and    // update the feature flag    let user = {key: auth.getToken(), custom: {groups:auth.getGroup()}, anonymous:false}    let flags = yield call(idLD, user)    console.log(flags['header-bar-color'])    yield put({type: SENDING_REQUEST, sending: false, flag: flags['user-type'], headerColor: flags['header-bar-color']})  }}function idLD (user) {  var ldPromise = Promise.promisify(ld.identify)  return ldPromise(user, null).then(function () {    return ld.allFlags()  })}

So, what do we do with the flags stored in our props? Well, in our Home component, we'll render a different page depending on the flag that was received. In our case, if a fallback flag is received, the “Anonymous” page will be shown no matter who is logged in.

/app/components/Home.jsclass Home extends Component {  render () {    switch( {      case 2:         return()      case 1:         return()      case 0:      default:         return()    }  }}

Meanwhile, every time a page is loaded, the Nav component takes in the headerColor prop and changes the background of our navbar to said color.

/app/components/common/Nav.jsclass Nav extends Component {  ...  render () {    …    let color = this.props.headerColor    return (      <div style={{backgroundColor:color}} className='nav'>        ...      </div>    )  }

See it in action

What's next?There are many ways to feature flag within a single page app, depending on your case and the complexity of your features. You can also check out this repo by TrueCar, which provides another avenue for feature flagging in React Redux.

Want to try out this feature flagging in React for yourself? Check out the open source repo!


Primary Contributor:  Arnold Trakhtenberg, Engineering Content Consultant at LaunchDarkly

You May Like
  •   BEST PRACTICESTesting in Production for Safety and Sanity
  •   BEST PRACTICESWhat Is Continuous Testing? A Straightforward Introduction
APRIL 8, 2021   •   BEST PRACTICES4 Software Release Management Best Practices
APRIL 6, 2021   •   BEST PRACTICESTest Environments 101: Definition, Types, and Best Practices
APRIL 1, 2021   •   Team & NewsWhat to Expect at LaunchDarkly Galaxy
MARCH 30, 2021   •   Team & NewsHow We've Been Celebrating Women’s History Month