Did you Redux too soon? React 16.3 context updates
May 28, 2018
The need
When crafting React apps we usually are eager to pull in Redux so that we can isolate the app state from the components and avoid state management that is kind of tedious and iffy due to the fact that some times the state needs to pass through intermediate components that do not even care about that part of the state or even state in general. Redux works great in that but it comes with a lot of boilerplate. React 16.3 introduces a new context API that allows centralised state management without having to inject the same props down to 10000 children.
The concept
I am going to briefly demonstrate how someone can achieve app state management with only React, thusly avoiding pulling in extra libraries when it is deemed unnecessary. Our app is going to be a textfield whose value is going to be synced in different parts of the React component tree without having to pass a single prop to intermediate children. Before getting to code you need to get familiar with the context API. React context consists of:
- a
<Provider />
which injects a value down the Component tree - a
<Consumer />
which consumes a Provider to get its value in a specific component
To the code!
The state
// AppWithState.jsx
import React, { Component, createContext } from 'react'
import App from './App'
const { Provider, Consumer } = createContext('app-state')
class AppWithState extends Component {
constructor(props) {
super(props)
this.state = {
root: { txt: '', sync: true }
}
this.actions = {
updateText: this.updateText,
toggleSync: this.toggleSync
}
}
updateText = txt => {
this.state.root.sync &&
this.setState(state => ({
root: { ...state.root, txt }
}))
}
toggleSync = txt => {
this.setState(state => ({
root: {
...state.root,
sync: !state.root.sync,
txt: !state.root.sync ? txt : state.root.txt
}
}))
}
render() {
return (
<Provider value={{ state: this.state, actions: this.actions }}>
<App />
</Provider>
)
}
}
export { Consumer }
export default AppWithState
<AppWithState>
is our main component which is rendered to the DOM with ReactDOM.render
. It defines the Consumer and Provider by calling createContext
with a default value which is only used when a Consumer does not find a Provider as an ancestor in the tree. The constructor
defines the state
and the actions
to update our state.
The state injection and updating
In a component far, far away:
// Main.jsx
import React from 'react'
import { DefaultButton } from 'office-ui-fabric-react/lib/Button'
import { TextField } from 'office-ui-fabric-react/lib/TextField'
import { Consumer as AppStateConsumer } from '../AppWithState'
const renderMain = ({
state: {
root: { txt, sync }
},
actions: { updateText, toggleSync }
}) => {
return (
<main>
<h1 style={{ marginBottom: 25 }}>{txt || 'Heading'}</h1>
<div style={{ marginBottom: 25 }}>
<DefaultButton
primary={sync}
onClick={() => {
txt.toLowerCase() === 'end'
? updateText('Thank you! Context rocks.')
: toggleSync(this.textFieldRef.value)
}}
>
{sync ? 'Broadcasting' : 'Off'}
</DefaultButton>
</div>
<TextField
label={
sync ? "I 'm broadcasting everywhere" : "I 'm just a simple textfield"
}
componentRef={ref => (this.textFieldRef = ref)}
onChanged={val => updateText(val)}
/>
</main>
)
}
export default () => <AppStateConsumer>{renderMain}</AppStateConsumer>
The <Consumer />
always has a function as its child in order to inject the <Provider />
state to the Component. We pick whatever we need from the state
and use it in our component. Notice that we are also using the actions injected from context in order to update the centralised state which lies in <AppWithState />
when certain events occur like an input changing its text or the click of a button.
The verdict
In the same manner, someone can use the app state
in other components which introduces a clean and efficient pattern of state management by using only React. Someone can realise that this can scale further by introducing state
modules. For example instead of having only one state component (<AppWithState />
), we could have a <RootState />
for the main app state properties and a <UserState />
which would hold everything related to a user session (auth token, IDs, etc.) in the same manner that we split the app state to reducers when using Redux.
It is up to the developer to realize what fits his needs. While the context approach for state management is declarative and clear enough, it binds the app state further to a specific framework (React). Further, the Redux boilerplate and conventions as well as its functional concepts which isolate side effects in reducers enhance the testability and predictability of the codebase which is critical to large scale applications.