Advertisement
Guest User

Untitled

a guest
Mar 30th, 2017
59
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 21.69 KB | None | 0 0
  1. In this tutorial, we'll understand what the Redux store does by building it ourselves, step by step.
  2.  
  3. <hr/>
  4.  
  5. # Part 1 - The Store
  6.  
  7. The main thing that Redux gives us is a method called `createStore`. We use `createStore` to get an instance of a store. We might imagine `createStore` as working like this:
  8.  
  9. ```js
  10. function createStore () {
  11. return new Store(); // we'll write this constructor function ourselves shortly!
  12. }
  13.  
  14. // now we can create a "store" object by invoking createStore
  15. const store = createStore();
  16. ```
  17.  
  18. The Redux `store` also has "private" access (via closure) to two variables - the "currentState" (an object) and a "reducer" (a function that we'll write). Let's update our `createStore` function to include them:
  19.  
  20. ```js
  21. function createStore () {
  22.  
  23. let currentState = {};
  24. let reducer = null; // for now
  25.  
  26. return new Store();
  27. }
  28. ```
  29.  
  30. The Redux `store` is just an object with three methods (actually there's 4, but we won't bother with the fourth for now because chances are you'll never need it).
  31.  
  32. `store.getState`
  33. `store.dispatch`
  34. `store.subscribe`
  35.  
  36. Let's write a constructor function within our `createStore`:
  37.  
  38. ```js
  39. function createStore () {
  40.  
  41. let currentState = {};
  42. let reducer = null;
  43.  
  44. // our constructor function
  45. function Store () {}
  46. // our .prototype methods
  47. Store.prototype.getState = function () {};
  48. Store.prototype.dispatch = function () {};
  49. Store.prototype.subscribe = function () {};
  50.  
  51. return new Store();
  52. }
  53. ```
  54.  
  55. The `store.getState` method is pretty easy - it just gives us access to that private `currentState` object!
  56.  
  57. ```js
  58. function createStore () {
  59.  
  60. let currentState = {};
  61. let reducer = null;
  62.  
  63. function Store () {}
  64. Store.prototype.getState = function () {
  65. return currentState; // piece of cake!
  66. };
  67. Store.prototype.dispatch = function () {};
  68. Store.prototype.subscribe = function () {};
  69.  
  70. return new Store();
  71. }
  72. ```
  73.  
  74. Now we can get the "currentState" object that the store is holding. But how do we _change_ the state? We need two things:
  75.  
  76. - A "reducer" function, which will change the currentState object
  77. - Our `store.dispatch` method, which will expose the ability to invoke the reducer to us (similar to the way `getState` exposes the currentState to us)
  78.  
  79. In the next section, we'll talk about the "reducer" function.
  80.  
  81. <hr/>
  82.  
  83. # Part 2 - Reducer Functions
  84.  
  85. Let's talk about the "reducer" function. The "reducer" function is going to be unique to each app. When we invoke `createStore`, we pass in our "reducer" function as the first argument:
  86.  
  87. ```js
  88. const reducer = function () {} // we'll flesh this out shortly!
  89.  
  90. function createStore (reducer) { // we pass the reducer in as a first argument!
  91.  
  92. let currentState = {};
  93. let reducer = reducer; // our store will have access to the reducer via closure
  94.  
  95. function Store () {}
  96. Store.prototype.getState = function () {
  97. return currentState;
  98. };
  99. Store.prototype.dispatch = function () {};
  100. Store.prototype.subscribe = function () {};
  101.  
  102. return new Store();
  103. }
  104. ```
  105.  
  106. Most of the work we do in an app using `redux` is spent writing the reducer function.
  107.  
  108. Say we have a toy car that we can operate via a remote control. We can make the car move forward, back, left and right. We could represent the "state" of this car as an x-coordinate and a y-coordinate on a 2D plane.
  109.  
  110. ```js
  111. // let's say our toy car starts at (0, 0)
  112. const remoteControlCarState = {
  113. x: 0,
  114. y: 0
  115. };
  116. ```
  117.  
  118. We can imagine that, when we tell the toy car to go "forward" (via our remote control), the y-coordinate on our toy car's state will increase by one unit, and when we tell it to go "back", the y-coordinate will decrease by one unit. Likewise, when we tell it to go "left", the x-coordinate on the toy car state will decrease, and increase when we tell it to go "right".
  119.  
  120. A "reducer" function for the remote control car would describe how the remote control car's "state" object changes with each command we give it. We might write out something like this:
  121.  
  122. ```js
  123. const remoteControlCarState = {
  124. x: 0,
  125. y: 0
  126. };
  127.  
  128. function reducer (command) {
  129. if (command === 'FORWARD') remoteControlCarState.y += 1;
  130. else if (command === 'BACK') remoteControlCarState.y -= 1;
  131. else if (command === 'LEFT') remoteControlCarState.x -= 1;
  132. else if (command === 'RIGHT') remoteControlCarState.x += 1;
  133. }
  134.  
  135. reducer("FORWARD");
  136. console.log(remoteControlCarState); // { y: 1, x: 0 }
  137. reducer("RIGHT");
  138. console.log(remoteControlCarState); // { y: 1, x: 1 }
  139. ```
  140.  
  141. It works! However, there's a problem - when we receive each command, we are `mutating` the state object (that is, we're directly changing the same object in memory). Say that someone enters a series of commands, and the car ends up in an unexpected place somehow. We want to figure out how it got there. However, we have no record of how the car's state changed over time. We would have to guess what combination of commands caused the car to get out of whack, and try to reproduce the problem ourselves. It would be MUCH easier if we could just look back and see how the car's state changed every time it got a command.
  142.  
  143. This is why, instead of mutating the state object, the reducer function gives us a _new_ object each time. Every time we make a change, the reducer gives out a new object representing our new state. We can refactor our previous function to work like this now:
  144.  
  145. ```js
  146. const initialRemoteControlCarState = {
  147. x: 0,
  148. y: 0
  149. };
  150.  
  151. // our reducer function will now take a command, AND the previous state object
  152. function reducer (previousState, command) {
  153.  
  154. // we can cleverly use Object.assign to make a copy of our previous state
  155. const newState = Object.assign({}, previousState);
  156.  
  157. if (command === 'FORWARD') newState.y += 1;
  158. else if (command === 'BACK') newState.y -= 1;
  159. else if (command === 'LEFT') newState.x -= 1;
  160. else if (command === 'RIGHT') newState.x += 1;
  161.  
  162. return newState; // now we return the new state object
  163. }
  164.  
  165. const state1 = reducer(initialRemoteControlCarState, "FORWARD");
  166. console.log(state1); // { y: 1, x: 0}
  167. const state2 = reducer(state1, "RIGHT");
  168. console.log(state2); // { y: 1, x: 1 }
  169. ```
  170.  
  171. This is nice because now we have our record of changes - this will be much easier to debug! This is a fairly small example, so you still may not understand the benefit of doing this. However, once you start writing larger and larger applications, you'll start to realize the advantages of doing this - please just humor us for now.
  172.  
  173. There are only a couple of differences between the reducer functions we'll write and the one we wrote above:
  174.  
  175. 1. What we've been calling "commands" are called "actions" by the Redux community.
  176. 2. "Actions" are usually not just string literals (like "FORWARD" or "RIGHT"), but objects that have a "type" property, and that "type" property has the name "FORWARD" or "RIGHT", etc. This is because actions might have more information on them that just their type.
  177. 3. Instead of using if...else if...else, many folks in the Redux community use the `switch` statement
  178. 4. We usually make our initial state the "default" value for the first argument (previousState). This way, if we don't have a previousState yet (that is, we're invoking the reducer for the first time), it will give us back our initial state!
  179.  
  180. Note this is all by **convention** - you could write it differently and name things differently and still have it work, but it would be confusing for other people in the Redux community.
  181.  
  182. ```js
  183. // command is called action
  184. // also, if someone invokes the reducer without a previousState, this will give us our initial state back!
  185. function reducer (previousState = { y: 0, x: 0}, action) {
  186.  
  187. const newState = Object.assign({}, previousState);
  188.  
  189. // we often use switch instead of if...else - many find it easier to read, because there are fewer curly braces
  190. switch (action.type) { // actions always have a property called type, which contains the name of the action
  191. case "FORWARD":
  192. newState.y += 1;
  193. break;
  194. case: "BACK":
  195. newState.y -= 1;
  196. break;
  197. case "LEFT":
  198. newState.x -= 1;
  199. break;
  200. case "RIGHT":
  201. newState.y += 1;
  202. break;
  203.  
  204. return newState;
  205. }
  206. }
  207.  
  208. const initialToyCarState = reducer(undefined, {});
  209. console.log(initialToyCarState) // { y: 0, x: 0 }
  210. const state1 = reducer(initialState, { type: "FORWARD" });
  211. console.log(state1); // { y: 1, x: 0}
  212. const state2 = reducer(state1, { type: "RIGHT" });
  213. console.log(state2); // { y: 1, x: 1 }
  214. ```
  215.  
  216. And that's how to write a basic reducer function! To summarize, here are the rules for writing a reducer function:
  217.  
  218. 1. It must take a (previous) "state" object as its first argument
  219. 2. It must take an "action" object as its second argument
  220. 3. It must return a new "state" object each time
  221. 4. It must never mutate the previous state object
  222.  
  223. Now that we have an idea of what reducer functions do and how to write them, let's see what the `store` does with it, in the next section.
  224.  
  225. <hr />
  226.  
  227. # Part 3 - The Store (con't)
  228.  
  229. Here's where we left off:
  230.  
  231. ```js
  232. const reducer = toyCarReducer; // pretend that this is the reducer for the toy car we just wrote!
  233.  
  234. function createStore (reducer) {
  235.  
  236. let currentState = {};
  237. let reducer = reducer;
  238.  
  239. function Store () {}
  240. Store.prototype.getState = function () {
  241. return currentState;
  242. };
  243. Store.prototype.dispatch = function () {};
  244. Store.prototype.subscribe = function () {};
  245.  
  246. return new Store();
  247. }
  248.  
  249. const store = createStore(toyCarReducer); // creates a store that uses our toyCarReducer
  250. ```
  251.  
  252. In the last section, we saw how we can use the reducer function we wrote to generate a new state object. We just needed pass in the previous state and an action. Now that the `store` has both our reducer and our currentState. We just need a way to pass actions to our reducer. This is what `store.dispatch` does.
  253.  
  254. ```js
  255. const reducer = toyCarReducer;
  256.  
  257. function createStore (reducer) {
  258.  
  259. let currentState = {};
  260. let reducer = reducer;
  261.  
  262. function Store () {}
  263. Store.prototype.getState = function () {
  264. return currentState;
  265. };
  266. Store.prototype.dispatch = function (action) {
  267. currentState = reducer(currentState, action); // invoking store.dispatch "resets" our currentState to be the result of invoking the reducer!
  268. };
  269. Store.prototype.subscribe = function () {};
  270.  
  271. return new Store();
  272. }
  273.  
  274. const store = createStore(toyCarReducer);
  275. ```
  276.  
  277. Also, remember that if we invoke the reducer with `undefined` as the first argument, it will give us our initial state! The `createStore` function expects you to write reducer functions that do this. As long as we follow the rules for how a reducer function should work, this will cause the first value of "currentState" to be our initial state.
  278.  
  279. ```js
  280. const reducer = toyCarReducer;
  281.  
  282. function createStore (reducer) {
  283.  
  284. // our first "currentState" is the result of invoking the reducer with undefined, and a dummy action
  285. let currentState = reducer(undefined, {});
  286. let reducer = reducer;
  287.  
  288. function Store () {}
  289. Store.prototype.getState = function () {
  290. return currentState;
  291. };
  292. Store.prototype.dispatch = function (action) {
  293. currentState = reducer(currentState, action);
  294. };
  295. Store.prototype.subscribe = function () {};
  296.  
  297. return new Store();
  298. }
  299.  
  300. const store = createStore(toyCarReducer);
  301.  
  302. const initialState = store.getState() // { y: 0, x: 0 }
  303. store.dispatch({ type: "FORWARD" });
  304. const state1 = store.getState() // { y: 1, x: 0 }
  305. ```
  306.  
  307. When we `store.dispatch` an action, we invoke our reducer with the currentState and the action, and reassign currentState to be the result.
  308.  
  309. This works great! It would be nice to know _when_ the state changes though. That way, if we need to do something every time our state changes (like update our UI), we can deal with it. This is exactly what `store.subscribe` is for.
  310.  
  311. `store.subscribe` takes a callback function we want to invoke every time the state is changed. We can make as many "subscriptions" as we like - it stores them in another "private" array.
  312.  
  313. ```js
  314. const reducer = toyCarReducer;
  315.  
  316. function createStore (reducer) {
  317.  
  318. let currentState = reducer(undefined, {});
  319. let reducer = reducer;
  320. let listeners = []; // set aside a listeners array that we have access to via closure
  321.  
  322. function Store () {}
  323. Store.prototype.getState = function () {
  324. return currentState;
  325. };
  326. Store.prototype.dispatch = function (action) {
  327. currentState = reducer(currentState, action);
  328. };
  329. Store.prototype.subscribe = function (callback) { // store.subscribe takes a callback...
  330. listeners.push(callback); // and pushes it in the listeners array
  331. };
  332.  
  333. return new Store();
  334. }
  335.  
  336. ```
  337.  
  338. Whenever we're done changing our state, we want to invoke all of these listeners. We can bake this right into `store.dispatch`.
  339.  
  340. ```js
  341. const reducer = toyCarReducer;
  342.  
  343. function createStore (reducer) {
  344.  
  345. let currentState = reducer(undefined, {});
  346. let reducer = reducer;
  347. let listeners = [];
  348.  
  349. function Store () {}
  350. Store.prototype.getState = function () {
  351. return currentState;
  352. };
  353. Store.prototype.dispatch = function (action) {
  354. currentState = reducer(currentState, action); // first we update the state...
  355. listeners.forEach(callback => callback()); // ...then we invoke all the listeners!
  356. };
  357. Store.prototype.subscribe = function (callback) {
  358. listeners.push(callback);
  359. };
  360.  
  361. return new Store();
  362. }
  363.  
  364.  
  365. const store = createStore(toyCarReducer);
  366.  
  367. // we "subscribe" a callback function
  368. store.subscribe(() => console.log('Hey, the state changed to be: ', store.getState()));
  369.  
  370. store.dispatch({ type: "FORWARD" }); // we would see "Hey, the state changed to be: { y: 1, x: 0 }" logged to the console
  371. ```
  372.  
  373. Sometimes we might want to cancel our subscription. To help us to this, `store.subscribe` returns a function. When we invoke that function, it will remove the callback from the listeners array. This is similar to how `setInterval` gives us an interval id back, which we can then pass to `clearInterval`.
  374.  
  375. ```js
  376. const reducer = toyCarReducer;
  377.  
  378. function createStore (reducer) {
  379.  
  380. let currentState = reducer(undefined, {});
  381. let reducer = reducer;
  382. let listeners = [];
  383.  
  384. function Store () {}
  385. Store.prototype.getState = function () {
  386. return currentState;
  387. };
  388. Store.prototype.dispatch = function (action) {
  389. currentState = reducer(currentState, action);
  390. listeners.forEach(callback => callback());
  391. };
  392. Store.prototype.subscribe = function (callback) {
  393. listeners.push(callback);
  394. // store.subscribe now returns a function
  395. return function () {
  396. // all this function does is remove the callback we passed in from the listeners array!
  397. listeners = listeners.filter(cb => cb !== callback);
  398. }
  399. };
  400.  
  401. return new Store();
  402. }
  403.  
  404.  
  405. const store = createStore(toyCarReducer);
  406.  
  407. // we "subscribe" a callback function, AND store the function we get back in a variable called "unsubscribe"
  408. const unsubscribe = store.subscribe(() => console.log('Hey, the state changed to be: ', store.getState()));
  409.  
  410. store.dispatch({ type: "FORWARD" }); // we would see "Hey, the state changed to be: { y: 1, x: 0 }" logged to the console
  411.  
  412. unsubscribe(); // invoking the function we stored in "unsubscribe" remove the subscription!
  413.  
  414. store.dispatch({ type: "FORWARD" }); // nothing logs to the console this time! We removed that listener!
  415. ```
  416.  
  417. And that's it! That's all there is to Redux! (Well, okay, the real Redux has a few more features, but this is fundamentally how a Redux store operates).
  418.  
  419. Here's our final result:
  420.  
  421. ```js
  422. const reducer = toyCarReducer;
  423.  
  424. function createStore (reducer) {
  425.  
  426. let currentState = reducer(undefined, {});
  427. let reducer = reducer;
  428. let listeners = [];
  429.  
  430. function Store () {}
  431. Store.prototype.getState = function () {
  432. return currentState;
  433. };
  434. Store.prototype.dispatch = function (action) {
  435. currentState = reducer(currentState, action);
  436. listeners.forEach(callback => callback());
  437. };
  438. Store.prototype.subscribe = function (callback) {
  439. listeners.push(callback);
  440. return function () {
  441. listeners = listeners.filter(cb => cb !== callback);
  442. }
  443. };
  444.  
  445. return new Store();
  446. }
  447. ```
  448.  
  449. # Part 4. Usage with React
  450.  
  451. Now that we know how `createStore` works, and we know how to write reducer functions, we can use it to manage state in our applications!
  452.  
  453. We want to write a React application that uses Redux for state management. However, Redux doesn't know anything about React. (There is a library called `react-redux`, which provides a special method that we can use to make React and Redux talk to each other in an intuitive way, but we won't use it **for now**. We'll see how it works fairly soon, though.)
  454.  
  455. Let's say we have a React component that renders a simple counter. Something like this:
  456.  
  457. ```js
  458. import React from 'react';
  459. class Counter extends React.Component {
  460.  
  461. constructor () {
  462. super();
  463. this.state = { count: 0};
  464. this.handleClick = this.handleClick.bind(this);
  465. }
  466.  
  467. handleClick () {
  468. this.setState({ counter: this.state.counter + 1 })
  469. }
  470.  
  471. render () {
  472. return (
  473. <div>
  474. <h3>The count is: { this.state.count }</h3>
  475. <button onClick={this.increment}>Increase</button>
  476. </div>
  477. )
  478. }
  479. }
  480. ```
  481.  
  482. Right now, this component's state is managed by the React component. However, we want it to be in the Redux store instead. To start writing our redux store, we'll ask ourselves three questions:
  483.  
  484. ### Question One: What actions can the user take in our app?
  485.  
  486. We want to create a store. To create a store, we need to create a reducer function. To create our reducer function, we need to think about the **actions** that a user can take in our application (either actively, by clicking a button, or passively, by loading the page).
  487.  
  488. What actions can be taken in our React component above?
  489.  
  490. ...
  491.  
  492. Just the one - clicking the button. Clicking the button causes the counter to increment. Let's imagine this action as a string constant, the same way we imagined the actions the user could take with the remote control car.
  493.  
  494. ```js
  495. const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
  496. ```
  497.  
  498. We've written an **action type**. Thinking about our application in terms of **action types** will help make sure that our state is focused on actual user interactions. This will help us keep our state simple and lean.
  499.  
  500. ### Question 2: How can we represent the state of our app?
  501.  
  502. In both React and Redux, we represent the state of our application as a JavaScript object. Ideally, we should represent it as a JavaScript object with all of the *default values* of our state filled in.
  503.  
  504. What would this look like for our counter application?
  505.  
  506. ...
  507.  
  508. It would look very similar to the way it does in our React component. We'll put it in a variable called "initialState":
  509.  
  510. ```js
  511. const initialState = {
  512. count: 0
  513. }
  514. ```
  515.  
  516. The only thing that changes over time in our application is the number of the count. Therefore, we'll represent state as an object with a key-value pair of count, which we initialize at 0.
  517.  
  518. ### Question 3: How does our state change in response to our actions?
  519.  
  520. When someone performs an action, we want our state to change in response. This is what the reducer is for! Remember what a reducer function looks like:
  521.  
  522. ```js
  523. const initialState = { counter: 0 };
  524.  
  525. function reducer (prevState = initialState, action) {
  526.  
  527. const newState = Object.assign({}, prevState);
  528.  
  529. switch (action.type) {
  530.  
  531. // switch on our action types here...
  532.  
  533. }
  534.  
  535. return newState;
  536.  
  537. }
  538. ```
  539.  
  540. Our only action type is `INCREMENT_COUNTER`, so we simply need to describe how the state changes in that case.
  541.  
  542. ```js
  543. const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
  544.  
  545. const initialState = { counter: 0 };
  546.  
  547. function reducer (state = initialState, action) {
  548.  
  549. const newState = Object.assign({}, state);
  550.  
  551. switch (action.type) {
  552.  
  553. case INCREMENT_COUNTER:
  554. newState.counter += 1;
  555. break;
  556.  
  557. }
  558.  
  559. return newState;
  560.  
  561. }
  562. ```
  563.  
  564. Remember, the reducer needs to return a **new** object each time - it must **not** mutate the previous state object directly. This is why we create a copy of the previous start first, and then apply the changes to that.
  565.  
  566. Now that we have our reducer, we can create our store:
  567.  
  568. ```js
  569. import { createStore } from 'redux';
  570.  
  571. const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
  572.  
  573. const initialState = { counter: 0 };
  574.  
  575. function reducer (state = initialState, action) {
  576.  
  577. const newState = Object.assign({}, state);
  578.  
  579. switch (action.type) {
  580.  
  581. case INCREMENT_COUNTER:
  582. newState.counter += 1;
  583. break;
  584.  
  585. }
  586.  
  587. return newState;
  588.  
  589. }
  590.  
  591. const store = createStore(reducer);
  592.  
  593. export default store;
  594. ```
  595.  
  596. Now that we have our store, let's examine the initial state that it contains:
  597.  
  598. ```js
  599. store.getState(); // { counter: 0}
  600. ```
  601.  
  602. It's equal to the default argument from our reducer function, just like we expected.
  603.  
  604. Remember that to change the state, we need to dispatch an action with a type of `INCREMENT_COUNTER`.
  605.  
  606. ```js
  607. store.dispatch({ type: INCREMENT_COUNTER });
  608. store.getState(); // { counter: 0 }
  609. ```
  610.  
  611. Let's return to our original task now: we want to make it so that our React component will use the state from the store instead of managing it on its own. When we update the state within the store (using dispatch), this should do the same thing that `setState` does.
  612.  
  613. That being said, React doesn't give us a way to replace `state` and `setState`. If we want a component to re-render, we need to use `setState` - it's the only way.
  614.  
  615. This could be tricky, so let's take it step by step.
  616.  
  617. First, let's make it so that `this.state` is initialized to be the result of saying `store.getState()`. This way, we at least start with the state on the component initialized to be the state inside the store.
  618.  
  619. ```js
  620. import React from 'react';
  621. import store from '../ourStore';
  622.  
  623. class Counter extends React.Component {
  624.  
  625. constructor () {
  626. super();
  627. this.state = store.getState(); // this.state starts out as the state inside the store
  628. this.handleClick = this.handleClick.bind(this);
  629. }
  630.  
  631. handleClick () {
  632. this.setState({ counter: this.state.counter + 1 })
  633. }
  634.  
  635. render () {
  636. return (
  637. <div>
  638. <h3>The count is: { this.state.count }</h3>
  639. <button onClick={this.increment}>Increase</button>
  640. </div>
  641. )
  642. }
  643. }
  644. ```
  645.  
  646. # Part 5 - React-Redux
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement