Application State Management with React

May 24, 2019 0 Comments

Application State Management with React

 

 

April 22, 2019

Video Blogger

Photo by Rene Böhmer


How React is all you need to manage your application state

Managing state is arguably the hardest part of any application. It's why thereare so many state management libraries available and more coming around everyday (and even some built on top of others... There are hundreds of "easierredux" abstractions on npm). Despite the fact that state management is a hardproblem, I would suggest that one of the things that makes it so difficult isthat we often over-engineer our solution to the problem.

There's one state management solution that I've personally tried to implementfor as long as I've been using React, and with the release of React hooks (andmassive improvements to React context) this method of state management has beendrastically simplified.

We often talk about React components as lego building blocks to build ourapplications, and I think that when people hear this, they somehow think thisexcludes the state aspect. The "secret" behind my personal solution to the statemanagement problem is to think of how your application's state maps to theapplication's tree structure.

One of the reasons redux was so successful was the fact that react-redux solvedthe prop drilling problem. The fact that you could sharedata across different parts of your tree by simply passing your component intosome magical connect function was wonderful. Its use of reducers/actioncreators/etc. is great too, but I'm convinced that the ubiquity of redux isbecause it solved the prop drilling pain point for developers.

Unfortunately, this led to the reason that I only ever used redux on oneproject: I consistently see developers putting all of their state into redux.Not just global application state, but local state as well. This leads to a lotof problems, not the least of which is that when you're maintaining any stateinteraction, it involves interacting with reducers, action creators/types, anddispatch calls, which ultimately results in having to open many files and tracethrough the code in your head to figure out what's happening and what impact ithas on the rest of the codebase.

To be clear, this is fine for state that is truly global, but for simple state(like whether a modal is open or form input value state) this is a big problem.To make matters worse, it doesn't scale very well. The larger your applicationgets, the harder this problem becomes. Sure you can hook up different reducersto manage different parts of your application, but the indirection of goingthrough all these action creators and reducers is not optimal.

Having all your application state in a single object can also lead to otherproblems, even if you're not using Redux. When a React <Context.Provider> getsa new value, all the components that consume that value are updated and have torender, even if it's a function component that only cares about part of thedata. That might lead to potential performance issues. (React-Redux v6 alsotried to use this approach until they realized it wouldn't work right withhooks, which forced them to use a different approach with v7 to solve theseissues.) But my point is that you don't have this problem if you have your statemore logically separated and located in the react tree closer to where itmatters.

Here's the real kicker, if you're building an application with React, youalready have a state management library installed in your application. You don'teven need to npm install (or yarn add) it. It costs no extra bytes for yourusers, it integrates with all React packages on npm, and it's already welldocumented by the React team. It's React itself.

React is a state management library

When you build a React application, you're assembling a bunch of components tomake a tree of components starting at your <App /> and ending at your<input />s, <div />s and <button />s. You don't manage all of thelow-level composite components that your application renders in one centrallocation. Instead, you let each individual component manage that and it ends upbeing a really effective way to build your UI. You can do this with your stateas well, and it's very likely that you do today:

1function Counter() {

2 const [count, setCount] = React.useState(0)

3 const increment = () => setCount(c => c + 1)

4 return <button onClick={increment}>{count}</button>

5}

6

7function App() {

8 return <Counter />

9}

Edit React Codesandbox

Note that everything I'm talking about here works with class components as well.Hooks just make things a bit easier (especially context which we'll get into ina minute).

1class Counter extends React.Component {

2 state = {count: 0}

3 increment = () => this.setState(({count}) => ({count: count + 1}))

4 render() {

5 return <button onClick={this.increment}>{this.state.count}</button>

6 }

7}

"Ok, Kent, sure having a single element of state managed in a single componentis easy, but what do you do when I need to share that state across components?For example, what if I wanted to do this:"

1function CountDisplay() {

2

3 return <div>The current counter count is {count}</div>

4}

5

6function App() {

7 return (

8 <div>

9 <CountDisplay />

10 <Counter />

11 </div>

12 )

13}

"The count is managed inside <Counter />, now I need a state managementlibrary to access that count value from the <CountDisplay /> and update itin <Counter />!"

The answer to this problem is as old as React itself (older?) and has been inthe docs for as long as I can remember:Lifting State Up

"Lifting State Up" is legitimately the answer to the state management problem inReact and it's a rock solid one. Here's how you apply it to this situation:

1function Counter({count, onIncrementClick}) {

2 return <button onClick={onIncrementClick}>{count}</button>

3}

4

5function CountDisplay({count}) {

6 return <div>The current counter count is {count}</div>

7}

8

9function App() {

10 const [count, setCount] = React.useState(0)

11 const increment = () => setCount(c => c + 1)

12 return (

13 <div>

14 <CountDisplay count={count} />

15 <Counter count={count} onIncrementClick={increment} />

16 </div>

17 )

18}

Edit React Codesandbox

We've just changed who's responsible for our state and it's reallystraightforward. And we could keep lifting state all the way to the top of ourapp.

"Sure Kent, ok, but what about the prop drillingproblem?"

