State Management in React and beyond

You have already learned the basics of state management in React in the previous chapters. This chapter digs a bit deeper into the topic. You will learn best practices, how to apply them and why you could consider using a third-party state management library.

Lifting State

Only the App component is a stateful ES6 component in your application. It handles a lot of application state and logic in its class methods. Maybe you have noticed that you pass a lot of properties to your Table component. Most of these props are only used in the Table component. In conclusion one could argue that it makes no sense that the App component knows about them.

The whole sort functionality is only used in the Table component. You could move it into the Table component, because the App component doesn’t need to know about it at all. The process of refactoring substate from one component to another is known as lifting state. In your case, you want to move state that isn’t used in the App component into the Table component. The state moves down from parent to child component.

In order to deal with state and class methods in the Table component, it has to become an ES6 class component. The refactoring from functional stateless component to ES6 class component is straight forward.

Your Table component as a functional stateless component:

{title=”src/App.js”,lang=javascript}

  1. const Table = ({
  2. list,
  3. sortKey,
  4. isSortReverse,
  5. onSort,
  6. onDismiss
  7. }) => {
  8. const sortedList = SORTS[sortKey](list);
  9. const reverseSortedList = isSortReverse
  10. ? sortedList.reverse()
  11. : sortedList;
  12. return(
  13. ...
  14. );
  15. }

Your Table component as an ES6 class component:

{title=”src/App.js”,lang=javascript}

  1. # leanpub-start-insert
  2. class Table extends Component {
  3. render() {
  4. const {
  5. list,
  6. sortKey,
  7. isSortReverse,
  8. onSort,
  9. onDismiss
  10. } = this.props;
  11. const sortedList = SORTS[sortKey](list);
  12. const reverseSortedList = isSortReverse
  13. ? sortedList.reverse()
  14. : sortedList;
  15. return(
  16. ...
  17. );
  18. }
  19. }
  20. # leanpub-end-insert

Since you want to deal with state and methods in your component, you have to add a constructor and initial state.

{title=”src/App.js”,lang=javascript}

  1. class Table extends Component {
  2. # leanpub-start-insert
  3. constructor(props) {
  4. super(props);
  5. this.state = {};
  6. }
  7. # leanpub-end-insert
  8. render() {
  9. ...
  10. }
  11. }

Now you can move state and class methods regarding the sort functionality from your App component down to your Table component.

{title=”src/App.js”,lang=javascript}

  1. class Table extends Component {
  2. constructor(props) {
  3. super(props);
  4. # leanpub-start-insert
  5. this.state = {
  6. sortKey: 'NONE',
  7. isSortReverse: false,
  8. };
  9. this.onSort = this.onSort.bind(this);
  10. # leanpub-end-insert
  11. }
  12. # leanpub-start-insert
  13. onSort(sortKey) {
  14. const isSortReverse = this.state.sortKey === sortKey && !this.state.isSortReverse;
  15. this.setState({ sortKey, isSortReverse });
  16. }
  17. # leanpub-end-insert
  18. render() {
  19. ...
  20. }
  21. }

Don’t forget to remove the moved state and onSort() class method from your App component.

{title=”src/App.js”,lang=javascript}

  1. class App extends Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {
  5. results: null,
  6. searchKey: '',
  7. searchTerm: DEFAULT_QUERY,
  8. error: null,
  9. isLoading: false,
  10. };
  11. this.setSearchTopStories = this.setSearchTopStories.bind(this);
  12. this.fetchSearchTopStories = this.fetchSearchTopStories.bind(this);
  13. this.onDismiss = this.onDismiss.bind(this);
  14. this.onSearchSubmit = this.onSearchSubmit.bind(this);
  15. this.onSearchChange = this.onSearchChange.bind(this);
  16. this.needsToSearchTopStories = this.needsToSearchTopStories.bind(this);
  17. }
  18. ...
  19. }

Additionally, you can make the Table component API more lightweight. Remove the props that are passed to it from the App component, because they are handled internally in the Table component now.

{title=”src/App.js”,lang=javascript}

  1. class App extends Component {
  2. ...
  3. render() {
  4. # leanpub-start-insert
  5. const {
  6. searchTerm,
  7. results,
  8. searchKey,
  9. error,
  10. isLoading
  11. } = this.state;
  12. # leanpub-end-insert
  13. ...
  14. return (
  15. <div className="page">
  16. ...
  17. # leanpub-start-insert
  18. <Table
  19. list={list}
  20. onDismiss={this.onDismiss}
  21. />
  22. # leanpub-end-insert
  23. ...
  24. </div>
  25. );
  26. }
  27. }

Now in your Table component you can use the internal onSort() method and the internal Table state.

