15. Use Actions To Manipulate The Store#

15.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. The first step is to factor out the Faq component into a separate file called components/Faq.jsx. It is almost an exact copy of App.js:

 1import { useState } from "react";
 2import FaqItem from "./FaqItem";
 3
 4function Faq() {
 5  const [faqList, setFaqList] = useState([
 6    {
 7      question: "What does the Plone Foundation do?",
 8      answer: "The mission of the Plone Foundation is to protect and...",
 9    },
10    {
11      question: "Why does Plone need a Foundation?",
12      answer: "Plone has reached critical mass, with enterprise...",
13    },
14  ]);
15
16  const [question, setQuestion] = useState("");
17  const [answer, setAnswer] = useState("");
18
19  const onDelete = (index) => {
20    let faq = [...faqList];
21    faq.splice(index, 1);
22    setFaqList(faq);
23  };
24
25  const onChangeAnswer = (e) => {
26    setAnswer(e.target.value);
27  };
28
29  const onChangeQuestion = (e) => {
30    setQuestion(e.target.value);
31  };
32
33  const onEdit = (index, question, answer) => {
34    const faq = [...faqList];
35    faq[index] = { question, answer };
36    setFaqList(faq);
37  };
38
39  const onSubmit = (e) => {
40    e.preventDefault();
41    setFaqList([...faqList, { question, answer }]);
42    setQuestion("");
43    setAnswer("");
44  };
45
46  return (
47    <div>
48      <ul>
49        {faqList.map((item, index) => (
50          <FaqItem
51            key={index}
52            question={item.question}
53            answer={item.answer}
54            index={index}
55            onDelete={onDelete}
56            onEdit={onEdit}
57          />
58        ))}
59      </ul>
60      <form onSubmit={onSubmit}>
61        <label>
62          Question:{" "}
63          <input
64            name="question"
65            type="text"
66            value={question}
67            onChange={onChangeQuestion}
68          />
69        </label>
70        <label>
71          Answer:{" "}
72          <textarea name="answer" value={answer} onChange={onChangeAnswer} />
73        </label>
74        <input type="submit" value="Add" />
75      </form>
76    </div>
77  );
78}
79
80export default Faq;

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

 1import { Provider } from "react-redux";
 2import { createStore } from "redux";
 3
 4import rootReducer from "./reducers";
 5import Faq from "./components/Faq";
 6
 7import "./App.css";
 8
 9const store = createStore(rootReducer);
10
11const App = () => {
12  return (
13    <Provider store={store}>
14      <Faq />
15    </Provider>
16  );
17};
18
19export default App;
Differences
--- a/src/App.js
+++ b/src/App.js
@@ -1,81 +1,19 @@
-import { useState } from "react";
-import "./App.css";
-import FaqItem from "./components/FaqItem";
-
-function App() {
-  const [faqList, setFaqList] = useState([
-    {
-      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...",
-    },
-  ]);
-
-  const [question, setQuestion] = useState("");
-  const [answer, setAnswer] = useState("");
+import { Provider } from "react-redux";
+import { createStore } from "redux";

-  const onDelete = (index) => {
-    let faq = [...faqList];
-    faq.splice(index, 1);
-    setFaqList(faq);
-  };
+import rootReducer from "./reducers";
+import Faq from "./components/Faq";

-  const onChangeAnswer = (e) => {
-    setAnswer(e.target.value);
-  };
-
-  const onChangeQuestion = (e) => {
-    setQuestion(e.target.value);
-  };
-
-  const onEdit = (index, question, answer) => {
-    const faq = [...faqList];
-    faq[index] = { question, answer };
-    setFaqList(faq);
-  };
+import "./App.css";

-  const onSubmit = (e) => {
-    e.preventDefault();
-    setFaqList([...faqList, { question, answer }]);
-    setQuestion("");
-    setAnswer("");
-  };
+const store = createStore(rootReducer);

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

 export default App;

15.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 hook useSelector for extracting the data from the store, and useDispatch for dispatching the action which is needed by the component.

2import { useSelector,useDispatch } from "react-redux";
3import { addFaqItem } from "../actions";

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

 1import { useState } from "react";
 2import { useSelector, useDispatch } from "react-redux";
 3
 4import { addFaqItem } from "../actions";
 5import FaqItem from "./FaqItem";
 6
 7function Faq() {
 8  const faqList = useSelector((state) => state.faq);
 9  const dispatch = useDispatch();
10
11  const [question, setQuestion] = useState("");
12  const [answer, setAnswer] = useState("");
13
14  const onChangeAnswer = (e) => {
15    setAnswer(e.target.value);
16  };
17
18  const onChangeQuestion = (e) => {
19    setQuestion(e.target.value);
20  };
21
22  const onSubmit = (e) => {
23    e.preventDefault();
24    setQuestion("");
25    dispatch(addFaqItem(question, answer));
26    setAnswer("");
27  };
28
29  return (
30    <div>
31      <ul>
32        {faqList.map((item, index) => (
33          <FaqItem
34            key={index}
35            question={item.question}
36            answer={item.answer}
37            index={index}
38          />
39        ))}
40      </ul>
41      <form onSubmit={onSubmit}>
42        <label>
43          Question:{" "}
44          <input
45            name="question"
46            type="text"
47            value={question}
48            onChange={onChangeQuestion}
49          />
50        </label>
51        <label>
52          Answer:{" "}
53          <textarea name="answer" value={answer} onChange={onChangeAnswer} />
54        </label>
55        <input type="submit" value="Add" />
56      </form>
57    </div>
58  );
59}
60
61export default Faq;
Differences
--- a/src/components/Faq.jsx
+++ b/src/components/Faq.jsx
@@ -1,27 +1,16 @@
 import { useState } from "react";
+import { useSelector, useDispatch } from "react-redux";
+
+import { addFaqItem } from "../actions";
 import FaqItem from "./FaqItem";

 function Faq() {
-  const [faqList, setFaqList] = useState([
-    {
-      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...",
-    },
-  ]);
+  const faqList = useSelector((state) => state.faq);
+  const dispatch = useDispatch();

   const [question, setQuestion] = useState("");
   const [answer, setAnswer] = useState("");

-  const onDelete = (index) => {
-    let faq = [...faqList];
-    faq.splice(index, 1);
-    setFaqList(faq);
-  };
-
   const onChangeAnswer = (e) => {
     setAnswer(e.target.value);
   };
@@ -30,16 +19,10 @@ function Faq() {
     setQuestion(e.target.value);
   };

-  const onEdit = (index, question, answer) => {
-    const faq = [...faqList];
-    faq[index] = { question, answer };
-    setFaqList(faq);
-  };
-
   const onSubmit = (e) => {
     e.preventDefault();
-    setFaqList([...faqList, { question, answer }]);
     setQuestion("");
+    dispatch(addFaqItem(question, answer));
     setAnswer("");
   };

@@ -51,8 +34,6 @@ function Faq() {
             question={item.question}
             answer={item.answer}
             index={index}
-            onDelete={onDelete}
-            onEdit={onEdit}
           />
         ))}
       </ul>

