├── .DS_Store ├── .gitignore ├── LICENSE ├── Notes ├── 1-setup.md ├── 2-firstComponent.md ├── 3-componentMethodsAndFunctionalComponents.md ├── 4-addingFunctionality.md ├── 5-settingUpRedux.md └── 6-reduxActionsAndReducers.md ├── README.md ├── build ├── bundle.js └── index.html ├── package.json ├── src ├── app.js ├── components │ ├── Input.js │ ├── List.js │ └── ToDoApp.js ├── containers │ └── toDoAppContainer.js └── redux │ ├── configureStore.js │ └── modules │ └── toDoApp.js ├── talking-points.md └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lijie33402/react-redux-ducks/f59cd0198ab1f74d4df8cef3d8483e798a23bc14/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Cody Barrus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Notes/1-setup.md: -------------------------------------------------------------------------------- 1 | # Setting up your React App - The most difficult part of any react app :) 2 | 3 | ## 1. NPM INIT 4 | 5 | `npm init` to create your `package.json` file. 6 | 7 | ## 2. Install dependencies: 8 | - `npm install --save react` - Install React. 9 | - `npm install --save react-dom` Install React Dom, the package that handles the virtual DOM. 10 | - `npm install --save-dev webpack` - Install Webpack, our module bundler. 11 | - `npm install --save-dev webpack-dev-server` - A dev server that enables hot reloading. 12 | - `npm install --save-dev babel` - Install Babel, which will transpile our ES6 code into ES5. 13 | - install these other babel dependencies: 14 | - `npm install --save-dev babel-core` - The main features of babel. 15 | - `npm install --save-dev babel-polyfill` - Babel includes a polyfill that includes a custom regenerator runtime and core.js. This will emulate a full ES6 environment. 16 | - `npm install --save-dev babel-loader` - This package allows transpiling JavaScript files using Babel and webpack. 17 | - `npm install --save-dev babel-runtime` - A dependency of Babel transform runtime plugin. 18 | - `npm install --save-dev babel-plugin-transform-runtime` - Externalise references to helpers and builtins, automatically polyfilling your code without polluting globals. 19 | - `npm install --save-dev babel-preset-es2015` - Babel preset for all es2015 plugins. 20 | - `npm install --save-dev babel-preset-react` - Strip flow types and transform JSX into createElement calls. 21 | - `npm install --save-dev babel-preset-stage-0` - All you need to use stage 0 (and greater) plugins (experimental javascript). 22 | 23 | ## 3. Open `package.json` and add some scripts: 24 | ``` 25 | "scripts": { 26 | "start": "webpack-dev-server --hot --inline --progress --colors", 27 | "build": "webpack --progress --colors" 28 | } 29 | ``` 30 | 31 | Running `npm start` will start your dev server. 32 | Running `npm build` will build your app for production. 33 | 34 | ## 4. Setup Webpack 35 | Webpack is our bundler. It's an important peice of our dev environment, and alows us to use some awesome features like hot reloading. Our `webpack.config.js` file is below with comments. As your app grows, your config file may change. 36 | 37 | ```javascript 38 | var webpack = require('webpack'); 39 | module.exports = { 40 | entry: [ 41 | 'babel-polyfill', 42 | 'webpack/hot/only-dev-server', 43 | './src/app.js' // the entry point of our app. All react apps have one parent component. Yours should live here. 44 | ], 45 | output: { 46 | path: __dirname + '/build', // When we run 'npm build', our bundled .js file will be exported here. 47 | filename: "bundle.js" // Our exported file will be called 'bundle.js' 48 | }, 49 | module: { 50 | loaders: [ 51 | { 52 | test: /\.js$/, // all .js files will be subjected to these loaders 53 | exclude: /node_modules/, // no files within the node_modules folder will be processed 54 | loader: 'babel-loader', // all .js files will be ran through babel for transpiling 55 | query: { 56 | plugins: ['transform-runtime'], 57 | presets:['es2015', 'react', 'stage-0'] // we are using es2015, plus reacts jsx, plus some experimental javascript 58 | } 59 | }, 60 | { test: /\.css$/, loader: "style!css" } // allows us to import css files in our javascript. 61 | ] 62 | } 63 | }; 64 | 65 | ``` 66 | Your boiler plate is now done. Time to start writing some react code. 67 | -------------------------------------------------------------------------------- /Notes/2-firstComponent.md: -------------------------------------------------------------------------------- 1 | # React Basics - Your First Component 2 | 3 | Now that we have our app boilerplate out of the way, it's time to get to the fun stuff. Let's set up our first component. 4 | 5 | ## 1. index.html 6 | 7 | The first thing we need to do is create out `index.html` page in the root of our project. This will be a very basic page. For this project, I've linked to a bootstrap cdn for basic styling. The only other things to look out for is the `
` tag (this is where our app will be injected), and the `` tag (which will run the javascript that powers our app). 8 | 9 | Here's the full HTML: 10 | 11 | ```html 12 | 13 | 14 | 15 | 16 | Document 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | ``` 25 | ## 2. Folders & Files 26 | 27 | Create a new folder called `src`. The majority of your app's code will live inside this folder from now on. 28 | 29 | Within `src` create a `app.js` file. Within every React app there is one root component, and for us, this is it. All other components will be nested within this one. 30 | 31 | ## 3. Imports 32 | 33 | First we need to import react into this file. To do this, we'll be using a little ES6. `import React from 'react';` The `import` functionality is an ES6 feature, but currently it's powered by Webpack (as opposed to the browser where it may be powered in the near future). 34 | 35 | We also need to import ReactDOM. Because react is used not only to power web apps (with ReactDOM), but also native mobile apps (with React Native), the rendering process is handled by ReactDOM. This is where the vertual DOM is created and diff'd against the actual DOM. 36 | 37 | To import ReactDOM, add `import ReactDOM from 'react-dom';` at the top of the file. 38 | 39 | ## 4. Our First Component 40 | 41 | Now we need to create our first Component. To do this we create an ES6 class that extends `React.Component`. At this point you may be wondering what is enabling all of this ES6 to work. While a lot of it is actually available in your browser right now, we are using Babel to power stage-0 and up features as well as make all ES6 backwards compatable. 42 | 43 | To create your first class: 44 | 45 | ```javascipt 46 | class App extends React.Component { 47 | render(){ // Every react component has a render method. 48 | return( // Every render method returns jsx. Jsx looks like HTML, but it's actually javascript and functions a lot like xml, with self closing tags requiring the `/` within the tag in order to work propperly 49 |
50 | Hello World 51 |
52 | ); 53 | } 54 | } 55 | ``` 56 | 57 | Notice that "Hello World" is nested within `div`s. All jsx must be nested within parent `div`s or it wont work. 58 | 59 | ## 5. Rendering 60 | 61 | Finally, we need to render this component to the DOM. To do this, we'll use the `ReactDOM.render` method. 62 | 63 | At the bottom of your `App.js` file, add: `ReactDOM.render(, document.getElementById('app'));` 64 | 65 | The first parameter is our App, which is rendered as ``. The second is the DOM element where we'll be injecting our app. In this case, a `div` tag with the id of "app". -------------------------------------------------------------------------------- /Notes/3-componentMethodsAndFunctionalComponents.md: -------------------------------------------------------------------------------- 1 | Ok, now we have our root component in the `app.js` file. It doesn't do much on it's own, so we're going to start making this app do stuff. 2 | 3 | The app we're making is a to do app. We'll be building an app with an input field that takes in to do items and adds them to a list. Once the item is clicked, we'll cross the item off the list. When a delete button is cliced, the item will be removed. Through this process, you'll learn all the basic building blocks of a fully funcitonal React app. 4 | 5 | But before we do all of this, we need to start with some smaller componets. A component is the building block of a react app. There are two types of components, class based components and functional componets. In this stage, we'll make both a class component and a functional component, use lifecycle methods, work with state, and pass props. 6 | 7 | ## 1. Components Folder 8 | 9 | Start by creating a `components` folder within `src`. So your file structure should look like `~/src/components`. 10 | 11 | ## 2. ToDoApp.js 12 | 13 | Inside `components`, we're going to create the file `ToDoApp.js`. As with all react components, we'll start by importing react with `import React from 'react';` 14 | 15 | ## 3. Test Component 16 | 17 | This will be a class component. As we saw before, all class components have a render method which returns jsx. This one will be no different. 18 | 19 | Your ToDoApp class should look like this: 20 | 21 | ```javascript 22 | class ToDoApp extends React.Component { 23 | render() { 24 | return ( 25 |
To Do App
26 | ); 27 | } 28 | } 29 | ``` 30 | 31 | ## 4. Export Component 32 | 33 | In order to import this component into our app, first we'll need to export it. At the bottom of the file add `export default ToDoApp;` 34 | 35 | ## 5. Import Component 36 | 37 | In `app.js`, at the top of the file, add `import ToDoApp from '.components/ToDoApp';` Then replace the text `Hello World` with our new component. We can now render our `ToDoApp` component by adding it to our JSX where our Hello World text was. To do this, add `` You'll notice that this is a self closing tag. 38 | 39 | ## 6. Smart Component 40 | 41 | In your browser, you should now see the words "To Do App" has replaced "Hello World"! Nice, you've now nested your first child component inside of your apps Root component. This is the general pattern you'll use when building any React app. So, now let's make this app smart. 42 | 43 | Back in `ToDoApp` we're going to start making our first list. First, let's make it look half decent with a little bootstrap. Copy the following JSX into the `return` statment of the `render` method, replacing all of the current jsx: 44 | 45 | ```html 46 |
47 |
48 |
49 |
50 |

My To Do App