{title=”src/App.js”,lang=javascript}

  1. class Table extends Component {
  2. ...
  3. render() {
  4. # leanpub-start-insert
  5. const {
  6. list,
  7. onDismiss
  8. } = this.props;
  9. const {
  10. sortKey,
  11. isSortReverse,
  12. } = this.state;
  13. # leanpub-end-insert
  14. const sortedList = SORTS[sortKey](list);
  15. const reverseSortedList = isSortReverse
  16. ? sortedList.reverse()
  17. : sortedList;
  18. return(
  19. <div className="table">
  20. <div className="table-header">
  21. <span style={{ width: '40%' }}>
  22. <Sort
  23. sortKey={'TITLE'}
  24. # leanpub-start-insert
  25. onSort={this.onSort}
  26. # leanpub-end-insert
  27. activeSortKey={sortKey}
  28. >
  29. Title
  30. </Sort>
  31. </span>
  32. <span style={{ width: '30%' }}>
  33. <Sort
  34. sortKey={'AUTHOR'}
  35. # leanpub-start-insert
  36. onSort={this.onSort}
  37. # leanpub-end-insert
  38. activeSortKey={sortKey}
  39. >
  40. Author
  41. </Sort>
  42. </span>
  43. <span style={{ width: '10%' }}>
  44. <Sort
  45. sortKey={'COMMENTS'}
  46. # leanpub-start-insert
  47. onSort={this.onSort}
  48. # leanpub-end-insert
  49. activeSortKey={sortKey}
  50. >
  51. Comments
  52. </Sort>
  53. </span>
  54. <span style={{ width: '10%' }}>
  55. <Sort
  56. sortKey={'POINTS'}
  57. # leanpub-start-insert
  58. onSort={this.onSort}
  59. # leanpub-end-insert
  60. activeSortKey={sortKey}
  61. >
  62. Points
  63. </Sort>
  64. </span>
  65. <span style={{ width: '10%' }}>
  66. Archive
  67. </span>
  68. </div>
  69. { reverseSortedList.map((item) =>
  70. ...
  71. )}
  72. </div>
  73. );
  74. }
  75. }

Your application should still work. But you made a crucial refactoring. You moved functionality and state closer into another component. Other components got more lightweight again. Additionally the component API of the Table got more lightweight because it deals internally with the sort functionality.

The process of lifting state can go the other way as well: from child to parent component. It is called as lifting state up. Imagine you were dealing with internal state in a child component. Now you want to fulfill a requirement to show the state in your parent component as well. You would have to lift up the state to your parent component. But it goes even further. Imagine you want to show the state in a sibling component of your child component. Again you would have to lift the state up to your parent component. The parent component deals with the internal state, but exposes it to both child components.

Exercises:

Revisited: setState()

So far, you have used React setState() to manage your internal component state. You can pass an object to the function where you can update partially the internal state.

{title=”Code Playground”,lang=”javascript”}

  1. this.setState({ foo: bar });

But setState() doesn’t take only an object. In its second version, you can pass a function to update the state.

{title=”Code Playground”,lang=”javascript”}

  1. this.setState((prevState, props) => {
  2. ...
  3. });

Why should you want to do that? There is one crucial use case where it makes sense to use a function over an object. It is when you update the state depending on the previous state or props. If you don’t use a function, the internal state management can cause bugs.

But why does it cause bugs to use an object over a function when the update depends on the previous state or props? The React setState() method is asynchronous. React batches setState() calls and executes them eventually. It can happen that the previous state or props changed in between when you would rely on it in your setState() call.

{title=”Code Playground”,lang=”javascript”}

  1. const { fooCount } = this.state;
  2. const { barCount } = this.props;
  3. this.setState({ count: fooCount + barCount });

Imagine that fooCount and barCount, thus the state or the props, change somewhere else asynchronously when you call setState(). In a growing application, you have more than one ‘setState()’ call across your application. Since setState() executes asynchronously, you could rely in the example on stale values.

With the function approach, the function in setState() is a callback that operates on the state and props at the time of executing the callback function. Even though setState() is asynchronous, with a function it takes the state and props at the time when it is executed.

{title=”Code Playground”,lang=”javascript”}

  1. this.setState((prevState, props) => {
  2. const { fooCount } = prevState;
  3. const { barCount } = props;
  4. return { count: fooCount + barCount };
  5. });

Now, lets get back to your code to fix this behavior. Together we will fix it for one place where setState() is used and relies on the state or props. Afterward, you are able to fix it at other places too.

The setSearchTopStories() method relies on the previous state and thus is a perfect example to use a function over an object in setState(). Right now, it looks like the following code snippet.

{title=”src/App.js”,lang=javascript}

  1. setSearchTopStories(result) {
  2. const { hits, page } = result;
  3. const { searchKey, results } = this.state;
  4. const oldHits = results && results[searchKey]
  5. ? results[searchKey].hits
  6. : [];
  7. const updatedHits = [
  8. ...oldHits,
  9. ...hits
  10. ];
  11. this.setState({
  12. results: {
  13. ...results,
  14. [searchKey]: { hits: updatedHits, page }
  15. },
  16. isLoading: false
  17. });
  18. }

