Recently, I published a post introducing Recompose — a utility belt for React — in a React VR meditation app called Find Your Zen. You can find the original post here. It covers some awesome aspects of Recompose and functional programming in general. We wound up with a refactored application, but as with all programming projects, there’s still more we can do to improve the code!
In our case, when we left off with our work last time, we were still drilling the same props down through more than one component. How annoying! Fortunately, Recompose gives us the tools to fix that.
Using withContext
and getContext
In Reactland, we have this handy tool called context
, which — the React docs put it nicely — “provides a way to pass data through the component tree without having to pass props down manually at every level.”
Recompose’s withContext
, in conjunction with its getContext
, allows us to easily and cleanly provide context
to the children of a component (often the app’s main component). getContext
, used in a child component, allows us to pull out useful information from the parent context
and provide it to the component at hand — No need to drill props down through the app!
Let’s first create a provider called withStateAndHandlers.js
and move your withState
and withHandlers
composition logic in there. This will allow us to share state and handlers as part of our context
.
// providers/withStateAndHandlers.js import React from 'react'; import { withState, withHandlers, compose } from 'recompose'; const withStateAndHandlers = compose( withState('selectedZen', 'zenClicked', 4), withHandlers({ zenClicked: (props) => (id, evt) => props.zenClicked(selectedZen => id) }), ) export default withStateAndHandlers;
Next, create a withAppContext
provider that will use Recompose’s withContext
and our previously defined withStateAndHandlers
. withContext
takes in two arguments: an object of React prop types (childContextTypes
) and a function that returns the child context
(getChildContext
) so we can access it as needed.
If you’re more familiar with traditional React state management via Redux, Recompose’s withAppContext
essentially allows us to utilize a Provider pattern similar to that in Redux. We will wind up wrapping our main MeditationApp
component with our withAppContext
provider.
Note: Recompose requires us to use prop types to define the shape of our app’s context
data (even if we’re already using TypeScript or Flow).
// providers/withAppContext.js import { withContext, compose } from 'recompose'; import * as PropTypes from 'prop-types'; import withStateAndHandlers from './withStateAndHandlers'; export const AppPropTypes = { selectedZen: PropTypes.number, zenClicked: PropTypes.func, } const AppContext = withContext( AppPropTypes, ({ selectedZen, zenClicked }) => ({ selectedZen, zenClicked, }) ); export default compose( withStateAndHandlers, AppContext, );
And, finally, we’ll construct a usingAppContext
provider which draws on Recompose’s getContext
and our previously defined prop types.
// providers/usingAppContext.js import { getContext } from 'recompose'; import { AppPropTypes } from "./withAppContext"; export default getContext(AppPropTypes);
Now, you can access the context
anywhere you desire within your app. I was able to replace my Pano
component with a WrappedPano
component responsible for retrieving its own data via usingAppContext
:
// components/wrapped-pano.js import React from 'react'; import { Pano } from 'react-vr'; import { usingAppContext } from '../providers/index.js'; import { Audio } from '../components/index.js'; import zens from '../consts/zens.js'; import { asset } from 'react-vr'; export default usingAppContext(({ selectedZen }) => { return ( <Pano source={asset(zens[selectedZen - 1].image)} > <Audio /> </Pano> ) });
While I could simply pass the same selectedZen
prop down to Audio
here, I’d like to make it similarly self-contained with regards to its data. I refactored it as such:
// components/audio.js import React from 'react'; import { Sound } from 'react-vr'; import zens from '../consts/zens.js'; import { compose } from 'recompose'; import { asset } from 'react-vr'; import { hideIf, usingAppContext } from '../providers/index.js'; const hideIfNoAudioUrl = hideIf(({ selectedZen }) => { const zenAudio = zens[selectedZen - 1].audio; return zenAudio === null || zenAudio === undefined || zenAudio.length === 0; }); export default compose( usingAppContext, hideIfNoAudioUrl, )(({ selectedZen }) => { const zenAudio = zens[selectedZen - 1].audio; return ( <Sound source={asset(zenAudio)} /> ) });
You’ll notice I was able to change the hideIf
provider here not to evaluate a url
prop but to use the selectedZen
value directly from context
.
I can then compose usingAppContext
with other components dependent on the selectedZen
information — Menu
, Mantra
, and HomeButton
.
// components/menu.js import React from 'react'; import { hideIf, usingAppContext } from '../providers/index.js'; import { compose } from 'recompose'; import { View } from 'react-vr'; import { Zens, Title } from '../components/index.js'; const hideMenu = hideIf(({ selectedZen }) => selectedZen !== 4); export default compose( usingAppContext, hideMenu, )(({ selectedZen, children }) => { return ( <View style={{marginTop: -0.2, height: 0.2}}> { children } </View> ) });
// components/mantra.js import React from 'react'; import { Text } from 'react-vr'; import zens from '../consts/zens.js'; import { hideIfHome, usingAppContext } from '../providers/index.js'; import { compose } from 'recompose'; export default compose( usingAppContext, hideIfHome, )(({ selectedZen }) => { const text = zens[selectedZen - 1].mantra; return ( <Text style={{ backgroundColor: 'transparent', color: 'lightcyan', fontSize: 0.3, fontWeight: '500', layoutOrigin: [0.5, 0.5], paddingLeft: 0.2, paddingRight: 0.2, textAlign: 'center', textAlignVertical: 'center', transform: [{translate: [0, 0, -3]}], }}> { text } </Text> ) });
// components/button/home-button.js import React from 'react'; import { VrButton, Text, View, } from 'react-vr'; import BaseButton from './base-button.js'; import { usingAppContext } from '../../providers/index.js'; import zens from '../../consts/zens.js'; export default usingAppContext(({ selectedZen, zenClicked }) => { return ( <View style={{marginBottom: 0.2}}> <BaseButton selectedZen={selectedZen} buttonClick={() => zenClicked(4)} text={zens[3].text} textStyle={{ backgroundColor: 'white', color: '#29ECCE', marginTop: 0.05, transform: [{translate: [0, 0, -3]}]}} /> </View> ) });
I can also make the choice to separate the mapping of the zens
into ZenButton
s out into its own component, Zens.js
, which now gets the click handler passed down from context
:
// index.vr.js import React from 'react'; import { ZenButton } from '../components/index.js'; import { usingAppContext } from '../providers/index.js'; import zens from '../consts/zens.js'; import { compose } from 'recompose'; import { View } from 'react-vr'; export default compose( usingAppContext )(({ zenClicked }) => { return ( <View> { zens.map((zen) => ( <ZenButton selectedZen={zen.id} key={zen.id} buttonClick={() => zenClicked(zen.id)} text={zen.text} /> )) } </View> ) })
My updated index.vr.js
component now is no longer responsible for passing data down to its child components. How refreshing!
import React from 'react'; import { AppRegistry, asset, Pano, VrButton, Text, View, Sound, Image, } from 'react-vr'; import zens from './consts/zens.js'; import { Zens, Mantra, Title, Menu, HomeButton, WrappedPano } from './components/index.js'; import { withState, withHandlers, compose } from 'recompose'; import { withAppContext } from './providers/index.js'; const MeditationApp = withAppContext(() => ( <View> <WrappedPano /> <HomeButton /> <Mantra /> <Menu> <Title>Choose your zen</Title> <Zens /> </Menu> </View> )); AppRegistry.registerComponent('MeditationApp', () => MeditationApp);
If I’ve managed to make you fall in love with context
but not functional programming (sad face), check out this helpful article on the Context API. Once experimental, it’s been officially rolled into the newest React release. Redux’s Provider
draws on this exact API, and it’s now stable enough for you to use it safely as a standalone device to help manage/access app state.
What else can Recompose do?
Recompose also comes with withReducer
, mapProps
, which works similarly to React-Redux‘s mapStateToProps
, and a lifecycle
utility for adding lifecycle methods such as componentDidMount
to functional components.
Viewing the finished demo code
$ git clone https://github.com/lilybarrett/find-your-zen.git $ cd find-your-zen $ npm i $ npm start
Navigate to http://localhost:8081/vr/index.html.
Note: As of May 2018, React VR has been revamped and rebranded as React 360. I plan to port my project over soon.
Useful Recompose resources