├── .gitignore ├── .npmrc ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── shared.css ├── scripts └── build.js ├── slides ├── Flux.png ├── Lifecycle.pdf ├── Redux.gif ├── migration.key ├── migration.pdf ├── routing.key ├── routing.pdf └── universal.svg ├── subjects ├── .babelrc ├── 00-Hello-World │ ├── exercise.js │ ├── lecture.js │ ├── notes.md │ └── utils │ │ └── searchWikipedia.js ├── 01-Rendering │ ├── exercise.js │ ├── lecture.js │ ├── solution.js │ └── tests.js ├── 02-Components │ ├── exercise.js │ ├── lecture.js │ ├── solution.js │ ├── styles.css │ └── tests.js ├── 03-Props-vs-State │ ├── data.js │ ├── exercise.js │ ├── images │ │ ├── asada.png │ │ ├── carnitas.png │ │ └── pollo.png │ ├── lecture.js │ ├── solution.js │ ├── styles.css │ └── styles.js ├── 04-Testing │ ├── components │ │ ├── ContentToggle.css │ │ ├── ContentToggle.js │ │ ├── Droppable.js │ │ ├── StatefulContentToggle.js │ │ └── Tabs.js │ ├── data.js │ ├── exercise.js │ ├── lecture.js │ ├── mocha-browser.js │ ├── mocha-setup.js │ └── solution.js ├── 05-Imperative-to-Declarative │ ├── exercise-no-bootstrap.js │ ├── exercise.js │ ├── lecture.js │ ├── solution-class.js │ ├── solution.js │ └── utils │ │ ├── AudioContextMonkeyPatch.js │ │ └── createOscillator.js ├── 06-Controlled-vs-Uncontrolled │ ├── exercise.js │ ├── lecture.js │ ├── solution-class.js │ ├── solution-extra.js │ └── solution.js ├── 07-Higher-Order-Components │ ├── exercise.js │ ├── images │ │ ├── cat.jpg │ │ └── mouse.png │ ├── lecture.js │ ├── solution-extra.js │ ├── solution-hooks.js │ ├── solution.js │ ├── styles.css │ └── utils │ │ └── createMediaListener.js ├── 08-Render-Props │ ├── LoadingDots.js │ ├── exercise.js │ ├── lecture.js │ ├── solution-hooks.js │ ├── solution.js │ └── utils │ │ ├── getAddressFromCoords.js │ │ └── getHeaderStyle.js ├── 09-Render-Optimizations │ ├── RainbowListDelegate.js │ ├── exercise.js │ ├── lecture.js │ ├── solution-class-extra.js │ ├── solution-class.js │ ├── solution.js │ ├── styles.css │ └── utils │ │ ├── computeHSLRainbowColor.js │ │ └── convertNumberToEnglish.js ├── 10-Compound-Components │ ├── exercise.js │ ├── lecture.js │ ├── solution-class-extra.js │ ├── solution-class.js │ ├── solution.js │ └── styles.js ├── 11-Context │ ├── exercise.js │ ├── lecture.js │ ├── solution-class-extra.js │ ├── solution-class.js │ ├── solution.js │ └── styles.js ├── 12-Routing │ ├── Gravatar.js │ ├── exercise.js │ ├── lecture.js │ ├── solution.js │ └── tests.js ├── 13-Server-Rendering │ ├── exercise.js │ ├── exercise │ │ ├── App.js │ │ ├── fetchContacts.js │ │ └── server.js │ ├── solution.js │ └── solution │ │ ├── App.js │ │ ├── fetchContacts.js │ │ └── server.js ├── 14-Transitions │ ├── components │ │ └── HeightFader.js │ ├── exercise.js │ ├── lecture.js │ ├── solution.js │ └── styles.css ├── 15-Motion │ ├── components │ │ ├── Draggable.js │ │ └── Tone.js │ ├── exercise.js │ ├── lecture.js │ ├── solution.js │ ├── styles.css │ └── utils │ │ ├── AudioContextMonkeyPatch.js │ │ └── createOscillator.js ├── 16-Redux │ ├── Redux.png │ ├── exercise.js │ ├── exercise │ │ ├── actions.js │ │ ├── components │ │ │ ├── App.js │ │ │ ├── ContactList.js │ │ │ └── CreateContactForm.js │ │ ├── index.js │ │ ├── logger.js │ │ ├── reducers.js │ │ ├── store.js │ │ └── utils │ │ │ ├── api.js │ │ │ └── xhr.js │ ├── lecture.js │ ├── notes.md │ ├── solution.js │ └── solution │ │ ├── actions.js │ │ ├── components │ │ ├── App.js │ │ ├── ContactList.js │ │ └── CreateContactForm.js │ │ ├── index.js │ │ ├── logger.js │ │ ├── reducers.js │ │ ├── store.js │ │ └── utils │ │ ├── api.js │ │ └── xhr.js ├── 17-Chat-App │ ├── exercise.js │ ├── solution.js │ ├── styles.css │ └── utils.js ├── 18-Mini-Router │ ├── exercise.js │ ├── exercise │ │ └── mini-router.js │ ├── solution.js │ └── solution │ │ ├── mini-router-hooks.js │ │ └── mini-router.js ├── 19-Mini-Redux │ ├── exercise.js │ ├── exercise │ │ ├── App.js │ │ └── mini-redux │ │ │ ├── Provider.js │ │ │ ├── connect.js │ │ │ └── createStore.js │ ├── solution.js │ └── solution │ │ ├── App.js │ │ └── mini-redux │ │ ├── Provider.js │ │ ├── ReduxContext.js │ │ ├── connect.js │ │ └── createStore.js ├── 20-Select │ ├── exercise.js │ ├── solution.js │ └── styles.css ├── assert.js ├── index.js ├── logo.png └── styles.css └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | npm-error.log 2 | /node_modules/ 3 | /public/**/*.html 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to React Training! 2 | 3 | This repo contains the course material for [React Training](https://reacttraining.com/). Before attending the training, please make sure you can run this repository. 4 | 5 | ## Install 6 | 7 | First, install [git](http://git-scm.com/downloads) and the latest stable version of [node](https://nodejs.org/). Then: 8 | 9 | ```sh 10 | $ git clone https://github.com/ReactTraining/react-workshop.git 11 | $ cd react-workshop 12 | $ npm install 13 | $ npm start 14 | ``` 15 | 16 | Then, open a web browser to [http://localhost:8080](http://localhost:8080) where you'll see a list of subjects. 17 | 18 | ## Updating 19 | 20 | If you've already cloned the repo but you need to get updated code, then follow these steps: 21 | 22 | - First, `cd` into the root directory of the repo 23 | - Then do an `ls` command to ensure you see a `package.json` file listed. If you don't you're not in the root folder of the repo 24 | - Then run these steps to get the updates: 25 | 26 | ```sh 27 | git pull origin master 28 | npm install 29 | ``` 30 | 31 | Then you should be able to do your `npm start` and open a web browser to [http://localhost:8080](http://localhost:8080) where you'll see a list of subjects. 32 | 33 | ## Be Prepared 34 | 35 | **IMPORTANT:** Please read our [JavaScript Primer](https://reacttraining.com/blog/javascript-the-react-parts/) before attending the workshop. It's a refresher on some of the newer bits of JavaScript you'll want to be familiar with in order to get the most out of the experience. 36 | 37 | ### Windows Machine? 38 | 39 | We'll be running commands like the ones from the install/update instructions above. These are _bash_ commands which means if you're on Windows you'll need a bash-enabled command-line tool. If you've installed [Git For Windows](https://gitforwindows.org) then you'll have a command-line tool called Git Bash already. This seems to work out well for doing other bash things that aren't just git specific (like NPM). 40 | 41 | ## Troubleshooting 42 | 43 | A few common problems: 44 | 45 | - **You're having problems cloning the repository.** Some corporate networks block port 22, which git uses to communicate with GitHub over SSH. Instead of using SSH, clone the repo over HTTPS. Use the following command to tell git to always use `https` instead of `git`: 46 | 47 | ```sh 48 | $ git config --global url."https://".insteadOf git:// 49 | 50 | # This adds the following to your `~/.gitconfig`: 51 | [url "https://"] 52 | insteadOf = git:// 53 | ``` 54 | 55 | - **You're having trouble installing node.** We recommend using [nvm](https://github.com/creationix/nvm). nvm makes it really easy to use multiple versions of node on the same machine painlessly. After you install nvm, install the latest stable version of node with the following command: 56 | 57 | ```sh 58 | $ nvm use default stable 59 | ``` 60 | 61 | - **You don't have permissions to install stuff.** You might see an error like `EACCES` during the `npm install` step. If that's the case, it probably means that at some point you did an `sudo npm install` and installed some stuff with root permissions. To fix this, you need to forcefully remove all files that npm caches on your machine and re-install without sudo. 62 | 63 | ```sh 64 | $ sudo rm -rf node_modules 65 | 66 | # If you installed node with nvm (suggested): 67 | $ sudo rm -rf ~/.npm 68 | 69 | # If you installed node with Homebrew: 70 | $ sudo rm -rf /usr/local/lib/node_modules 71 | 72 | # Then (look ma, no sudo!): 73 | $ npm install 74 | ``` 75 | 76 | ## License 77 | 78 | This material is available for private, non-commercial use under the [GPL version 3](http://www.gnu.org/licenses/gpl-3.0-standalone.html). If you would like to use this material to conduct your own workshop, please contact us at [hello@reacttraining.com](mailto:hello@reacttraining.com). 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "react-workshop", 4 | "description": "Lectures and exercises for React Training workshops", 5 | "repository": "ReactTraining/react-workshop", 6 | "homepage": "https://reacttraining.com", 7 | "author": "React Training LLC", 8 | "license": "GPL-3.0", 9 | "scripts": { 10 | "clean": "git clean -fdX .", 11 | "start": "node ./scripts/build.js && webpack-dev-server --inline --content-base public", 12 | "ssr-exercise": "supervisor -- -r @babel/register 'subjects/13-Server-Rendering/exercise/server.js'", 13 | "ssr-solution": "supervisor -- -r @babel/register 'subjects/13-Server-Rendering/solution/server.js'" 14 | }, 15 | "dependencies": { 16 | "@babel/core": "^7.1.5", 17 | "@babel/plugin-proposal-class-properties": "^7.1.0", 18 | "@babel/preset-env": "^7.1.5", 19 | "@babel/preset-react": "^7.0.0", 20 | "@babel/register": "^7.0.0", 21 | "angular": "1.5.8", 22 | "babel-loader": "^8.0.4", 23 | "backbone": "^1.2.3", 24 | "body-parser": "^1.15.2", 25 | "bootstrap": "3", 26 | "bootstrap-webpack": "0.0.5", 27 | "create-react-class": "^15.6.3", 28 | "css-loader": "^1.0.0", 29 | "events": "^1.0.2", 30 | "expect": "^23.0.0", 31 | "exports-loader": "^0.7.0", 32 | "expose-loader": "0.7.5", 33 | "express": "^4.14.0", 34 | "extract-text-webpack-plugin": "^3.0.2", 35 | "file-loader": "^1.1.11", 36 | "firebase": "^5.2.0", 37 | "form-serialize": "0.7.1", 38 | "imports-loader": "0.8.0", 39 | "invariant": "^2.1.0", 40 | "isomorphic-fetch": "^2.2.1", 41 | "jquery": "^3.1.0", 42 | "jsonp": "^0.2.1", 43 | "less": "^3.8.0", 44 | "less-loader": "^4.1.0", 45 | "md5": "2.1.0", 46 | "mkdirp": "^0.5.1", 47 | "mocha": "^5.2.0", 48 | "mustache": "^2.2.1", 49 | "prop-types": "^15.5.10", 50 | "purecss": "0.6.0", 51 | "react": "^16.8.3", 52 | "react-addons-css-transition-group": "^15.1.0", 53 | "react-addons-transition-group": "^15.1.0", 54 | "react-dom": "^16.8.3", 55 | "react-motion": "^0.4.2", 56 | "react-redux": "^5.0.2", 57 | "react-router-dom": "^4.0.0", 58 | "react-tween-state": "0.1.5", 59 | "redux": "^3.6.0", 60 | "redux-logger": "2.2.1", 61 | "redux-thunk": "1.0.0", 62 | "sort-by": "^1.1.0", 63 | "style-loader": "^0.21.0", 64 | "supervisor": "^0.11.0", 65 | "underscore": "^1.8.3", 66 | "url-loader": "^1.0.1", 67 | "webpack": "^4.16.4", 68 | "webpack-cli": "^3.1.0", 69 | "webpack-dev-server": "^3.1.14" 70 | }, 71 | "prettier": { 72 | "printWidth": 72 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactTraining/classic-react-workshop/251d8ba4c6f1da0c5815947fdb40bc32453d0443/public/favicon.ico -------------------------------------------------------------------------------- /public/shared.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | *, 5 | *:before, 6 | *:after { 7 | box-sizing: inherit; 8 | } 9 | 10 | body { 11 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 12 | Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 13 | background: white; 14 | } 15 | 16 | a:link, 17 | a:visited { 18 | color: rebeccapurple; 19 | text-decoration: none; 20 | } 21 | a:link:hover { 22 | text-decoration: underline; 23 | } 24 | 25 | .hot { 26 | color: red; 27 | } 28 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const mkdirp = require("mkdirp"); 4 | const React = require("react"); 5 | const ReactDOMServer = require("react-dom/server"); 6 | 7 | function writeFile(file, contents) { 8 | mkdirp.sync(path.dirname(file)); 9 | fs.writeFileSync(file, contents); 10 | } 11 | 12 | function renderPage(element) { 13 | return ( 14 | "" + ReactDOMServer.renderToStaticMarkup(element) 15 | ); 16 | } 17 | 18 | const e = React.createElement; 19 | 20 | function HostPage({ chunk, data, title = "React Training" }) { 21 | return e( 22 | "html", 23 | null, 24 | e( 25 | "head", 26 | null, 27 | e("meta", { charSet: "utf-8" }), 28 | e("meta", { 29 | name: "viewport", 30 | content: "width=device-width, initial-scale=1" 31 | }), 32 | e("title", null, title), 33 | e("link", { rel: "icon", href: "/favicon.ico?react-workshop" }), 34 | e("link", { rel: "stylesheet", href: "/shared.css" }), 35 | data && 36 | e("script", { 37 | dangerouslySetInnerHTML: { 38 | __html: `window.__DATA__ = ${JSON.stringify(data)}` 39 | } 40 | }) 41 | ), 42 | e( 43 | "body", 44 | null, 45 | e("div", { id: "app" }), 46 | e("script", { src: "/shared.js" }), 47 | e("script", { src: `/${chunk}.js` }) 48 | ) 49 | ); 50 | } 51 | 52 | const publicDir = path.resolve(__dirname, "..", "public"); 53 | const subjectsDir = path.resolve(__dirname, "..", "subjects"); 54 | const subjectDirs = fs 55 | .readdirSync(subjectsDir) 56 | .map(file => path.join(subjectsDir, file)) 57 | .filter(file => fs.statSync(file).isDirectory()); 58 | 59 | const subjects = []; 60 | 61 | subjectDirs.forEach(dir => { 62 | const base = path.basename(dir); 63 | const match = base.match(/^(\d+)-(.+)$/); 64 | const subject = { 65 | number: match[1], 66 | name: match[2].replace(/-/g, " ") 67 | }; 68 | 69 | ["lecture", "exercise", "solution"].forEach(name => { 70 | if (fs.existsSync(path.join(dir, `${name}.js`))) { 71 | console.log(`Building /${base}/${name}.html...`); 72 | 73 | writeFile( 74 | path.join(publicDir, base, `${name}.html`), 75 | renderPage(e(HostPage, { chunk: `${base}-${name}` })) 76 | ); 77 | 78 | subject[name] = `/${base}/${name}.html`; 79 | } 80 | }); 81 | 82 | subjects.push(subject); 83 | }); 84 | 85 | console.log(`Building /index.html...`); 86 | 87 | writeFile( 88 | path.join(publicDir, "index.html"), 89 | renderPage(e(HostPage, { chunk: "index", data: { subjects } })) 90 | ); 91 | -------------------------------------------------------------------------------- /slides/Flux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactTraining/classic-react-workshop/251d8ba4c6f1da0c5815947fdb40bc32453d0443/slides/Flux.png -------------------------------------------------------------------------------- /slides/Lifecycle.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactTraining/classic-react-workshop/251d8ba4c6f1da0c5815947fdb40bc32453d0443/slides/Lifecycle.pdf -------------------------------------------------------------------------------- /slides/Redux.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactTraining/classic-react-workshop/251d8ba4c6f1da0c5815947fdb40bc32453d0443/slides/Redux.gif -------------------------------------------------------------------------------- /slides/migration.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactTraining/classic-react-workshop/251d8ba4c6f1da0c5815947fdb40bc32453d0443/slides/migration.key -------------------------------------------------------------------------------- /slides/migration.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactTraining/classic-react-workshop/251d8ba4c6f1da0c5815947fdb40bc32453d0443/slides/migration.pdf -------------------------------------------------------------------------------- /slides/routing.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactTraining/classic-react-workshop/251d8ba4c6f1da0c5815947fdb40bc32453d0443/slides/routing.key -------------------------------------------------------------------------------- /slides/routing.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactTraining/classic-react-workshop/251d8ba4c6f1da0c5815947fdb40bc32453d0443/slides/routing.pdf -------------------------------------------------------------------------------- /subjects/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env", "@babel/react"], 3 | "plugins": ["@babel/proposal-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /subjects/00-Hello-World/exercise.js: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Exercise: 3 | // 4 | // - Change the contents of the render function and save the file 5 | // - See the updates automatically in your browser without refreshing! 6 | //////////////////////////////////////////////////////////////////////////////// 7 | import React from "react"; 8 | import ReactDOM from "react-dom"; 9 | 10 | function App() { 11 | return
Hello world!
; 12 | } 13 | 14 | ReactDOM.render(, document.getElementById("app")); 15 | -------------------------------------------------------------------------------- /subjects/00-Hello-World/utils/searchWikipedia.js: -------------------------------------------------------------------------------- 1 | import jsonp from "jsonp"; 2 | 3 | const API = 4 | "https://en.wikipedia.org/w/api.php?action=opensearch&format=json"; 5 | 6 | export function search(term, cb) { 7 | jsonp(`${API}&search=${term}`, (err, data) => { 8 | if (err) { 9 | cb(err); 10 | } else { 11 | const [searchTerm, titles, descriptions, urls] = data; 12 | cb( 13 | null, 14 | titles.sort().map((title, index) => { 15 | return { 16 | title, 17 | description: descriptions[index], 18 | url: urls[index] 19 | }; 20 | }) 21 | ); 22 | } 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /subjects/01-Rendering/exercise.js: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Exercise: 3 | // 4 | // - Render `DATA.title` in an

5 | // - Render a 60 | ); 61 | } 62 | } 63 | 64 | export default connect(state => state)(ContactList); 65 | -------------------------------------------------------------------------------- /subjects/16-Redux/solution/components/CreateContactForm.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import serializeForm from "form-serialize"; 4 | 5 | const transparentGif = 6 | "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; 7 | 8 | function generateId() { 9 | return Math.random() 10 | .toString(36) 11 | .substring(7); 12 | } 13 | 14 | class CreateContactForm extends React.Component { 15 | static propTypes = { 16 | onCreate: PropTypes.func.isRequired 17 | }; 18 | 19 | handleSubmit = event => { 20 | event.preventDefault(); 21 | const contact = serializeForm(event.target, { hash: true }); 22 | contact.id = generateId(); 23 | this.props.onCreate(contact); 24 | event.target.reset(); 25 | }; 26 | 27 | render() { 28 | return ( 29 |
30 | {" "} 36 | 42 | 48 | 54 | 55 |
56 | ); 57 | } 58 | } 59 | 60 | export default CreateContactForm; 61 | -------------------------------------------------------------------------------- /subjects/16-Redux/solution/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | The goal of this exercise is to gain some hands-on experience using Redux to 3 | manage the state of a React application. In the process, we'll also learn how to 4 | use Redux to communicate changes to a real API server. 5 | 6 | - Get the the props it needs by using connect() to connect it to 7 | the in its parent, the . 8 | - Once it's connected, if you open the console you'll see the contacts being 9 | loaded from the API. 10 | - Add a delete {" "} 23 | {this.props.counter}{" "} 24 | 25 | 26 | ); 27 | } 28 | } 29 | 30 | export default connect(state => { 31 | return { counter: state }; 32 | })(App); 33 | -------------------------------------------------------------------------------- /subjects/19-Mini-Redux/exercise/mini-redux/Provider.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class Provider extends React.Component { 4 | render() { 5 | return
{this.props.children}
; 6 | } 7 | } 8 | 9 | export default Provider; 10 | -------------------------------------------------------------------------------- /subjects/19-Mini-Redux/exercise/mini-redux/connect.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function connect(mapStateToProps) { 4 | return Component => { 5 | return Component; 6 | }; 7 | } 8 | 9 | export default connect; 10 | -------------------------------------------------------------------------------- /subjects/19-Mini-Redux/exercise/mini-redux/createStore.js: -------------------------------------------------------------------------------- 1 | function createStore(reducer) { 2 | let state = reducer(undefined, { type: "@INIT" }); 3 | let listeners = []; 4 | 5 | const getState = () => state; 6 | 7 | const dispatch = action => { 8 | state = reducer(state, action); 9 | listeners.forEach(listener => listener()); 10 | }; 11 | 12 | const subscribe = listener => { 13 | listeners.push(listener); 14 | 15 | return () => { 16 | listeners = listeners.filter(item => item !== listener); 17 | }; 18 | }; 19 | 20 | return { 21 | getState, 22 | dispatch, 23 | subscribe 24 | }; 25 | } 26 | 27 | export default createStore; 28 | -------------------------------------------------------------------------------- /subjects/19-Mini-Redux/solution.js: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Exercise: 3 | // 4 | // Implement the React bindings for the Redux state manager using context and 5 | // a higher-order component. 6 | // 7 | // 1. Implement to make the store accessible on context to the rest 8 | // of the components rendered below it 9 | // 2. Implement `connect`. It should: 10 | // a) Return a function that takes a component 11 | // b) The new function should return a new component that wraps the component 12 | // passed to it 13 | // c) The new component, when rendered, will pass state from 14 | // the store as props to your App component. You'll use the function 15 | // passed to `connect` to map store state to component props 16 | import React from "react"; 17 | import ReactDOM from "react-dom"; 18 | 19 | import createStore from "./solution/mini-redux/createStore"; 20 | import Provider from "./solution/mini-redux/Provider"; 21 | 22 | import App from "./solution/App"; 23 | 24 | const store = createStore((state = 0, action) => { 25 | if (action.type === "INCREMENT") { 26 | return state + 1; 27 | } else if (action.type === "DECREMENT") { 28 | return state - 1; 29 | } else { 30 | return state; 31 | } 32 | }); 33 | 34 | ReactDOM.render( 35 | 36 | 37 | , 38 | document.getElementById("app") 39 | ); 40 | -------------------------------------------------------------------------------- /subjects/19-Mini-Redux/solution/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import connect from "./mini-redux/connect"; 4 | 5 | class App extends React.Component { 6 | increment = () => { 7 | this.props.dispatch({ type: "INCREMENT" }); 8 | }; 9 | 10 | decrement = () => { 11 | this.props.dispatch({ type: "DECREMENT" }); 12 | }; 13 | 14 | render() { 15 | return ( 16 |
17 |