You extract values from the state, but update the state depending on the previous state asynchronously. Now you can use the functional approach to prevent bugs because of a stale state.

{title=”src/App.js”,lang=javascript}

  1. setSearchTopStories(result) {
  2. const { hits, page } = result;
  3. # leanpub-start-insert
  4. this.setState(prevState => {
  5. ...
  6. });
  7. # leanpub-end-insert
  8. }

You can move the whole block that you have already implemented into the function. You only have to exchange that you operate on the prevState rather than this.state.

{title=”src/App.js”,lang=javascript}

  1. setSearchTopStories(result) {
  2. const { hits, page } = result;
  3. this.setState(prevState => {
  4. # leanpub-start-insert
  5. const { searchKey, results } = prevState;
  6. const oldHits = results && results[searchKey]
  7. ? results[searchKey].hits
  8. : [];
  9. const updatedHits = [
  10. ...oldHits,
  11. ...hits
  12. ];
  13. return {
  14. results: {
  15. ...results,
  16. [searchKey]: { hits: updatedHits, page }
  17. },
  18. isLoading: false
  19. };
  20. # leanpub-end-insert
  21. });
  22. }

That will fix the issue with a stale state. There is one more improvement. Since it is a function, you can extract the function for an improved readability. That’s one more advantage to use a function over an object. The function can live outside of the component. But you have to use a higher order function to pass the result to it. After all, you want to update the state based on the fetched result from the API.

{title=”src/App.js”,lang=javascript}

  1. setSearchTopStories(result) {
  2. const { hits, page } = result;
  3. this.setState(updateSearchTopStoriesState(hits, page));
  4. }

The updateSearchTopStoriesState() function has to return a function. It is a higher order function. You can define this higher order function outside of your App component. Note how the function signature changes slightly now.

{title=”src/App.js”,lang=javascript}

  1. # leanpub-start-insert
  2. const updateSearchTopStoriesState = (hits, page) => (prevState) => {
  3. const { searchKey, results } = prevState;
  4. const oldHits = results && results[searchKey]
  5. ? results[searchKey].hits
  6. : [];
  7. const updatedHits = [
  8. ...oldHits,
  9. ...hits
  10. ];
  11. return {
  12. results: {
  13. ...results,
  14. [searchKey]: { hits: updatedHits, page }
  15. },
  16. isLoading: false
  17. };
  18. };
  19. # leanpub-end-insert
  20. class App extends Component {
  21. ...
  22. }

That’s it. The function over an object approach in setState() fixes potential bugs yet increases readability and maintainability of your code. Furthermore, it becomes testable outside of the App component. You could export it and write a test for it as exercise.

Exercise:

  • read more about React using state correctly
  • refactor all setState() methods to use a function
    • but only when it makes sense, because it relies on props or state
  • run your tests again and verify that everything is up to date

Taming the State

The previous chapters have shown you that state management can be a crucial topic in larger applications. In general, not only React but a lot of SPA frameworks struggle with it. Applications got more complex in the recent years. One big challenge in web applications nowadays is to tame and control the state.

Compared to other solutions, React already made a big step forward. The unidirectional data flow and a simple API to manage state in a component are indispensable. These concepts make it easier to reason about your state and your state changes. It makes it easier to reason about it on a component level and to a certain degree on a application level.

In a growing application, it gets harder to reason about state changes. You can introduce bugs by operating on stale state when using an object over a function in setState(). You have to lift state around to share necessary or hide unnecessary state across components. It can happen that a component needs to lift up state, because its sibling component depends on it. Perhaps the component is far away in the component tree and thus you have to share the state across the whole component tree. In conclusion components get involved to a greater extent in state management. But after all, the main responsibility of components should be representing the UI, shouldn’t it?

Because of all these reasons, there exist standalone solutions to take care of the state management. These solutions are not only used in React. However, that’s what makes the React ecosystem such a powerful place. You can use different solutions to solve your problems. To address the problem of scaling state management, you might have heard of the libraries Redux or MobX. You can use either of these solutions in a React application. They come with extensions, react-redux and mobx-react, to integrate them into the React view layer.

Redux and MobX are outside of the scope of this book. When you have finished the book, you will get guidance on how you can continue to learn React and its ecosystem. One learning path could be to learn Redux. Before you dive into the topic of external state management, I can recommend to read this article. It aims to give you a better understanding of how to learn external state management.

Exercises:

{pagebreak}

You have learned advanced state management in React! Let’s recap the last chapters:

  • React
    • lift state management up and down to suitable components
    • setState can use a function to prevent stale state bugs
    • existing external solutions that help you to tame the state

You can find the source code in the official repository.