51 |
52 | List goes here. 53 |
54 |
55 |
56 |
57 | ``` 58 | 59 | Now if you look at your browser, the text should read "My To Do App" followed by "List Goes Here" within a bootstrap panel. So where are we going to store the data that populates our list? The answer is `state`. Every class based component has `state` which can be accessed anywhere in your component with `this.state` and can be update with `this.setState({ key: "value" })`. While we try not to use component state unless it's absolutely necessary, for the sake of this example, and for the immediate future, we will be using state only in this component until we setup redux. 60 | 61 | Inside `ToDoApp` we also have access to lifecycle methods, one of which is `componentWillMount`. This method is run once when the page loads and before the render method is called, so if you need to access data from a server or API, this is a good place to do that. In our case, we're going to start with a dummy list. 62 | 63 | Inside `ToDoApp`, before the `render` method, add: 64 | 65 | ```javascript 66 | componentWillMount(){ // run before the render method 67 | this.setState({ // add an array of strings to state. 68 | list: ['thing1', 'thing2', 'thing3'] 69 | }) 70 | }; 71 | ``` 72 | 73 | And now we have access to a dummy list. It's important to note that React depends on state, as well as props (which we'll cover soon). Only when state or props change will the react app refresh. This means if data changes somewhere else, and is used in the view, the view won't update to display the change. Only when state or props change. 74 | 75 | ## 7. A List 76 | 77 | Now we need to add our list to the view. Instead of simply adding the JSX into our `render` method, we'll be creating a functional component to handle the list. Once again, we'll be following the pattern of nesting components within components. Functional components are new to React, and it is considered a best practice to use them whenever possible. However, it's important to note that functional components dont have lifecycle methods or state. They are simply a function that returns jsx and contain props as an argument. 78 | 79 | We've talked about props a bit now. Props are the name of data that is passed down from a parent component into a child component. This is typically the way data is passed through a react app. Keeping the data near the top of the app, and letting that data trickle down through components keeps your app running proficiantly. Poor handling of data and props can slow your app down, but if you follow the practices within this course you're app will run very fast. 80 | 81 | To create our functional component, first create a new file in `components` called `List.js`. Start by importing react, then we'll create a function that returns JSX. Our function will be a `const` because it wont be manipulated over time. It'll take one argument called `props`. 82 | 83 | Your function should look like this: 84 | 85 | ```javascript 86 | const List = (props) => { // we're using an arrow function and const variable type, a ES6 features 87 | 88 | return ( 89 |
90 | I'm a list!!! 91 |
92 | ) 93 | }; 94 | 95 | export default List; 96 | ``` 97 | 98 | ## 8. Dynamic List 99 | 100 | Inside `ToDoApp.js`, import List. Then replace the words `List goes here.` with the `List` component. It will look just like a class component in our JSX: ``. Now in the browser you should see the words "I'm a list!!!" 101 | 102 | Now, to make our list, we need to give `List` some data, which we'll pass through props. We'll call this prop "listItems" which will read from state. We do this by adding what will look very similar to an HTML attribute to our JSX `List` component call. It will look like this: `` Now `List` has access to the data which is containe in `ToDoApp` through props! 103 | 104 | To access this data, in the `List` component we'll need to render the list. Replace the JSX within the `return` statement with the following code: 105 | 106 | ```javascript 107 |
108 |
    109 | { 110 | list // this is a variable we'll define next 111 | } 112 |
113 |
114 | ``` 115 | 116 | Notice the curly braces. Within these sections, javascript can execute and what is returned will be added to the view. In this case, the result of a yet to be defined list variable will be added to the view. So let's define that variable: 117 | 118 | ```javascript 119 | const list = props.listItems.map((el, i)=>( 120 | // All where doing here is getting the items listItems prop 121 | // (which is stored in the state of the parent component) 122 | // which is an array, and we're running the .map method 123 | // which returns a new array of list items. The key attribute is 124 | // required, and must be unique. 125 |
  • {el}
  • 126 | )); 127 | ``` 128 | 129 | The entire component should look like: 130 | 131 | ```javascript 132 | import React from 'react'; 133 | 134 | const List = (props) => { 135 | 136 | const list = props.listItems.map((el, i)=>( 137 |
  • {el}
  • 138 | )); 139 | 140 | return ( 141 |
    142 |
      143 | { 144 | list 145 | } 146 |
    147 |
    148 | ) 149 | }; 150 | 151 | export default List; 152 | ``` 153 | Now if we look at the browser, we should see a list of items! Next we'll need to add some functionality to our app. 154 | -------------------------------------------------------------------------------- /Notes/4-addingFunctionality.md: -------------------------------------------------------------------------------- 1 | Now that we have something showing up in our view, it's time to add some functionality to our app. Because this is a to do app, we'll need to add a way for new items to be added to the list, a way to make off items as completed, and a way to delete any items we no longer want. 2 | 3 | ## 1. Functional Component 4 | 5 | First we need to add an input element so the user can add a todo item. Let's do this by creating a new component. In the `components` folder, create `Input.js`, and within create and export a functional component called Input. 6 | 7 | Remember that functional components return JSX and have access to the props argument. For now, we just need to return the form. Paste the following JSX into the return statement of your functional component: 8 | 9 | ```html 10 |
    11 |
    13 | 17 | 23 | 27 |
    28 |
    29 | ``` 30 | 31 | ## 2. Input 32 | 33 | Right now our JSX isn't doing anything special, just adding basic HTML to the view. Let's test it out to make sure everything is hunky dorey by importing the `Input` component into `ToDoApp.js` and instanciating it below the `List` component. It should look like this ``. 34 | 35 | Now's the time when I'll start keeping some of my instructions that we've already covered in past sections a little more vague. If you ever find yourself unable to figure things out, remember that you can reference the source code in this repo at any time. 36 | 37 | Ok, let's check the view. Everything there? Good! Let's make it do stuff. 38 | 39 | ## 3. Props 40 | 41 | The first thing we need to do is give ourselves access to the value of the input field. This value will need to be accessed in other components, so we don't want to handle the data storage within our `Input` component. In fact, I'll just reiterate that it's never a good idea to store data in a child component whenever possible. Most of the time, data should be stored at the top of your app and trickle down components through props. 42 | 43 | Another thing to remember is that even though we are currently storing state in our `ToDoApp` component, eventually we'll be adding Redux to handle data for our entire app. We are simply working with local State to display the many things you can do in a React component. 44 | 45 | Ok, now that we've got that out of the way, we're going to store the value of the input within the state of `ToDoApp` (later to be handled by Redux). To begin, within the `componentWillMount` method, add `newToDo: ''` within the `this.setState` call. It should look like this: 46 | 47 | ```javascript 48 | componentWillMount(){ 49 | this.setState({ 50 | list: ['thing1', 'thing2', 'thing3'], 51 | newToDo: 'test' 52 | }) 53 | }; 54 | ``` 55 | 56 | Now we need to pass that value through props. Find the `` and add a new prop called value. Can you remember how to access a value from state? Pass it in. Look to the `listItems` prop on the `List` component for a hint. 57 | 58 | ## 4. Destructuring 59 | 60 | In `Input.js` we now have access to the value prop, but we're going to be handling props in a slightly different way this time around. In this case we'll be adding a new ES6 feature called "destructuring" to make dealing with props just a little nicer. 61 | 62 | Instead of adding the `props` argument to the `Input` component, we're going to add this `({ value })`. What this does is take the `props` argument, and breaks it down into variables we can have access to based on the key value pairs within the props object. 63 | 64 | For example, this: 65 | 66 | ``` 67 | var props = { 68 | name: 'hector', 69 | age: 21 70 | } 71 | 72 | 73 | function log(props){ 74 | console.log(props.name); 75 | console.log(props.age); 76 | } 77 | 78 | log(props); 79 | ``` 80 | 81 | is the same as this: 82 | 83 | ``` 84 | let props = { 85 | name: 'hector', 86 | age: 21 87 | } 88 | 89 | log = ({name, age}) => { 90 | console.log(name); 91 | console.log(age); 92 | } 93 | 94 | log(props); 95 | ``` 96 | 97 | So let's give our input access to the value. In the `input` within our JSX, add `value={value}`. Check the browser to confirm that this is indeed the value. 98 | 99 | ## 5. setState 100 | 101 | You might have noticed that now that we've declared the value, we can't change it. That's because the value is being held in state and we haven't created a method to update state with our new value based on user input. 102 | 103 | To do this, we'll need to add an `onChange` method to the same `input` we added value. Under value add 'onChange={onChange}', then where we destructure the props, add `onChange` so it looks like `({ onChange, value })` 104 | 105 | Next, go within `ToDoApp.js`. Under `componentWillMount` add a new custom method called `onInputChange`. This will be a simple method that updates the state whenever the user adds or deletes something from the input. It'll take a single argument, `event` where it will capture the user input from. Within the method, add `this.setState({ newToDo: e.target.value });` 106 | 107 | The method should look like this: 108 | 109 | ```javascript 110 | onInputChange = (event) => { 111 | this.setState({ newToDo: event.target.value}); // updates state to new value when user changes the input value 112 | }; 113 | ``` 114 | 115 | Now, we just need to give our custom `Input` component access to this method through props. Within the JSX, add `onChange={this.onInputChange}`. That's everything we need to make the input editable, as well as give our app access to it's value at any time. 116 | 117 | ## 6. Adding items 118 | 119 | Now time to add new items to our list. To do this, we're going to create another custom method within `ToDoApp` called `onInputSubmit`. This also contains an `event` arugment. Within the body of the method, we're going to `event.preventDefault()`, then use the `setState` method to push a new item to the list array. However, it's important to note that state should never be modified directly, otherwise React may not render the change, or state may be overridden unexpectedly. 120 | 121 | To get around this, `this.setState` also can take a callback function with access to `previousState`. To take advantage of this, we could write this (but don't): 122 | 123 | ```javascript 124 | this.state((previousState)=>({ 125 | list: previousState.list.push(previousState.newToDo) 126 | })) 127 | ``` 128 | 129 | Above you can see how we use the `previousState` to update existing state. This method will work fine, but when we move on to Redux, our data handling layer, the `push` method will no longer be a viable option. This is because Redux depends on state being immutable. That means we can never change state directly, and unfortunately, push modifies the array it's called upon. 130 | 131 | To get around this, and to make our development easier when it comes to implementing Redux, ES6 has some handy new features we're going to use. In this case, we're going to use an array spread opperator. What this does is return a new array by itterating through the old array and leaving the old array in tact. Then we'll simply add our item into the end of an array literal. It'll look like this: 132 | 133 | ```javascript 134 | this.setState((previousState)=>({ 135 | list: [...previousState.list, previousState.newToDo ], // the spread opperator is called by using the ... preceding the array 136 | })); 137 | ``` 138 | 139 | Now that we have that taken care of, there's one more step we need to take. When a new list item is submitted, we need to reset the `newToDo` value to ''. To do this, simply add `newToDo: ''` within the `setState` method. It'll look like this: 140 | 141 | ```javascript 142 | this.setState((previousState)=>({ 143 | list: [...previousState.list, previousState.newToDo ], 144 | newToDo: '' 145 | })); 146 | ``` 147 | 148 | Now that the method is done, see if you can pass it to the `Input` component, and add it to the `onSubmit` method on the `form` tag within the JSX. If you can't figure it out, take a look at the source code. 149 | 150 | ## 7. Cross off items 151 | 152 | It's time to add the ability to cross off list items. To do this, we'll need to change the way we store our listItems. Instead of storing an array of strings, we'll store an array of objects with an `item` key which will hold a string of the item name, and a `done` boolean. 153 | 154 | Now that we've made this discovery, it's time to update our data accordingly. For now, go to the `componentWillMount` method, and remove the strings from the list key. They've served their purpose, we can work without them from now on. It should look like: `list: []`. Then go to `onInputSubmit` and change the `setState` method to account for the new change in data structure. It'll look like this: 155 | 156 | ```javascript 157 | onInputSubmit = (event) => { 158 | event.preventDefault(); 159 | this.setState((previousState)=>({ 160 | list: [...previousState.list, {item: previousState.newToDo, done: false }], // notice the change here 161 | newToDo: '' 162 | })); 163 | }; 164 | ``` 165 | 166 | With that change in place, we need a method to change the `done` boolean in each list item without modifying existing state. Try to figure it out without looking at the snippet below. It'll look like: 167 | 168 | ```javascript 169 | onListItemClick = (i) => { // takes the index of the element to be updated 170 | this.setState((previousState)=>({ 171 | list: [ 172 | ...previousState.list.slice(0, i), // slice returns a new array without modifying the existing array. Takes everything up to, but not including, the index passed in. 173 | Object.assign({}, previousState.list[i], {done: !previousState.list[i].done}), // Object.assign is a new ES6 feature that creates a new object based on the first param (in this case an empty object). Other objects can be passed in and will be added to the first object without being modified. 174 | ...previousState.list.slice(i+1) // takes everything after the index passed in and adds it to the array. 175 | ] 176 | })) 177 | }; 178 | ``` 179 | 180 | With this method ready to role, let's pass it down to the `List` component through a prop called `onClick`. We'll need to make some changes to how this component displays it's list to make this work. There is one gotcha here, we need to pass in the index of the element clicked, but we can't call the function here otherwise our app won't function properly. To get around this we'll use the `.bind()` method with the first parameter being `null` and the second being the index. I'll look like this: 181 | 182 | ```javascript 183 | onClick={props.onClick.bind(null, i)} 184 | ``` 185 | 186 | First, add an `onClick` method to the JSX `span` wrapping `{el}` and pass in the `onClick` prop we've given `List`. Let's also change `{el}` to `{el.item}` while we're here, to handle the new data. 187 | 188 | Next, we're going to give this `span` some style. Within a `style` attribute, we're going to check `el.done`, and if it's `true` return `{textDecoration: 'line-through', fontSize: '20px'}`, and if false return `{textDecoration: 'none', fontSize: '20px'}`. I like doing this with a turnery. It'll look like this: 189 | 190 | ```javascript 191 | 199 | ``` 200 | 201 | Now, when we click on the element, it'll be crossed out. Cool! Try adding some items and crossing them out in your browser. 202 | 203 | ## 8. Delete Items 204 | 205 | Finally, we need to be able to delete list items. This will be a very similar action to crossing list items off. In fact, it'll be nearly identical once we've added a delete button. Try figuring it out, and if you can't, come back and follow along. 206 | 207 | First we need a delete button. Add it after the list item like so: 208 | 209 | ```javascript 210 | 215 | ``` 216 | 217 | Now we need to give it an onClick method. Let's create that method first within the `ToDoApp` component. This method will take the index and update state to contain all list items except the on deleted. It'll look like this: 218 | 219 | ```javascript 220 | deleteListItem = (i) => { 221 | this.setState((previousState)=>({ // using previous state again 222 | list: [ 223 | ...previousState.list.slice(0, i), // again with the slice method 224 | ...previousState.list.slice(i+1) // the only diffence here is we're leaving out the clicked element 225 | ] 226 | })) 227 | }; 228 | ``` 229 | 230 | Pass the `deleteListItem` method to list, then add it to the delete button. Remeber to use `.bind`. Check the source files if you're having troubling with this last step. 231 | 232 | Now we have an app with some functionality. It's pretty nice, but this method of handling data won't scale to larger apps, so next we'll learn how to convert this app to use Redux! 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /Notes/5-settingUpRedux.md: -------------------------------------------------------------------------------- 1 | So far we've learned how to setup our app with Webpack and Babel, how to create class based and functional components, handling state within a component, and adding functionality. While this alone is sufficent to power a small app like ours, as our app grows, we'll quickly find that handling data and actions in this way is insufficient. That's where Redux comes in. 2 | 3 | So how does Redux handle data differently? First, Redux gives your app a single state object for your entire app. This is in contrast to other data handling methods like Flux which often will contain multiple states based on the view. So how does Redux deal with a complex app where a single state object may get too complex? It breaks data handling into seperate reducer functions, action creators, and actions that work together to make data handling streamlined. 4 | 5 | To learn more about Redux, look at the readme at the root of this project where you'll find many informative articles that delve in depth on the subject, including some that compair redux with flux. 6 | 7 | Ok, let's get started. 8 | 9 | ## 1. First we'll need to install some new packages. 10 | 11 | First, let's install `redux` and `react-redux` (which has some bindings for pairing redux with react). 12 | 13 | ``` 14 | npm install --save redux 15 | npm install --save react-redux 16 | ``` 17 | 18 | Next, we're going to install a piece of redux middleware called `redux-logger` which will help us while we develop. 19 | 20 | ``` 21 | npm install --save redux-logger 22 | ``` 23 | 24 | When you begin working on your own apps, you might find other middleware useful as well, including `redux-thunk` and `redux-promise`, but for the sake of this project, we're going to be sticking to the logger middleware only. 25 | 26 | ## 2. Setup 27 | 28 | Now we need to setup our app to handle Redux. There's a little boilerplating involved that you just need to do every time you fit an app with redux, so we're going to keep it simple this time. 29 | 30 | The first thing we need to do is configure our redux store. Create a new file in the within `/src/` called `redux`. Within this folder, create a new file called `configureStore.js`. 31 | 32 | Open our new file, and add the following imports: 33 | 34 | ```javascript 35 | import { createStore, applyMiddleware, combineReducers } from 'redux'; 36 | import createLogger from 'redux-logger'; 37 | ``` 38 | 39 | `createStore` is the function provided by redux to initialize our store, while `applyMiddleware` is an entry point for us to add any middleware we'd like to apply. 40 | 41 | `combineReducers` is used as your app grows to combine multiple reducers into a single entity. 42 | 43 | `createLogger` is a middleware that enhances our console to give us a detailed look of how data is being handled over time with each action. It's super cool. 44 | 45 | 46 | Under these imports, add the following code: 47 | 48 | ```javascript 49 | const loggerMiddleware = createLogger(); // initialize logger 50 | 51 | const createStoreWithMiddleware = applyMiddleware( loggerMiddleware)(createStore); // apply logger to redux 52 | ``` 53 | 54 | We're not done here, but we'll need to create another file before we can progress. 55 | 56 | ## 3. Modules 57 | 58 | Within `src/redux/` create another folder called `modules`. Within here we'll store all our reducers, action creators, and constants needed to make redux work. We're going to be using an orginizational structure called ducks where we store related constants, reducers, and action creators in a single file, rather than having multiple files for each. I find this makes our code much easier to work with, though there are conflicting points of view here. 59 | 60 | Within the `modules` folder, create 'toDoApp.js'. I like to name my ducks files after the parent component they are most closely associated with. Notice that the filename begins with a lowercase. 61 | 62 | Now were going to create our initial state and our reducer function. This is actually very simple as state is just a JavaScript object and the reducer is just a JavaScript switch statement. It should look like this: 63 | 64 | ```javascript 65 | const initialState = {}; //The initial state of this reducer (will be combined with the states of other reducers as your app grows) 66 | 67 | export default function reducer(state = initialState, action){ // a function that has two parameters, state (which is initialized as our initialState obj), and action, which we'll cover soon. 68 | switch (action.type){ 69 | default: 70 | return state; 71 | } 72 | } 73 | ``` 74 | 75 | ## 4. Configure Store 76 | 77 | Now that we have our first reducer, it's time to add it to our `configureStore.js` file. Go back to that file and add this import statement `import toDoApp from './modules/toDoApp';` 78 | 79 | Now we're going to use the combineReducers function to combine our current reducer with any future reducers that may come along as our app grows. At the bottom of the file, add: 80 | 81 | ```javascript 82 | const reducer = combineReducers({ 83 | toDoApp 84 | }); 85 | ``` 86 | 87 | Finally, add the following line of code to bring it all together: 88 | 89 | ```javascript 90 | const configureStore = (initialState) => createStoreWithMiddleware(reducer, initialState); 91 | export default configureStore; 92 | ``` 93 | 94 | Cool. We're done here. 95 | 96 | ## 5. Connect 97 | 98 | Now that we have a reducer, we need a way to give our app access to it. This will take two more steps, and then we can get to the good stuff. 99 | 100 | We've talked before about class components and functional components. These are sometimes also known as smart components and dumb components. We'll we're going to add one more kind of component into the mix called a container. This is simply a wrapper for a React component that give the component it wraps access to actions (the functions that make a Redux app work), and state. 101 | 102 | To create our first container, first we'll need to create another new folder within `/src/` called `containers`. Within `containers`, create a new file called `toDoAppContainer.js`. 103 | 104 | At the top of this new file, we're going to import the `connect` function from `react-redux` which ties together the container to the component, as well as the component. It should look like: 105 | 106 | ```javascript 107 | import { connect } from 'react-redux'; 108 | import ToDoApp from '../components/ToDoApp.js' 109 | ``` 110 | 111 | `connect` is a function that is called twice, first with two callbacks: `mapStateToProps` and `mapDispatchToProps`. The second time it's called, it we pass in the component we're mapping state and dispatch to. 112 | 113 | We already know what state is, but dispatch is new. When we want something to happen in Redux, we call a dispatch function and give it an action which in turn sends our reducer a constant that tells it how to manipulate state. This will become more clear as we press forward. Currently we don't have any actions to dispatch, so we'll only be using the first for a short while, but we'll write both functions anyway. 114 | 115 | ```javascript 116 | function mapStateToProps(state) { 117 | return { 118 | toDoApp: state.toDoApp // gives our component access to state through props.toDoApp 119 | } 120 | } 121 | 122 | function mapDispatchToProps(dispatch) { 123 | return {}; // here we'll soon be mapping actions to props 124 | } 125 | ``` 126 | 127 | Now all we need to do is export and call the connect function: 128 | 129 | ```javascript 130 | export default connect( 131 | mapStateToProps, 132 | mapDispatchToProps 133 | )(ToDoApp); 134 | ``` 135 | 136 | 6. Provider 137 | 138 | We're nearly done with setting up Redux. For the final step in getting all of this revved, we need to go back to the `app.js` file. 139 | 140 | First, we need to import a few things. We no longer need our `ToDoApp` component, because that is now going to be replaced by our `ToDoAppContainer`. We also need to import our `configureStore` function, as well as a final helper Component from `react-redux` called `Provider`. So add these to the top of the `app.js` file: 141 | 142 | ```javascript 143 | import { Provider } from 'react-redux'; 144 | import ToDoAppContainer from './containers/ToDoAppContainer'; 145 | import configureStore from './redux/configureStore'; 146 | ``` 147 | 148 | `configureStore` is the function we created that takes our combined reducers and our redux middleware and mashes them all together. Let's intialize that with the following line: 149 | 150 | ```javascript 151 | const store = configureStore(); 152 | ``` 153 | 154 | Now, within the JSX of the `App` component, we have two more changes. Change the `ToDoApp` component to the new `ToDoAppContainer` component. Then, we'll wrap it with the `Provider` component provided to us by `react-redux`. This component passes down the state of our entire app to the containers nested within it, giving those containers access to the state. 155 | 156 | Our JSX should now look like this: 157 | 158 | ```javascript 159 | // we pass the store through to Provider with props 160 | 161 | 162 | ``` 163 | 164 | Now go back to the browser. Everything should be functioning as it was before. If not, then something went wronge. Take a look at the source files within this branch to make sure you have everything correct, then move on to the next branch to start moving our logic over to the Redux layer. 165 | 166 | -------------------------------------------------------------------------------- /Notes/6-reduxActionsAndReducers.md: -------------------------------------------------------------------------------- 1 | # Redux Actions and Reducers 2 | 3 | Setting up Redux takes some leg work, but once you get passed that, it's a very nice paradim to work with, especially when compaiered to flux, the data handling implementation that it came to replace. 4 | 5 | Before we get started, let's quickly cover a few concepts. Types, Actions, Action Creators, and Reducers are the elements you'll be working with when building a Redux powered app. 6 | 7 | ## Ducks 8 | 9 | First let's talk about how we are originizing our Redux code. We're going to use Ducks, which is a simple system that has a few requirement which are: 10 | 11 | ### Each Ducks file: 12 | - MUST export default a function called reducer() 13 | - MUST export its action creators as functions 14 | - MUST have action types in the form npm-module-or-app/reducer/ACTION_TYPE 15 | - MAY export its action types as UPPER_SNAKE_CASE, if an external reducer needs to listen for them, or if it is a published reusable library 16 | 17 | We'll see these principles in action as we go. 18 | 19 | ## Types 20 | 21 | Types are constants that are typically defined either in their own file or, when following the ducks methodology like we are, at the top of the file containing your reducer. They are `const`s, all uppercase, and my be exported if other reducers need to listen for them. 22 | 23 | Types are listened for within the reducer switch whenever an action is fired off. The action is sent to the reducer with a type, and when that type matches the type within the a case of the switch, some sort of data manipulation is fired off. 24 | 25 | Types look like this: 26 | 27 | ```javascript 28 | const ADD_ITEM = 'ADD_ITEM'; 29 | const DELETE_ITEM = 'DELETE_ITEM'; 30 | ``` 31 | 32 | ## Actions 33 | 34 | Actions are simple javascript objects that at least contain a type, and may also contain data that can be sent to the reducer. When the user clicks on something that has an effect on the state of an app, an action creator sends an action to the reducer where the data manipulation happens. 35 | 36 | An action typically looks like this: 37 | 38 | ```javascript 39 | { type: ADD_ITEM, item: 'Adding this item' } 40 | { type: DELETE_ITEM, index: 1 } 41 | { type: POP_ITEM } 42 | ``` 43 | 44 | ## Action Creators 45 | 46 | Action creators are the functions that create actions and send them to the reducer. They usually return an action, sometimes can dispatch multiple actions (with the help of middleware like thunk), or can begin asyncronous events like API calls (with the help of the Promise middleware). In this tutorial, we'll keep them simple. 47 | 48 | A simple action creature looks like this: 49 | 50 | ```javascript 51 | function addItem(item){ 52 | return { 53 | type: ADD_ITEM, 54 | item // this is new ES6 shorthand for when the key is the same as a variable or perameter within the scope of the object. It's the same as item: item 55 | } 56 | } 57 | ``` 58 | 59 | ## Reducers 60 | 61 | The reducer is typically the only thing that touches the store. It only deals within a particular part of the store, inicialized as `initialState`. It's a pure switch statement that does not directly change the state because state is immutable. That means you cannot use a method like `.pop` or `.push` that manipulates the array it's called on. Luckily for us, we've been keeping our data immutiable this whole time :) Finally, all reducers have a default case that just returns state. 62 | 63 | Here's an example of a reducer: 64 | 65 | ```javascript 66 | const initialState = { 67 | list: [] 68 | }; 69 | 70 | export default function reducer(state = initialState, action){ 71 | switch (action.type){ 72 | case ADD_ITEM: 73 | return Object.assign( 74 | {}, 75 | state, 76 | { list: [...state.list, action.item]} // here we see object.assign again, and we're returning a new state built from the old state without directly manipulating it 77 | ) 78 | default: 79 | return state; 80 | } 81 | } 82 | ``` 83 | 84 | Ok, those are the concepts. Now time for the real work. 85 | 86 | ## 1. Initial state 87 | 88 | The first thing we'll want to do is make sure our data is coming in fine. To do this, add something into the `initialState` within `src/redux/modules/toDoApp`. Here's what mine looks like: 89 | 90 | ```javascript 91 | const initialState = { 92 | list: [{item: 'test', done: false}] // just added this to test that state is being passed down propperly, 93 | newToDo: '' 94 | }; 95 | 96 | export default function reducer(state = initialState, action){ 97 | switch (action.type){ 98 | default: 99 | return state; 100 | } 101 | } 102 | ``` 103 | Now, in `ToDoApp.js`, in the `render()` method, before the return, I added `console.log(this.props)` to see what I get. I got this in my browser console: 104 | 105 | ```javascript 106 | toDoApp: Object 107 | list: Array[1] 108 | 0: "test" 109 | length: 1 110 | __proto__: Array[0] 111 | __proto__: Object 112 | __proto__: Object 113 | ``` 114 | Looks good to me. 115 | 116 | So the test passed. Let's take that data and pass it to the view. For this, all we need to do is to go to the JSX within `ToDoApp` and replace the `listItems` prop in the `List` component, and the `value` prop in the `Input` component. Try to implement this change yourself before moving forward. 117 | 118 | Did you get it? The `List` and `Input` components should look like this: 119 | 120 | ```javascript 121 | 126 | 131 | ``` 132 | 133 | Now, if you look at your app, and see the test list item. If you type in the input box or click something, you should be seeing errors in your console. Nothing happens on submit or click though, so we're going to add some actions. 134 | 135 | ## 3. Input action 136 | 137 | The process we're going to take now is migrate all the logic we have in our `ToDoApp` component into our `toDoApp` module. 138 | 139 | In 'toDoApp.js' (lowercase t), we're going to create new type called `INPUT_CHANGED'. Then create and export a new function called 'onInputChange' that takes `value` as a parameter and returns an action with the type equal to `INPUT_CHANGED` and `value` equal to `value`. These new features should look like this: 140 | 141 | ```javascript 142 | const INPUT_CHANGED = 'INPUT_CHANGED'; 143 | 144 | export function inputChange(newToDo){ 145 | return { 146 | type: INPUT_CHANGED, 147 | newToDo 148 | } 149 | } 150 | ``` 151 | 152 | Now we need to add the logic to add this input to the store within the reducer. Within the switch statement, add: 153 | 154 | ```javascript 155 | case INPUT_CHANGED: 156 | return Object.assign( 157 | {}, 158 | state, 159 | {input: action.value} 160 | ) 161 | ``` 162 | 163 | Now, when the action creator is fired off, the action with the type of `INPUT_CHANGED` will be sent to the reducer, and state will be updated without being modified. Now we need to pass this functionality to the app. 164 | 165 | In `toDoAppContainer.js` within the `mapDispatchToProps` function, we're going to add this new function. First import `onInputChange`, then in the return statement of `mapDispatchToProps` add: `onInputChange: (value) => dispatch(onInputChange(value))`. 166 | 167 | The entire file should look like this: 168 | 169 | ```javascript 170 | import { connect } from 'react-redux'; 171 | import ToDoApp from '../components/ToDoApp.js' 172 | import { 173 | inputChange 174 | } from '../redux/modules/toDoApp'; // we added this 175 | 176 | function mapStateToProps(state) { 177 | return { 178 | toDoApp: state.toDoApp // gives our component access to state through props.toDoApp 179 | } 180 | } 181 | 182 | function mapDispatchToProps(dispatch) { 183 | return { 184 | inputChange: (value) => dispatch(inputChange(value)) // we added this 185 | }; 186 | } 187 | 188 | export default connect( 189 | mapStateToProps, 190 | mapDispatchToProps 191 | )(ToDoApp); 192 | ``` 193 | Now we should have acces to that in our `ToDoApp` component. An easy way to tell is to check in our browser console to see if it shows up in props. If so, you did it correctly. If not, check the source files of this project. 194 | 195 | So, within the `onInputChange` method of `ToDoApp`, replace the line `this.setState({ newToDo: event.target.value});` to `this.props.inputChange(event.target.value);`. To test that this is coming through, go to the browser, find the input box and start typing. You should see some enchanced logs appearing. They'll say `prev state`, `action` and `next state`. This is the function of the Logger middleware, and it's very useful in development. 196 | 197 | ## 4. The other actions 198 | 199 | All of the changes from here on will follow the same pattern as above. If you understand the pattern, try to retrofit the app on your own. If you get stumped, peek at the source code in this repo. Otherwise, let's get started with the next action. 200 | 201 | Next we have `onInputSubmit`. Go to the redux file, `toDoApp.js` and start my making a type constant and an action creator. They should look like this: 202 | 203 | ```javascript 204 | 205 | const INPUT_SUBMIT = 'INPUT_SUBMIT'; 206 | 207 | export function inputSubmit(){ 208 | return { 209 | type: INPUT_SUBMIT 210 | }; 211 | } 212 | ``` 213 | 214 | Now we need to add a case for the type in our reducer that updates state. It should look like this: 215 | 216 | ```javascript 217 | export default function reducer(state = initialState, action){ 218 | switch (action.type){ 219 | case INPUT_SUBMIT: 220 | return Object.assign( 221 | {}, 222 | state, 223 | { list: [...state.list, {item: state.newToDo, done: false }], 224 | newToDo: ''} 225 | ); 226 | case INPUT_CHANGED: 227 | return Object.assign( 228 | {}, 229 | state, 230 | {newToDo: action.value} 231 | ); 232 | default: 233 | return state; 234 | } 235 | } 236 | ``` 237 | 238 | Now we need to add this action to our `toDoAppContainer`. Import it, then add it to the `mapDispatchToProps` function. 239 | 240 | ```javascript 241 | import { connect } from 'react-redux'; 242 | import ToDoApp from '../components/ToDoApp.js' 243 | import { 244 | inputChange, 245 | inputSubmit 246 | } from '../redux/modules/toDoApp'; 247 | 248 | ... 249 | 250 | function mapDispatchToProps(dispatch) { 251 | return { 252 | onInputChange: (value) => dispatch(onInputChange(value)), 253 | inputSubmit: () => dispatch(inputSubmit()) 254 | }; 255 | } 256 | ``` 257 | 258 | Cool. Now we have access to the in the `ToDoApp` component through props. Go there and remove the `setState` portion of the `onInputSubmit` method and call our new function. It should look like this: 259 | 260 | ```javascript 261 | onInputSubmit = (event) => { 262 | event.preventDefault(); 263 | this.props.inputSubmit(); 264 | }; 265 | ``` 266 | 267 | Now repeat this process for the remaining actions. Let's do the remainder in bulk. We just have `onListItemClick` and `deleteListItem` remaining. In the redux file `toDoApp.js` add the remaining constants and action creators. They should look like this: 268 | 269 | ```javascript 270 | const LIST_ITEM_CLICK = 'LIST_ITEM_CLICK'; 271 | const DELETE_LIST_ITEM = 'DELETE_LIST_ITEM'; 272 | 273 | export function listItemClick(index){ 274 | return { 275 | type: { 276 | LIST_ITEM_CLICK, 277 | index 278 | } 279 | } 280 | } 281 | 282 | export function deleteListItem(index) { 283 | return { 284 | type: DELETE_LIST_ITEM, 285 | index 286 | } 287 | } 288 | ``` 289 | 290 | Now we need to adjust the reducer to include these: 291 | 292 | ```javascript 293 | case LIST_ITEM_CLICK: 294 | return Object.assign( 295 | {}, 296 | state, 297 | { 298 | list: [ 299 | ...state.list.slice(0, action.index), 300 | Object.assign({}, state.list[action.index], {done: !state.list[action.index].done}), 301 | ...state.list.slice(action.index+1) 302 | ] 303 | } 304 | ); 305 | case DELETE_LIST_ITEM: 306 | return Object.assign( 307 | {}, 308 | state, 309 | { 310 | list: [ 311 | ...state.list.slice(0, action.index), 312 | ...state.list.slice(action.index+1) 313 | ] 314 | } 315 | ); 316 | ``` 317 | Then pass them through to `ToDoApp` using the `ToDoAppContainer`: 318 | 319 | ```javascript 320 | function mapDispatchToProps(dispatch) { 321 | return { 322 | inputChange: (value) => dispatch(inputChange(value)), 323 | inputSubmit: () => dispatch(inputSubmit()), 324 | deleteListItem: (i) => dispatch(deleteListItem(i)), 325 | listItemClick: (i) => dispatch(listItemClick(i)) 326 | }; // here we're mapping actions to props 327 | } 328 | ``` 329 | 330 | Now, adjust the logic in `ToDoApp`: 331 | 332 | ```javascript 333 | onInputChange = (event) => { 334 | this.props.inputChange(event.target.value); 335 | }; 336 | 337 | onListItemClick = (i) => { 338 | this.props.listItemClick(i) 339 | }; 340 | ``` 341 | 342 | And finally, we can remove the `componentWillMount` method, because the state that it's setting is no longer being used. 343 | 344 | ## There you have it 345 | 346 | You now should have a fully functional To Do App built with Redact, Webpack, Babel and Redux. There are still some more optimizations we can make if we wanted, still more functionality we could add. But what we have now is the first step for your next project. Keep these files with you to reference them when you need. Continue learning by starting with the suggested reading materials in the Readme. But most of all, never stop coding! 347 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 这是一个从头搭建一个react简单应用的教程,最开始学redux时候从别人那copy后用到的,现在回过头看还是可以算作最好的一个demo了,重新更新了代码和一些配置,在github找到了初始版本,在此感谢[作者](https://github.com/goopscoop/ga-react-tutorial) 2 | 3 | # 从头开始建立一个React App - 项目基本配置 4 | 5 | 1. `npm init` 生成 `package.json` 文件. 6 | 2. 安装各种需要的依赖: 7 | - `npm install --save react` - 安装React. 8 | - `npm install --save react-dom` 安装React Dom,这个包是用来处理virtual DOM。这里提一下用React Native的话,这里就是安装react-native。 9 | - `npm install --save-dev webpack` - 安装Webpack, 现在最流行的模块打包工具. 10 | - `npm install --save-dev webpack-dev-server` - webpack官网出的一个小型express服务器,主要特性是支持热加载. 11 | - `npm install --save-dev babel-core` - 安装Babel, 可以把ES6转换为ES5,注意Babel最新的V6版本分为babel-cli和babel-core两个模块,这里只需要用babel-cor即可。 12 | - 安装其他的babel依赖(babel真心是一个全家桶,具体的介绍去官网看吧..我后面再总结,这里反正全装上就是了): 13 | - `npm install --save babel-polyfill` - Babel includes a polyfill that includes a custom regenerator runtime and core.js. This will emulate a full ES6 environment 14 | - `npm install --save-dev babel-loader` - webpack中需要用到的loader. 15 | - `npm install --save babel-runtime` - Babel transform runtime 插件的依赖. 16 | - `npm install --save-dev babel-plugin-transform-runtime` - Externalise references to helpers and builtins, automatically polyfilling your code without polluting globals. 17 | - `npm install --save-dev babel-preset-es2015` - Babel preset for all es2015 plugins. 18 | - `npm install --save-dev babel-preset-react` - Strip flow types and transform JSX into createElement calls. 19 | - `npm install --save-dev babel-preset-stage-2` - All you need to use stage 2 (and greater) plugins (experimental javascript). 20 | 3. 打开 `package.json` 然后添加下面的scripts: 21 | ``` 22 | "scripts": { 23 | "start": "webpack-dev-server --hot --inline --colors --content-base ./build", 24 | "build": "webpack --progress --colors" 25 | } 26 | ``` 27 | 28 | 命令行输入 `npm start` 将要启动webpack dev server. 29 | 30 | 命令行输入 `npm build` 将会进行生产环境打包. 31 | 4. 启动webpack 32 | 33 | Webpack是我们的打包工具,在我们的开发环境中具体很重要的作用,具有很多非常便捷的特性,尤其是热加载hot reloading. `webpack.config.js` 是如下所示的webpack的配置文件. 随着app的不断变化,配置文件也会不断的更新,这里我们就用默认的`webpack.config.js`来命名这个配置文件,假如你用别的名字比如`webpack.config.prod.js`那么上面的脚本build就需要相应的改变指定相应的配置文件名字:`"build": "webpack webpack.config.prod.js --progress --colors"` 34 | 35 | ```javascript 36 | var webpack = require('webpack'); 37 | module.exports = { 38 | entry: './src/app.js', 39 | output: { 40 | path: __dirname + '/build', 41 | filename: "bundle.js" 42 | }, 43 | module: { 44 | rules: [{ 45 | test: /\.js$/, 46 | exclude: /node_modules/, 47 | loader: 'babel-loader', 48 | query: { 49 | plugins: ['transform-runtime'], 50 | presets: ['es2015', 'react', 'stage-2'] 51 | } 52 | }, { 53 | test: /\.css$/, 54 | loader: "style-loader!css-loader" 55 | }] 56 | } 57 | }; 58 | 59 | ``` 60 | 5. OK,我们项目的基本配置终于完成了,是时候开始写Reac代码了. 61 | 62 | # React 基础 - 建立你的第一个Component 63 | 64 | 在上面的项目的基本配置基础上,我们开始书写React的第一个组件来熟悉React的写法与组件思想。 65 | 66 | 1. 首先我们在项目根目录中新建一个 `index.html` 文件。 在这个基础工程中, 我们使用bootstrap的样式,直接引入一个cdn即可. 然后添加一个html标签 `
    `,我们的app就会注入到这个div中。 最后再引入 ``,这是最后打包生成的js代码。 67 | 68 | 以下是完整的代码: 69 | 70 | ```html 71 | 72 | 73 | 74 | 75 | Document 76 | 77 | 78 | 79 |
    80 | 81 | 82 | 83 | ``` 84 | 2. 建立一个新的文件夹 `src`. 我们app的大部分代码都将放在这个文件夹里面。在 `src` 中建立 `app.js`,作为React App的根组件, 其他所有的组件都会注入到这个跟组件中。 85 | 3. 首先我们需要导入react,现在都已经用ES6的语法, `import React from 'react';` , 然后我们要引入react-dom. 这里面有react中最重要的一个虚拟dom的概念.引入代码:`import ReactDOM from 'react-dom';` 86 | 87 | 4. 现在需要引入的依赖都已经完毕我们可以写第一个组件了: 88 | 89 | ```javascript 90 | class App extends React.Component { 91 | render(){ // Every react component has a render method. 92 | return( // Every render method returns jsx. Jsx looks like HTML, but it's actually javascript and functions a lot like xml, with self closing tags requiring the `/` within the tag in order to work propperly 93 |
    94 | Hello World 95 |
    96 | ); 97 | } 98 | } 99 | ``` 100 | 101 | 注意这里"Hello World"写在 `div`中. 所有的jsx代码都需要写在一个父div中. 102 | 103 | 5. 最后我们需要把我们写好的组件render给Dom,这里就需要用到 `ReactDOM.render` 方法. 104 | 105 | 在 `App.js` 的下面添加: `ReactDOM.render(, document.getElementById('app'));` 106 | 107 | 第一个参数就是我们App的根组件, 写作``的形式. 第二个参数就是我们的APP将要主要的DOM元素. 在这个项目中,就是我们在index中写的id为`app`的 `div`标签。 108 | 109 | 110 | Ok,我们的APP结构已经出来了,经典的hello world已经实现。马上我们就在这个基础上再实现经典的todo app。大致的原型就有一个输入框用来输入代办事项然后添加到事件列表中。事件列表中每一个代办事项被点击就会标注一条删除线表示完成,点击后面的删除按钮则会将其从列表中删除。通过完成这个APP的过程你将学会一个完整的react app的所有的基本构建块。 111 | 112 | # 生命周期方法和两种形式的组件构建 113 | 114 | 我们从一些小的模块开始起步.一个组件component就是一个react app的构件块. 有两种形式的组件: 类组件(Class)和函数型组件(Functional). 在这个项目中,这两种形式的组件我们都会使用, 并且使用生命周期的钩子,同时也会使用state和props两个react中重要的属性。 115 | 116 | 1. 首先在 `src`文件夹中新建`components` 文件夹,你的文件结构就是这样 `~/src/components`。 117 | 118 | 2. 然后在`components`中新建文件 `ToDoApp.js`。 对于所有的react组件我们都需要在头部引入react`import React from 'react';`。 119 | 120 | 3. 下面我们写一个类组件. 所有的class 组件有一个render方法用来返回jsx。 121 | 122 | ToDoApp的class就如下所示: 123 | 124 | ```javascript 125 | class ToDoApp extends React.Component { 126 | render() { 127 | return ( 128 |
    To Do App
    129 | ); 130 | } 131 | } 132 | ``` 133 | 134 | 4. 为了将这个组件注入到我们的APP中, 首先我们需要输出它。 在这个组件代码底部添加 `export default ToDoApp;`。 135 | 136 | 5. 然后在`app.js`顶部我们添加 `import ToDoApp from '.components/ToDoApp';` 导入组件用来代替 `Hello World` 。 render中替换为新的jsx代码 ``半闭合类型的标签即可。 137 | 138 | 6. 然后在浏览器中你就可以看到"To Do App" 代替了原来的 "Hello World"!这样我们就完成了将第一个子组件嵌入到根组件之中了,这就是构建react app的常规模式。下面继续完善我们的组件。 139 | 7. 返回到`ToDoApp` 中来构建我们的第一个代办事项列表。首先我们使用bootstrap来构建比较方便且美观。 用下面的jsx替换当前`render`方法中 `return` 中的jsx: 140 | ```html 141 |
    142 |
    143 |
    144 |
    145 |

    My To Do App

    146 |
    147 | List goes here. 148 |
    149 |
    150 |
    151 |
    152 | ``` 153 | 154 | 8. 现在打开浏览器, 你将会看到一个标题 "My To Do App" 下面跟随一个bootstrap的panel组件里面写有 "List Goes Here",我们将在这个地方构建列表。 那么我们如何将数据存储在我们的列表中呢? 答案就是使用 `state`. 每一个类组件都有 `state` 属性,可以通过 `this.state`在组件任何位置获取并且用 `this.setState({ key: "value" })`这种方法来更新状态。但是除非必要我们比较少使用`state`,这里暂时先使用作为了解,后期会使用redux来管理状态。 155 | 156 | 在`ToDoApp`中我们可以使用许多生命周期方法的钩子, 其中一个就是`componentWillMount`。 这个方法的执行是在页面加载并且render方法之前。可以在其中获取列表数据,在我们的APP中直接用一个虚拟的数组提供。(值得注意的是componentWillMount会引起很多小问题,因此真实项目中尽量不要使用,而是应该用componentDidMount)。 157 | 在 `ToDoApp`中 `render` 方法之前添加: 158 | 159 | ```javascript 160 | componentWillMount(){ // run before the render method 161 | this.setState({ // add an array of strings to state. 162 | list: ['thing1', 'thing2', 'thing3'] 163 | }) 164 | }; 165 | ``` 166 | 167 | 现在我们获取了一个虚拟列表,需要重点注意的就是react依赖于state和props,只有当state和props改变的时候react组件才会刷新。 168 | 169 | 9. 现在我们添加列表到这个view里,这里不是直接简单的在里面修改jsx,而是再创建一个新的组件来构建列表,这次我们学习使用函数型组件,需要注意的是函数型组件没有生命周期方法和state属性,它仅仅是一个返回jsx的函数,并且参数是props。 170 | 171 | 那么props到底是什么呢?props是从父组件传递进子组件的数据的名字,这是一个很重要的概念,也是react app数据传递的最典型与最推荐的方法。通常我们将数据保持在app的顶端组件,通过组件让数据流下来保证APP的精确运行。这些数据和props的一些处理可能会影响APP的运行,但是假如你按照这个课程的实践流程来做,这些影响都会很小。 172 | 173 | 再新建一个`components`文件夹并在其中新建一个`List.js`作为我们要创建的函数型组件。用const来新建一个函数,参数名字写作props。 174 | 函数形式如下所示: 175 | 176 | ```javascript 177 | const List = (props) => { // we're using an arrow function and const variable type, a ES6 features 178 | 179 | return ( 180 |
    181 | I'm a list!!! 182 |
    183 | ) 184 | }; 185 | 186 | export default List; 187 | ``` 188 | 189 | 10. 在 `ToDoApp.js`引入 List用`List` 组件替换 `List goes here.`,写法为 ``.现在在浏览器中就可以看到"I'm a list!!!" 190 | 191 | 现在我们来把这个变成真实的列表,首先就需要通过props传递数据,我们把这个从state中获取的数据list通过命名为listItems的props传递,写作: `` ,现在 `List` 已经通过props获取了 `ToDoApp`中的数据。 192 | 193 | 然后在 `List` 组件中我们需要render一个列表,先用下面的jsx代码代替: 194 | 195 | ```javascript 196 |
    197 |
      198 | { 199 | list // this is a variable we'll define next 200 | } 201 |
    202 |
    203 | ``` 204 | 205 | 注意这个大括号,js可以在这里面执行并将返回添加到view里。首先我们定义一个列表变量: 206 | 207 | ```javascript 208 | const list = props.listItems.map((el, i)=>( 209 | // All where doing here is getting the items listItems prop 210 | // (which is stored in the state of the parent component) 211 | // which is an array, and we're running the .map method 212 | // which returns a new array of list items. The key attribute is 213 | // required, and must be unique. 214 |
  • el

  • 215 | )); 216 | ``` 217 | 218 | 完整的组件如下: 219 | 220 | ```javascript 221 | import React from 'react'; 222 | 223 | const List = (props) => { 224 | 225 | const list = props.listItems.map((el, i)=>( 226 |
  • el

  • 227 | )); 228 | 229 | return ( 230 |
    231 |
      232 | { 233 | list 234 | } 235 |
    236 |
    237 | ) 238 | }; 239 | 240 | export default List; 241 | ``` 242 | 243 | 11. 现在打开浏览器就可以看到一列列表了。接下来就是给我们的项目加入功能了,包括添加新的事项,标注事项完成和删除列表中事项。 244 | 245 | # 给APP添加功能 246 | 247 | ## 1.函数型组件 248 | 249 | 首先我们需要添加一个input元素以便可以输入代办事项。因此我们在`components`文件夹中新建一个`Input.js`,然后在其中创建并输出一个名叫Input的函数型组件。 250 | 把下面的jsx代码粘贴到你的函数型组件return之中: 251 | 252 | ```html 253 |
    254 |
    256 | 260 | 266 | 270 |
    271 |
    272 | ``` 273 | 274 | ## 2. Input 275 | 276 | 现在我们的jsx没有做任何特殊的事情,仅仅是一个基本的html视图,不过我们先测试一下把其导入到`ToDoApp.js`,形式就是``。 277 | 278 | 这时候会发现一个输入框和按钮的视图,这个组件的静态视图已经写好了,下面就需要添加功能了。 279 | 280 | ## 3. Props 281 | 282 | 首先我们需要做的是如何获取输入框的值,因为这个输入框的值需要在其他组件中获取,所以我们并不想要在`Input`组件中来处理这个数据存储。事实上,在子组件中存储数据在任何时候都是不推荐的,我们应该将数据存储在app的顶端组件并且通过props传递下来。 283 | 284 | 另一个需要记住的是即使我们目前把数据存储在了上层的 `ToDoApp` 组件,后期还是会用redux来代替来处理整个app的数据。这里先仅仅使用react的state来实现。 285 | 286 | ok,我们在`ToDoApp`的 `componentWillMount`的`setState`中新增一个`newToDo`属性用来存储输入框的值。 287 | 288 | ```javascript 289 | componentWillMount(){ 290 | this.setState({ 291 | list: ['thing1', 'thing2', 'thing3'], 292 | newToDo: 'test' 293 | }) 294 | }; 295 | ``` 296 | 297 | 同样的就可以通过在``上通过props传递下去。 298 | 299 | ## 4. 解构(Destructuring) 300 | 301 | 在`Input.js`中我们通过参数props可以获得上级组件传递下来的值, 但是还可以用ES6的新特性解构来作为参数,这样看起来更加酷! 302 | 303 | 把`Input`组件的`props`参数修改为`({ value })`这样的参数形式,这样可以把`props`这个对象参数解构为一个个键值对。直接看个小例子来就很明白了: 304 | 305 | ``` 306 | var props = { 307 | name: 'hector', 308 | age: 21 309 | } 310 | 311 | 312 | function log(props){ 313 | console.log(props.name); 314 | console.log(props.age); 315 | } 316 | 317 | log(props); 318 | ``` 319 | 320 | is the same as this: 321 | 322 | ``` 323 | let props = { 324 | name: 'hector', 325 | age: 21 326 | } 327 | 328 | log = ({name, age}) => { 329 | console.log(name); 330 | console.log(age); 331 | } 332 | 333 | log(props); 334 | ``` 335 | 336 | 337 | ## 5. setState 338 | 339 | 上面的`newToDo`仅仅是添加了一个state用来存储输入框的值,给定一个值,输入框就会显示,明显还不是我们要的效果,我们需要做的是基于输入框的值的改变来动态改变这个state。 340 | 341 | 为了实现这个功能,我们需要再添加一个`onChange`方法同样利用props传进`Input`组件: `onChange={onChange}`, 然后解构参数就是`({ onChange, value })`。 342 | 343 | 然后在 `ToDoApp.js`的`componentWillMount` 添加一个新的方法 `onInputChange`。这个方法有一个参数`event`, 它将要捕获用户在输入框输入的值。 344 | 345 | ```javascript 346 | onInputChange = (event) => { 347 | this.setState({ newToDo: event.target.value}); // updates state to new value when user changes the input value 348 | }; 349 | ``` 350 | 351 | 352 | ## 6. 添加新列表事项 353 | 354 | 现在需要向列表中添加新的事项,也就是在提交后能把输入框的值存储并显示到列表中。我们需要再新建一个`onInputSubmit`的方法,参数同样是`event`,函数体内首先需要写 `event.preventDefault()`,然后用 `setState` 方法把新事项添加到列表数组中,但是,一定要注意我们的state应该是immutable的,这是react中必须遵循的一个准则,这样才能保证对比性与可靠性。 355 | 356 | 为了实现这个功能, 需要用到`this.setState` 回调函数,参数为`previousState`: 357 | 358 | ```javascript 359 | this.setState((previousState)=>({ 360 | list: previousState.list.push(previousState.newToDo) 361 | })) 362 | ``` 363 | 364 | 正如我上面的描述,最开始写state的时候很多人都会犯这样的错误,直接用push这样的方法,修改了state,这样就不算immutable的,我们一定要保证绝不直接修改原state。 365 | 366 | 这里又可以用到ES6中的新特性了,扩展操作符,它通过遍历旧数组返回一个新数组,使旧的数组保持原样,这样我们就把事项添加到列表数组末尾: 367 | 368 | ```javascript 369 | this.setState((previousState)=>({ 370 | list: [...previousState.list, previousState.newToDo ], // the spread opperator is called by using the ... preceding the array 371 | })); 372 | ``` 373 | 374 | 在提交添加新事项的同时,需要将`newToDo`重置为`''`: 375 | 376 | ```javascript 377 | this.setState((previousState)=>({ 378 | list: [...previousState.list, previousState.newToDo ], 379 | newToDo: '' 380 | })); 381 | ``` 382 | 383 | ## 7. 划掉事项 384 | 385 | 是时候添加划掉事项的功能了。为了实现这个功能需要添加一个新的属性用来标注是否需要划掉,因此需要改变原来的数组为一个对象数组,每一个事项都是一个对象,一个key为`item`表示原来的事项内容,一个key为`done`用布尔值来表示是否划掉。 然后先把原来的`onInputSubmit `方法修改,同样要注意immutable,使用扩展操作符如下: 386 | 387 | ```javascript 388 | onInputSubmit = (event) => { 389 | event.preventDefault(); 390 | this.setState((previousState)=>({ 391 | list: [...previousState.list, {item: previousState.newToDo, done: false }], // notice the change here 392 | newToDo: '' 393 | })); 394 | }; 395 | ``` 396 | 397 | 属性`done`添加完成后就需要新增一个方法当点击事项时候来改变这个值: 398 | 399 | ```javascript 400 | onListItemClick = (i) => { // takes the index of the element to be updated 401 | this.setState((previousState)=>({ 402 | list: [ 403 | ...previousState.list.slice(0, i), // slice returns a new array without modifying the existing array. Takes everything up to, but not including, the index passed in. 404 | Object.assign({}, previousState.list[i], {done: !previousState.list[i].done}), // Object.assign is a new ES6 feature that creates a new object based on the first param (in this case an empty object). Other objects can be passed in and will be added to the first object without being modified. 405 | ...previousState.list.slice(i+1) // takes everything after the index passed in and adds it to the array. 406 | ] 407 | })) 408 | }; 409 | ``` 410 | 然后把这个方法通过props传递给`List` 组件,这里就没有使用解构参数传递,用来和`Input`的做对比。因为这个函数需要一个参数就是当前列表的序列号,但是肯定不能直接call这个函数否则会报错,因此使用bind方法,出入`i`参数: 411 | 412 | ```javascript 413 | onClick={props.onClick.bind(null, i)} 414 | ``` 415 | 416 | 当然还有另一种方法: 417 | 418 | ```javascript 419 | onClick={() => props.onClick(i)} 420 | ``` 421 | 422 | 然后在事项内容的`span`标签上添加 `onClick` 方法,改变当前事项的`done`值后,在通过判断此布尔值来进行样式的修改添加或者划掉删除线。 423 | 424 | ```javascript 425 | 433 | ``` 434 | 435 | ## 8. 删除事项 436 | 437 | 最后我们在添加删除事项的功能,这个和划掉事项非常相似,我们只需要新增一个删除按钮,然后再新增一个方法修改列表,具体代码如下: 438 | 439 | ```javascript 440 | 445 | ``` 446 | 447 | 448 | ```javascript 449 | deleteListItem = (i) => { 450 | this.setState((previousState)=>({ // using previous state again 451 | list: [ 452 | ...previousState.list.slice(0, i), // again with the slice method 453 | ...previousState.list.slice(i+1) // the only diffence here is we're leaving out the clicked element 454 | ] 455 | })) 456 | }; 457 | ``` 458 | 459 | 把`deleteListItem` 方法传递到列表组件中然后在删除按钮上绑定即可,仿照上一个自己写一下就好。 460 | 461 | 现在我们有一个完整功能的APP了,是不是感觉很cool,这个就是不用redux时候的形态了,但是你会发现当状态越来越复杂时候很繁琐,因此我们下面就要介绍redux来管理状态了。 462 | 463 | # 迁移到redux的准备工作 464 | 465 | 截至目前我们已经学会如何用webpack和babel搭建react应用,构建类组件和函数型组件并处理state,添加功能。然而这只是基本满足一个小型应用的需求,随着app的增长,处理数据和行为会越来越吃力,这就是要引入redux的必要性。 466 | 467 | 那么redux如何处理数据?首先,redux给你的app一个单一的state对象,与flux等根据view来划分为多个state对象正好相反。你可能会有疑问,一个单一的对象来处理一个复杂的app岂不是非常复杂?redux采用的方法是把数据处理分为`reducer functions`、`action creators`和`actions`然后组合在一起工作流线型的处理数据。 468 | 469 | 470 | ## 1. 首先安装必须的依赖 471 | 472 | 首先安装 `redux` and `react-redux` 473 | 474 | ``` 475 | npm install --save redux 476 | npm install --save react-redux 477 | ``` 478 | 479 | 然后安装 redux middleware,这里就先安装 `redux-logger`,它的功能是帮助我们开发。 480 | 481 | ``` 482 | npm install --save redux-logger 483 | ``` 484 | 还有一些常用的中间件,比如 `redux-thunk` and `redux-promise`, 但是在我们的这个项目中暂时先不需要,可以自行去github了解。 485 | ## 2. 构建 486 | 使用redux构建react应用一般都有一个标准的模板,可能不同模板形式上有区别,但是思想都是一样的,下面就先按照一种文件结构来构建。 487 | 488 | 首先我们在`src`中新建一个文件夹`redux`,然后在其中新建一个文件`configureStore.js`,添加以下代码: 489 | 490 | ```javascript 491 | import { createStore, applyMiddleware, combineReducers } from 'redux'; 492 | import createLogger from 'redux-logger'; 493 | ``` 494 | 495 | `createStore` 是由redux提供的用来初始化store的函数, `applyMiddleware`是用来添加我们需要的中间件的。 496 | 497 | `combineReducers` 用来把多个`reducers`合并为一个单一实体。 498 | 499 | `createLogger` 就是我们这里唯一使用的一个中间件,可以`console`出每一个`action`后数据的详细处理过程,给调试带来了很大方便。 500 | 501 | 502 | 然后添加下面代码: 503 | 504 | ```javascript 505 | const loggerMiddleware = createLogger(); // initialize logger 506 | 507 | const createStoreWithMiddleware = applyMiddleware( loggerMiddleware)(createStore); // apply logger to redux 508 | ``` 509 | 510 | 这里暂时没有完成,需要后面的模块写完了再导入到这里继续来完成。 511 | 512 | ## 3. 模块Modules 513 | 514 | 在 `src/redux/` 新建一个文件夹 `modules`。在这个文件夹中我们将存放所有的`reducers`,`action creators`和`constants`。这里我们使用的redux组织结构叫做`ducks`,思想就是把相关的`reducers`,`action creators`和`constants`都放在一个单独的文件中,而不是分开放在多个文件中,这样修改一个功能时候直接在一个文件中修改就可以。 515 | 516 | 在 `modules` 文件中新建 'toDoApp.js',注意这里的命名是依据容器组件的名字来命名,这个也是规范,容易管理代码。 517 | 518 | 现在我们可以开始创建`initial state `和 `reducer function`,这其实非常简单,`state`就是一个js对象,`reducer`就是js的switch语句: 519 | ```javascript 520 | const initialState = {}; //The initial state of this reducer (will be combined with the states of other reducers as your app grows) 521 | 522 | export default function reducer(state = initialState, action){ // a function that has two parameters, state (which is initialized as our initialState obj), and action, which we'll cover soon. 523 | switch (action.type){ 524 | default: 525 | return state; 526 | } 527 | } 528 | ``` 529 | 530 | ## 4. 完善Store 531 | 现在我们已经完成了第一个`reducer`,可以将其添加到 `configureStore.js` 中去了, 导入: `import toDoApp from './modules/toDoApp';` 532 | 533 | 然后用`combineReducers ` 来组合当前的`reducer`,因为未来会有更多的模块加入。 534 | 535 | ```javascript 536 | const reducer = combineReducers({ 537 | toDoApp 538 | }); 539 | ``` 540 | 541 | 最后在底部加入下面完整的代码: 542 | 543 | ```javascript 544 | const configureStore = (initialState) => createStoreWithMiddleware(reducer, initialState); 545 | export default configureStore; 546 | ``` 547 | 548 | Cool. We're done here. 549 | 550 | ## 5. Connect 551 | 552 | 现在我们已经有了一个`reducer`,那么怎么和app建立联系呢?这需要两步工作。 553 | 554 | 前面已经讲过类组件和函数型组件,有时候也可以称为`smart components`和` dumb components`,这里我们新增一种容器组件,顾名思义,这种组件就是作为一个容器用来给组件提供`actions`和`state`。 555 | 556 | 下面来创建第一个容器组件,首先在 `/src/` 下新增一个文件夹`containers`,然后再其下面新建一个文件 `toDoAppContainer.js`。 557 | 在文件顶部首先导入 `connect` 用来将容器和组件联系在一起, 558 | 559 | ```javascript 560 | import { connect } from 'react-redux'; 561 | import ToDoApp from '../components/ToDoApp.js' 562 | ``` 563 | 564 | `connect` 这个函数被调用两次, 第一次是两个回调函数: `mapStateToProps` and `mapDispatchToProps`。 第二次是把`state`和`dispatch`传入组件的时候。这里的`dispatch`又是什么呢? 565 | 566 | 当我们需要在redux中发生某些行为时候,就需要调用`dispatch`函数传递一个`action`然后调用`reducer`这一套流程。因为我们还没有编写具体的行为,这里就暂时空白,后面再补,代码形式如下: 567 | 568 | ```javascript 569 | function mapStateToProps(state) { 570 | return { 571 | toDoApp: state.toDoApp // gives our component access to state through props.toDoApp 572 | } 573 | } 574 | 575 | function mapDispatchToProps(dispatch) { 576 | return {}; // here we'll soon be mapping actions to props 577 | } 578 | ``` 579 | 580 | 然后在底部添加: 581 | 582 | ```javascript 583 | export default connect( 584 | mapStateToProps, 585 | mapDispatchToProps 586 | )(ToDoApp); 587 | ``` 588 | 589 | 6. Provider 590 | 591 | redux的基本工作已经完成,最后一步就是返回到`app.js` 文件, 首先我们不再需要导入 `ToDoApp` 组件,而是用容器组件`ToDoAppContainer`来替代,然后需要导入 `configureStore` 函数和 `Provider`,在头部添加代码: 592 | 593 | ```javascript 594 | import { Provider } from 'react-redux'; 595 | import ToDoAppContainer from './containers/ToDoAppContainer'; 596 | import configureStore from './redux/configureStore'; 597 | ``` 598 | 599 | `configureStore` is the function we created that takes our combined reducers and our redux middleware and mashes them all together. Let's intialize that with the following line: 600 | 601 | ```javascript 602 | const store = configureStore(); 603 | ``` 604 | 然后return的jsx中同样需要把`ToDoApp` 改为 `ToDoAppContainer`,然后需要用`Provider` 组件将其包裹,它的作用就是将整个app的state传递给它所包裹的容器,从而使容器组件可以获取这些state。 605 | 606 | ```javascript 607 | // we pass the store through to Provider with props 608 | 609 | 610 | ``` 611 | 612 | 现在整个redux的基本结构已经搭建起来,下一步就可以把整个行为逻辑代码补充进去就可以了。 613 | 614 | # Redux Actions 和 Reducers 615 | 616 | 搭建起redux的基本结构后,就可以填充redux的元素了,简单来说我们只需要记住四个概念, `Types`, `Actions`, `Action Creators`, and `Reducers`。然后把这些元素用`ducks`的文件组织结构组织起来就可以了。 617 | 618 | ## Ducks 619 | 620 | ### 规则 621 | 622 | 在module中我们需要遵循下面的代码风格和命名方式: 623 | 624 | 1. 须用 `export default` 输出名为 `reducer()`的函数 625 | 2. 须用 `export` 输出 函数形式的`action creators` 626 | 3. 须用 `npm-module-or-app/reducer/ACTION_TYPE` 627 | 的命名形式来命名`action types`,因为到后期很多reducer,不同的人协同工作难免会出现命名重复,这样子加上app和模块的前缀的话就不会出现命名冲突的问题。 628 | 4. 须用大写的蛇形方式`UPPER_SNAKE_CASE`来命名`action types`。 629 | 630 | 631 | ## Types 632 | 633 | 这个`types`就是上面第三条中需要按照`ducks`的规范命名的常量名字,将其写在文件的顶部,当`action` 触发时候会传递给`reducer`,`reducer`的switch语句会根据这个type来进行相应的数据处理。 634 | 635 | ```javascript 636 | const ADD_ITEM = 'my-app/toDoApp/ADD_ITEM'; 637 | const DELETE_ITEM = 'my-app/toDoApp/DELETE_ITEM'; 638 | ``` 639 | 640 | ## Actions 641 | 642 | 643 | `Actions` 就是一个至少包含`type`的简单的js对象,同时可以包含数据以便传递给`reducer`。当用户在页面上触发了某种行为,一个`aciton creator`将会发送`aciton`给`reducer`做数据处理。 644 | 645 | `action`示例如下: 646 | 647 | ```javascript 648 | { type: ADD_ITEM, item: 'Adding this item' } 649 | { type: DELETE_ITEM, index: 1 } 650 | { type: POP_ITEM } 651 | ``` 652 | 653 | ## Action Creators 654 | 655 | `Action creators` 是创建`acitons`并传递给`reducer`的函数,它通常返回一个`action`对象,有时候借用`thunk`这样的中间件也可以返回`dispatch`多个`actions`,在我们的app中为了简化暂时不涉及这个模式。 656 | 657 | ```javascript 658 | function addItem(item){ 659 | return { 660 | type: ADD_ITEM, 661 | item // this is new ES6 shorthand for when the key is the same as a variable or perameter within the scope of the object. It's the same as item: item 662 | } 663 | } 664 | ``` 665 | 666 | ## Reducers 667 | 668 | `reducer`是唯一可以触碰`store`的元素,初始值为`initialState`,形式上就是一个简单的switch语句,但是注意不能直接改变state,因为state是immutable。也就是说我们不能直接使用`.pop` or `.push`这些方法操作数组。 669 | 670 | 下面是示例代码: 671 | 672 | ```javascript 673 | const initialState = { 674 | list: [] 675 | }; 676 | 677 | export default function reducer(state = initialState, action){ 678 | switch (action.type){ 679 | case ADD_ITEM: 680 | return Object.assign( 681 | {}, 682 | state, 683 | { list: [...state.list, action.item]} // here we see object.assign again, and we're returning a new state built from the old state without directly manipulating it 684 | ) 685 | default: 686 | return state; 687 | } 688 | } 689 | ``` 690 | 691 | 概念已经介绍完毕,下面开始将原来的功能逻辑用redux重写。 692 | 693 | ## 1. Initial state 694 | 695 | 首先我们在 `src/redux/modules/toDoApp`中声明`initialState`。 696 | 697 | ```javascript 698 | const initialState = { 699 | list: [{item: 'test', done: false}] // just added this to test that state is being passed down propperly, 700 | newToDo: '' 701 | }; 702 | 703 | export default function reducer(state = initialState, action){ 704 | switch (action.type){ 705 | default: 706 | return state; 707 | } 708 | } 709 | ``` 710 | 现在在 `ToDoApp.js`的 `render()` 方法中`return`之前添加`console.log(this.props)` 会打印出下面的对象: 711 | ```javascript 712 | toDoApp: Object 713 | list: Array[1] 714 | 0: "test" 715 | length: 1 716 | __proto__: Array[0] 717 | __proto__: Object 718 | __proto__: Object 719 | ``` 720 | 721 | 测试通过,我们就可以传递这些数据给子组件了,这里就可以把原来`List`组件的 `listItems` prop和`Input`的`value` prop替换掉了。 722 | 723 | ```javascript 724 | 729 | 734 | ``` 735 | 736 | 这里只是替换掉了数据,下面还需要把action也替换。 737 | 738 | ## 3. Input action 739 | 740 | 这个过程就是把我们原来在`ToDoApp` 组件的行为逻辑全部迁移到redux文件夹下的 `toDoApp` module中去。 741 | 742 | 743 | ```javascript 744 | const INPUT_CHANGED = 'INPUT_CHANGED'; 745 | 746 | export function inputChange(newToDo){ 747 | return { 748 | type: INPUT_CHANGED, 749 | newToDo 750 | } 751 | } 752 | ``` 753 | 754 | 然后在reducer的switch中新增如下处理: 755 | 756 | ```javascript 757 | case INPUT_CHANGED: 758 | return Object.assign( 759 | {}, 760 | state, 761 | {newToDo: action.value} 762 | ); 763 | ``` 764 | 765 | 766 | 在 `toDoAppContainer.js` 的 `mapDispatchToProps` 函数就需要返回相应的action,首先导入 `inputChange`, 具体代码如下: 767 | 768 | ```javascript 769 | import { connect } from 'react-redux'; 770 | import ToDoApp from '../components/ToDoApp.js' 771 | import { 772 | inputChange 773 | } from '../redux/modules/toDoApp'; // we added this 774 | 775 | function mapStateToProps(state) { 776 | return { 777 | toDoApp: state.toDoApp // gives our component access to state through props.toDoApp 778 | } 779 | } 780 | 781 | function mapDispatchToProps(dispatch) { 782 | return { 783 | inputChange: (value) => dispatch(inputChange(value)) // we added this 784 | }; 785 | } 786 | 787 | export default connect( 788 | mapStateToProps, 789 | mapDispatchToProps 790 | )(ToDoApp); 791 | ``` 792 | 793 | 这样state和action都传递给了`toDoApp`然后再通过props传递给子组件就可以使用了,具体都可以看项目最终代码。 794 | 795 | ## 4. 其他 actions 796 | 797 | 其他acitons的代码模式跟上面的基本一样,这里不在赘述。 798 | 799 | 800 | ## 总结 801 | 802 | 到这里一个使用webpack打包的react+redux(ducks)的基本应用模型就出来了,虽然简单但是是我们进行更复杂项目的基础,并且有了这些基础后面的路程将会顺畅多了,一起加入react的大家庭吧。 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 |
    10 | 11 | 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-ducks", 3 | "version": "1.0.0", 4 | "description": "react redux ducks app", 5 | "scripts": { 6 | "start": "webpack-dev-server --hot --inline --colors --content-base ./build", 7 | "build": "webpack --progress --colors" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "babel-core": "^6.24.1", 13 | "babel-loader": "^6.4.1", 14 | "babel-plugin-transform-runtime": "^6.3.13", 15 | "babel-preset-es2015": "^6.24.1", 16 | "babel-preset-react": "^6.24.1", 17 | "babel-preset-stage-2": "^6.24.1", 18 | "webpack": "^2.3.3", 19 | "webpack-dev-server": "^2.4.2" 20 | }, 21 | "dependencies": { 22 | "babel-polyfill": "^6.3.14", 23 | "babel-runtime": "^6.23.0", 24 | "react": "^15.5.3", 25 | "react-dom": "^15.5.3", 26 | "react-hot-loader": "^3.0.0-beta.6", 27 | "react-redux": "^4.4.0", 28 | "redux": "^3.3.1", 29 | "redux-logger": "^2.5.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import ToDoAppContainer from './containers/ToDoAppContainer'; 5 | import configureStore from './redux/configureStore'; 6 | 7 | const store = configureStore(); 8 | 9 | class App extends React.Component { 10 | render(){ 11 | return( 12 | 13 | 14 | 15 | ); 16 | } 17 | } 18 | 19 | ReactDOM.render(, document.getElementById('app')); 20 | 21 | // 热替换HMR,需要加入这段代码才会进行生效 22 | if(module.hot) 23 | module.hot.accept(); -------------------------------------------------------------------------------- /src/components/Input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Input = ({ onChange, onSubmit, value }) => ( 4 |
    6 |
    8 | 12 | 20 | 24 |
    25 |
    26 | ) 27 | 28 | export default Input; -------------------------------------------------------------------------------- /src/components/List.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const List = (props) => { 4 | 5 | return ( 6 |
    7 |
      8 | { 9 | props.listItems.map((el, i)=>( 10 |
    • 12 |
      13 |
      14 | props.onClick(i)} 21 | > 22 | {el.item} 23 | 24 | 29 |
      30 |
      31 |
    • 32 | )) 33 | } 34 |
    35 |
    36 | ) 37 | }; 38 | 39 | export default List; -------------------------------------------------------------------------------- /src/components/ToDoApp.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import List from './List'; 3 | import Input from './Input'; 4 | 5 | class ToDoApp extends React.Component { 6 | 7 | onInputChange = (event) => { 8 | this.props.inputChange(event.target.value); 9 | }; 10 | 11 | onInputSubmit = (event) => { 12 | event.preventDefault(); 13 | this.props.inputSubmit(); 14 | }; 15 | 16 | onListItemClick = (i) => { 17 | this.props.listItemClick(i) 18 | }; 19 | 20 | deleteListItem = (i) => { 21 | this.props.deleteListItem(i) 22 | }; 23 | 24 | render(){ 25 | console.log(this.props) 26 | return ( 27 |
    28 |
    29 |
    30 |
    31 |

    My To Do App

    32 |
    33 | 38 | 43 |
    44 |
    45 |
    46 |
    47 | ); 48 | } 49 | } 50 | 51 | export default ToDoApp; -------------------------------------------------------------------------------- /src/containers/toDoAppContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import ToDoApp from '../components/ToDoApp.js' 3 | import { 4 | inputChange, 5 | inputSubmit, 6 | deleteListItem, 7 | listItemClick 8 | } from '../redux/modules/toDoApp'; 9 | 10 | function mapStateToProps(state) { 11 | return { 12 | toDoApp: state.toDoApp // gives our component access to state through props.toDoApp 13 | } 14 | } 15 | 16 | function mapDispatchToProps(dispatch) { 17 | return { 18 | inputChange: (value) => dispatch(inputChange(value)), 19 | inputSubmit: () => dispatch(inputSubmit()), 20 | deleteListItem: (i) => dispatch(deleteListItem(i)), 21 | listItemClick: (i) => dispatch(listItemClick(i)) 22 | }; // here we're mapping actions to props 23 | } 24 | 25 | export default connect( 26 | mapStateToProps, 27 | mapDispatchToProps 28 | )(ToDoApp); -------------------------------------------------------------------------------- /src/redux/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, combineReducers } from 'redux'; 2 | import createLogger from 'redux-logger'; 3 | import toDoApp from './modules/toDoApp'; 4 | 5 | const loggerMiddleware = createLogger(); // initialize logger 6 | 7 | const createStoreWithMiddleware = applyMiddleware( loggerMiddleware)(createStore); // apply logger to redux 8 | 9 | const reducer = combineReducers({ 10 | toDoApp 11 | }); 12 | 13 | const configureStore = (initialState) => createStoreWithMiddleware(reducer, initialState); 14 | export default configureStore; -------------------------------------------------------------------------------- /src/redux/modules/toDoApp.js: -------------------------------------------------------------------------------- 1 | const INPUT_CHANGED = 'react-redux-ducks/toDoApp/INPUT_CHANGED'; 2 | const INPUT_SUBMIT = 'react-redux-ducks/toDoApp/INPUT_SUBMIT'; 3 | const LIST_ITEM_CLICK = 'react-redux-ducks/toDoApp/LIST_ITEM_CLICK'; 4 | const DELETE_LIST_ITEM = 'react-redux-ducks/toDoApp/DELETE_LIST_ITEM'; 5 | 6 | export function listItemClick(index){ 7 | return { 8 | type: LIST_ITEM_CLICK, 9 | index 10 | } 11 | } 12 | 13 | export function deleteListItem(index) { 14 | return { 15 | type: DELETE_LIST_ITEM, 16 | index 17 | } 18 | } 19 | 20 | export function inputSubmit(){ 21 | return { 22 | type: INPUT_SUBMIT 23 | }; 24 | } 25 | 26 | export function inputChange(value){ 27 | return { 28 | type: INPUT_CHANGED, 29 | value 30 | } 31 | } 32 | 33 | const initialState = { 34 | list: [{item: 'test', done: false}], 35 | newToDo: '' 36 | }; 37 | 38 | export default function reducer(state = initialState, action){ 39 | switch (action.type){ 40 | case INPUT_SUBMIT: 41 | return Object.assign( 42 | {}, 43 | state, 44 | { 45 | list: [...state.list, {item: state.newToDo, done: false }], 46 | newToDo: '' 47 | } 48 | ); 49 | case INPUT_CHANGED: 50 | return Object.assign( 51 | {}, 52 | state, 53 | {newToDo: action.value} 54 | ); 55 | case LIST_ITEM_CLICK: 56 | return Object.assign( 57 | {}, 58 | state, 59 | { 60 | list: [ 61 | ...state.list.slice(0, action.index), 62 | Object.assign({}, state.list[action.index], {done: !state.list[action.index].done}), 63 | ...state.list.slice(action.index+1) 64 | ] 65 | } 66 | ); 67 | case DELETE_LIST_ITEM: 68 | return Object.assign( 69 | {}, 70 | state, 71 | { 72 | list: [ 73 | ...state.list.slice(0, action.index), 74 | ...state.list.slice(action.index+1) 75 | ] 76 | } 77 | ); 78 | default: 79 | return state; 80 | } 81 | } -------------------------------------------------------------------------------- /talking-points.md: -------------------------------------------------------------------------------- 1 | - What is react: 2 | - The V in MVC 3 | - Built on Components 4 | - The Virtual DOM 5 | - JSX 6 | - Getting Started 7 | + Setup Webpack 8 | * What is Webpack (Browserify) 9 | * Quick look at file structure 10 | * Quick look at webpack.config.js 11 | - Components 12 | + React and ES6 13 | + Structure 14 | * Create Calculator Component 15 | * Create VeiwPort Component 16 | - Pass props `total` from Calculator to ViewPort 17 | - Set state in Calculator 18 | - Pass `AddOne` onClick prop to ViewPort so that on click, total += 1 19 | * Create Button Component 20 | + Props (ViewPort) 21 | + Default Props 22 | - Flux 23 | + What is flux 24 | + How to implement it 25 | 26 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | module.exports = { 3 | entry: [ 4 | // 'babel-polyfill', 5 | // 'webpack/hot/only-dev-server', 6 | './src/app.js' 7 | ], 8 | output: { 9 | path: __dirname + '/build', 10 | filename: "bundle.js" 11 | }, 12 | module: { 13 | rules: [{ 14 | test: /\.js$/, 15 | exclude: /node_modules/, 16 | loader: 'babel-loader', 17 | query: { 18 | plugins: ['transform-runtime'], 19 | presets: ['es2015', 'react', 'stage-2'] 20 | } 21 | }, { 22 | test: /\.css$/, 23 | loader: "style-loader!css-loader" 24 | }] 25 | } 26 | }; 27 | --------------------------------------------------------------------------------