15.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 { useState } from "react";
 2import { useDispatch } from "react-redux";
 3import PropTypes from "prop-types";
 4
 5import { editFaqItem, deleteFaqItem } from "../actions";
 6import "./FaqItem.css";
 7
 8const FaqItem = (props) => {
 9  const [isAnswer, setAnswer] = useState(false);
10  const [isEditMode, setIsEditMode] = useState(false);
11  const [question, setQuestion] = useState("");
12  const [answer, setQuestionAnswer] = useState("");
13  const dispatch = useDispatch();
14
15  const toggle = () => {
16    setAnswer(!isAnswer);
17  };
18  const ondelete = () => {
19    dispatch(deleteFaqItem(props.index));
20  };
21
22  const onEdit = () => {
23    setIsEditMode(true);
24    setQuestionAnswer(props.answer);
25    setQuestion(props.question);
26  };
27
28  const onChangeAnswer = (e) => {
29    setQuestionAnswer(e.target.value);
30  };
31  const onChangeQuestion = (e) => {
32    setQuestion(e.target.value);
33  };
34
35  const onSave = (e) => {
36    e.preventDefault();
37    setIsEditMode(false);
38    dispatch(editFaqItem(props.index, question, answer));
39  };
40
41  return (
42    <>
43      {isEditMode ? (
44        <li className="faq-item">
45          <form onSubmit={onSave}>
46            <label>
47              Question:
48              <input
49                name="question"
50                value={question}
51                onChange={onChangeQuestion}
52              />
53            </label>
54            <label>
55              Answer:
56              <textarea
57                name="answer"
58                value={answer}
59                onChange={onChangeAnswer}
60              />
61            </label>
62            <input type="submit" value="Save" />
63          </form>
64        </li>
65      ) : (
66        <li className="faq-item">
67          <h2 className="question" onClick={toggle}>
68            {props.question}
69          </h2>
70          {isAnswer && <p>{props.answer}</p>}
71          <button onClick={ondelete}>Delete</button>
72          <button onClick={onEdit}>Edit</button>
73        </li>
74      )}
75    </>
76  );
77};
78
79FaqItem.propTypes = {
80  question: PropTypes.string.isRequired,
81  answer: PropTypes.string.isRequired,
82  index: PropTypes.number.isRequired,
83};
84
85export default FaqItem;
--- a/src/components/FaqItem.jsx
+++ b/src/components/FaqItem.jsx
@@ -1,18 +1,22 @@
 import { useState } from "react";
-import "./FaqItem.css";
+import { useDispatch } from "react-redux";
 import PropTypes from "prop-types";

+import { editFaqItem, deleteFaqItem } from "../actions";
+import "./FaqItem.css";
+
 const FaqItem = (props) => {
   const [isAnswer, setAnswer] = useState(false);
   const [isEditMode, setIsEditMode] = useState(false);
   const [question, setQuestion] = useState("");
   const [answer, setQuestionAnswer] = useState("");
+  const dispatch = useDispatch();

   const toggle = () => {
     setAnswer(!isAnswer);
   };
   const ondelete = () => {
-    props.onDelete(props.index);
+    dispatch(deleteFaqItem(props.index));
   };

   const onEdit = () => {
@@ -31,7 +35,7 @@ const FaqItem = (props) => {
   const onSave = (e) => {
     e.preventDefault();
     setIsEditMode(false);
-    props.onEdit(props.index, question, answer);
+    dispatch(editFaqItem(props.index, question, answer));
   };

   return (
@@ -76,8 +80,6 @@ FaqItem.propTypes = {
   question: PropTypes.string.isRequired,
   answer: PropTypes.string.isRequired,
   index: PropTypes.number.isRequired,
-  onDelete: PropTypes.func.isRequired,
-  onEdit: PropTypes.func.isRequired,
 };