Mini Redux!

18 | {" "} 19 | {this.props.counter}{" "} 20 | 21 |
22 | ); 23 | } 24 | } 25 | 26 | export default connect(state => { 27 | return { counter: state }; 28 | })(App); 29 | -------------------------------------------------------------------------------- /subjects/19-Mini-Redux/solution/mini-redux/Provider.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import ReduxContext from "./ReduxContext"; 4 | 5 | class Provider extends React.Component { 6 | state = { 7 | storeState: this.props.store.getState() 8 | }; 9 | 10 | componentDidMount() { 11 | this.unsub = this.props.store.subscribe(() => { 12 | this.setState({ 13 | storeState: this.props.store.getState() 14 | }); 15 | }); 16 | } 17 | 18 | componentWillUnmount() { 19 | this.unsub(); 20 | } 21 | 22 | handleDispatch = action => { 23 | this.props.store.dispatch(action); 24 | }; 25 | 26 | render() { 27 | return ( 28 | 34 |
{this.props.children}
35 |
36 | ); 37 | } 38 | } 39 | 40 | export default Provider; 41 | -------------------------------------------------------------------------------- /subjects/19-Mini-Redux/solution/mini-redux/ReduxContext.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ReduxContext = React.createContext(); 4 | 5 | export default ReduxContext; 6 | -------------------------------------------------------------------------------- /subjects/19-Mini-Redux/solution/mini-redux/connect.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import ReduxContext from "./ReduxContext"; 4 | 5 | function connect(mapStateToProps) { 6 | return Component => { 7 | return props => { 8 | return ( 9 | 10 | {redux => ( 11 | 16 | )} 17 | 18 | ); 19 | }; 20 | }; 21 | } 22 | 23 | export default connect; 24 | -------------------------------------------------------------------------------- /subjects/19-Mini-Redux/solution/mini-redux/createStore.js: -------------------------------------------------------------------------------- 1 | function createStore(reducer) { 2 | let state = reducer(undefined, { type: "@INIT" }); 3 | let listeners = []; 4 | 5 | const getState = () => state; 6 | 7 | const dispatch = action => { 8 | state = reducer(state, action); 9 | listeners.forEach(listener => listener()); 10 | }; 11 | 12 | const subscribe = listener => { 13 | listeners.push(listener); 14 | 15 | return () => { 16 | listeners = listeners.filter(item => item !== listener); 17 | }; 18 | }; 19 | 20 | return { 21 | getState, 22 | dispatch, 23 | subscribe 24 | }; 25 | } 26 | 27 | export default createStore; 28 | -------------------------------------------------------------------------------- /subjects/20-Select/exercise.js: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Exercise: 3 | // 4 | // Make this work like a normal 40 | 41 | 42 | 43 | 44 | 45 | 46 |

