Responsive Layouts and Redux, a perfect match.

This is an updated version of my previous post concerning responsive states in javascript. This update targets Redux, a javascript library for managing your data that is heavily influenced by flux. If you haven't checked out yet, I highly recommend it.

Ever since Christopher Chadeau gave this talk on the perils of css, React developers have slowly moved towards using exclusively javascript to handle component styling. While doing this answers each of the 7 issues outlined, there are new problems that we need to deal with which were previously handled quite nicely by css. Namely, media queries and animations. Out of the box, React offers minimal support for animations, but there is still nothing from the team about how to have components respond to browser width.

This post will describe how I manage responsive layouts in React applications. Spoiler: I use a redux reducer.

A Quick Intro to Flux

Flux is an application architecture developed by Facebook as a replacement for the common MVC pattern. The general structure looks something like:

Simply put, Flux is a pub-sub system where views listen to stores and fire actions to cause changes in the store. The dispatcher is there to ensure that only one action is handled at a time. When a store updates, the views that are listening automatically get the new data from the store. Given this, a flux store is a natural place to keep data that is common amongst some/all of your components. A great example of this is the current responsive state of your app.

In this tutorial I will be using redux which is not purely flux but instead relies on a single store that is composed of many smaller stores which are called "reducers." If you haven't given it a chance yet, I highly recommend it. The tutorials are some of the best I've seen in any project.

Why is this better?

There are many solutions for cleanly handling responsive designs in React applications. One common approach is to wrap a component in another component which is responsible for handling the behavior and passing the information down as a prop. While this at first seems good and the "react way", as the behavior gets more complicated, this quickly leads to a lot of boilerplate code in a single component. Also, depending on the implementation, it is possible that many copies of the responsive wrapper would create many different resize handlers.

Managing the responsive state in a single redux reducer not only reduces the overall noise in a component, but also guarantees that only a single event listener is waiting for resize.

So what does this look like?

The Reducer

The first thing we have to do is to create the responsive state reducer that we will add to our store

// reducers/responsiveStateReducer.js

// third party imports
import MediaQuery from 'mediaquery'
import transform from 'lodash/object/transform'
// local imports
import {CALCULATE_RESPONSIVE_STATE} from '../actions/calculateResponsiveState'


// default breakpoints
const breakpoints = {
    extraSmall: 480,
    small: 768,
    medium: 992,
    large: 1200,
}

// media queries associated with the breakpoints
const mediaQueries = MediaQuery.asObject(breakpoints)

// export the reducer factory
export default (state, action) => {
    // if told to recalculate state or state has not yet been initialized
    if (action.type === CALCULATE_RESPONSIVE_STATE || typeof state === 'undefined') {
        // return calculated state
        return {
            width: action.innerWidth,
            lessThan: getLessThan(action.innerWidth, breakpoints),
            greaterThan: getGreaterThan(action.innerWidth, breakpoints),
            mediaType: getMediaType(action.matchMedia, mediaQueries),
        }
    }
    // otherwise return the previous state
    return state
}


/**
 * Compute the `lessThan` object based on the browser width.
 * @arg {number} browserWidth - Width of the browser.
 * @arg {object} breakpoints - The breakpoints object.
 * @returns {object} The `lessThan` object.  Its keys are the same as the
 * keys of the breakpoints object.  The value for each key indicates whether
 * or not the browser width is less than the breakpoint.
 */
function getLessThan(browserWidth, breakpoints) {
    return transform(breakpoints, (result, breakpoint, mediaType) => {
        // if the breakpoint is a number
        if (typeof breakpoint === 'number') {
            // store wether or not it is less than the breakpoint
            result[mediaType] = browserWidth < breakpoint
        } else {
            result[mediaType] = false
        }
    })
}


/**
 * Compute the `greaterThan` object based on the browser width.
 * @arg {number} browserWidth - Width of the browser.
 * @arg {object} breakpoints - The breakpoints object.
 * @returns {object} The `greaterThan` object.  Its keys are the same as the
 * keys of the breakpoints object.  The value for each key indicates whether
 * or not the browser width is greater than the breakpoint.
 */