This is one problem that's actually also had a "solution" for a long time, butonly recently was that solution "official" and "blessed." As I said, many peoplereached for react-redux because it solved this problem using the mechanism I'mreferring to without them having to be worried about the warning that was in theReact docs. But now that context is an officially supported part of the ReactAPI, we can use this directly without any problem:

1

2import React from 'react'

3

4const CountContext = React.createContext()

5

6function useCount() {

7 const context = React.useContext(CountContext)

8 if (!context) {

9 throw new Error(useCount must be used within a CountProvider)

10 }

11 return context

12}

13

14function CountProvider(props) {

15 const [count, setCount] = React.useState(0)

16 const value = React.useMemo(() => [count, setCount], [count])

17 return <CountContext.Provider value={value} {...props} />

18}

19

20export {CountProvider, useCount}

21

22

23import React from 'react'

24import {CountProvider, useCount} from './count-context'

25

26function Counter() {

27 const [count, setCount] = useCount()

28 const increment = () => setCount(c => c + 1)

29 return <button onClick={increment}>{count}</button>

30}

31

32function CountDisplay() {

33 const [count] = useCount()

34 return <div>The current counter count is {count}</div>

35}

36

37function CountPage() {

38 return (

39 <div>

40 <CountProvider>

41 <CountDisplay />

42 <Counter />

43 </CountProvider>

44 </div>

45 )

46}

Edit React Codesandbox

NOTE: That particular code example is VERY contrived and I would NOT recommendyou reach for context to solve this specific scenario. Please readProp Drilling to get a better sense for why propdrilling isn't necessarily a problem and is often desirable. Don't reach forcontext too soon!

And what's cool about this approach is that we could put all the logic forcommon ways to update the state in our useContext hook (or directly in contextif you want I guess):

1function useCount() {

2 const context = React.useContext(CountContext)

3 if (!context) {

4 throw new Error(useCount must be used within a CountProvider)

5 }

6 const [count, setCount] = context

7

8 const increment = () => setCount(c => c + 1)

9 return {

10 count,

11 setCount,

12 increment,

13 }

14}

Edit React Codesandbox

And you could easily change this to useReducer rather than useState as well:

1function countReducer(state, action) {

2 switch (action.type) {

3 case 'INCREMENT': {

4 return {count: state.count + 1}

5 }

6 default: {

7 throw new Error(Unsupported action type: </span><span class="token template-string interpolation interpolation-punctuation">${</span><span class="token template-string interpolation">action</span><span class="token template-string interpolation punctuation">.</span><span class="token template-string interpolation">type</span><span class="token template-string interpolation interpolation-punctuation">}</span><span class="token template-string string">)

8 }

9 }

10}

11

12function CountProvider(props) {

13 const [state, dispatch] = React.useReducer(countReducer, {count: 0})

14 const value = React.useMemo(() => [state, dispatch], [state])

15 return <CountContext.Provider value={value} {...props} />

16}

17

18function useCount() {

19 const context = React.useContext(CountContext)

20 if (!context) {

21 throw new Error(useCount must be used within a CountProvider)

22 }

23 const [state, dispatch] = context

24

25 const increment = () => dispatch({type: 'INCREMENT'})

26 return {

27 state,

28 dispatch,

29 increment,

30 }

31}

Edit React Codesandbox

This gives you an immense amount of flexibility and reduces complexity by ordersof magnitude. Here are a few important things to remember when doing things thisway:

  1. Not everything in your application needs to be in a single state object. Keepthings logically separated (user settings does not necessarily have to be inthe same context as notifications). You will have multiple providers withthis approach.
  2. Not all of your context needs to be globally accessible! Keep state asclose to where it's needed as possible.

More on that second point. Your app tree could look something like this:

1function App() {

2 return (

3 <ThemeProvider>

4 <AuthenticationProvider>

5 <Router>

6 <Home path="/" />

7 <About path="/about" />

8 <UserPage path="/:userId" />

9 <UserSettings path="/settings" />

10 <Notifications path="/notifications" />

11 </Router>

12 </AuthenticationProvider>

13 </ThemeProvider>

14 )

15}

16

17function Notifications() {

18 return (

19 <NotificationsProvider>

20 <NotificationsTab />

21 <NotificationsTypeList />

22 <NotificationsList />

23 </NotificationsProvider>

24 )

25}

26

27function UserPage({username}) {

28 return (

29 <UserProvider username={username}>

30 <UserInfo />

31 <UserNav />

32 <UserActivity />

33 </UserProvider>

34 )

35}

36

37function UserSettings() {

38

39 const {user} = useAuthenticatedUser()

40}

Notice that each page can have its own provider that has data necessary for thecomponents underneath it. Code splitting "just works" for this stuff as well.How you get data into each provider is up to the hooks those providers use andhow you retrieve data in your application, but you know just where to startlooking to find out how that works (in the provider).

Conclusion

Again, this is something that you can do with class components (you don't haveto use hooks). Hooks make this much easier, but you could implement thisphilosophy with React 15 no problem. Keep state as local as possible and usecontext only when prop drilling really becomes a problem. Doing things this waywill make it easier for you to maintain state interactions.


Tag cloud