14. Use Actions To Manipulate The Store

14.1. Wiring The Store

Now that we have our store ready it’s time to connect the store to our code and remove all the unneeded functionality. First step is to factor out the Faq component into a separate file called components/Faq.jsx, it is almost a 100% copy of App.js:

  1import React, { Component } from "react";
  2import FaqItem from "./FaqItem";
  3
  4class Faq extends Component {
  5  constructor(props) {
  6    super(props);
  7    this.state = {
  8      faq: [
  9        {
 10          question: "What does the Plone Foundation do?",
 11          answer:
 12            "The mission of the Plone Foundation is to protect and..."
 13        },
 14        {
 15          question: "Why does Plone need a Foundation?",
 16          answer:
 17            "Plone has reached critical mass, with enterprise..."
 18        }
 19      ],
 20      question: "",
 21      answer: ""
 22    };
 23  }
 24
 25  onDelete = (index) => {
 26    let faq = this.state.faq;
 27    faq.splice(index, 1);
 28    this.setState({
 29      faq
 30    });
 31  }
 32
 33  onEdit = (index, question, answer) => {
 34    let faq = this.state.faq;
 35    faq[index] = {
 36      question,
 37      answer
 38    };
 39    this.setState({
 40      faq
 41    });
 42  }
 43
 44  onChangeQuestion = (event) => {
 45    this.setState({
 46      question: event.target.value
 47    });
 48  }
 49
 50  onChangeAnswer = (event) => {
 51    this.setState({
 52      answer: event.target.value
 53    });
 54  }
 55
 56  onSubmit = (event) => {
 57    this.setState({
 58      faq: [
 59        ...this.state.faq,
 60        {
 61          question: this.state.question,
 62          answer: this.state.answer
 63        }
 64      ],
 65      question: "",
 66      answer: ""
 67    });
 68    event.preventDefault();
 69  }
 70
 71  render() {
 72    return (
 73      <div>
 74        <ul>
 75          {this.state.faq.map((item, index) => (
 76            <FaqItem
 77              question={item.question}
 78              answer={item.answer}
 79              index={index}
 80              onDelete={this.onDelete}
 81              onEdit={this.onEdit}
 82            />
 83          ))}
 84        </ul>
 85        <form onSubmit={this.onSubmit}>
 86          <label>
 87            Question:
 88            <input
 89              name="question"
 90              type="text"
 91              value={this.state.question}
 92              onChange={this.onChangeQuestion}
 93            />
 94          </label>
 95          <label>
 96            Answer:
 97            <textarea
 98              name="answer"
 99              value={this.state.answer}
100              onChange={this.onChangeAnswer}
101            />
102          </label>
103          <input type="submit" value="Add" />
104        </form>
105      </div>
106    );
107  }
108}
109
110export default Faq;

Next we will create an App component with just the store and a reference to our newly created Faq component:

 1import React, { Component } from "react";
 2import { Provider } from "react-redux";
 3import { createStore } from "redux";
 4
 5import rootReducer from "./reducers";
 6import Faq from "./components/Faq";
 7
 8import "./App.css";
 9
10const store = createStore(rootReducer);
11
12class App extends Component {
13  render() {
14    return (
15      <Provider store={store}>
16        <Faq />
17      </Provider>
18    );
19  }
20}
21
22export default App;

Differences

