- Notifications
You must be signed in to change notification settings - Fork38
Manage the responsive state of your application using a redux reducer
License
AlecAivazis/redux-responsive
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
A redux reducer for managing the responsive state of your application.
// MyComponent.jsimportReactfrom'react'import{connect}from'react-redux'// grab only the responsive state from the store// (assuming you have put the `responsiveStateReducer` under// the key `browser` in your state tree)functionbrowserSelector({browser}){return{browser}}@connect(browserSelector)classMyComponentextendsReact.Component{render(){// grab the responsive state off of propsconst{browser}=this.propsletmessage=`The viewport's current media type is:${browser.mediaType}.`if(browser.lessThan.small){message+='Secret message for viewports smaller than than the "small" breakpoint!'}elseif(browser.lessThan.medium){message+='Secret message for viewports between the "small" and "medium" breakpoints!'}else{message+='Message for viewports greater than the "medium" breakpoint.'}return(<p>{message}</p>)}}
redux-responsivedoes not require that you use React as your view library. However, since that is what is commonly used alongside redux, this documentation employs common React patterns.
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.
Using a specialized store not only reduces the overall noise in a component, but also guarantees that only a single event listener is listening for resize.
First, add the reducer to the root of your reducer tree (you can name it whatever you want).
// reducer.jsimport{combineReducers}from'redux'import{responsiveStateReducer}from'redux-responsive'exportdefaultcombineReducers({browser:responsiveStateReducer,})
Second, you must add required event handlers to keep the responsive state up to date.redux-responsive usesMediaQueryLists to efficiently update thestore only when required. To do this, use the provided store enhancer.
// store.jsimport{createStore}from'redux'import{responsiveStoreEnhancer}from'redux-responsive'importreducerfrom'./reducer'conststore=createStore(reducer,responsiveStoreEnhancer)// or, if you have an initial state for the storeconststore=createStore(reducer,initialState,responsiveStoreEnhancer)exportdefaultstore
Note that if you are also using somemiddlewares, the call will look more like this:
import{createStore,applyMiddleware,compose}from'redux'import{responsiveStoreEnhancer}from'redux-responsive'importreducerfrom'./reducer'conststore=createStore(reducer,compose(responsiveStoreEnhancer,applyMiddleware(middleware1,middleware2)))// or, if you have an initial state for the storeconststore=createStore(reducer,initialState,compose(responsiveStoreEnhancer,applyMiddleware(middleware1,middleware2)))exportdefaultstore
Now your store is ready to use. The store's default breakpoints match common device sizes and are accessible by the following names in your view:
constdefaultBreakpoints={extraSmall:480,small:768,medium:992,large:1200,}
TheresponsiveStateReducer (and the reducer returned bycreateResponsiveStateReducer) adds an object with the following keys to the store:
mediaType: (string) The largest breakpoint category that the browser satisfies.orientation: (string) The browser orientation. Has three possible values: "portrait", "landscape", ornull.lessThan: (object) An object of booleans that indicate whether the browser is currently less than a particular breakpoint.greaterThan: (object) An object of booleans that indicate whether the browser is currently greater than a particular breakpoint.is: (object) An object of booleans that indicate whether the browser is current that particular breakpoint.
For example, if you put the responsive state under the keybrowser (as is done in the examples above) then you can access the browser's width and current media type, and determine if the browser is wider than the medium breakpoint like so
// get the current state from the storeconststate=store.getState()// browser media type (e.g. "large")state.browser.mediaType// browser orientation (takes a null value on desktops)state.browser.orientation// true if browser width is greater than the "medium" breakpointstate.browser.greaterThan.medium// true if browser.mediaType === 'small'state.browser.is.small
You can also create your own reducer based on custom breakpoints:
// reducer.jsimport{combineReducers}from'redux'import{createResponsiveStateReducer}from'redux-responsive'exportdefaultcombineReducers({browser:createResponsiveStateReducer({extraSmall:500,small:700,medium:1000,large:1280,extraLarge:1400,}),})
When the browser is wider than the largest breakpoint, it'smediaType value isinfinity. In order tochange this value, add theinfinity field to the object pass as a second argument tocreateResponsiveStateReducer:
// reducer.jsimport{combineReducers}from'redux'import{createResponsiveStateReducer}from'redux-responsive'exportdefaultcombineReducers({// passing null to the reducer factory uses the default breakpointsbrowser:createResponsiveStateReducer(null,{infinity:"veryBig"})})
In some cases, you may want to add computed fields to the responsive state. For example,an application may frequently need to know when the browser isgreaterThanOrEqual toa particular breakpoint. In order to support this,redux-responsive lets you pass afunction and tocreateResponsiveStateReducer arguments as theextraFields key.This function will recieve an object with the responsive state and returns an objectwith the new keys to be injected into the state whenever it is recalculated:
// reducer.jsimport{combineReducers}from'redux'import{createResponsiveStateReducer}from'redux-responsive'import{transform}from'lodash'exportdefaultcombineReducers({browser:createResponsiveStateReducer(null,{extraFields:({ greaterThan, is})=>({// greaterThanOrEqual is built by transforming greaterThangreaterThanOrEqual:transform(greaterThan,(result,value,mediaType)=>{// and combining the value with the `is` fieldresult[mediaType]=value||is[mediaType]},{})}),})})
In some cases, you may want to have awindow attributes tracked in your responsive state (for example,width).To accomplish this, the first step is to add the custom field as described above.
// reducer.jsimport{combineReducers}from'redux'import{createResponsiveStateReducer}from'redux-responsive'import{transform}from'lodash'exportdefaultcombineReducers({browser:createResponsiveStateReducer(null,{extraFields:()=>({width:window.innerWidth}),})})
When doing this, keep in mind that the responsive state enhancer only causes theresponsive state to be recalculated when the browser actually transitions betweenstates.It does not recalculate on every resize. Therefore, you might also needto add an event handler that recalculates the state at another time:
// store.jsimport{calculateResponsiveState}from'redux-responsive'conststore= ...window.addEventListener('resize',()=>store.dispatch(calculateResponsiveState(window)))
Isomorphic applications must make sure that the sever-rendered markup matches theDOM rendered by the client. Setting thecalculateInitialState option in thecreateResponsiveStoreEnhancer factory method tofalse tells the reducerto skip the initial responsive state calculation. The responsive state willcontain the default values on both the server and the client side.
// store/configureStore.jsimport{createStore}from'redux'import{createResponsiveStoreEnhancer}from'redux-responsive'importreducerfrom'./reducer'conststore=createStore(reducer,createResponsiveStoreEnhancer({calculateInitialState:false}))exportdefaultstore
The application should explicitly dispatch the action to recalculate the responsivestate when the application is rendered by the client.
// client.js// external importsimportReactDOMfrom'react-dom'import{calculateResponsiveState}from'redux-responsive'// local importsimportstorefrom'path/to/store'// render the applicationReactDOM.render(<Providerstore={store}> // ...</Provider>,document.getElementById('app'))// calculate the initial statestore.dispatch(calculateResponsiveState(window))
If you know the initial media type for your application (by doing something like looking atthe user-agent) you can set the initial media type with theinitialMediaType key to thereducer factory:
constreducer=createResponsiveStateReducer(null,{initialMediaType:'small'})
When building responsive applications in react, it's common toimplement styles for each breakpoint and then apply them like so:
constcommonStyle={...}conststyles={element:{ ...commonStyle,color:'blue',},elementThin:{ ...commonStyle,color:'black',}}// somewhere in your component...<divstyle={browser.lessThan.medium ?styles.elementThin :styles.element}/>
However this becomes very repetitive rather quickly. To help, redux-responsiveprovides a higher-order component for managing these styles. TheStyleSheethigher-order component takes a function of two arguments, the current state of theresponsive reducer, and any props passed to the component. The follow isequivalent to the logic above:
import{StyleSheet}from'redux-responsive/react'conststylesheet=(browser,props)=>({element:{color:'blue',_lessThan_medium:{color:'black',}}})constcomponent=StyleSheet(stylesheet)(({styles})=>(<divstyle={styles.element}/>))
This library supports usingredux-immutable to make theroot of your state an Immutable.js Map or Record.
However, transforming the branch of state managed byredux-responsive into Immutable data is not supported, because theredux-responsive reducer expects vanilla JS. Please keep this in mind if you're using SSR and transforming your state before hydrating.
redux-responsive is returning the wrong breakpoint on mobile devices, what may be causing this?
This may be caused by not having the
<meta name="viewport">tag in the head of your HTML. Adding this code may resolve your issue:<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0">. Read morehere on how it works.
Semver is followed as closely as possible. For updates and migration instructions, see thechangelog.
About
Manage the responsive state of your application using a redux reducer
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors15
Uh oh!
There was an error while loading.Please reload this page.