├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── assets │ └── style │ │ └── index.css ├── components │ ├── App.js │ ├── FilteredItems.js │ ├── InputBox.js │ ├── TodoCount.js │ ├── TodoFilter.js │ ├── TodoItem.js │ └── TodoList.js └── index.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Saugat Acharya 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learn React 2 | 3 | A step-by-step guide to learn React and it's fundamentals. 4 | 5 | Yes, yet another todo app. 6 | 7 | [Demo](https://mesaugat.github.io/learn-react/) 8 | 9 | ## Prerequisites 10 | 11 | * HTML/CSS 12 | * Basic JavaScript 13 | * [ES6](https://github.com/lukehoban/es6features) 14 | 15 | ## Required Tools 16 | 17 | * [Node.js](https://nodejs.org/en/download/) 18 | * [NPM](https://www.npmjs.com/) - Comes installed with Node.js 19 | * [Git](https://git-scm.com/downloads) 20 | 21 | ## Install create-react-app 22 | 23 | [create-react-app](https://github.com/facebookincubator/create-react-app) is a tool to build React apps in no time. It allows us to create React apps without any build configuration. 24 | 25 | From your terminal, install `create-react-app` globally. 26 | 27 | ```bash 28 | npm install -g create-react-app 29 | 30 | create-react-app my-app 31 | cd my-app 32 | npm start 33 | ``` 34 | 35 | Let's get started. 36 | 37 | ## Steps 38 | 39 | Clone the repository. 40 | 41 | ```bash 42 | git clone git@github.com:mesaugat/learn-react.git 43 | ``` 44 | 45 | `git checkout ` in your terminal to the branch you want to navigate to. 46 | 47 | * [step-0](https://github.com/mesaugat/learn-react/tree/step-0) - Initialize create-react-app 48 | * [step-1](https://github.com/mesaugat/learn-react/compare/step-0...step-1) - The Hello World of React 49 | * [step-2](https://github.com/mesaugat/learn-react/compare/step-1...step-2) - Add some HTML/CSS for the todo list 50 | * [step-3](https://github.com/mesaugat/learn-react/compare/step-2...step-3) - Extract todo items to an array 51 | * [step-4](https://github.com/mesaugat/learn-react/compare/step-3...step-4) - Create a TodoList component 52 | * [step-5](https://github.com/mesaugat/learn-react/compare/step-4...step-5) - Extract a TodoItem component 53 | * [step-6](https://github.com/mesaugat/learn-react/compare/step-5...step-6) - And TodoCount component and move files to a components directory 54 | * [step-7](https://github.com/mesaugat/learn-react/compare/step-6...step-7) - Add InputBox component for adding a note 55 | * [step-8](https://github.com/mesaugat/learn-react/compare/step-7...step-8) - Make App component stateful 56 | * [step-9](https://github.com/mesaugat/learn-react/compare/step-8...step-9) - Add prop types validation 57 | * [step-10](https://github.com/mesaugat/learn-react/compare/step-9...step-10) - Make a controlled input component 58 | * [step-11](https://github.com/mesaugat/learn-react/compare/step-10...step-11) - Add event handler to change todo status 59 | * [step-12](https://github.com/mesaugat/learn-react/compare/step-11...step-12) - Add a new component TodoFilter and it's event handlers 60 | * [step-13](https://github.com/mesaugat/learn-react/compare/step-12...step-13) - Make HTTP request to fetch notes 61 | * [step-14](https://github.com/mesaugat/learn-react/compare/step-13...step-14) - Deploy app 62 | * [step-15](https://github.com/mesaugat/learn-react/compare/step-14...step-15) - Finalize 63 | 64 | ## Resources 65 | 66 | * [Thinking in React](https://reactjs.org/docs/thinking-in-react.html) 67 | * [Components](https://reactjs.org/docs/react-component.html) 68 | * [JSX in Depth](https://reactjs.org/docs/jsx-in-depth.html) 69 | * [State and Lifecycle](https://reactjs.org/docs/state-and-lifecycle.html) 70 | * [The Beginner's Guide to ReactJS - Free Video Tutorial](https://egghead.io/courses/the-beginner-s-guide-to-reactjs) 71 | * [React FAQ](https://reactfaq.site/) 72 | 73 | ## License 74 | 75 | [MIT](LICENSE) 76 | 77 | --- 78 | 79 | This guide is heavily inspired from [kabirbaidhya/react-todo-app](https://github.com/kabirbaidhya/react-todo-app). 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-react", 3 | "version": "1.0.0", 4 | "private": true, 5 | "homepage": "https://mesaugat.github.io/learn-react", 6 | "dependencies": { 7 | "axios": "^0.17.1", 8 | "bootstrap": "^3.3.7", 9 | "gh-pages": "^1.1.0", 10 | "prop-types": "^15.6.0", 11 | "react": "^16.2.0", 12 | "react-dom": "^16.2.0", 13 | "react-scripts": "1.0.17" 14 | }, 15 | "scripts": { 16 | "predeploy": "npm run build", 17 | "deploy": "gh-pages -d build", 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test --env=jsdom", 21 | "eject": "react-scripts eject" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mesaugat/learn-react/ca11f7fe2648187e46d5bbcd2c9dd948d997793d/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | Learn React 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Learn React", 3 | "name": "Learn React", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/style/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #eee; 3 | color: #555; 4 | } 5 | 6 | .todolist { 7 | background-color: #FFF; 8 | padding: 20px 20px 10px 20px; 9 | margin: 30px auto; 10 | border: 1px solid #ddd; 11 | border-radius: 2px; 12 | position: relative; 13 | } 14 | 15 | .todolist h1 { 16 | margin: 0; 17 | padding-bottom: 20px; 18 | text-align: center; 19 | } 20 | 21 | .todolist .info { 22 | position: absolute; 23 | margin-top: 20px; 24 | left: 0; 25 | right: 0; 26 | color: #999; 27 | text-align: center; 28 | } 29 | 30 | .form-control { 31 | border-radius: 0; 32 | } 33 | 34 | li.ui-state-default { 35 | background: #fff; 36 | border: none; 37 | border-bottom: 1px solid #ddd; 38 | } 39 | 40 | li.ui-state-default:last-child { 41 | border-bottom: none; 42 | } 43 | 44 | li.completed label { 45 | text-decoration: line-through; 46 | color: #aaa; 47 | } 48 | 49 | footer { 50 | border-top: 1px solid #ddd; 51 | background-color: #F4FCE8; 52 | margin: 0 -20px -10px -20px; 53 | padding: 12px 20px; 54 | position: relative; 55 | color: #777; 56 | } 57 | 58 | .filters { 59 | margin: 0; 60 | padding: 0; 61 | list-style: none; 62 | } 63 | 64 | .filters li { 65 | display: inline-block; 66 | } 67 | 68 | .filters li a { 69 | cursor: pointer; 70 | color: inherit; 71 | margin: 3px; 72 | padding: 4px 8px; 73 | text-decoration: none; 74 | border: 1px solid transparent; 75 | border-radius: 3px; 76 | } 77 | 78 | .filters li a.selected, .filters li a:hover { 79 | border-color: rgba(175, 47, 47, 0.1); 80 | } 81 | 82 | .filters li a.selected { 83 | border-color: rgba(175, 47, 47, 0.2); 84 | color: #555; 85 | } 86 | 87 | .alert.alert-info { 88 | margin: 10px 0; 89 | padding: 10px; 90 | border-radius: 0; 91 | background: #f2f2f2; 92 | border: 1px solid rgba(229, 229, 229, 0.5); 93 | color: #888; 94 | } 95 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import axios from 'axios'; 3 | 4 | import TodoList from './TodoList'; 5 | import { FILTER_ALL, FILTER_ACTIVE, FILTER_COMPLETED } from './TodoFilter'; 6 | 7 | /** 8 | * App component. 9 | */ 10 | class App extends Component { 11 | state = { 12 | filter: FILTER_ALL, 13 | items: [] 14 | }; 15 | 16 | /** 17 | * Make HTTP requests inside component did mount lifecycle method. 18 | */ 19 | componentDidMount() { 20 | axios.get('https://jsonplaceholder.typicode.com/todos?_limit=10') 21 | .then(response => { 22 | this.setState({ items: response.data }) 23 | }) 24 | } 25 | 26 | /** 27 | * Add a new todo to state items. 28 | * 29 | * @param {String} title 30 | */ 31 | addNewTodo = (title) => { 32 | this.setState(state => { 33 | const todo = { 34 | title, 35 | completed: false, 36 | id: state.items.length + 1 37 | }; 38 | 39 | return { items: [...state.items, todo] }; 40 | }) 41 | } 42 | 43 | /** 44 | * Change todo completion status. 45 | * 46 | * @param {Number} todoId 47 | * @param {Boolean} completed 48 | */ 49 | changeTodoStatus = (todoId, completed) => { 50 | this.setState(state => { 51 | const items = state.items.map(item => { 52 | if (item.id !== todoId) { 53 | return item; 54 | } 55 | 56 | return { ...item, completed }; 57 | }) 58 | 59 | return { items }; 60 | }) 61 | } 62 | 63 | /** 64 | * Filter todos by completion status. 65 | * 66 | * @param {String} status 67 | * @returns {Object} 68 | */ 69 | filterTodos = (status) => { 70 | const { items } = this.state; 71 | 72 | switch (status) { 73 | case FILTER_COMPLETED: 74 | return items.filter(item => item.completed === true); 75 | 76 | case FILTER_ACTIVE: 77 | return items.filter(item => item.completed !== true); 78 | 79 | default: 80 | return items; 81 | } 82 | } 83 | 84 | /** 85 | * Change filter by status. 86 | * 87 | * @param {String} status 88 | */ 89 | changeFilter = (status) => { 90 | this.setState({ filter: status }); 91 | } 92 | 93 | render() { 94 | return ( 95 |
96 |
97 |
98 | 106 |
107 |
108 |
109 | ); 110 | } 111 | } 112 | 113 | export default App; 114 | -------------------------------------------------------------------------------- /src/components/FilteredItems.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import TodoItem from './TodoItem'; 5 | 6 | /** 7 | * Component to show filtered list of items. 8 | * 9 | * @param {Object} props 10 | */ 11 | const FilteredItems = (props) => { 12 | const { items, changeTodoStatus } = props; 13 | 14 | if (items.length === 0) { 15 | return ( 16 |

No items to display.

17 | ); 18 | } 19 | 20 | return ( 21 | 28 | ) 29 | } 30 | 31 | FilteredItems.propTypes = { 32 | items: PropTypes.array.isRequired, 33 | changeTodoStatus: PropTypes.func.isRequired 34 | } 35 | 36 | export default FilteredItems; 37 | -------------------------------------------------------------------------------- /src/components/InputBox.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * Input component to add a new todo. 6 | */ 7 | class InputBox extends Component { 8 | static propTypes = { 9 | addNewTodo: PropTypes.func.isRequired 10 | } 11 | 12 | state = { 13 | value: '' 14 | } 15 | 16 | /** 17 | * Set input text value on change event. 18 | */ 19 | handleChange = (event) => { 20 | this.setState({value: event.target.value}) 21 | } 22 | 23 | /** 24 | * Add a note when 'Enter' key is pressed. 25 | */ 26 | handleKeyUp = (event) => { 27 | const value = event.target.value.trim(); 28 | 29 | if (event.keyCode === 13 && value !== '') { 30 | this.props.addNewTodo(value); 31 | 32 | this.setState({value: ''}); 33 | } 34 | } 35 | 36 | render() { 37 | return ( 38 | 45 | ) 46 | } 47 | } 48 | 49 | export default InputBox; 50 | -------------------------------------------------------------------------------- /src/components/TodoCount.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * Component to display number of todos. 6 | * 7 | * @param {Object} props 8 | */ 9 | const TodoCount = (props) => { 10 | const {count} = props; 11 | 12 | return ( 13 |
14 | 15 | {count} 16 | 17 | {count === 1 ? ' item' : ' items'} 18 |
19 | ); 20 | } 21 | 22 | TodoCount.propTypes = { 23 | count: PropTypes.number 24 | } 25 | 26 | export default TodoCount; 27 | -------------------------------------------------------------------------------- /src/components/TodoFilter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export const FILTER_ALL = 'all'; 5 | export const FILTER_ACTIVE = 'active'; 6 | export const FILTER_COMPLETED = 'completed'; 7 | 8 | const options = { 9 | [FILTER_ALL]: 'All', 10 | [FILTER_ACTIVE]: 'Active', 11 | [FILTER_COMPLETED]: 'Completed' 12 | }; 13 | 14 | /** 15 | * Component to filter list of todos by it's status. 16 | * 17 | * @param {Object} props 18 | */ 19 | const TodoFilter = (props) => { 20 | const { filter, changeFilter } = props; 21 | const className = (key) => (key === filter ? 'selected' : ''); 22 | 23 | return ( 24 | 33 | ) 34 | } 35 | 36 | TodoFilter.propTypes = { 37 | filter: PropTypes.string.isRequired, 38 | changeFilter: PropTypes.func.isRequired 39 | } 40 | 41 | export default TodoFilter; 42 | -------------------------------------------------------------------------------- /src/components/TodoItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | /** 5 | * Each todo item. 6 | * 7 | * @param {Object} props 8 | */ 9 | class TodoItem extends Component { 10 | static propTypes = { 11 | item: PropTypes.object.isRequired, 12 | changeTodoStatus: PropTypes.func.isRequired 13 | } 14 | 15 | handleChange = (event) => this.props.changeTodoStatus(this.props.item.id, event.target.checked); 16 | 17 | render() { 18 | const { item } = this.props; 19 | const className = `todo-item ui-state-default ${item.completed === true ? 'completed' : 'pending'}`; 20 | 21 | return ( 22 |
  • 23 |
    24 | 27 |
    28 |
  • 29 | ) 30 | } 31 | } 32 | 33 | export default TodoItem; 34 | -------------------------------------------------------------------------------- /src/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import InputBox from './InputBox'; 5 | import TodoCount from './TodoCount'; 6 | import TodoFilter from './TodoFilter'; 7 | import FilteredItems from './FilteredItems'; 8 | 9 | /** 10 | * Component to show list of todos. 11 | * 12 | * @param {Object} props 13 | */ 14 | const TodoList = (props) => { 15 | const { filter, addNewTodo, filterTodos, changeFilter, changeTodoStatus } = props; 16 | 17 | const items = filterTodos(filter); 18 | const count = items.length; 19 | 20 | return ( 21 |
    22 |

    My Todo List

    23 | 24 | 25 |
    26 | 27 |
    28 | 29 |
    30 |
    31 |
    32 | ) 33 | } 34 | 35 | TodoList.propTypes = { 36 | items: PropTypes.array.isRequired, 37 | filterTodos: PropTypes.func.isRequired, 38 | changeTodoStatus: PropTypes.func.isRequired 39 | } 40 | 41 | export default TodoList; 42 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './components/App'; 5 | 6 | // Add boostrap css 7 | import 'bootstrap/dist/css/bootstrap.css'; 8 | 9 | // Add our style 10 | import './assets/style/index.css'; 11 | 12 | // Render a React element to the DOM 13 | ReactDOM.render(, document.getElementById('root')); 14 | --------------------------------------------------------------------------------