--- a/src/App.js
+++ b/src/App.js
@@ -1,114 +1,20 @@
import React, { Component } from "react";
-import FaqItem from "./components/FaqItem";
-import "./App.css";
-
-class App extends Component {
-  constructor(props) {
-    super(props);
-    this.state = {
-      faq: [
-        {
-          question: "What does the Plone Foundation do?",
-          answer:
-            "The mission of the Plone Foundation is to protect and promote Plone. The Foundation provides marketing assistance, awareness, and evangelism assistance to the Plone community. The Foundation also assists with development funding and coordination of funding for large feature implementations. In this way, our role is similar to the role of the Apache Software Foundation and its relationship with the Apache Project."
-        },
-        {
-          question: "Why does Plone need a Foundation?",
-          answer:
-            "Plone has reached critical mass, with enterprise implementations and worldwide usage. The Foundation is able to speak for Plone, and provide strong and consistent advocacy for both the project and the community. The Plone Foundation also helps ensure a level playing field, to preserve what is good about Plone as new participants arrive."
-        }
-      ],
-      question: "",
-      answer: ""
-    };
-  }
+import { Provider } from "react-redux";
+import { createStore } from "redux";

-  onDelete = (index) => {
-    let faq = this.state.faq;
-    faq.splice(index, 1);
-    this.setState({
-      faq
-    });
-  }
-
-  onEdit = (index, question, answer) => {
-    let faq = this.state.faq;
-    faq[index] = {
-      question,
-      answer
-    };
-    this.setState({
-      faq
-    });
-  }
+import rootReducer from "./reducers";
+import Faq from "./components/Faq";

-  onChangeQuestion = (event) => {
-    this.setState({
-      question: event.target.value
-    });
-  }
-
-  onChangeAnswer = (event) => {
-    this.setState({
-      answer: event.target.value
-    });
-  }
+import "./App.css";

-  onSubmit = (event) => {
-    this.setState({
-      faq: [
-        ...this.state.faq,
-        {
-          question: this.state.question,
-          answer: this.state.answer
-        }
-      ],
-      question: "",
-      answer: ""
-    });
-    event.preventDefault();
-  }
+const store = createStore(rootReducer);

+class App extends Component {
  render() {
    return (
-      <div>
-        <ul>
-          {this.state.faq.map((item, index) => (
-            <FaqItem
-              question={item.question}
-              answer={item.answer}
-              index={index}
-              onDelete={this.onDelete}
-              onEdit={this.onEdit}
-            />
-          ))}
-        </ul>
-        <form onSubmit={this.onSubmit}>
-          <label>
-            Question:
-            <input
-              name="question"
-              type="text"
-              value={this.state.question}
-              onChange={this.onChangeQuestion}
-            />
-          </label>
-          <label>
-            Answer:
-            <textarea
-              name="answer"
-              value={this.state.answer}
-              onChange={this.onChangeAnswer}
-            />
-          </label>
-          <input type="submit" value="Add" />
-        </form>
-      </div>
+      <Provider store={store}>
+        <Faq />
+      </Provider>
    );
  }
}

14.2. Use The Data From The Store

Now that we have our store wired we can start using the store data instead of our local state. We will use the helper method connect as a decorator to map both the data and the actions to our components. The connect call takes two parameters; the first is a method which provides the redux state and props and returns an object which will be mapped to props of the component. The second is an object with all the actions which will also be mapped to props on the component.

 3import { connect } from "react-redux";
 4import { addFaqItem } from "../actions";
 5
 6class Faq extends Component {
 7  static propTypes = {
 8    faq: PropTypes.arrayOf(
 9      PropTypes.shape({
10        question: PropTypes.string.isRequired,
11        answer: PropTypes.string.isRequired
12      })
13    ),
14    addFaqItem: PropTypes.func.isRequired
15  };
125export default connect(
126  (state, props) => ({
127    faq: state.faq
128  }),
129  { addFaqItem }
130)(Faq);

We can remove all the edit and delete references since that will be handled by the FaqItem to clean up our code. We will also change the onSubmit handler to use the attached addFaqItem method. The result will be as follows:

 1import React, { Component } from "react";
 2import { connect } from "react-redux";
 3import PropTypes from "prop-types";
 4
 5import FaqItem from "./FaqItem";
 6import { addFaqItem } from "../actions";
 7
 8class Faq extends Component {
 9  static propTypes = {
10    faq: PropTypes.arrayOf(
11      PropTypes.shape({
12        question: PropTypes.string.isRequired,
13        answer: PropTypes.string.isRequired
14      })
15    ),
16    addFaqItem: PropTypes.func.isRequired
17  };
18
19  constructor(props) {
20    super(props);
21    this.state = {
22      question: "",
23      answer: ""
24    };
25  }
26
27  onChangeQuestion = (event) => {
28    this.setState({
29      question: event.target.value
30    });
31  }
32
33  onChangeAnswer = (event) => {
34    this.setState({
35      answer: event.target.value
36    });
37  }
38
39  onSubmit = (event) => {
40    this.props.addFaqItem(this.state.question, this.state.answer);
41    this.setState({
42      question: "",
43      answer: ""
44    });
45    event.preventDefault();
46  }
47
48  render() {
49    return (
50      <div>
51        <ul>
52          {this.props.faq.map((item, index) => (
53            <FaqItem
54              question={item.question}
55              answer={item.answer}
56              index={index}
57            />
58          ))}
59        </ul>
60        <form onSubmit={this.onSubmit}>
61          <label>
62            Question:
63            <input
64              name="question"
65              type="text"
66              value={this.state.question}
67              onChange={this.onChangeQuestion}
68            />
69          </label>
70          <label>
71            Answer:
72            <textarea
73              name="answer"
74              value={this.state.answer}
75              onChange={this.onChangeAnswer}
76            />
77          </label>
78          <input type="submit" value="Add" />
79        </form>
80      </div>
81    );
82  }
83}
84
85export default connect(
86  (state, props) => ({
87    faq: state.faq
88  }),
89  { addFaqItem }
90)(Faq);

Differences

--- a/src/components/Faq.jsx
+++ b/src/components/Faq.jsx
@@ -1,49 +1,32 @@
import React, { Component } from "react";
+import { connect } from "react-redux";
+import PropTypes from "prop-types";
+
import FaqItem from "./FaqItem";
+import { addFaqItem } from "../actions";

class Faq extends Component {
+  static propTypes = {
+    faq: PropTypes.arrayOf(
+      PropTypes.shape({
+        question: PropTypes.string.isRequired,
+        answer: PropTypes.string.isRequired
+      })
+    ),
+    addFaqItem: PropTypes.func.isRequired
+  };
+
  constructor(props) {
    super(props);
    this.state = {
-      faq: [
-        {
-          question: "What does the Plone Foundation do?",
-          answer: "The mission of the Plone Foundation is to protect and..."
-        },
-        {
-          question: "Why does Plone need a Foundation?",
-          answer: "Plone has reached critical mass, with enterprise..."
-        }
-      ],
      question: "",
      answer: ""
    };
  }

-  onDelete = (index) => {
-    let faq = this.state.faq;
-    faq.splice(index, 1);
-    this.setState({
-      faq
-    });
-  }
-
-  onEdit = (index, question, answer) => {
-    let faq = this.state.faq;
-    faq[index] = {
-      question,
-      answer
-    };
-    this.setState({
-      faq
-    });
-  }
-
  onChangeQuestion = (event) => {
    this.setState({
      question: event.target.value
@@ -57,14 +40,8 @@ class Faq extends Component {
  }

  onSubmit = (event) => {
+    this.props.addFaqItem(this.state.question, this.state.answer);
    this.setState({
-      faq: [
-        ...this.state.faq,
-        {
-          question: this.state.question,
-          answer: this.state.answer
-        }
-      ],
      question: "",
      answer: ""
    });
@@ -75,13 +52,11 @@ class Faq extends Component {
    return (
      <div>
        <ul>
-          {this.state.faq.map((item, index) => (
+          {this.props.faq.map((item, index) => (
            <FaqItem
              question={item.question}
              answer={item.answer}
              index={index}
-              onDelete={this.onDelete}
-              onEdit={this.onEdit}
            />
          ))}
        </ul>
@@ -110,4 +85,9 @@ class Faq extends Component {
  }
}

-export default Faq;
+export default connect(
+  (state, props) => ({
+    faq: state.faq
+  }),
+  { addFaqItem }
+)(Faq);

14.3. Exercise

Now that we factored out the edit and delete actions from the Faq component update the FaqItem component to call the actions we created for our store.

Solution

  1import React, { Component } from "react";
  2import PropTypes from "prop-types";
  3import { connect } from "react-redux";
  4
  5import { editFaqItem, deleteFaqItem } from "../actions";
  6
  7import "./FaqItem.css";
  8
  9class FaqItem extends Component {
 10  static propTypes = {
 11    question: PropTypes.string.isRequired,
 12    answer: PropTypes.string.isRequired,
 13    index: PropTypes.number.isRequired,
 14    editFaqItem: PropTypes.func.isRequired,
 15    deleteFaqItem: PropTypes.func.isRequired
 16  };
 17
 18  constructor(props) {
 19    super(props);
 20    this.state = {
 21      show: false,
 22      mode: "view",
 23      question: "",
 24      answer: ""
 25    };
 26  }
 27
 28  toggle = () => {
 29    this.setState({
 30      show: !this.state.show
 31    });
 32  }
 33
 34  onDelete = () => {
 35    this.props.deleteFaqItem(this.props.index);
 36  }
 37
 38  onEdit = () => {
 39    this.setState({
 40      mode: "edit",
 41      question: this.props.question,
 42      answer: this.props.answer
 43    });
 44  }
 45
 46  onChangeQuestion = (event) => {
 47    this.setState({
 48      question: event.target.value
 49    });
 50  }
 51
 52  onChangeAnswer = (event) => {
 53    this.setState({
 54      answer: event.target.value
 55    });
 56  }
 57
 58  onSave = (event) => {
 59    this.setState({
 60      mode: "view"
 61    });
 62    this.props.editFaqItem(
 63      this.props.index,
 64      this.state.question,
 65      this.state.answer
 66    );
 67    event.preventDefault();
 68  }
 69
 70  render() {
 71    return this.state.mode === "edit" ? (
 72      <li className="faq-item">
 73        <form onSubmit={this.onSave}>
 74          <label>
 75            Question:
 76            <input
 77              name="question"
 78              value={this.state.question}
 79              onChange={this.onChangeQuestion}
 80            />
 81          </label>
 82          <label>
 83            Answer:
 84            <textarea
 85              name="answer"
 86              value={this.state.answer}
 87              onChange={this.onChangeAnswer}
 88            />
 89          </label>
 90          <input type="submit" value="Save" />
 91        </form>
 92      </li>
 93    ) : (
 94      <li className="faq-item">
 95        <h2 onClick={this.toggle} className="question">
 96          {this.props.question}
 97        </h2>
 98        {this.state.show && <p>{this.props.answer}</p>}
 99        <button onClick={this.onDelete}>Delete</button>
100        <button onClick={this.onEdit}>Edit</button>
101      </li>
102    );
103  }
104}
105
106export default connect(
107  () => ({}),
108  { editFaqItem, deleteFaqItem }
109)(FaqItem);
--- a/src/components/FaqItem.jsx
+++ b/src/components/FaqItem.jsx
@@ -1,5 +1,9 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
+import { connect } from "react-redux";
+
+import { editFaqItem, deleteFaqItem } from "../actions";
+
import "./FaqItem.css";

class FaqItem extends Component {
@@ -7,8 +11,8 @@ class FaqItem extends Component {
    question: PropTypes.string.isRequired,
    answer: PropTypes.string.isRequired,
    index: PropTypes.number.isRequired,
-    onDelete: PropTypes.func.isRequired,
-    onEdit: PropTypes.func.isRequired
+    editFaqItem: PropTypes.func.isRequired,
+    deleteFaqItem: PropTypes.func.isRequired
  };

  constructor(props) {
@@ -34,7 +38,7 @@ class FaqItem extends Component {
  }

  onDelete = () => {
-    this.props.onDelete(this.props.index);
+    this.props.deleteFaqItem(this.props.index);
  }

  onEdit = () => {
@@ -61,7 +65,11 @@ class FaqItem extends Component {
    this.setState({
      mode: "view"
    });
-    this.props.onEdit(this.props.index, this.state.question, this.state.answer);
+    this.props.editFaqItem(
+      this.props.index,
+      this.state.question,
+      this.state.answer
+    );
    event.preventDefault();
  }

@@ -101,4 +109,7 @@ class FaqItem extends Component {
  }
}

-export default FaqItem;
+export default connect(
+  () => ({}),
+  { editFaqItem, deleteFaqItem }
+)(FaqItem);