Controlled

47 | 48 |

Current value: {selectValue}

49 |

50 | 51 |

52 | 53 | 59 | 60 | ); 61 | } 62 | 63 | ReactDOM.render(, document.getElementById("app")); 64 | -------------------------------------------------------------------------------- /subjects/20-Select/solution.js: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Exercise: 3 | // 4 | // Make this work like a normal with a `value` prop but no `onChange`, so it will be read-only..." 45 | ); 46 | } 47 | }, []); 48 | 49 | return ( 50 |
51 |
52 | {label} 53 |
54 | {showOptions && ( 55 |
56 | {React.Children.map(children, child => 57 | React.cloneElement(child, { 58 | onSelect: () => selectValue(child.props.value) 59 | }) 60 | )} 61 |
62 | )} 63 |
64 | ); 65 | } 66 | 67 | function Option({ children, onSelect }) { 68 | return ( 69 |
70 | {children} 71 |
72 | ); 73 | } 74 | 75 | function App() { 76 | const [selectValue, setSelectValue] = useState("dosa"); 77 | 78 | function setToMintChutney() { 79 | setSelectValue("mint-chutney"); 80 | } 81 | 82 | return ( 83 |
84 |

Select

85 | 86 |

Uncontrolled

87 | 88 | 94 | 95 |

Controlled

96 | 97 |

Current value: {selectValue}

98 |

99 | 100 |

101 | 102 | 108 |
109 | ); 110 | } 111 | 112 | ReactDOM.render(, document.getElementById("app")); 113 | -------------------------------------------------------------------------------- /subjects/20-Select/styles.css: -------------------------------------------------------------------------------- 1 | .select { 2 | border: 1px solid #ccc; 3 | display: inline-block; 4 | margin: 4px; 5 | cursor: pointer; 6 | } 7 | 8 | .label { 9 | padding: 4px; 10 | } 11 | 12 | .arrow { 13 | float: right; 14 | padding-left: 4; 15 | } 16 | 17 | .options { 18 | position: absolute; 19 | background: #fff; 20 | border: 1px solid #ccc; 21 | } 22 | 23 | .option { 24 | padding: 4px; 25 | } 26 | 27 | .option:hover { 28 | background: #eee; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /subjects/assert.js: -------------------------------------------------------------------------------- 1 | function assert(pass, description) { 2 | if (pass) { 3 | console.log("%c✔︎ ok", "color: green", description); 4 | } else { 5 | console.assert(pass, description); 6 | } 7 | } 8 | 9 | export default assert; 10 | -------------------------------------------------------------------------------- /subjects/index.js: -------------------------------------------------------------------------------- 1 | import "./styles.css"; 2 | 3 | import React from "react"; 4 | import ReactDOM from "react-dom"; 5 | 6 | import logoURL from "./logo.png"; 7 | 8 | function Index() { 9 | const subjects = (window.__DATA__ || {}).subjects || []; 10 | 11 | return ( 12 |
13 |
14 | 15 | React Training 16 | 17 |
18 | 23 | 24 | {subjects.map((subject, index) => ( 25 | 26 | 27 | 39 | 49 | 59 | 60 | ))} 61 | 62 |
{subject.number} 28 | {subject.lecture ? ( 29 | 33 | {subject.name} 34 | 35 | ) : ( 36 | subject.name 37 | )} 38 | 40 | {subject.exercise && ( 41 | 45 | exercise 46 | 47 | )} 48 | 50 | {subject.solution && ( 51 | 55 | solution 56 | 57 | )} 58 |
63 | 70 |
71 | ); 72 | } 73 | 74 | ReactDOM.render(, document.getElementById("app")); 75 | -------------------------------------------------------------------------------- /subjects/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactTraining/classic-react-workshop/251d8ba4c6f1da0c5815947fdb40bc32453d0443/subjects/logo.png -------------------------------------------------------------------------------- /subjects/styles.css: -------------------------------------------------------------------------------- 1 | .index-header { 2 | max-width: 800px; 3 | margin: 30px auto 40px; 4 | text-align: center; 5 | } 6 | .index-header img { 7 | width: 300px; 8 | } 9 | @media (max-width: 600px) { 10 | .index-header { 11 | margin: 10px auto 20px; 12 | } 13 | } 14 | 15 | .index-subjectsTable { 16 | width: 100%; 17 | max-width: 800px; 18 | margin: 0 auto; 19 | } 20 | .index-subjectsTable a:link, 21 | .index-subjectsTable a:visited { 22 | color: blue; 23 | } 24 | .index-subjectsTable tr:nth-child(even) { 25 | background: #eef; 26 | } 27 | .index-subjectsTable td { 28 | padding: 10px; 29 | text-align: left; 30 | } 31 | 32 | .index-subjectNumber { 33 | font-size: 1.5em; 34 | color: #aaa; 35 | } 36 | 37 | .index-lecture { 38 | font-size: 1.5em; 39 | } 40 | .index-lecture a:link, 41 | .index-lecture a:visited { 42 | color: black; 43 | } 44 | 45 | .index-exercise { 46 | padding-left: 20px; 47 | } 48 | 49 | .index-footer { 50 | max-width: 800px; 51 | margin: 40px auto 60px; 52 | text-align: center; 53 | font-size: 0.8em; 54 | color: #ccc; 55 | } 56 | .index-footer a:link, 57 | .index-footer a:visited { 58 | color: #ccc; 59 | } 60 | @media (max-width: 600px) { 61 | .index-footer { 62 | margin: 20px auto 40px; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const webpack = require("webpack"); 4 | 5 | const subjectsDir = path.join(__dirname, "subjects"); 6 | const subjectDirs = fs 7 | .readdirSync(subjectsDir) 8 | .map(file => path.join(subjectsDir, file)) 9 | .filter(file => fs.lstatSync(file).isDirectory()); 10 | 11 | module.exports = { 12 | devtool: "source-map", 13 | mode: "development", 14 | 15 | entry: subjectDirs.reduce( 16 | (chunks, dir) => { 17 | const base = path.basename(dir); 18 | 19 | ["lecture", "exercise", "solution"].forEach(name => { 20 | const file = path.join(dir, `${name}.js`); 21 | 22 | if (fs.existsSync(file)) { 23 | chunks[`${base}-${name}`] = file; 24 | } 25 | }); 26 | 27 | return chunks; 28 | }, 29 | { 30 | shared: ["react", "react-dom"], 31 | index: path.join(subjectsDir, "index.js") 32 | } 33 | ), 34 | 35 | output: { 36 | path: __dirname + "public", 37 | filename: "[name].js", 38 | chunkFilename: "[id].chunk.js", 39 | publicPath: "/" 40 | }, 41 | 42 | module: { 43 | rules: [ 44 | { 45 | test: /\.js$/, 46 | exclude: /node_modules|mocha-browser\.js/, 47 | loader: "babel-loader" 48 | }, 49 | { test: /\.css$/, use: ["style-loader", "css-loader"] }, 50 | { test: /\.(ttf|eot|svg|png|jpg)$/, loader: "file-loader" }, 51 | { 52 | test: /\.woff(2)?$/, 53 | loader: "url-loader?limit=10000&mimetype=application/font-woff" 54 | }, 55 | { 56 | test: require.resolve("jquery"), 57 | loader: "expose-loader?jQuery" 58 | } 59 | ] 60 | }, 61 | 62 | devServer: { 63 | quiet: false, 64 | noInfo: false, 65 | overlay: true, 66 | historyApiFallback: { 67 | rewrites: [] 68 | }, 69 | stats: { 70 | // Config for minimal console.log mess. 71 | assets: true, 72 | colors: true, 73 | version: true, 74 | hash: true, 75 | timings: true, 76 | chunks: false, 77 | chunkModules: false 78 | } 79 | }, 80 | 81 | optimization: { 82 | splitChunks: { 83 | name: "shared" 84 | } 85 | } 86 | }; 87 | --------------------------------------------------------------------------------