function getGreaterThan(browserWidth, breakpoints) {
    return transform(breakpoints, (result, breakpoint, mediaType) => {
        // if the breakpoint is a number
        if (typeof breakpoint === 'number') {
            // store wether or not it is greater than the breakpoint
            result[mediaType] = browserWidth > breakpoint
        } else {
            result[mediaType] = false
        }
    })
}


/**
 * Gets the current media type from the global `window`.
 * @arg {object} mediaQueries - The media queries object.
 * @returns {string} The window's current media type.  This is the key of the
 * breakpoint that is the next breakpoint larger than the window.
 */
function getMediaType(matchMedia, mediaQueries) {
    // if there's no window
    if (typeof matchMedia === 'undefined') {
        // return the default
        return defaultMediaType
    }

    // there is a window, so compute the true media type
    return transform(mediaQueries, (result, query, type) => {
        // if the browser matches the media query
        if (matchMedia(query).matches) {
            // use the current media type
            /* eslint-disable no-param-reassign */
            result = type
            /* eslint-enable no-param-reassign */
        }
    })
}

The Action Creator

Now that we have the reducer in place, it's time to add the action creator we will use to dispatch the change:

// action type
export const CALCULATE_RESPONSIVE_STATE = 'CALCULATE_RESPONSIVE_STATE'


/**
 * Action creator taking window-like object and returning action to calculate
 * responsive state.
 * @arg {object} window - Any window-like object (has keys `innerWidth` and
 * `matchMedia`).
 * @arg {number} window.innerWidth - The value for the browser width (to pass to
 * the responsive state reducer logic).  See browser global `window.innerWidth`.
 * @arg {function} window.matchMedia - The method with which to match media
 * queries (to pass to the responsive sate reducer logic).  See global
 * `window.matchMedia`.
 * @returns {object} The resulting action.  Action will have type
 * `CALCULATE_RESPONSIVE_STATE`, and will be directly given the two keys taken
 * from the `window` argument.
 */
export default ({innerWidth, matchMedia} = {}) => {
    return {
        type: CALCULATE_RESPONSIVE_STATE,
        innerWidth,
        matchMedia,
    }

The Listener

Now that we have the reducer and the action creator ready, we need to add the resize event listener to our store so that we can rely on a single event listener for browser resize. In order to do this in a relatively DRY manner, my friend created a function which takes the store and adds an event handler to the window that updates the store with the current information:

// util/addResponsiveHandler.js

// third party imports
import throttle from 'lodash/function/throttle'
// local imports
import calculateResponsiveState from '../actions/calculateResponsiveState'


/**
 * Dispatches an action to calculate the responsive state, then kicks of the
 * (throttled) window resize event listener (which will dispatch further such
 * actions).
 * @arg {object} store - The redux store.  Really you only need to pass `{dispatch}`.
 * @arg {number} [throttleTime=100] - Throttle time (in miliseconds) for the
 * window resize event handler.
 */
export default (store, throttleTime = 100) => {
    // throttled event handler for window resize
    const throttledHandler = throttle(
        // just dispatch action to calculate responsive state
        () => store.dispatch(calculateResponsiveState(
            // annoying existential check for window
            typeof window === 'undefined' ? {} : window
        )),
        throttleTime
    )
    // initialize the responsive state
    throttledHandler()
    // if there is a `window`
    if (typeof window !== 'undefined') {
        // add the resize event listener
        window.addEventListener('resize', () => throttledHandler())
    }
    // return the store so that the call is transparent
    return store
}

With this function defined, adding the event listener to your store is only a few lines of javascript:

import {createStore} from 'redux'
import responsiveStateReducer from 'reducers/responsiveStateReducer'
import addResponsiveHandler from 'util/addResponsiveHandler'

const reducers = combineReducers({
    browser: responsiveStateReducer,
})

const store = createStore(reducers)

// adds window resize event handler
addResponsiveHandlers(store)

export default store

Now you're reducer is ready to capture the responsive state of your javascript application. Go ahead, shrink your browser and see your state change.

Conclusion

In this post I explained how I use a redux reducer to manage the responsive state of my javascript applications. Even though I used React, this reducer would work for any application that is based on redux. For an implementation along with a few extras for more customization, visit my github repository here.

What do you think? Please leave any comments or questions below.

blog comments powered by Disqus