├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── Calculator.css ├── Calculator.js ├── index.css ├── index.js └── registerServiceWorker.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # IDE 4 | .idea 5 | 6 | # dependencies 7 | /node_modules 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | *.swp 22 | *~ 23 | Thumbs.db 24 | .project 25 | .nvm-version 26 | /tags 27 | /atom-shell/ 28 | /out/ 29 | docs/output 30 | docs/includes 31 | out/ 32 | /electron/ 33 | 34 | debug.log 35 | npm-debug.log* 36 | yarn-debug.log* 37 | yarn-error.log* 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Learning React and Redux: decoupling with stateless components 2 | 3 | Welcome to official repository for Udemy course 4 | [Learning React and Redux: decoupling with stateless components](https://www.udemy.com/course/1326760/) 5 | by [Mateusz Grzesiukiewicz](https://www.linkedin.com/in/mateusz-grzesiukiewicz-8556a030/) 6 | 7 | ## Install 8 | ``` 9 | yarn install 10 | yarn start 11 | ``` 12 | 13 | ## Who is this course for? 14 | - Those with **Javascript** skills who want to learn React library and start with good practises 15 | - Experienced **React** developers who struggle to maintain their projects 16 | - Anyone who strives to write **reusable code** using modern Javascript libraries 17 | - Redux users who embrace Flux architecture but use other library for Views than React 18 | - Those who struggle to write easily **testable React or Redux code** 19 | 20 | ## You will learn how to... 21 | - Create reusable **stateless** and easily testable components 22 | - Create pure & easily testable action handlers (**reducers**) 23 | - Connect stateless views with stateless reducers through **React containers** 24 | - Refactor applications to be more testable and reusable (**decoupled**) 25 | - Understand React **Flux architecture** and how to connect all the bits 26 | 27 | ## Take the course 28 | [Learning React and Redux: decoupling with stateless components](https://www.udemy.com/course/1326760/) 29 | 30 | ## Create react app boilerplate 31 | 32 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "decouple-react-redux", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^15.6.1", 7 | "react-dom": "^15.6.1", 8 | "react-scripts": "1.0.11", 9 | "wolfy87-eventemitter": "^5.2.2" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test --env=jsdom", 15 | "eject": "react-scripts eject" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ajdija/Learning-React-Redux-decoupling-with-stateless-components/ba40a3b04c421bb4eeba771bdf2e5eedc5af5a65/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Learning React & Redux: decoupling with stateless components", 3 | "name": "Repository for Udemy course Learning React & Redux: decoupling with stateless components by Mateusz Grzesiukiewicz", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/Calculator.css: -------------------------------------------------------------------------------- 1 | /** 2 | * This css was inspired by https://codepen.io/tbremer/pen/wKpaWe 3 | */ 4 | 5 | .react-calculator { 6 | position: relative; 7 | margin: 0 auto; 8 | width: 440px; 9 | 10 | box-shadow: 0 15px 45px rgba(19, 19, 19, .24), 0 15px 45px rgba(19, 19, 19, .12); 11 | } 12 | 13 | button { 14 | outline: none; 15 | border: 0; 16 | padding: 1rem; 17 | background-color: #292f36; 18 | font-size: 1.5rem; 19 | line-height: 1; 20 | color: #FFFDF7; 21 | 22 | -webkit-transition: all 300ms cubic-bezier(1, 1, 1, 1); 23 | transition: all 300ms cubic-bezier(1, 1, 1, 1); 24 | } 25 | 26 | button:hover { 27 | color: #FFE66D; 28 | } 29 | 30 | button .block { 31 | width: 100%; 32 | } 33 | 34 | button .transparent { 35 | background-color: transparent; 36 | } 37 | 38 | button .no-padding { 39 | padding: 0; 40 | } 41 | 42 | button .control { 43 | font-size: 1rem; 44 | } 45 | 46 | -webkit-scrollbar, .display::-webkit-scrollbar, .history::-webkit-scrollbar { 47 | width: .5rem; 48 | } 49 | 50 | -webkit-scrollbar:horizontal, 51 | .display::-webkit-scrollbar:horizontal, 52 | .history::-webkit-scrollbar:horizontal { 53 | height: .5rem; 54 | } 55 | 56 | -webkit-scrollbar-track, 57 | -webkit-scrollbar:horizontal, 58 | .display::-webkit-scrollbar-track, 59 | .display::-webkit-scrollbar:horizontal, 60 | .history::-webkit-scrollbar-track, 61 | .history::-webkit-scrollbar:horizontal { 62 | background-color: #131313; 63 | } 64 | 65 | -webkit-scrollbar-thumb, 66 | -webkit-scrollbar:horizontal, 67 | .display::-webkit-scrollbar-thumb, 68 | .display::-webkit-scrollbar:horizontal, 69 | .history::-webkit-scrollbar-thumb, 70 | .history::-webkit-scrollbar:horizontal { 71 | background-color: #FFE66D; 72 | } 73 | 74 | hover::-webkit-scrollbar-thumb, 75 | hover::-webkit-scrollbar:horizontal, 76 | .display:hover::-webkit-scrollbar-thumb, 77 | .display:hover::-webkit-scrollbar:horizontal, 78 | .history:hover::-webkit-scrollbar-thumb, 79 | .history:hover::-webkit-scrollbar:horizontal { 80 | background-color: #FFE66D; 81 | } 82 | 83 | .display { 84 | /*Positioning*/ 85 | position: relative; 86 | width: 440px; 87 | height: 120px; 88 | z-index: 10; 89 | 90 | /*Formatting*/ 91 | outline: none; 92 | box-shadow: 0 4px 2px -2px rgba(19, 19, 19, .64); 93 | padding: .5rem; 94 | overflow-y: hidden; 95 | overflow-x: scroll; 96 | font-size: 3rem; 97 | line-height: 2; 98 | text-align: right; 99 | direction: rtl; 100 | white-space: nowrap; 101 | 102 | /*Colors*/ 103 | background-color: rgba(19, 19, 19, .64); 104 | color: #FFE66D; 105 | } 106 | 107 | .history { 108 | /*Positioning*/ 109 | position: absolute; 110 | top: 120px; 111 | left: 0; 112 | z-index: 10; 113 | 114 | /*Formatting*/ 115 | width: 100%; 116 | height: 0; 117 | overflow: hidden; 118 | padding: 0; 119 | 120 | /*Colors*/ 121 | background-color: rgba(19, 19, 19, .64); 122 | color: #00a3f5; 123 | 124 | /*Animation*/ 125 | -webkit-transition: height 150ms cubic-bezier(1, 1, 1, 1), 126 | overflow 1ms cubic-bezier(1, 1, 1, 1) 200ms, 127 | padding 1ms cubic-bezier(1, 1, 1, 1) 200ms; 128 | 129 | transition: height 150ms cubic-bezier(1, 1, 1, 1), 130 | overflow 1ms cubic-bezier(1, 1, 1, 1) 200ms, 131 | padding 1ms cubic-bezier(1, 1, 1, 1) 200ms; 132 | } 133 | 134 | .history .toggle-close { 135 | position: absolute; 136 | top: 5px; 137 | right: 5px; 138 | padding: 2px 5px; 139 | } 140 | 141 | .history .toggle-close .title { 142 | display: inline-block; 143 | 144 | /*Rotation - plus sign*/ 145 | -webkit-transform: rotate(135deg); 146 | transform: rotate(135deg); 147 | } 148 | 149 | .history.visible { 150 | /*Formatting*/ 151 | height: calc(100% - 120px); 152 | padding: 10px; 153 | 154 | /*Overflow*/ 155 | overflow-y: auto; 156 | 157 | /*Animation*/ 158 | -webkit-transition: height 200ms cubic-bezier(1, 1, 1, 1), padding 1ms cubic-bezier(1, 1, 1, 1); 159 | transition: height 200ms cubic-bezier(1, 1, 1, 1), padding 1ms cubic-bezier(1, 1, 1, 1); 160 | } 161 | 162 | .buttons--controls, 163 | .buttons--operators { 164 | background-color: #292f36; 165 | } 166 | 167 | .buttons--controls button, .buttons--operators button { 168 | /*Formatting*/ 169 | display: inline-block; 170 | width: 110px; 171 | height: 110px; 172 | vertical-align: top; 173 | 174 | /*Color*/ 175 | color: #FFE66D; 176 | 177 | /*Uppercase transform*/ 178 | text-transform: uppercase; 179 | -webkit-font-feature-settings: "c2sc", "c2sc", "c2sc"; 180 | -moz-font-feature-settings: "c2sc", "c2sc", "c2sc"; 181 | font-feature-settings: "c2sc", "c2sc", "c2sc"; 182 | font-variant: small-caps; 183 | } 184 | 185 | .buttons--controls button:hover, .buttons--operators button:hover { 186 | color: #c41c4f; 187 | } 188 | 189 | .buttons--digits { 190 | /*Formatting*/ 191 | width: 330px; 192 | float: left; 193 | 194 | /*Color*/ 195 | background-color: #292f36; 196 | } 197 | 198 | .buttons--digits button { 199 | /*Formatting*/ 200 | display: block; 201 | position: relative; 202 | width: 110px; 203 | height: 110px; 204 | float: left; 205 | 206 | /*Color*/ 207 | background-color: #292f36; 208 | 209 | /*Transforms*/ 210 | -webkit-transition: box-shadow 300ms cubic-bezier(1, 1, 1, 1); 211 | transition: box-shadow 300ms cubic-bezier(1, 1, 1, 1); 212 | } 213 | 214 | .buttons--digits button:last-child { 215 | width: 100%; 216 | } 217 | 218 | .buttons--controls { 219 | clear: left; 220 | float: left; 221 | width: 330px; 222 | height: 110px; 223 | } 224 | -------------------------------------------------------------------------------- /src/Calculator.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './Calculator.css'; 3 | import EventEmitter from 'wolfy87-eventemitter'; 4 | 5 | class Calculator extends Component { 6 | render() { 7 | return ( 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | ) 16 | } 17 | } 18 | 19 | const ee = new EventEmitter(); 20 | let store = { 21 | displayedExpression: 0, 22 | get curExpression() { 23 | return this.displayedExpression; 24 | }, 25 | set newExpression(curExpression) { 26 | this.displayedExpression = curExpression; 27 | ee.emitEvent('displayUpdate', [this.curExpression]); 28 | ee.emitEvent('historyUpdate', [this.curExpression]); 29 | } 30 | }; 31 | 32 | 33 | class Display extends Component { 34 | constructor(props) { 35 | super(props); 36 | 37 | this.state = { text: this.props.text || '0' }; 38 | this.updateDisplay = this.updateDisplay.bind(this); 39 | this.onClickHandler = this.onClickHandler.bind(this); 40 | } 41 | 42 | updateDisplay(newStr) { 43 | return this.setState({ text: newStr.toString().split(' ').reverse().join(' ') }); 44 | } 45 | 46 | componentWillMount() { 47 | ee.addListener('displayUpdate', this.updateDisplay); 48 | } 49 | 50 | onClickHandler() { 51 | if (this.props.clickHandler) { 52 | this.props.clickHandler.call(this); 53 | } 54 | } 55 | 56 | render() { 57 | return
{this.state.text}
58 | } 59 | } 60 | 61 | class ControlPanel extends Component { 62 | showHistory() { 63 | ee.emitEvent('toggle-history'); 64 | } 65 | 66 | clearDisplay() { 67 | store.newExpression = 0; 68 | } 69 | 70 | removeOneChar() { 71 | const curExpression = String(store.curExpression); 72 | const newExpWithRemovedChar = curExpression.toString().trim().substring(0, (curExpression.length - 1)); 73 | 74 | return store.newExpression = newExpWithRemovedChar === '' ? 0 : newExpWithRemovedChar; 75 | } 76 | 77 | render() { 78 | return ( 79 |
80 |
84 | ) 85 | } 86 | } 87 | 88 | class Operators extends Component { 89 | opHandler(type) { 90 | store.newExpression = `${store.curExpression} ${type} `; 91 | } 92 | 93 | calculateExpression() { 94 | /* eslint-disable */ 95 | // This rule is important in production apps! 96 | // Read more: https://eslint.org/docs/rules/no-eval 97 | // To simplify the functionality in this course we use eval 98 | const calcFunc = eval; 99 | /* eslint-enable */ 100 | try { 101 | store.newExpression = calcFunc(store.curExpression); 102 | } catch (e) { 103 | console.error("Error: Incorrect Expression of digits & operators :(") 104 | } 105 | } 106 | 107 | render() { 108 | return ( 109 |
110 | {["+", "-", "*", "/"] 111 | .map((op, i) => ( 112 |
116 | ) 117 | } 118 | } 119 | 120 | class Digits extends Component { 121 | digitClickHandler(num) { 122 | if (!store.curExpression) { 123 | return store.newExpression = num; 124 | } 125 | 126 | return store.newExpression = `${store.curExpression}${num}`; 127 | } 128 | 129 | render() { 130 | return
131 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 0] 132 | .map(nr =>
134 | } 135 | } 136 | 137 | class History extends Component { 138 | constructor(props) { 139 | super(props); 140 | 141 | this.state = { show: false, history: [] }; 142 | this.toggleHistory = this.toggleHistory.bind(this); 143 | this.addHistoryItem = this.addHistoryItem.bind(this); 144 | this.getHistoryItems = this.getHistoryItems.bind(this); 145 | } 146 | 147 | componentWillMount() { 148 | ee.addListener('historyUpdate', this.addHistoryItem); 149 | ee.addListener('toggle-history', this.toggleHistory); 150 | } 151 | 152 | addHistoryItem(historyItem) { 153 | const trimmedItem = historyItem.toString().trim(); 154 | if (this.getHistoryItems().filter(i => i === trimmedItem).length === 0) { 155 | this.setState({ 156 | ...this.state, 157 | history: [ 158 | ...this.state.history, 159 | trimmedItem 160 | ] 161 | }); 162 | } 163 | } 164 | 165 | getHistoryItems() { 166 | return this.state.history.filter(h => !!h); 167 | } 168 | 169 | toggleHistory() { 170 | this.setState({ ...this.state, show: !this.state.show }); 171 | } 172 | 173 | historyItemClickHandler(history) { 174 | store.newExpression = history; 175 | ee.emitEvent('toggle-history'); 176 | } 177 | 178 | render() { 179 | return ( 180 |
181 |
187 | ) 188 | } 189 | } 190 | 191 | class Button extends Component { 192 | constructor(props) { 193 | super(props); 194 | this.onClickHandler = this.onClickHandler.bind(this); 195 | } 196 | 197 | onClickHandler() { 198 | if (this.props.clickHandler) { 199 | this.props.clickHandler.call(null, this.props.text); 200 | } 201 | } 202 | 203 | render() { 204 | return ( 205 | 208 | ); 209 | } 210 | } 211 | 212 | export default Calculator; 213 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | *, 2 | *:before, 3 | *:after { 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body { 9 | position: relative; 10 | 11 | /*Formatting*/ 12 | padding-top: 20px; 13 | font-size: 16px; 14 | width: 100%; 15 | height: 100%; 16 | 17 | /*Font*/ 18 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 19 | 20 | /*Select clear*/ 21 | -webkit-user-select: none; 22 | -moz-user-select: none; 23 | -ms-user-select: none; 24 | user-select: none; 25 | } 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import Calculator from './Calculator'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (!isLocalhost) { 36 | // Is not local host. Just register service worker 37 | registerValidSW(swUrl); 38 | } else { 39 | // This is running on localhost. Lets check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === 'installed') { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log('New content is available; please refresh.'); 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log('Content is cached for offline use.'); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error('Error during service worker registration:', error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get('content-type').indexOf('javascript') === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | 'No internet connection found. App is running in offline mode.' 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ('serviceWorker' in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | --------------------------------------------------------------------------------