├── .gitignore ├── .vscode └── settings.json ├── 00-boiler-plate ├── Readme.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── demo.js │ ├── index.js │ └── styles.css └── yarn.lock ├── 01-basic ├── 00-use-state │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── demo.js │ │ ├── index.js │ │ └── styles.css ├── 01-use-state-object │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── demo.js │ │ ├── index.js │ │ └── styles.css ├── 02-component-did-mount │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── demo.js │ │ ├── index.js │ │ └── styles.css ├── 03-component-unmount │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── demo.js │ │ ├── index.js │ │ └── styles.css ├── 04 ajax-field-change │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── demo.js │ │ ├── index.js │ │ └── styles.css └── Readme.md ├── 02-class-pitfalls └── Readme.md ├── 03-block-1 ├── 00-custom-hook │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── demo.js │ │ ├── index.js │ │ └── styles.css ├── 01-pure-component │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── demo.js │ │ ├── index.js │ │ └── styles.css ├── 02-pure-component-callback │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── demo.js │ │ ├── index.js │ │ └── styles.css └── Readme.md ├── 04-block-2 ├── 00-use-reducer │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── demo.js │ │ ├── index.js │ │ └── styles.css ├── 01-use-context │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── demo.js │ │ ├── index.js │ │ └── styles.css └── Readme.md ├── 05 block-3 ├── 00 async-closure │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── demo.js │ │ ├── index.js │ │ └── styles.css ├── 01-promise-unmounted │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── demo.js │ │ ├── index.js │ │ └── styles.css ├── 03-should-update │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── demo.js │ │ ├── five.png │ │ ├── four.png │ │ ├── index.js │ │ ├── one.png │ │ ├── styles.css │ │ ├── three.png │ │ └── two.png └── Readme.md ├── 06 Exercise 1 ├── 00-start │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── api.js │ │ ├── index.js │ │ ├── page-a.js │ │ ├── page-b.js │ │ ├── router.js │ │ └── styles.css ├── 01-implemented │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── api.js │ │ ├── index.js │ │ ├── lookup.context.js │ │ ├── page-a.js │ │ ├── page-b.js │ │ ├── router.js │ │ └── styles.css ├── 02-implemented │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── api.js │ │ ├── index.js │ │ ├── page-a.js │ │ ├── page-b.js │ │ ├── router.js │ │ ├── styles.css │ │ ├── user.context.js │ │ └── user.hooks.js ├── 03-implemented │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── api.js │ │ ├── index.js │ │ ├── page-a.js │ │ ├── page-b.js │ │ ├── router.js │ │ ├── styles.css │ │ ├── user.context.js │ │ └── user.hooks.js └── Readme.md ├── 07 Exercise 2 ├── 00-start │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── demo.js │ │ ├── index.js │ │ └── styles.css ├── 01-implemented │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── demo.js │ │ ├── index.js │ │ ├── styles.css │ │ └── use-debounce.js └── Readme.md ├── 08 Exercise 3 ├── 00-start │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── components │ │ ├── header.jsx │ │ ├── header.styles.js │ │ ├── index.js │ │ └── menu.jsx │ │ ├── demo.js │ │ └── index.js ├── 01-implemented │ ├── Readme.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── components │ │ ├── header.jsx │ │ ├── header.styles.js │ │ ├── index.js │ │ └── menu.jsx │ │ ├── demo.js │ │ ├── index.js │ │ └── use-media-query.js └── Readme.md ├── LICENSE ├── README.md └── [React Alicante 19] [Workshop] Hooked on Hooks.pdf /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /00-boiler-plate/Readme.md: -------------------------------------------------------------------------------- 1 | # Get Started 2 | 3 | In order to run this project execute: 4 | 5 | ```bash 6 | npm install 7 | ``` 8 | 9 | ```bash 10 | npm start 11 | ``` 12 | -------------------------------------------------------------------------------- /00-boiler-plate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.9.0", 7 | "react-dom": "^16.9.0", 8 | "react-scripts": "3.1.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /00-boiler-plate/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/00-boiler-plate/public/favicon.ico -------------------------------------------------------------------------------- /00-boiler-plate/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /00-boiler-plate/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/00-boiler-plate/public/logo192.png -------------------------------------------------------------------------------- /00-boiler-plate/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/00-boiler-plate/public/logo512.png -------------------------------------------------------------------------------- /00-boiler-plate/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /00-boiler-plate/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /00-boiler-plate/src/demo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const MyComponent = props => { 4 | return

My Component

; 5 | }; 6 | -------------------------------------------------------------------------------- /00-boiler-plate/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { MyComponent } from "./demo"; 4 | import "./styles.css"; 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | const rootElement = document.getElementById("root"); 15 | ReactDOM.render(, rootElement); 16 | -------------------------------------------------------------------------------- /00-boiler-plate/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /01-basic/00-use-state/Readme.md: -------------------------------------------------------------------------------- 1 | # 01 Use State 2 | 3 | In this basic sample we will just state to a function component, using 4 | _React.useState_ 5 | 6 | # Steps 7 | 8 | - We will take as starting point sample _00 boilerplate_. Copy the content of the 9 | project to a fresh folder an execute _npm install_. 10 | 11 | ```bash 12 | npm install 13 | ``` 14 | 15 | - Let's open the _demo.js_ file and add our hooks based state. 16 | 17 | _./src/demo.js_ 18 | 19 | ```diff 20 | import React from "react"; 21 | 22 | export const MyComponent = props => { 23 | + const [myName, setMyName] = React.useState('John Doe'); 24 | 25 | - return

My Component

; 26 | + return( 27 | + <> 28 | +

{myName}

29 | + setMyName(e.target.value)} 32 | + /> 33 | + 34 | + ); 35 | }; 36 | ``` 37 | 38 | - Now if you run the sample you can check that the name (John Doe) will be displayed 39 | and you can edit it in the same functional component, we don't need a class component 40 | to hold state anymore, _React.useState_ does all the magic for you. 41 | 42 | # About Basefactor + Lemoncode 43 | 44 | We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. 45 | 46 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 47 | 48 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 49 | 50 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 51 | -------------------------------------------------------------------------------- /01-basic/00-use-state/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "16.8.6", 9 | "react-dom": "16.8.6", 10 | "react-scripts": "2.1.8" 11 | }, 12 | "devDependencies": { 13 | "typescript": "3.3.3" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /01-basic/00-use-state/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /01-basic/00-use-state/src/demo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const MyComponent = props => { 4 | const [myName, setMyName] = React.useState("John Doe"); 5 | 6 | return ( 7 | <> 8 |

{myName}

9 | setMyName(e.target.value)} /> 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /01-basic/00-use-state/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { MyComponent } from "./demo"; 4 | import "./styles.css"; 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | const rootElement = document.getElementById("root"); 15 | ReactDOM.render(, rootElement); 16 | -------------------------------------------------------------------------------- /01-basic/00-use-state/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /01-basic/01-use-state-object/Readme.md: -------------------------------------------------------------------------------- 1 | # 02 Use State Object 2 | 3 | In the previous sample we learnt how to make use of _useState_ to add state 4 | to a functional component. We just added a simple field (string), but what 5 | if we want to useState on an object? What is the equivalent to class 6 | component based _SetState_? Your friend spread operator :), let's hop on that. 7 | 8 | # Steps 9 | 10 | - We will take as starting point sample _00-use-state. Copy the content of the 11 | project to a fresh folder an execute \_npm install_. 12 | 13 | ```bash 14 | npm install 15 | ``` 16 | 17 | - Let's open the _demo.js_ file and add our hooks based state. 18 | 19 | _./src/demo.js_ 20 | 21 | ```diff 22 | import React from "react"; 23 | 24 | export const MyComponent = props => { 25 | - const [myName, setMyName] = React.useState("John Doe"); 26 | + const [userInfo, setUserInfo] = React.useState({ 27 | + name: 'John', 28 | + lastname: 'Doe', 29 | + }); 30 | 31 | return ( 32 | <> 33 | -

{myName}

34 | +

{userInfo.name} {userInfo.lastname}

35 | 36 | - setMyName(e.target.value)} /> 37 | + setUserInfo({ 40 | + ...userInfo, 41 | + name: e.target.value 42 | + })} 43 | + /> 44 | + setUserInfo({ 47 | + ...userInfo, 48 | + lastname: e.target.value 49 | + })} 50 | + /> 51 | + 52 | 53 | ); 54 | }; 55 | ``` 56 | 57 | - Now if you run the sample you can check that you can update both properties 58 | _name_ and _lastname_, you can easily assign an object to useState, and in order 59 | to update it you can just make use of the _spread operator_ instead of using 60 | _setState_. 61 | 62 | # About Basefactor + Lemoncode 63 | 64 | We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. 65 | 66 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 67 | 68 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 69 | 70 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 71 | -------------------------------------------------------------------------------- /01-basic/01-use-state-object/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "16.8.6", 9 | "react-dom": "16.8.6", 10 | "react-scripts": "2.1.8" 11 | }, 12 | "devDependencies": { 13 | "typescript": "3.3.3" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /01-basic/01-use-state-object/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /01-basic/01-use-state-object/src/demo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const MyComponent = props => { 4 | const [userInfo, setUserInfo] = React.useState({ 5 | name: "John", 6 | lastname: "Doe" 7 | }); 8 | 9 | return ( 10 | <> 11 |

12 | {userInfo.name} {userInfo.lastname} 13 |

14 | 17 | setUserInfo({ 18 | ...userInfo, 19 | name: e.target.value 20 | }) 21 | } 22 | /> 23 | 26 | setUserInfo({ 27 | ...userInfo, 28 | lastname: e.target.value 29 | }) 30 | } 31 | /> 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /01-basic/01-use-state-object/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { MyComponent } from "./demo"; 4 | import "./styles.css"; 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | const rootElement = document.getElementById("root"); 15 | ReactDOM.render(, rootElement); 16 | -------------------------------------------------------------------------------- /01-basic/01-use-state-object/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /01-basic/02-component-did-mount/Readme.md: -------------------------------------------------------------------------------- 1 | # 02 Component Did Mount 2 | 3 | Reading from the state and updating it on a functional component is something great, 4 | but we are missing another important part of class components, what about 5 | lifecycle event handlers like _componentDidMount_? How can we listen to an event 6 | like that in a functional component? _React.useEffect_ is your friend. 7 | 8 | # Steps 9 | 10 | - We will take as starting point sample _01 use-state-object_. Copy the content of the 11 | project to a fresh folder an execute _npm install_. 12 | 13 | ```bash 14 | npm install 15 | ``` 16 | 17 | - Let's open the _demo.js_ file, and overwrite it with the following content. 18 | 19 | _./src/demo.js_ 20 | 21 | ```jsx 22 | import React from "react"; 23 | 24 | export const MyComponent = () => { 25 | const [username, setUsername] = React.useState(""); 26 | 27 | return ( 28 | <> 29 |

{username}

30 | setUsername(e.target.value)} /> 31 | 32 | ); 33 | }; 34 | ``` 35 | 36 | - If we run the sample, nothing wil be shown (name is empty), what if we want 37 | to assign some value right when the component is mounted? We can make use of 38 | _React.useEffect_ passing as a second argument an empty array (that's important 39 | if we don't pass this the code inside the _useEffect_ would be executed on 40 | mount and after every render). 41 | 42 | _./src/demo.js_ 43 | 44 | ```diff 45 | import React from "react"; 46 | 47 | export const MyComponent = () => { 48 | const [username, setUsername] = React.useState(""); 49 | 50 | + React.useEffect(() => { 51 | + setUsername("John"); 52 | + }, []); 53 | 54 | return ( 55 | <> 56 |

{username}

57 | setUsername(e.target.value)} /> 58 | 59 | ); 60 | }; 61 | ``` 62 | 63 | - Now if you run the sample you can check that _John_ is displayed as user name. 64 | 65 | * Let's go one step further, let's simulate an asynchronous call (we will do it 66 | using _setTimeout_). 67 | 68 | _./src/demo.js_ 69 | 70 | ```diff 71 | import React from "react"; 72 | 73 | export const MyComponent = () => { 74 | const [username, setUsername] = React.useState(""); 75 | 76 | React.useEffect(() => { 77 | - setUsername("John"); 78 | + // Simulating async call 79 | + setTimeout(() => { 80 | + setUsername("John"); 81 | + }, 1500); 82 | }, []); 83 | 84 | return ( 85 | <> 86 |

{username}

87 | setUsername(e.target.value)} /> 88 | 89 | ); 90 | }; 91 | ``` 92 | 93 | - Now _John_ is displayed after 1,5 seconds, instead of _setTimeout_ you could 94 | use here _fetch_ or any other similar approach to make an ajax request. 95 | 96 | - What happens if we remove the _[]_ on the useEffect? The code in this effect will be 97 | triggered when the function component is instantiated and when any render is going to be triggered. 98 | 99 | # About Basefactor + Lemoncode 100 | 101 | We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. 102 | 103 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 104 | 105 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 106 | 107 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 108 | -------------------------------------------------------------------------------- /01-basic/02-component-did-mount/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "16.8.6", 9 | "react-dom": "16.8.6", 10 | "react-scripts": "2.1.8" 11 | }, 12 | "devDependencies": { 13 | "typescript": "3.3.3" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /01-basic/02-component-did-mount/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /01-basic/02-component-did-mount/src/demo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const MyComponent = () => { 4 | const [username, setUsername] = React.useState(""); 5 | 6 | React.useEffect(() => { 7 | // Simulating async call 8 | setTimeout(() => { 9 | setUsername("John"); 10 | }, 1500); 11 | }, []); 12 | 13 | return ( 14 | <> 15 |

{username}

16 | setUsername(e.target.value)} /> 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /01-basic/02-component-did-mount/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { MyComponent } from "./demo"; 4 | import "./styles.css"; 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | const rootElement = document.getElementById("root"); 15 | ReactDOM.render(, rootElement); 16 | -------------------------------------------------------------------------------- /01-basic/02-component-did-mount/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /01-basic/03-component-unmount/Readme.md: -------------------------------------------------------------------------------- 1 | # 03 Component unmount 2 | 3 | When we worked with Class component there was a way to free resources (e.g. 4 | a socket connection, or trapping x,y mouse coordinates...) when the component 5 | was unmounted (componentWillUnMount), is there a way to do something like 6 | that using hooks? The answer is yes, including more scenarios (beware to 7 | proper learning them). 8 | 9 | # Steps 10 | 11 | - We will take as starting point sample _02-component-did-mount_. Copy the content of the 12 | project to a fresh folder an execute _npm install_. 13 | 14 | ```bash 15 | npm install 16 | ``` 17 | 18 | - Let's open the _demo.js_ file: this time we will create a parent component 19 | and a child component, the child component will be mounted / unmounted by 20 | clicking a button on the parent component. 21 | 22 | In the child component we will make use of _React.useEffect_ and using 23 | as a second parameter an empty array to ensure that the code that will 24 | called by _useEffect_ will be only executed when the component is mounted. 25 | 26 | Overwrite _demo.js_ file with the following content. 27 | 28 | _./src/demo.js_ 29 | 30 | ```jsx 31 | import React from "react"; 32 | 33 | export const MyComponent = () => { 34 | const [visible, setVisible] = React.useState(false); 35 | 36 | return ( 37 | <> 38 | {visible && } 39 | 42 | 43 | ); 44 | }; 45 | 46 | const MyChildComponent = () => { 47 | const [userInfo, setUserInfo] = React.useState({ 48 | name: "John", 49 | lastname: "Doe" 50 | }); 51 | 52 | React.useEffect(() => { 53 | console.log("called when the component is mounted"); 54 | }, []); 55 | 56 | return ( 57 |
58 |

59 | {userInfo.name} {userInfo.lastname} 60 |

61 | setUserInfo({ ...userInfo, name: e.target.value })} 64 | /> 65 | setUserInfo({ ...userInfo, lastname: e.target.value })} 68 | /> 69 |
70 | ); 71 | }; 72 | ``` 73 | 74 | - What can be done to execute some code just when the component is unmounted? 75 | We only need to return a function inside the _useEffect_ entry (in _MyChildComponent_), by doing this the function will be executed when the component is unmounted (since we 76 | are using as a second parameter an empty string). 77 | 78 | _./src/demo.js_ 79 | 80 | ```diff 81 | React.useEffect(() => { 82 | console.log("called when the component is mounted"); 83 | 84 | + return () => console.log('Called on component unmounted, check the [] on the react use effect'); 85 | }, []); 86 | ``` 87 | 88 | - If you run the sample and open the browser console, you can check whenever we click to 89 | hide the child component the _unmounted_ function will be executed and the message 90 | will be displayed in the browser console log. 91 | 92 | # About Basefactor + Lemoncode 93 | 94 | We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. 95 | 96 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 97 | 98 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 99 | 100 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 101 | -------------------------------------------------------------------------------- /01-basic/03-component-unmount/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "16.8.6", 9 | "react-dom": "16.8.6", 10 | "react-scripts": "2.1.8" 11 | }, 12 | "devDependencies": { 13 | "typescript": "3.3.3" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /01-basic/03-component-unmount/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /01-basic/03-component-unmount/src/demo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const MyComponent = () => { 4 | const [visible, setVisible] = React.useState(false); 5 | 6 | return ( 7 | <> 8 | {visible && } 9 | 12 | 13 | ); 14 | }; 15 | 16 | const MyChildComponent = () => { 17 | const [userInfo, setUserInfo] = React.useState({ 18 | name: "John", 19 | lastname: "Doe" 20 | }); 21 | 22 | React.useEffect(() => { 23 | console.log("called when the component is mounted"); 24 | 25 | return () => 26 | console.log( 27 | "Called on component unmounted, check the [] on the react use effect" 28 | ); 29 | }, []); 30 | 31 | return ( 32 |
33 |

34 | {userInfo.name} {userInfo.lastname} 35 |

36 | setUserInfo({ ...userInfo, name: e.target.value })} 39 | /> 40 | setUserInfo({ ...userInfo, lastname: e.target.value })} 43 | /> 44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /01-basic/03-component-unmount/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { MyComponent } from "./demo"; 4 | import "./styles.css"; 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | const rootElement = document.getElementById("root"); 15 | ReactDOM.render(, rootElement); 16 | -------------------------------------------------------------------------------- /01-basic/03-component-unmount/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /01-basic/04 ajax-field-change/Readme.md: -------------------------------------------------------------------------------- 1 | # 04 Ajax field change 2 | 3 | Let's face the following scenario. A given user can enter a name on a input field, 4 | we want to trigger an ajax called each time the user types a value on the input 5 | (returned the filtered list of names). We can do this using _useEffect_ indicating 6 | in the second argument, instead of an empty array, the field name used to trigger 7 | the call. 8 | 9 | > Note down: ideally we should implement some kind of debounce behavior, we will 10 | > implement this as an exercise later on. 11 | 12 | # Steps 13 | 14 | - We will take as starting point sample _03 component unmount_. Copy the content of the 15 | project to a fresh folder an execute _npm install_. 16 | 17 | ```bash 18 | npm install 19 | ``` 20 | 21 | - Let's open the _demo.js_, we will create the boiler plate code 22 | (add a filter input, display a list of names) 23 | 24 | _./src/demo.js_ 25 | 26 | ```jsx 27 | import React from "react"; 28 | 29 | export const MyComponent = () => { 30 | const [filter, setFilter] = React.useState(""); 31 | const [userCollection, setUserCollection] = React.useState([]); 32 | 33 | return ( 34 |
35 | setFilter(e.target.value)} /> 36 |
    37 | {userCollection.map((user, index) => ( 38 |
  • {user.name}
  • 39 | ))} 40 |
41 |
42 | ); 43 | }; 44 | ``` 45 | 46 | - Now we want to fire an ajax request every time user types on the filter input. 47 | 48 | Let's do this in two steps, first let's define the effect and output an console.log 49 | with the filter updated. 50 | 51 | _./src/demo.js_ 52 | 53 | ```diff 54 | export const MyComponent = () => { 55 | const [filter, setFilter] = React.useState(''); 56 | const [userCollection, setUserCollection] = React.useState([]); 57 | 58 | + // Load full list when the component gets mounted and filter gets updated 59 | + React.useEffect(() => { 60 | + console.log(filter); 61 | + }, [filter]); 62 | 63 | return ( 64 | ``` 65 | 66 | if we run this sample we can check in the console log that every time we type in the filter 67 | the code located under the _useEffect_ gets executed. 68 | 69 | Now let's fire an ajax request on every call to update the list of users: 70 | 71 | _./src/demo.js_ 72 | 73 | ```diff 74 | export const MyComponent = () => { 75 | const [filter, setFilter] = React.useState(''); 76 | const [userCollection, setUserCollection] = React.useState([]); 77 | 78 | React.useEffect(() => { 79 | - console.log(filter); 80 | + fetch(`https://jsonplaceholder.typicode.com/users?name_like=${filter}`) 81 | + .then(response => response.json()) 82 | + .then(json => setUserCollection(json)); 83 | }, [filter]); 84 | 85 | return ( 86 | ``` 87 | 88 | > We are making use here of the create _jsonplaceholder_ rest mock api. 89 | 90 | - If we run the sample we can check that every time we start typing on the input 91 | an ajax called is triggered returning the list of results filtered by the filter 92 | field. 93 | 94 | # About Basefactor + Lemoncode 95 | 96 | We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. 97 | 98 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 99 | 100 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 101 | 102 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 103 | -------------------------------------------------------------------------------- /01-basic/04 ajax-field-change/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "16.8.6", 9 | "react-dom": "16.8.6", 10 | "react-scripts": "2.1.8" 11 | }, 12 | "devDependencies": { 13 | "typescript": "3.3.3" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /01-basic/04 ajax-field-change/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /01-basic/04 ajax-field-change/src/demo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const MyComponent = () => { 4 | const [filter, setFilter] = React.useState(""); 5 | const [userCollection, setUserCollection] = React.useState([]); 6 | 7 | React.useEffect(() => { 8 | fetch(`https://jsonplaceholder.typicode.com/users?name_like=${filter}`) 9 | .then(response => response.json()) 10 | .then(json => setUserCollection(json)); 11 | }, [filter]); 12 | 13 | return ( 14 |
15 | setFilter(e.target.value)} /> 16 |
    17 | {userCollection.map((user, index) => ( 18 |
  • {user.name}
  • 19 | ))} 20 |
21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /01-basic/04 ajax-field-change/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { MyComponent } from "./demo"; 4 | import "./styles.css"; 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | const rootElement = document.getElementById("root"); 15 | ReactDOM.render(, rootElement); 16 | -------------------------------------------------------------------------------- /01-basic/04 ajax-field-change/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /01-basic/Readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/01-basic/Readme.md -------------------------------------------------------------------------------- /02-class-pitfalls/Readme.md: -------------------------------------------------------------------------------- 1 | # Class components pitfalls: 2 | 3 | Check this post for a detailed explanation: 4 | 5 | https://www.basefactor.com/react-class-components-pitfalls 6 | 7 | **Note to the trainer O:-): Remember forking the codesandbox or entering in incognito mode** 8 | 9 | ## This nightmare (uncomment line number 7): 10 | 11 | https://codesandbox.io/s/71opm61rzq 12 | 13 | ## Monolyth State: 14 | 15 | Class 16 | 17 | https://codesandbox.io/s/yp9v4yyr8x 18 | 19 | Hooks: 20 | 21 | https://codesandbox.io/s/ppom543mj7 22 | 23 | Custom Hooks: 24 | 25 | https://codesandbox.io/s/ly7wlq9mo9 26 | 27 | ## Managing related concerns in separate event handlers 28 | 29 | Class component (check ChildComponent, didMount, didUpdate, Unmount): 30 | 31 | https://codesandbox.io/s/oqyo3159jq 32 | 33 | Hook version (plus remove [] for did update): 34 | 35 | https://codesandbox.io/s/q91qjql8r4 36 | 37 | ## Swap from functional to class component 38 | 39 | Functional component (demo.js ChildComponent) 40 | 41 | https://codesandbox.io/s/985wyr1olw 42 | 43 | > Try to migrate to class component, holding in state 44 | > userName 45 | 46 | Class component: https://codesandbox.io/s/40l5k4q1o9 47 | -------------------------------------------------------------------------------- /03-block-1/00-custom-hook/Readme.md: -------------------------------------------------------------------------------- 1 | # 00 Custom hooks 2 | 3 | Hooks are cool, but our functional component seems to get cluttered, is 4 | there a way to extract functionality outside the functional component? 5 | and what's more important is there any chance to make it reusable for 6 | other components? Yups ! Custom hooks to the rescue. 7 | 8 | # Steps 9 | 10 | - We will take as starting point sample _00 boilerplate_. Copy the content of the 11 | project to a fresh folder an execute _npm install_. 12 | 13 | ```bash 14 | npm install 15 | ``` 16 | 17 | - Let's open the _demo.js_, we will copy the content from sample 06 18 | into this sample (the filter name + ajax call sample) 19 | 20 | _./src/demo.js_ 21 | 22 | ```jsx 23 | import React from "react"; 24 | 25 | export const MyComponent = () => { 26 | const [filter, setFilter] = React.useState(""); 27 | const [userCollection, setUserCollection] = React.useState([]); 28 | 29 | // Load full list when the component gets mounted and filter gets updated 30 | React.useEffect(() => { 31 | fetch(`https://jsonplaceholder.typicode.com/users?name_like=${filter}`) 32 | .then(response => response.json()) 33 | .then(json => setUserCollection(json)); 34 | }, [filter]); 35 | 36 | return ( 37 |
38 | setFilter(e.target.value)} /> 39 |
    40 | {userCollection.map((user, index) => ( 41 |
  • {user.name}
  • 42 | ))} 43 |
44 |
45 | ); 46 | }; 47 | ``` 48 | 49 | - Now let's extract the load + filter functionality in a custom hooks 50 | we will implement it using two flavours. 51 | 52 | A. Encapsulating as well the _UseEffect_ 53 | 54 | > Create react app compiler forces to create hooks using __function__ javascript keyword 55 | 56 | _./src/demo.js_ 57 | 58 | ```diff 59 | import React from "react"; 60 | 61 | + const useUserCollection = () => { 62 | + const [filter, setFilter] = React.useState(""); 63 | + const [userCollection, setUserCollection] = React.useState([]); 64 | + 65 | + // Load full list when the component gets mounted and filter gets updated 66 | + React.useEffect(() => { 67 | + fetch(`https://jsonplaceholder.typicode.com/users?name_like=${filter}`) 68 | + .then(response => response.json()) 69 | + .then(json => setUserCollection(json)); 70 | + }, [filter]); 71 | + 72 | + return {userCollection, filter, setFilter} 73 | + } 74 | 75 | export const MyComponent = () => { 76 | - const [filter, setFilter] = React.useState(""); 77 | - const [userCollection, setUserCollection] = React.useState([]); 78 | + const {userCollection, filter, setFilter} = useUserCollection(); 79 | 80 | - // Load full list when the component gets mounted and filter gets updated 81 | - React.useEffect(() => { 82 | - fetch(`https://jsonplaceholder.typicode.com/users?name_like=${filter}`) 83 | - .then(response => response.json()) 84 | - .then(json => setUserCollection(json)); 85 | - }, [filter]); 86 | 87 | return ( 88 |
89 | setFilter(e.target.value)} /> 90 |
    91 | {userCollection.map((user, index) => ( 92 |
  • {user.name}
  • 93 | ))} 94 |
95 |
96 | ); 97 | }; 98 | ``` 99 | 100 | B. Encapsulating only the states plus load functions (the component will be 101 | responsible of deciding when to call this methods) 102 | 103 | > Notedown warning linter [filter, loadUsers] 104 | 105 | _./src/demo.js_ 106 | 107 | ```diff 108 | import React from "react"; 109 | 110 | + const useUserCollection = () => { 111 | + const [filter, setFilter] = React.useState(""); 112 | + const [userCollection, setUserCollection] = React.useState([]); 113 | + 114 | + const loadUsers = () => { 115 | + fetch(`https://jsonplaceholder.typicode.com/users?name_like=${filter}`) 116 | + .then(response => response.json()) 117 | + .then(json => setUserCollection(json)); 118 | + } 119 | + 120 | + return {userCollection, loadUsers, filter, setFilter} 121 | + } 122 | 123 | 124 | export const MyComponent = () => { 125 | + const {userCollection, loadUsers, filter, setFilter} = useUserCollection(); 126 | + 127 | + React.useEffect(() => { 128 | + loadUsers(); 129 | + }, [filter]); 130 | 131 | - const [filter, setFilter] = React.useState(""); 132 | - const [userCollection, setUserCollection] = React.useState([]); 133 | 134 | - // Load full list when the component gets mounted and filter gets updated 135 | - React.useEffect(() => { 136 | - fetch(`https://jsonplaceholder.typicode.com/users?name_like=${filter}`) 137 | - .then(response => response.json()) 138 | - .then(json => setUserCollection(json)); 139 | - }, [filter]); 140 | 141 | return ( 142 |
143 | setFilter(e.target.value)} /> 144 |
    145 | {userCollection.map((user, index) => ( 146 |
  • {user.name}
  • 147 | ))} 148 |
149 |
150 | ); 151 | }; 152 | ``` 153 | 154 | > Discussion here... which approach do you think can be more reusable and under 155 | > which circumstances (e.g. just create a custom hook to load the list of 156 | > names without taking into account the filter). 157 | 158 | - If we add `loadUsers` to dependencies array as linter suggest: 159 | 160 | ```diff 161 | React.useEffect(() => { 162 | loadUsers(); 163 | - }, [filter]); 164 | + }, [filter, loadUsers]); 165 | ``` 166 | 167 | - It will not stop fetching to typicode server. We will learn how to resolve it. 168 | 169 | # About Basefactor + Lemoncode 170 | 171 | We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. 172 | 173 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 174 | 175 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 176 | 177 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 178 | -------------------------------------------------------------------------------- /03-block-1/00-custom-hook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.9.0", 7 | "react-dom": "^16.9.0", 8 | "react-scripts": "3.1.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /03-block-1/00-custom-hook/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/03-block-1/00-custom-hook/public/favicon.ico -------------------------------------------------------------------------------- /03-block-1/00-custom-hook/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /03-block-1/00-custom-hook/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/03-block-1/00-custom-hook/public/logo192.png -------------------------------------------------------------------------------- /03-block-1/00-custom-hook/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/03-block-1/00-custom-hook/public/logo512.png -------------------------------------------------------------------------------- /03-block-1/00-custom-hook/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /03-block-1/00-custom-hook/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /03-block-1/00-custom-hook/src/demo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const useUserCollection = () => { 4 | const [filter, setFilter] = React.useState(""); 5 | const [userCollection, setUserCollection] = React.useState([]); 6 | 7 | // Load full list when the component gets mounted and filter gets updated 8 | const loadUsers = () => { 9 | fetch(`https://jsonplaceholder.typicode.com/users?name_like=${filter}`) 10 | .then(response => response.json()) 11 | .then(json => setUserCollection(json)); 12 | }; 13 | 14 | return { userCollection, loadUsers, filter, setFilter }; 15 | }; 16 | 17 | export const MyComponent = () => { 18 | const { userCollection, loadUsers, filter, setFilter } = useUserCollection(); 19 | 20 | React.useEffect(() => { 21 | loadUsers(); 22 | }, [filter]); 23 | 24 | return ( 25 |
26 | setFilter(e.target.value)} /> 27 |
    28 | {userCollection.map((user, index) => ( 29 |
  • {user.name}
  • 30 | ))} 31 |
32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /03-block-1/00-custom-hook/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { MyComponent } from "./demo"; 4 | import "./styles.css"; 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | const rootElement = document.getElementById("root"); 15 | ReactDOM.render(, rootElement); 16 | -------------------------------------------------------------------------------- /03-block-1/00-custom-hook/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /03-block-1/01-pure-component/Readme.md: -------------------------------------------------------------------------------- 1 | # 01 Pure Components 2 | 3 | When we used class components we could make use of PureComponents, this 4 | components will just make a shallow compare of the props and only render 5 | if there were changes. Is there a way to do this using hooks? Yes, 6 | using _React.memo_ 7 | 8 | # Steps 9 | 10 | - We will take as starting point sample _00 custom hooks_. Copy the content of the 11 | project to a fresh folder an execute _npm install_. 12 | 13 | ```bash 14 | npm install 15 | ``` 16 | 17 | - Let's open the _demo.js_, we will create a parent and a child component 18 | 19 | _./src/demo.js_ 20 | 21 | ```jsx 22 | import React from "react"; 23 | 24 | export const MyComponent = () => { 25 | const [userInfo, setUserInfo] = React.useState({ 26 | name: " John ", 27 | lastname: "Doe" 28 | }); 29 | 30 | return ( 31 | <> 32 | 33 | 36 | setUserInfo({ 37 | ...userInfo, 38 | name: e.target.value 39 | }) 40 | } 41 | /> 42 | 45 | setUserInfo({ 46 | ...userInfo, 47 | lastname: e.target.value 48 | }) 49 | } 50 | /> 51 | 52 | ); 53 | }; 54 | 55 | export const DisplayUsername = props => { 56 | console.log( 57 | "Hey I'm only rerendered when name gets updated, check React.memo" 58 | ); 59 | 60 | return

{props.name}

; 61 | }; 62 | ``` 63 | 64 | - If we run the sample we can check that any time we change for instance 65 | _lastname_ it will rerender _DisplayUsername_, the optimal approach 66 | could be only to rerender _DisplayUsername_ just when the _props.name_ 67 | is updated, if we wrap the _DisplayUsername_ component using _React.memo_ 68 | it will do the trick for us. 69 | 70 | _./src/demo.js_ 71 | 72 | ```diff 73 | - export const DisplayUsername = props => { 74 | + export const DisplayUsername = React.memo(props => { 75 | 76 | console.log( 77 | "Hey I'm only rerendered when name gets updated, check React.memo" 78 | ); 79 | 80 | return

{props.name}

; 81 | - }; 82 | + }); 83 | ``` 84 | 85 | - Now if we run the sample we can check (by showing the console or open react dev 86 | tools) that the _DisplayUsername_ component is only rerendered when the _name_ property 87 | changes. 88 | 89 | # About Basefactor + Lemoncode 90 | 91 | We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. 92 | 93 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 94 | 95 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 96 | 97 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 98 | -------------------------------------------------------------------------------- /03-block-1/01-pure-component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.9.0", 7 | "react-dom": "^16.9.0", 8 | "react-scripts": "3.1.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /03-block-1/01-pure-component/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/03-block-1/01-pure-component/public/favicon.ico -------------------------------------------------------------------------------- /03-block-1/01-pure-component/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /03-block-1/01-pure-component/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/03-block-1/01-pure-component/public/logo192.png -------------------------------------------------------------------------------- /03-block-1/01-pure-component/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/03-block-1/01-pure-component/public/logo512.png -------------------------------------------------------------------------------- /03-block-1/01-pure-component/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /03-block-1/01-pure-component/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /03-block-1/01-pure-component/src/demo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const MyComponent = () => { 4 | const [userInfo, setUserInfo] = React.useState({ 5 | name: " John ", 6 | lastname: "Doe" 7 | }); 8 | 9 | return ( 10 | <> 11 | 12 | 15 | setUserInfo({ 16 | ...userInfo, 17 | name: e.target.value 18 | }) 19 | } 20 | /> 21 | 24 | setUserInfo({ 25 | ...userInfo, 26 | lastname: e.target.value 27 | }) 28 | } 29 | /> 30 | 31 | ); 32 | }; 33 | 34 | export const DisplayUsername = React.memo(props => { 35 | console.log( 36 | "Hey I'm only rerendered when name gets updated, check React.memo" 37 | ); 38 | 39 | return

{props.name}

; 40 | }); 41 | -------------------------------------------------------------------------------- /03-block-1/01-pure-component/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { MyComponent } from "./demo"; 4 | import "./styles.css"; 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | const rootElement = document.getElementById("root"); 15 | ReactDOM.render(, rootElement); 16 | -------------------------------------------------------------------------------- /03-block-1/01-pure-component/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /03-block-1/02-pure-component-callback/Readme.md: -------------------------------------------------------------------------------- 1 | # 02 Pure Components Callback 2 | 3 | In the previous sample we saw how to make a component to be pure using 4 | _React.memo_, that's great, but when there's an issue 5 | what happens if we pass the function created inside function component to the child component? 6 | That function will be always different on every render thus 7 | the _memo_ won't take effect. 8 | 9 | How can we solve this? We can make use of _useCallback_, this won't mutate the setter 10 | function unless we indicate any dependency (same approach as with React.useEffect). 11 | 12 | # Steps 13 | 14 | - We will take as starting point sample _09 pure component callback_. Copy the content of the 15 | project to a fresh folder an execute _npm install_. 16 | 17 | ```bash 18 | npm install 19 | ``` 20 | 21 | - Let's open the _demo.js_. We will create a parent and a child component 22 | (this time the child component will just reset the name content). 23 | 24 | _./src/demo.js_ 25 | 26 | ```jsx 27 | import React from "react"; 28 | 29 | export const MyComponent = () => { 30 | const [username, setUsername] = React.useState("John"); 31 | const [lastname, setLastname] = React.useState("Doe"); 32 | 33 | const resetNameCallback = () => { 34 | setUsername(""); 35 | }; 36 | 37 | return ( 38 | <> 39 |

40 | {username} {lastname} 41 |

42 | setUsername(e.target.value)} /> 43 | setLastname(e.target.value)} /> 44 | Reset name 45 | 46 | ); 47 | }; 48 | 49 | const ResetValue = React.memo(props => { 50 | console.log( 51 | "Hey I'm only rendered the first time, check React.memo + callback" 52 | ); 53 | 54 | return ; 55 | }); 56 | ``` 57 | 58 | - If we run the sample we will check that the render is always triggered 59 | (_resetNameCallback_ is always recreated, shallow compare will fail). 60 | 61 | - The trick here is to use _React.useCallback_ and passing as a second 62 | argument an empty array (it will just hold the reference for the function 63 | forever). 64 | 65 | ```diff 66 | import React from "react"; 67 | 68 | export const MyComponent = () => { 69 | const [username, setUsername] = React.useState("John"); 70 | const [lastname, setLastname] = React.useState("Doe"); 71 | 72 | 73 | - const resetNameCallback = () => {setUsername('');} 74 | + const resetNameCallback = React.useCallback(() => setUsername(''), []); 75 | 76 | return ( 77 | <> 78 |

79 | {username} {lastname} 80 |

81 | setUsername(e.target.value)} /> 82 | setLastname(e.target.value)} /> 83 | Reset name 84 | 85 | ); 86 | }; 87 | 88 | const ResetValue = React.memo(props => { 89 | console.log( 90 | "Hey I'm only rendered the first time, check React.memo + callback" 91 | ); 92 | 93 | return ( 94 | 95 | ); 96 | }); 97 | ``` 98 | 99 | - Now if we run the sample we can check the expected behavior. 100 | 101 | > Exercise: what if we group _username_ and _lastname_ in an object (single useState) and use the spread operator to assign the object, would that work? 102 | > check why :-) 103 | 104 | # About Basefactor + Lemoncode 105 | 106 | We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. 107 | 108 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 109 | 110 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 111 | 112 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 113 | -------------------------------------------------------------------------------- /03-block-1/02-pure-component-callback/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.9.0", 7 | "react-dom": "^16.9.0", 8 | "react-scripts": "3.1.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /03-block-1/02-pure-component-callback/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/03-block-1/02-pure-component-callback/public/favicon.ico -------------------------------------------------------------------------------- /03-block-1/02-pure-component-callback/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /03-block-1/02-pure-component-callback/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/03-block-1/02-pure-component-callback/public/logo192.png -------------------------------------------------------------------------------- /03-block-1/02-pure-component-callback/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/03-block-1/02-pure-component-callback/public/logo512.png -------------------------------------------------------------------------------- /03-block-1/02-pure-component-callback/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /03-block-1/02-pure-component-callback/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /03-block-1/02-pure-component-callback/src/demo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const MyComponent = () => { 4 | const [username, setUsername] = React.useState("John"); 5 | const [lastname, setLastname] = React.useState("Doe"); 6 | 7 | const resetNameCallback = React.useCallback(() => { 8 | setUsername(""); 9 | }, []); 10 | 11 | return ( 12 | <> 13 |

14 | {username} {lastname} 15 |

16 | setUsername(e.target.value)} /> 17 | setLastname(e.target.value)} /> 18 | Reset name 19 | 20 | ); 21 | }; 22 | 23 | const ResetValue = React.memo(props => { 24 | console.log( 25 | "Hey I'm only rendered the first time, check React.memo + callback" 26 | ); 27 | 28 | return ; 29 | }); 30 | -------------------------------------------------------------------------------- /03-block-1/02-pure-component-callback/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { MyComponent } from "./demo"; 4 | import "./styles.css"; 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | const rootElement = document.getElementById("root"); 15 | ReactDOM.render(, rootElement); 16 | -------------------------------------------------------------------------------- /03-block-1/02-pure-component-callback/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /03-block-1/Readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/03-block-1/Readme.md -------------------------------------------------------------------------------- /04-block-2/00-use-reducer/Readme.md: -------------------------------------------------------------------------------- 1 | # 00 useReducer 2 | 3 | In the previous sample we worked around the issue with the function 4 | that was getting updated on every render by using _useCallback_, this 5 | approach is cool, but for more complex scenarios you may want to organize 6 | your code using a different approach. Another way of solving this issue 7 | is using _useReducer_, this hook will return a _dispatch_ 8 | function that remains stable. 9 | 10 | # Steps 11 | 12 | - We will take as starting point sample _00 boilerplate_. Copy the content of the 13 | project to a fresh folder an execute _npm install_. 14 | 15 | ```bash 16 | npm install 17 | ``` 18 | 19 | - Let's open the _demo.js_. We will create a parent and a child component 20 | (this time the child component will be able to display and edit a given name). 21 | 22 | _./src/demo.js_ 23 | 24 | ```jsx 25 | import React from "react"; 26 | 27 | export const MyComponent = () => { 28 | const [userInfo, setInfo] = React.useState({ name: "John", lastname: "Doe" }); 29 | 30 | return ( 31 | <> 32 |

33 | {userInfo.name} {userInfo.lastname} 34 |

35 | 38 | setInfo({ 39 | ...userInfo, 40 | name 41 | }) 42 | } 43 | /> 44 | 47 | setInfo({ 48 | ...userInfo, 49 | lastname: e.target.value 50 | }) 51 | } 52 | /> 53 | 54 | ); 55 | }; 56 | 57 | const EditUsername = React.memo(props => { 58 | console.log( 59 | "Hey I'm only rerendered when name gets updated, check React.memo" 60 | ); 61 | 62 | return ( 63 | props.onChange(e.target.value)} /> 64 | ); 65 | }); 66 | ``` 67 | 68 | - If we run the sample we will check that the render is always triggered 69 | (onChangeProp callback is always recreated, shallow compare will fail). 70 | 71 | - Let's fix this using _useReducer_ 72 | 73 | _./src/demo.js_ 74 | 75 | ```diff 76 | import React from "react"; 77 | 78 | + const userInfoReducer = (state, action) => { 79 | + switch(action.type) { 80 | + case 'setusername': 81 | + return { 82 | + ...state, 83 | + name: action.payload, 84 | + } 85 | + case 'setlastname': 86 | + return { 87 | + ...state, 88 | + lastname: action.payload, 89 | + } 90 | + default: 91 | + return state; 92 | + } 93 | + } 94 | 95 | export const MyComponent = () => { 96 | - const [userInfo, setInfo] = React.useState({name: 'John', lastname: 'Doe'}); 97 | + const [reducer, dispatch] = React.useReducer(userInfoReducer, {name: 'John', lastname: 'Doe'}); 98 | 99 | return ( 100 | <> 101 |

102 | - {userInfo.username} {userInfo.lastname} 103 | + {reducer.name} {reducer.lastname} 104 |

105 | - setInfo({ 106 | - ...userInfo, 107 | - name, 108 | - })} /> 109 | + 110 | - setInfo({ 113 | - ...userInfo, 114 | - lastname: e.target.value, 115 | - })} 116 | + 119 | + dispatch({ 120 | + type: "setlastname", 121 | + payload: e.target.value 122 | + }) 123 | + } 124 | + /> 125 | /> 126 | 127 | ); 128 | }; 129 | 130 | const EditUsername = React.memo(props => { 131 | console.log( 132 | "Hey I'm only rerendered when name gets updated, check React.memo" 133 | ); 134 | 135 | return ( 136 | - props.onChange(e.target.value)} /> 137 | + 140 | + props.dispatch({ 141 | + type: "setusername", 142 | + payload: e.target.value 143 | + }) 144 | + } 145 | + /> 146 | ); 147 | }); 148 | ``` 149 | 150 | - Now if we run the sample we will get the expected behavior. 151 | 152 | Discussion here: seems to be an elegant approach, but we have to pass down the dispatcher, 153 | child components may be tied to this dispatcher. 154 | 155 | # About Basefactor + Lemoncode 156 | 157 | We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. 158 | 159 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 160 | 161 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 162 | 163 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 164 | -------------------------------------------------------------------------------- /04-block-2/00-use-reducer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.9.0", 7 | "react-dom": "^16.9.0", 8 | "react-scripts": "3.1.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /04-block-2/00-use-reducer/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/04-block-2/00-use-reducer/public/favicon.ico -------------------------------------------------------------------------------- /04-block-2/00-use-reducer/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /04-block-2/00-use-reducer/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/04-block-2/00-use-reducer/public/logo192.png -------------------------------------------------------------------------------- /04-block-2/00-use-reducer/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/04-block-2/00-use-reducer/public/logo512.png -------------------------------------------------------------------------------- /04-block-2/00-use-reducer/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /04-block-2/00-use-reducer/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /04-block-2/00-use-reducer/src/demo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const userInfoReducer = (state, action) => { 4 | switch (action.type) { 5 | case "setusername": 6 | return { 7 | ...state, 8 | name: action.payload 9 | }; 10 | case "setlastname": 11 | return { 12 | ...state, 13 | lastname: action.payload 14 | }; 15 | default: 16 | return state; 17 | } 18 | }; 19 | 20 | export const MyComponent = () => { 21 | const [reducer, dispatch] = React.useReducer(userInfoReducer, { 22 | name: "John", 23 | lastname: "Doe" 24 | }); 25 | 26 | return ( 27 | <> 28 |

29 | {reducer.name} {reducer.lastname} 30 |

31 | 32 | 35 | dispatch({ 36 | type: "setlastname", 37 | payload: e.target.value 38 | }) 39 | } 40 | /> 41 | 42 | ); 43 | }; 44 | 45 | const EditUsername = React.memo(props => { 46 | console.log( 47 | "Hey I'm only rerendered when name gets updated, check React.memo" 48 | ); 49 | 50 | return ( 51 | 54 | props.dispatch({ 55 | type: "setusername", 56 | payload: e.target.value 57 | }) 58 | } 59 | /> 60 | ); 61 | }); 62 | -------------------------------------------------------------------------------- /04-block-2/00-use-reducer/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { MyComponent } from "./demo"; 4 | import "./styles.css"; 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | const rootElement = document.getElementById("root"); 15 | ReactDOM.render(, rootElement); 16 | -------------------------------------------------------------------------------- /04-block-2/00-use-reducer/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /04-block-2/01-use-context/Readme.md: -------------------------------------------------------------------------------- 1 | # 01 useContext 2 | 3 | Accessing the context via hooks is pretty straight forward. 4 | 5 | # Steps 6 | 7 | - We will take as starting point sample _00-use-reducer_. Copy the content of the 8 | project to a fresh folder an execute _npm install_. 9 | 10 | ```bash 11 | npm install 12 | ``` 13 | 14 | - Now we are going to spend a couple of steps setting up the context (the hooks part is just 15 | one line of code, you will find it in the last steps of this readme). 16 | 17 | _./src/demo.js_ 18 | 19 | ```jsx 20 | import React from "react"; 21 | 22 | const MyContext = React.createContext({ 23 | username: "", 24 | setUsername: () => {} 25 | }); 26 | 27 | export const MyContextProvider = props => { 28 | const [username, setUsername] = React.useState("John Doe"); 29 | 30 | return ( 31 | 32 | {props.children} 33 | 34 | ); 35 | }; 36 | 37 | export const MyComponent = () => { 38 | return

Hello from my component

; 39 | }; 40 | ``` 41 | 42 | - Let's instantiate the provider on top of our application. 43 | 44 | _./src/index.js_ 45 | 46 | ```diff 47 | import React from "react"; 48 | import ReactDOM from "react-dom"; 49 | - import { MyComponent } from "./demo"; 50 | + import { MyComponent, MyContextProvider } from "./demo"; 51 | import "./styles.css"; 52 | 53 | function App() { 54 | return ( 55 | + 56 |
57 | 58 |
59 | +
60 | ); 61 | } 62 | ``` 63 | 64 | - Now let's update _MyComponent_ component under _demo.js_ 65 | **append this to the existing content on \_demo.js** 66 | 67 | ```diff 68 | export const MyComponent = () => { 69 | + const myContext = React.useContext(MyContext); 70 | 71 | return ( 72 | -

Hello from my component

; 73 | +

{myContext.username}

74 | ); 75 | }; 76 | ``` 77 | 78 | - Now if we run the sample we will get the expected behavior. 79 | 80 | # About Basefactor + Lemoncode 81 | 82 | We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. 83 | 84 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 85 | 86 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 87 | 88 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 89 | -------------------------------------------------------------------------------- /04-block-2/01-use-context/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.9.0", 7 | "react-dom": "^16.9.0", 8 | "react-scripts": "3.1.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /04-block-2/01-use-context/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/04-block-2/01-use-context/public/favicon.ico -------------------------------------------------------------------------------- /04-block-2/01-use-context/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /04-block-2/01-use-context/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/04-block-2/01-use-context/public/logo192.png -------------------------------------------------------------------------------- /04-block-2/01-use-context/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/04-block-2/01-use-context/public/logo512.png -------------------------------------------------------------------------------- /04-block-2/01-use-context/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /04-block-2/01-use-context/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /04-block-2/01-use-context/src/demo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const MyContext = React.createContext({ 4 | username: "", 5 | setUsername: () => {} 6 | }); 7 | 8 | export const MyContextProvider = props => { 9 | const [username, setUsername] = React.useState("John Doe"); 10 | 11 | return ( 12 | 13 | {props.children} 14 | 15 | ); 16 | }; 17 | 18 | export const MyComponent = () => { 19 | const myContext = React.useContext(MyContext); 20 | 21 | return

{myContext.username}

; 22 | }; 23 | -------------------------------------------------------------------------------- /04-block-2/01-use-context/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { MyComponent, MyContextProvider } from "./demo"; 4 | import "./styles.css"; 5 | 6 | function App() { 7 | return ( 8 | 9 |
10 | 11 |
12 |
13 | ); 14 | } 15 | 16 | const rootElement = document.getElementById("root"); 17 | ReactDOM.render(, rootElement); 18 | -------------------------------------------------------------------------------- /04-block-2/01-use-context/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /04-block-2/Readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/04-block-2/Readme.md -------------------------------------------------------------------------------- /05 block-3/00 async-closure/Readme.md: -------------------------------------------------------------------------------- 1 | # 00 Async Closure 2 | 3 | When you are new to hooks, you can struggle if you need to manage async callbacks and get 4 | current values from useState, this doesn't work out of the box because the variables 5 | generated by useState are frozen (following js closure principle you don't get the current 6 | value). 7 | 8 | How can we solve this? by using _useRef_ 9 | 10 | # Steps 11 | 12 | - We will take as starting point sample _00 boilerplate_. Copy the content of the 13 | project to a fresh folder an execute _npm install_. 14 | 15 | ```bash 16 | npm install 17 | ``` 18 | 19 | > If you come from the _context_ example remember to remove unused provider in _./src/index.js_ 20 | 21 | _./src/index.js_ 22 | 23 | ```diff 24 | import React from "react"; 25 | import ReactDOM from "react-dom"; 26 | - import { MyComponent, MyContextProvider } from "./demo"; 27 | + import { MyComponent } from "./demo"; 28 | 29 | import "./styles.css"; 30 | 31 | function App() { 32 | return ( 33 | - 34 |
35 | 36 |
37 | -
38 | ); 39 | } 40 | 41 | const rootElement = document.getElementById("root"); 42 | ReactDOM.render(, rootElement); 43 | ``` 44 | 45 | - Let's create an edge case, we will use _useEffect_. Inside we will have to asynchronous calls, 46 | the first one should setup a _seconds_ state to 1 (it will be executed after 1 seconds), then 47 | a second async call will be executed after 2 seconds of time elapse, in theory it should 48 | show the current seconds value (1) but instead we get 0. Let's give a try: 49 | 50 | _./src/demo.js_ 51 | 52 | ```jsx 53 | import React from "react"; 54 | 55 | export const MyComponent = () => { 56 | const [message, setMessage] = React.useState("initial message"); 57 | const [seconds, setSeconds] = React.useState(0); 58 | 59 | React.useEffect(() => { 60 | setTimeout(() => { 61 | console.log(seconds); 62 | setSeconds(seconds + 1); 63 | }, 1000); 64 | 65 | setTimeout(() => { 66 | setMessage(`Total seconds: ${seconds}`); 67 | }, 2000); 68 | }, []); // Important right click and ignore linter rule (later on check what happens) 69 | 70 | return ( 71 | <> 72 |

{message}

73 |

{seconds}

74 | 75 | ); 76 | }; 77 | ``` 78 | 79 | > Check the linter rule, is asking us to add seconds, we will explore this later on 80 | 81 | - The problem here is that seconds points out to the previous value (check javascript closures 82 | for more information), how can we solve this? using _useRef_ (it will hold a mutable value in 83 | a field called _current_). 84 | 85 | ```diff 86 | import React from "react"; 87 | 88 | export const MyComponent = () => { 89 | const [message, setMessage] = React.useState("initial message"); 90 | const [seconds, setSeconds] = React.useState(0); 91 | 92 | + const secondsRef = React.useRef(seconds); 93 | 94 | 95 | React.useEffect(() => { 96 | setTimeout(() => { 97 | console.log(seconds); 98 | setSeconds(seconds + 1); 99 | + secondsRef.current = seconds + 1; 100 | }, 1000); 101 | 102 | setTimeout(() => { 103 | - setMessage(`Total seconds: ${seconds}`); 104 | + setMessage(`Total seconds: ${secondsRef.current}`); 105 | }, 2000); 106 | }, []); 107 | 108 | return ( 109 | <> 110 |

{message}

111 |

{seconds}

112 | 113 | ); 114 | }; 115 | ``` 116 | 117 | - Now if we run the sample we will get the expected behavior. 118 | 119 | - What would happen if we follow the linter rule? 120 | 121 | _./src/demo.js_ 122 | 123 | ```diff 124 | React.useEffect(() => { 125 | setTimeout(() => { 126 | console.log(seconds); 127 | setSeconds(seconds + 1); 128 | secondsRef.current = seconds + 1; 129 | }, 1000); 130 | 131 | setTimeout(() => { 132 | setMessage(`Total seconds: ${secondsRef.current}`); 133 | }, 2000); 134 | // eslint-disable-next-line react-hooks/exhaustive-deps 135 | - }, []); 136 | + }, [seconds]); 137 | ``` 138 | 139 | All this exercise was great to know a bit about issues and edge 140 | cases we can find when using hooks, but let's apply to real life 141 | issues, let's cover the following case: we have a rich gmail client 142 | running, we want to detect whether the user has an slow internet 143 | connection to redirect him to the lite html client (less 144 | functionallity but cover basic with decent speed on poor 145 | connections). 146 | 147 | https://codesandbox.io/s/ajax-checking-usestate-hx02u 148 | 149 | # About Basefactor + Lemoncode 150 | 151 | We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. 152 | 153 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 154 | 155 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 156 | 157 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 158 | -------------------------------------------------------------------------------- /05 block-3/00 async-closure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.9.0", 7 | "react-dom": "^16.9.0", 8 | "react-scripts": "3.1.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /05 block-3/00 async-closure/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/05 block-3/00 async-closure/public/favicon.ico -------------------------------------------------------------------------------- /05 block-3/00 async-closure/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /05 block-3/00 async-closure/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/05 block-3/00 async-closure/public/logo192.png -------------------------------------------------------------------------------- /05 block-3/00 async-closure/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/05 block-3/00 async-closure/public/logo512.png -------------------------------------------------------------------------------- /05 block-3/00 async-closure/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /05 block-3/00 async-closure/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /05 block-3/00 async-closure/src/demo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const MyComponent = () => { 4 | const [message, setMessage] = React.useState("initial message"); 5 | const [seconds, setSeconds] = React.useState(0); 6 | 7 | const secondsRef = React.useRef(seconds); 8 | 9 | React.useEffect(() => { 10 | setTimeout(() => { 11 | console.log(seconds); 12 | setSeconds(seconds + 1); 13 | secondsRef.current = seconds + 1; 14 | }, 1000); 15 | 16 | setTimeout(() => { 17 | setMessage(`Total seconds: ${secondsRef.current}`); 18 | }, 2000); 19 | // eslint-disable-next-line react-hooks/exhaustive-deps 20 | }, []); 21 | 22 | return ( 23 | <> 24 |

{message}

25 |

{seconds}

26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /05 block-3/00 async-closure/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { MyComponent} from "./demo"; 4 | import "./styles.css"; 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | const rootElement = document.getElementById("root"); 15 | ReactDOM.render(, rootElement); 16 | -------------------------------------------------------------------------------- /05 block-3/00 async-closure/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /05 block-3/01-promise-unmounted/Readme.md: -------------------------------------------------------------------------------- 1 | # 01 Promise Unmounted 2 | 3 | There are situations where we just fire an AJAX request and we cannot cancel it, it can happen 4 | that the users navigates away from a given page (or the component making the ajax call 5 | gets unmounted) before the promise gets resolved, What happens when the promise gets resolved? 6 | We get an error, how can we avoid this? tracking when a component gets mounted and unmounted, 7 | let's check how can we do this using hooks. 8 | 9 | # Steps 10 | 11 | - We will take as starting point sample _00 boilerplate_. Copy the content of the 12 | project to a fresh folder an execute _npm install_. 13 | 14 | ```bash 15 | npm install 16 | ``` 17 | 18 | - Let's open the _demo.js_, we will create the boiler plate code 19 | (add a filter input, display a list of names) 20 | 21 | _./src/demo.js_ 22 | 23 | ```jsx 24 | import React from "react"; 25 | 26 | export const MyComponent = () => { 27 | const [visible, setVisible] = React.useState(false); 28 | 29 | return ( 30 | <> 31 | {visible && } 32 | 35 | 36 | ); 37 | }; 38 | 39 | export const MyChildComponent = () => { 40 | const [filter, setFilter] = React.useState(""); 41 | const [userCollection, setUserCollection] = React.useState([]); 42 | 43 | return ( 44 |
45 | setFilter(e.target.value)} /> 46 |
    47 | {userCollection.map((user, index) => ( 48 |
  • {user.name}
  • 49 | ))} 50 |
51 |
52 | ); 53 | }; 54 | ``` 55 | 56 | - Now we want to fire an ajax request every time user types on the filter input (we will add 57 | some latency). 58 | 59 | _./src/demo.js_ 60 | 61 | ```diff 62 | export const MyChildComponent = () => { 63 | const [filter, setFilter] = React.useState(''); 64 | const [userCollection, setUserCollection] = React.useState([]); 65 | 66 | + // Load full list when the component gets mounted and filter gets updated 67 | + React.useEffect(() => { 68 | + setTimeout(() => { 69 | + fetch(`https://jsonplaceholder.typicode.com/users?name_like=${filter}`) 70 | + .then(response => response.json()) 71 | + .then(json => setUserCollection(json)); 72 | + } 73 | + , 2500) 74 | + }, [filter]); 75 | 76 | return ( 77 | ``` 78 | 79 | - If we run the sample, type a letter on the input and quickly hit on the toggle child component 80 | visibility button an error will popup in the console log. 81 | 82 | _Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function._ 83 | 84 | - We can detect when a given component has been unmounted by using a Ref flag. 85 | 86 | ```diff 87 | export const MyChildComponent = () => { 88 | const [filter, setFilter] = React.useState(""); 89 | const [userCollection, setUserCollection] = React.useState([]); 90 | 91 | + const mountedRef = React.useRef(false); 92 | 93 | + React.useEffect(() => { 94 | + mountedRef.current = true; 95 | + return () => (mountedRef.current = false) 96 | + }, []) 97 | 98 | + const setSafeUserCollection = (userCollection) => mountedRef.current && setUserCollection(userCollection); 99 | 100 | // Load full list when the component gets mounted and filter gets updated 101 | React.useEffect(() => { 102 | ``` 103 | 104 | Then in our _fetch_ call we can resolve it in the following way: 105 | 106 | ```diff 107 | React.useEffect(() => { 108 | setTimeout(() => { 109 | fetch(`https://jsonplaceholder.typicode.com/users?name_like=${filter}`) 110 | .then(response => response.json()) 111 | - .then(json => setUserCollection(json)); 112 | + .then(json => setSafeUserCollection(json)); 113 | }, 2500); 114 | }, [filter]); 115 | ``` 116 | 117 | Note down: we are simulating an ajax request using SetTimeout, ... if we are using a setTimeout 118 | we have an easier way to cancel the request. 119 | 120 | ```javascript 121 | React.useEffect(() => { 122 | const t = setTimeout(() => { 123 | fetch(`https://jsonplaceholder.typicode.com/users?name_like=${filter}`) 124 | .then(response => response.json()) 125 | .then(json => setUserCollection(json)); 126 | }, 2500); 127 | 128 | return () => clearTimeout(t); 129 | }, [filter]); 130 | ``` 131 | 132 | > **Exercise:** we could encapsulate the fetching plus the setSafeUserCollection in a hook, why not giving a try? ;) 133 | 134 | # About Basefactor + Lemoncode 135 | 136 | We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. 137 | 138 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 139 | 140 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 141 | 142 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 143 | -------------------------------------------------------------------------------- /05 block-3/01-promise-unmounted/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.9.0", 7 | "react-dom": "^16.9.0", 8 | "react-scripts": "3.1.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /05 block-3/01-promise-unmounted/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/05 block-3/01-promise-unmounted/public/favicon.ico -------------------------------------------------------------------------------- /05 block-3/01-promise-unmounted/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /05 block-3/01-promise-unmounted/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/05 block-3/01-promise-unmounted/public/logo192.png -------------------------------------------------------------------------------- /05 block-3/01-promise-unmounted/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/05 block-3/01-promise-unmounted/public/logo512.png -------------------------------------------------------------------------------- /05 block-3/01-promise-unmounted/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /05 block-3/01-promise-unmounted/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /05 block-3/01-promise-unmounted/src/demo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const MyComponent = () => { 4 | const [visible, setVisible] = React.useState(false); 5 | 6 | return ( 7 | <> 8 | {visible && } 9 | 12 | 13 | ); 14 | }; 15 | 16 | export const MyChildComponent = () => { 17 | const [filter, setFilter] = React.useState(""); 18 | const [userCollection, setUserCollection] = React.useState([]); 19 | 20 | const mountedRef = React.useRef(false); 21 | 22 | React.useEffect(() => { 23 | mountedRef.current = true; 24 | return () => (mountedRef.current = false); 25 | }, []); 26 | 27 | const setSafeUserCollection = userCollection => 28 | mountedRef.current && setUserCollection(userCollection); 29 | 30 | // Load full list when the component gets mounted and filter gets updated 31 | React.useEffect(() => { 32 | setTimeout(() => { 33 | fetch(`https://jsonplaceholder.typicode.com/users?name_like=${filter}`) 34 | .then(response => response.json()) 35 | .then(json => setSafeUserCollection(json)); 36 | }, 2500); 37 | }, [filter]); 38 | 39 | return ( 40 |
41 | setFilter(e.target.value)} /> 42 |
    43 | {userCollection.map((user, index) => ( 44 |
  • {user.name}
  • 45 | ))} 46 |
47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /05 block-3/01-promise-unmounted/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { MyComponent} from "./demo"; 4 | import "./styles.css"; 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | const rootElement = document.getElementById("root"); 15 | ReactDOM.render(, rootElement); 16 | -------------------------------------------------------------------------------- /05 block-3/01-promise-unmounted/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /05 block-3/03-should-update/Readme.md: -------------------------------------------------------------------------------- 1 | # 03 Should Update (Memo predicate) 2 | 3 | In this sample we will enhance rendering performance hooking to 'shouldComponentUpdate'. 4 | 5 | We are going to implement a customer satisfaction widget, based on smily faces, it will accept a range value (0 to 500), and the faces will have a range of values 0..100, 100..200, 200..300, 300..400, 400..500. We will only fire the render option whenever the value jumps into the next or previous range. 6 | 7 | # Steps 8 | 9 | - We will take as starting point sample _00 boilerplate_. Copy the content of the project to a fresh folder an execute _npm install_. 10 | 11 | ```bash 12 | npm install 13 | ``` 14 | 15 | - Let's copy the five smiley faces (you can copy them from the sample implementation in github: https://github.com/Lemoncode/react-hooks-by-example/tree/master/15-memo-predicate/src). 16 | 17 | - Let's add the following content into the _src/styles.css_ css file to add the smileys styles: 18 | 19 | ```css 20 | .very-dissatisfied { 21 | width: 100%; 22 | height: 80px; 23 | background: url("./one.png") no-repeat; 24 | } 25 | 26 | .somewhat-dissatisfied { 27 | width: 100%; 28 | height: 80px; 29 | background: url("./two.png") no-repeat; 30 | } 31 | 32 | .neither { 33 | width: 100%; 34 | height: 80px; 35 | background: url("./three.png") no-repeat; 36 | } 37 | 38 | .somewhat-satisfied { 39 | width: 100%; 40 | height: 80px; 41 | background: url("./four.png") no-repeat; 42 | } 43 | 44 | .very-satisfied { 45 | width: 100%; 46 | height: 80px; 47 | background: url("./five.png") no-repeat; 48 | } 49 | ``` 50 | 51 | - Let's create a simple component in _src/demo.js_, we will start by just adding something hardcoded in file: 52 | 53 | ```diff 54 | import * as React from 'react'; 55 | 56 | export const FaceComponent = props => { 57 | 58 | const { level } = props; 59 | 60 | console.log("Rerendering..."); 61 | 62 | return ( 63 |
64 | ); 65 | } 66 | ``` 67 | 68 | - Let's make a quick test on _index.js_: 69 | 70 | _./src/index.js_ 71 | 72 | ```diff 73 | import React from "react"; 74 | import ReactDOM from "react-dom"; 75 | - import { MyComponent } from "./demo"; 76 | + import { FaceComponent } from "./demo"; 77 | import "./styles.css"; 78 | 79 | function App() { 80 | return ( 81 |
82 | - 83 | + 84 |
85 | ); 86 | } 87 | 88 | const rootElement = document.getElementById("root"); 89 | ReactDOM.render(, rootElement); 90 | ``` 91 | 92 | - Let's make a check point and run the sample: check that is working as expected. 93 | 94 | ``` 95 | npm start 96 | ``` 97 | 98 | - Now it's time to link the property with the proper faces, let's create a style function for that in _demo.js_ 99 | 100 | _./src/demo.js_ 101 | 102 | ```diff 103 | import * as React from 'react'; 104 | 105 | + const setSatisfactionClass = level => { 106 | + 107 | + if (level < 100) { 108 | + return "very-dissatisfied" 109 | + } 110 | + 111 | + if (level < 200) { 112 | + return "somewhat-dissatisfied" 113 | + } 114 | + 115 | + if (level < 300) { 116 | + return "neither" 117 | + } 118 | + 119 | + if (level < 400) { 120 | + return "somewhat-satisfied" 121 | + } 122 | + 123 | + return "very-satisfied" 124 | + } 125 | 126 | export const FaceComponent = props => { 127 | 128 | const { level } = props; 129 | 130 | return ( 131 | -
132 | +
133 | ); 134 | } 135 | ``` 136 | 137 | - In _index.js_ let's add a state variable to hold the current satisfaction level plus an slider to let the user update it. 138 | 139 | _./src/index.js_ 140 | 141 | ```diff 142 | import React from "react"; 143 | import ReactDOM from "react-dom"; 144 | import { FaceComponent } from "./demo"; 145 | import "./styles.css"; 146 | 147 | function App() { 148 | + 149 | + const [satisfactionLevel, setSatisfactionLevel] = React.useState(300); 150 | + 151 | return ( 152 |
153 | + setSatisfactionLevel(+event.target.value)} 158 | + /> 159 | +
160 | + {satisfactionLevel} 161 | +
162 | - 163 | + 164 |
165 | ); 166 | } 167 | 168 | const rootElement = document.getElementById("root"); 169 | ReactDOM.render(, rootElement); 170 | ``` 171 | 172 | - Let's run the sample: 173 | 174 | ``` 175 | npm start 176 | ``` 177 | 178 | - Let's add a rendering optimization, we should only trigger the render whenever the level just changes the satisfaction range: 179 | 180 | _./src/demo.js_ 181 | 182 | ```diff 183 | import * as React from 'react'; 184 | 185 | const setSatisfactionClass = level => { 186 | 187 | if (level < 100) { 188 | return "very-dissatisfied" 189 | } 190 | 191 | if (level < 200) { 192 | return "somewhat-dissatisfied" 193 | } 194 | 195 | if (level < 300) { 196 | return "neither" 197 | } 198 | 199 | if (level < 400) { 200 | return "somewhat-satisfied" 201 | } 202 | 203 | return "very-satisfied" 204 | } 205 | 206 | + const isSameRange = (prevValue, nextValue) => { 207 | + 208 | + const prevValueClass = setSatisfactionClass(prevValue.level); 209 | + const nextValueClass = setSatisfactionClass(nextValue.level); 210 | + 211 | + return prevValueClass === nextValueClass; 212 | + } 213 | 214 | - export const FaceComponent = props => { 215 | + export const FaceComponent = React.memo(props => { 216 | 217 | const { level } = props; 218 | 219 | console.log("Rerendering..."); 220 | 221 | return ( 222 |
223 | ); 224 | - } 225 | + }, isSameRange); 226 | ``` 227 | 228 | - Now if we place a breakpoint in the faceComponent render method we can see that render is only triggered when you change from a satisfaction range (e.g. 99 to 100). 229 | 230 | ``` 231 | npm start 232 | ``` 233 | 234 | # About Basefactor + Lemoncode 235 | 236 | We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. 237 | 238 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 239 | 240 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 241 | 242 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 243 | -------------------------------------------------------------------------------- /05 block-3/03-should-update/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.9.0", 7 | "react-dom": "^16.9.0", 8 | "react-scripts": "3.1.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /05 block-3/03-should-update/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/05 block-3/03-should-update/public/favicon.ico -------------------------------------------------------------------------------- /05 block-3/03-should-update/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /05 block-3/03-should-update/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/05 block-3/03-should-update/public/logo192.png -------------------------------------------------------------------------------- /05 block-3/03-should-update/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/05 block-3/03-should-update/public/logo512.png -------------------------------------------------------------------------------- /05 block-3/03-should-update/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /05 block-3/03-should-update/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /05 block-3/03-should-update/src/demo.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const setSatisfactionClass = level => { 4 | if (level < 100) { 5 | return "very-dissatisfied"; 6 | } 7 | 8 | if (level < 200) { 9 | return "somewhat-dissatisfied"; 10 | } 11 | 12 | if (level < 300) { 13 | return "neither"; 14 | } 15 | 16 | if (level < 400) { 17 | return "somewhat-satisfied"; 18 | } 19 | 20 | return "very-satisfied"; 21 | }; 22 | 23 | const isSameRange = (prevValue, nextValue) => { 24 | const prevValueClass = setSatisfactionClass(prevValue.level); 25 | const nextValueClass = setSatisfactionClass(nextValue.level); 26 | 27 | return prevValueClass === nextValueClass; 28 | }; 29 | 30 | export const FaceComponent = React.memo(props => { 31 | const { level } = props; 32 | 33 | console.log("Rerendering..."); 34 | 35 | return
; 36 | }, isSameRange); 37 | -------------------------------------------------------------------------------- /05 block-3/03-should-update/src/five.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/05 block-3/03-should-update/src/five.png -------------------------------------------------------------------------------- /05 block-3/03-should-update/src/four.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/05 block-3/03-should-update/src/four.png -------------------------------------------------------------------------------- /05 block-3/03-should-update/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { FaceComponent } from "./demo"; 4 | import "./styles.css"; 5 | 6 | function App() { 7 | const [satisfactionLevel, setSatisfactionLevel] = React.useState(300); 8 | 9 | return ( 10 |
11 |
12 | setSatisfactionLevel(event.target.value)} 18 | /> 19 |
20 |
21 | {satisfactionLevel} 22 |
23 |
24 | 25 |
26 |
27 | ); 28 | } 29 | 30 | const rootElement = document.getElementById("root"); 31 | ReactDOM.render(, rootElement); 32 | -------------------------------------------------------------------------------- /05 block-3/03-should-update/src/one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/05 block-3/03-should-update/src/one.png -------------------------------------------------------------------------------- /05 block-3/03-should-update/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | 6 | .very-dissatisfied { 7 | width: 100%; 8 | height: 80px; 9 | background: url("./one.png") no-repeat; 10 | } 11 | 12 | .somewhat-dissatisfied { 13 | width: 100%; 14 | height: 80px; 15 | background: url("./two.png") no-repeat; 16 | } 17 | 18 | .neither { 19 | width: 100%; 20 | height: 80px; 21 | background: url("./three.png") no-repeat; 22 | } 23 | 24 | .somewhat-satisfied { 25 | width: 100%; 26 | height: 80px; 27 | background: url("./four.png") no-repeat; 28 | } 29 | 30 | .very-satisfied { 31 | width: 100%; 32 | height: 80px; 33 | background: url("./five.png") no-repeat; 34 | } 35 | -------------------------------------------------------------------------------- /05 block-3/03-should-update/src/three.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/05 block-3/03-should-update/src/three.png -------------------------------------------------------------------------------- /05 block-3/03-should-update/src/two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/05 block-3/03-should-update/src/two.png -------------------------------------------------------------------------------- /05 block-3/Readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/05 block-3/Readme.md -------------------------------------------------------------------------------- /06 Exercise 1/00-start/Readme.md: -------------------------------------------------------------------------------- 1 | # Challenge 1 - 00-start 2 | 3 | ## Steps 4 | 5 | - Copy `00-start` project folder. 6 | - Run `npm install`. 7 | - Run `npm start` and implement the challenge. 8 | 9 | ## Tips 10 | 11 | A. Make it global: 12 | 13 | - Move the lookup data to context. 14 | - Create a hook that has Access to this context and exposes a loadMethod. 15 | - Use the hook on PageA and PageB. 16 | 17 | B. Make it lazy: 18 | 19 | - Just add a state on the custom hook you have created to check if the lookup has been already loaded, and depending on the result just return the promise resolved with the data if not fire the async request. 20 | 21 | C. Extra: 22 | - what if we can launch in parallel several request at the same for the same lookup? Could we just store the first promise launched and return that? 23 | 24 | # About Basefactor + Lemoncode 25 | 26 | We are an innovating team of Front End Developers, passionate about turning your ideas into robust products. 27 | 28 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 29 | 30 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 31 | 32 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 33 | -------------------------------------------------------------------------------- /06 Exercise 1/00-start/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "16.8.6", 9 | "react-dom": "16.8.6", 10 | "react-router-dom": "^5.0.1", 11 | "react-scripts": "2.1.8" 12 | }, 13 | "devDependencies": { 14 | "typescript": "3.3.3" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | }, 22 | "browserslist": [ 23 | ">0.2%", 24 | "not dead", 25 | "not ie <= 11", 26 | "not op_mini all" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /06 Exercise 1/00-start/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /06 Exercise 1/00-start/src/api.js: -------------------------------------------------------------------------------- 1 | export const fetchUserLookups = () => 2 | fetch(`https://jsonplaceholder.typicode.com/users`) 3 | .then(response => response.json()) 4 | .then(data => { 5 | console.log('User lookups fetched from server'); 6 | console.log({ data }); 7 | return data; 8 | }); 9 | -------------------------------------------------------------------------------- /06 Exercise 1/00-start/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Router } from './router'; 4 | import './styles.css'; 5 | 6 | const App = () => { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | }; 13 | 14 | const rootElement = document.getElementById('root'); 15 | ReactDOM.render(, rootElement); 16 | -------------------------------------------------------------------------------- /06 Exercise 1/00-start/src/page-a.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { fetchUserLookups } from './api'; 4 | 5 | export const PageA = () => { 6 | const [userCollection, setUserCollection] = React.useState([]); 7 | 8 | const onLoadLookups = () => { 9 | fetchUserLookups().then(users => { 10 | setUserCollection(users); 11 | }); 12 | }; 13 | 14 | return ( 15 | <> 16 |

Page A

17 |
    18 | {userCollection.map((user, index) => ( 19 |
  • {user.name}
  • 20 | ))} 21 |
22 | 23 | 24 | Navigate Page B → 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /06 Exercise 1/00-start/src/page-b.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { fetchUserLookups } from './api'; 4 | 5 | export const PageB = () => { 6 | const [userCollection, setUserCollection] = React.useState([]); 7 | 8 | const onLoadLookups = () => { 9 | fetchUserLookups().then(users => { 10 | setUserCollection(users); 11 | }); 12 | }; 13 | 14 | return ( 15 | <> 16 |

Page B

17 |
    18 | {userCollection.map((user, index) => ( 19 |
  • {user.name}
  • 20 | ))} 21 |
22 | 23 | 24 | ← Navigate Page A 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /06 Exercise 1/00-start/src/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HashRouter, Switch, Route } from 'react-router-dom'; 3 | import { PageA } from './page-a'; 4 | import { PageB } from './page-b'; 5 | 6 | export const Router = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /06 Exercise 1/00-start/src/styles.css: -------------------------------------------------------------------------------- 1 | .app { 2 | font-family: sans-serif; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | 8 | .buttons-container { 9 | width: 50%; 10 | display: flex; 11 | justify-content: center; 12 | } 13 | 14 | .link-button { 15 | margin-top: 5rem; 16 | text-decoration: none; 17 | } 18 | -------------------------------------------------------------------------------- /06 Exercise 1/01-implemented/Readme.md: -------------------------------------------------------------------------------- 1 | # Challenge 1 - 01-implemented 2 | 3 | ## Steps 4 | 5 | - Copy `00-start` project folder. 6 | 7 | - Run `npm install`. 8 | 9 | - First, we will create the `lookup-context` to store the data related with the lookups: 10 | 11 | _./src/lookup.context.js_ 12 | 13 | ```javascript 14 | import React from 'react'; 15 | import * as api from './api'; 16 | 17 | export const LookupContext = React.createContext({ 18 | fetchUserLookups: () => 19 | console.warn('You need to use LookupProvider in your app'), 20 | }); 21 | 22 | export const LookupProvider = props => { 23 | const [userCollection, setUserCollection] = React.useState([]); 24 | 25 | const fetchUserLookups = React.useCallback(() => { 26 | return api.fetchUserLookups().then(users => { 27 | setUserCollection(users); 28 | return users; 29 | }); 30 | }, []); 31 | 32 | return ( 33 | 38 | {props.children} 39 | 40 | ); 41 | }; 42 | ``` 43 | 44 | - Now, we need to use `LookupProvider` in our app: 45 | 46 | _./src/index.js_ 47 | 48 | ```diff 49 | import React from 'react'; 50 | import ReactDOM from 'react-dom'; 51 | import { Router } from './router'; 52 | + import { LookupProvider } from './lookup.context'; 53 | import './styles.css'; 54 | 55 | const App = () => { 56 | return ( 57 |
58 | + 59 | 60 | + 61 |
62 | ); 63 | }; 64 | ... 65 | 66 | ``` 67 | 68 | - Use it on `page-a`: 69 | 70 | _./src/page-a.js_ 71 | 72 | ```diff 73 | import React from 'react'; 74 | import { Link } from 'react-router-dom'; 75 | - import { fetchUserLookups } from './api'; 76 | + import { LookupContext } from './lookup.context'; 77 | 78 | export const PageA = () => { 79 | const [userCollection, setUserCollection] = React.useState([]); 80 | + const { fetchUserLookups } = React.useContext(LookupContext); 81 | ... 82 | 83 | ``` 84 | 85 | - Use it on `page-b`: 86 | 87 | _./src/page-b.js_ 88 | 89 | ```diff 90 | import React from 'react'; 91 | import { Link } from 'react-router-dom'; 92 | - import { fetchUserLookups } from './api'; 93 | + import { LookupContext } from './lookup.context'; 94 | 95 | export const PageB = () => { 96 | const [userCollection, setUserCollection] = React.useState([]); 97 | + const { fetchUserLookups } = React.useContext(LookupContext); 98 | ... 99 | ``` 100 | 101 | - Let's make it lazy: 102 | 103 | _./src/lookup.context.js_ 104 | 105 | ```diff 106 | ... 107 | export const LookupProvider = props => { 108 | const [userCollection, setUserCollection] = React.useState([]); 109 | 110 | const fetchUserLookups = React.useCallback(() => { 111 | - return api.fetchUserLookups().then(users => { 112 | - setUserCollection(users); 113 | - return users; 114 | - }); 115 | + return userCollection.length > 0 116 | + ? Promise.resolve(userCollection) 117 | + : api.fetchUserLookups().then(users => { 118 | + setUserCollection(users); 119 | + return users; 120 | + }); 121 | - }, []); 122 | + }, [userCollection]); 123 | ... 124 | 125 | ``` 126 | 127 | # About Basefactor + Lemoncode 128 | 129 | We are an innovating team of Front End Developers, passionate about turning your ideas into robust products. 130 | 131 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 132 | 133 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 134 | 135 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 136 | -------------------------------------------------------------------------------- /06 Exercise 1/01-implemented/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "16.8.6", 9 | "react-dom": "16.8.6", 10 | "react-router-dom": "^5.0.1", 11 | "react-scripts": "2.1.8" 12 | }, 13 | "devDependencies": { 14 | "typescript": "3.3.3" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | }, 22 | "browserslist": [ 23 | ">0.2%", 24 | "not dead", 25 | "not ie <= 11", 26 | "not op_mini all" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /06 Exercise 1/01-implemented/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /06 Exercise 1/01-implemented/src/api.js: -------------------------------------------------------------------------------- 1 | export const fetchUserLookups = () => 2 | fetch(`https://jsonplaceholder.typicode.com/users`) 3 | .then(response => response.json()) 4 | .then(data => { 5 | console.log('User lookups fetched from server'); 6 | console.log({ data }); 7 | return data; 8 | }); 9 | -------------------------------------------------------------------------------- /06 Exercise 1/01-implemented/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Router } from './router'; 4 | import { LookupProvider } from './lookup.context'; 5 | import './styles.css'; 6 | 7 | const App = () => { 8 | return ( 9 |
10 | 11 | 12 | 13 |
14 | ); 15 | }; 16 | 17 | const rootElement = document.getElementById('root'); 18 | ReactDOM.render(, rootElement); 19 | -------------------------------------------------------------------------------- /06 Exercise 1/01-implemented/src/lookup.context.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as api from "./api"; 3 | 4 | export const LookupContext = React.createContext({ 5 | fetchUserLookups: () => console.warn("You need to use LookupProvider in your app"), 6 | }); 7 | 8 | export const LookupProvider = props => { 9 | const [userCollection, setUserCollection] = React.useState([]); 10 | 11 | const fetchUserLookups = React.useCallback(() => { 12 | return userCollection.length > 0 13 | ? Promise.resolve(userCollection).then(users => { 14 | console.log("User lookups fetched from cache"); 15 | return users; 16 | }) 17 | : api.fetchUserLookups().then(users => { 18 | setUserCollection(users); 19 | return users; 20 | }); 21 | }, [userCollection]); 22 | 23 | return ( 24 | 29 | {props.children} 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /06 Exercise 1/01-implemented/src/page-a.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { LookupContext } from './lookup.context'; 4 | 5 | export const PageA = () => { 6 | const [userCollection, setUserCollection] = React.useState([]); 7 | const { fetchUserLookups } = React.useContext(LookupContext); 8 | 9 | const onLoadLookups = () => { 10 | fetchUserLookups().then(users => { 11 | setUserCollection(users); 12 | }); 13 | }; 14 | 15 | return ( 16 | <> 17 |

Page A

18 |
    19 | {userCollection.map((user, index) => ( 20 |
  • {user.name}
  • 21 | ))} 22 |
23 | 24 | 25 | Navigate Page B → 26 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /06 Exercise 1/01-implemented/src/page-b.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { LookupContext } from './lookup.context'; 4 | 5 | export const PageB = () => { 6 | const [userCollection, setUserCollection] = React.useState([]); 7 | const { fetchUserLookups } = React.useContext(LookupContext); 8 | 9 | const onLoadLookups = () => { 10 | fetchUserLookups().then(users => { 11 | setUserCollection(users); 12 | }); 13 | }; 14 | 15 | return ( 16 | <> 17 |

Page B

18 |
    19 | {userCollection.map((user, index) => ( 20 |
  • {user.name}
  • 21 | ))} 22 |
23 | 24 | 25 | ← Navigate Page A 26 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /06 Exercise 1/01-implemented/src/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HashRouter, Switch, Route } from 'react-router-dom'; 3 | import { PageA } from './page-a'; 4 | import { PageB } from './page-b'; 5 | 6 | export const Router = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /06 Exercise 1/01-implemented/src/styles.css: -------------------------------------------------------------------------------- 1 | .app { 2 | font-family: sans-serif; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | 8 | .buttons-container { 9 | width: 50%; 10 | display: flex; 11 | justify-content: center; 12 | } 13 | 14 | .link-button { 15 | margin-top: 5rem; 16 | text-decoration: none; 17 | } 18 | -------------------------------------------------------------------------------- /06 Exercise 1/02-implemented/Readme.md: -------------------------------------------------------------------------------- 1 | # Challenge 1 - 01-implemented 2 | 3 | ## Steps 4 | 5 | - Copy `00-start` project folder. 6 | 7 | - Run `npm install`. 8 | 9 | - First, we will create the `lookup-context` to store the data related with the lookups: 10 | 11 | _./src/lookup.context.js_ 12 | 13 | ```javascript 14 | import React from 'react'; 15 | import * as api from './api'; 16 | 17 | export const LookupContext = React.createContext({ 18 | fetchUserLookups: () => 19 | console.warn('You need to use LookupProvider in your app'), 20 | }); 21 | 22 | export const LookupProvider = props => { 23 | const [userCollection, setUserCollection] = React.useState([]); 24 | 25 | const fetchUserLookups = React.useCallback(() => { 26 | return api.fetchUserLookups().then(users => { 27 | setUserCollection(users); 28 | return users; 29 | }); 30 | }, []); 31 | 32 | return ( 33 | 38 | {props.children} 39 | 40 | ); 41 | }; 42 | ``` 43 | 44 | - Now, we need to use `LookupProvider` in our app: 45 | 46 | _./src/index.js_ 47 | 48 | ```diff 49 | import React from 'react'; 50 | import ReactDOM from 'react-dom'; 51 | import { Router } from './router'; 52 | + import { LookupProvider } from './lookup.context'; 53 | import './styles.css'; 54 | 55 | const App = () => { 56 | return ( 57 |
58 | + 59 | 60 | + 61 |
62 | ); 63 | }; 64 | ... 65 | 66 | ``` 67 | 68 | - Use it on `page-a`: 69 | 70 | _./src/page-a.js_ 71 | 72 | ```diff 73 | import React from 'react'; 74 | import { Link } from 'react-router-dom'; 75 | - import { fetchUserLookups } from './api'; 76 | + import { LookupContext } from './lookup.context'; 77 | 78 | export const PageA = () => { 79 | const [userCollection, setUserCollection] = React.useState([]); 80 | + const { fetchUserLookups } = React.useContext(LookupContext); 81 | ... 82 | 83 | ``` 84 | 85 | - Use it on `page-b`: 86 | 87 | _./src/page-b.js_ 88 | 89 | ```diff 90 | import React from 'react'; 91 | import { Link } from 'react-router-dom'; 92 | - import { fetchUserLookups } from './api'; 93 | + import { LookupContext } from './lookup.context'; 94 | 95 | export const PageB = () => { 96 | const [userCollection, setUserCollection] = React.useState([]); 97 | + const { fetchUserLookups } = React.useContext(LookupContext); 98 | ... 99 | ``` 100 | 101 | - Let's make it lazy: 102 | 103 | _./src/lookup.context.js_ 104 | 105 | ```diff 106 | ... 107 | export const LookupProvider = props => { 108 | const [userCollection, setUserCollection] = React.useState([]); 109 | 110 | const fetchUserLookups = React.useCallback(() => { 111 | - return api.fetchUserLookups().then(users => { 112 | - setUserCollection(users); 113 | - return users; 114 | - }); 115 | + return userCollection.length > 0 116 | + ? Promise.resolve(userCollection) 117 | + : api.fetchUserLookups().then(users => { 118 | + setUserCollection(users); 119 | + return users; 120 | + }); 121 | - }, []); 122 | + }, [userCollection]); 123 | ... 124 | 125 | ``` 126 | 127 | # About Basefactor + Lemoncode 128 | 129 | We are an innovating team of Front End Developers, passionate about turning your ideas into robust products. 130 | 131 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 132 | 133 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 134 | 135 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 136 | -------------------------------------------------------------------------------- /06 Exercise 1/02-implemented/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "16.8.6", 9 | "react-dom": "16.8.6", 10 | "react-router-dom": "^5.0.1", 11 | "react-scripts": "2.1.8" 12 | }, 13 | "devDependencies": { 14 | "typescript": "3.3.3" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | }, 22 | "browserslist": [ 23 | ">0.2%", 24 | "not dead", 25 | "not ie <= 11", 26 | "not op_mini all" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /06 Exercise 1/02-implemented/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /06 Exercise 1/02-implemented/src/api.js: -------------------------------------------------------------------------------- 1 | export const fetchUserLookups = () => 2 | fetch(`https://jsonplaceholder.typicode.com/users`) 3 | .then(response => response.json()) 4 | .then(data => { 5 | console.log('User lookups fetched from server'); 6 | console.log({ data }); 7 | return data; 8 | }); 9 | -------------------------------------------------------------------------------- /06 Exercise 1/02-implemented/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Router } from "./router"; 4 | import { UserProvider } from "./user.context"; 5 | import "./styles.css"; 6 | 7 | const App = () => { 8 | return ( 9 |
10 | 11 | 12 | 13 |
14 | ); 15 | }; 16 | 17 | const rootElement = document.getElementById("root"); 18 | ReactDOM.render(, rootElement); 19 | -------------------------------------------------------------------------------- /06 Exercise 1/02-implemented/src/page-a.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { useUserCollection } from "./user.hooks"; 4 | 5 | export const PageA = () => { 6 | const [userCollection, setUserCollection] = React.useState([]); 7 | const { lookupUserCollection } = useUserCollection(); 8 | 9 | const onLoadLookups = () => { 10 | lookupUserCollection().then(users => { 11 | setUserCollection(users); 12 | }); 13 | }; 14 | 15 | return ( 16 | <> 17 |

Page A

18 |
    19 | {userCollection.map((user, index) => ( 20 |
  • {user.name}
  • 21 | ))} 22 |
23 | 24 | 25 | Navigate Page B → 26 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /06 Exercise 1/02-implemented/src/page-b.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { useUserCollection } from "./user.hooks"; 4 | 5 | export const PageB = () => { 6 | const [userCollection, setUserCollection] = React.useState([]); 7 | const { lookupUserCollection } = useUserCollection(); 8 | 9 | const onLoadLookups = () => { 10 | lookupUserCollection().then(users => { 11 | setUserCollection(users); 12 | }); 13 | }; 14 | 15 | return ( 16 | <> 17 |

Page B

18 |
    19 | {userCollection.map((user, index) => ( 20 |
  • {user.name}
  • 21 | ))} 22 |
23 | 24 | 25 | ← Navigate Page A 26 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /06 Exercise 1/02-implemented/src/router.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { HashRouter, Switch, Route } from "react-router-dom"; 3 | import { PageA } from "./page-a"; 4 | import { PageB } from "./page-b"; 5 | 6 | export const Router = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /06 Exercise 1/02-implemented/src/styles.css: -------------------------------------------------------------------------------- 1 | .app { 2 | font-family: sans-serif; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | 8 | .buttons-container { 9 | width: 50%; 10 | display: flex; 11 | justify-content: center; 12 | } 13 | 14 | .link-button { 15 | margin-top: 5rem; 16 | text-decoration: none; 17 | } 18 | -------------------------------------------------------------------------------- /06 Exercise 1/02-implemented/src/user.context.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export const UserContext = React.createContext(); 4 | 5 | export const UserProvider = ({ children }) => { 6 | const [userCollection, setUserCollection] = React.useState([]); 7 | return {children}; 8 | }; 9 | -------------------------------------------------------------------------------- /06 Exercise 1/02-implemented/src/user.hooks.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as api from "./api"; 3 | import { UserContext } from "./user.context"; 4 | 5 | export const useUserCollection = () => { 6 | const { userCollection, setUserCollection } = React.useContext(UserContext); 7 | 8 | const lookupUserCollection = React.useCallback(() => { 9 | return userCollection && userCollection.length > 0 10 | ? Promise.resolve(userCollection).then(userCollection => { 11 | console.log("User collection consumed from context"); 12 | return userCollection; 13 | }) 14 | : api.fetchUserLookups().then(userCollection => { 15 | setUserCollection(userCollection); 16 | return userCollection; 17 | }); 18 | }, [userCollection]); 19 | 20 | return { lookupUserCollection }; 21 | }; 22 | -------------------------------------------------------------------------------- /06 Exercise 1/03-implemented/Readme.md: -------------------------------------------------------------------------------- 1 | # Challenge 1 - 01-implemented 2 | 3 | ## Steps 4 | 5 | - Copy `00-start` project folder. 6 | 7 | - Run `npm install`. 8 | 9 | - First, we will create the `lookup-context` to store the data related with the lookups: 10 | 11 | _./src/lookup.context.js_ 12 | 13 | ```javascript 14 | import React from 'react'; 15 | import * as api from './api'; 16 | 17 | export const LookupContext = React.createContext({ 18 | fetchUserLookups: () => 19 | console.warn('You need to use LookupProvider in your app'), 20 | }); 21 | 22 | export const LookupProvider = props => { 23 | const [userCollection, setUserCollection] = React.useState([]); 24 | 25 | const fetchUserLookups = React.useCallback(() => { 26 | return api.fetchUserLookups().then(users => { 27 | setUserCollection(users); 28 | return users; 29 | }); 30 | }, []); 31 | 32 | return ( 33 | 38 | {props.children} 39 | 40 | ); 41 | }; 42 | ``` 43 | 44 | - Now, we need to use `LookupProvider` in our app: 45 | 46 | _./src/index.js_ 47 | 48 | ```diff 49 | import React from 'react'; 50 | import ReactDOM from 'react-dom'; 51 | import { Router } from './router'; 52 | + import { LookupProvider } from './lookup.context'; 53 | import './styles.css'; 54 | 55 | const App = () => { 56 | return ( 57 |
58 | + 59 | 60 | + 61 |
62 | ); 63 | }; 64 | ... 65 | 66 | ``` 67 | 68 | - Use it on `page-a`: 69 | 70 | _./src/page-a.js_ 71 | 72 | ```diff 73 | import React from 'react'; 74 | import { Link } from 'react-router-dom'; 75 | - import { fetchUserLookups } from './api'; 76 | + import { LookupContext } from './lookup.context'; 77 | 78 | export const PageA = () => { 79 | const [userCollection, setUserCollection] = React.useState([]); 80 | + const { fetchUserLookups } = React.useContext(LookupContext); 81 | ... 82 | 83 | ``` 84 | 85 | - Use it on `page-b`: 86 | 87 | _./src/page-b.js_ 88 | 89 | ```diff 90 | import React from 'react'; 91 | import { Link } from 'react-router-dom'; 92 | - import { fetchUserLookups } from './api'; 93 | + import { LookupContext } from './lookup.context'; 94 | 95 | export const PageB = () => { 96 | const [userCollection, setUserCollection] = React.useState([]); 97 | + const { fetchUserLookups } = React.useContext(LookupContext); 98 | ... 99 | ``` 100 | 101 | - Let's make it lazy: 102 | 103 | _./src/lookup.context.js_ 104 | 105 | ```diff 106 | ... 107 | export const LookupProvider = props => { 108 | const [userCollection, setUserCollection] = React.useState([]); 109 | 110 | const fetchUserLookups = React.useCallback(() => { 111 | - return api.fetchUserLookups().then(users => { 112 | - setUserCollection(users); 113 | - return users; 114 | - }); 115 | + return userCollection.length > 0 116 | + ? Promise.resolve(userCollection) 117 | + : api.fetchUserLookups().then(users => { 118 | + setUserCollection(users); 119 | + return users; 120 | + }); 121 | - }, []); 122 | + }, [userCollection]); 123 | ... 124 | 125 | ``` 126 | 127 | # About Basefactor + Lemoncode 128 | 129 | We are an innovating team of Front End Developers, passionate about turning your ideas into robust products. 130 | 131 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 132 | 133 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 134 | 135 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 136 | -------------------------------------------------------------------------------- /06 Exercise 1/03-implemented/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "16.8.6", 9 | "react-dom": "16.8.6", 10 | "react-router-dom": "^5.0.1", 11 | "react-scripts": "2.1.8" 12 | }, 13 | "devDependencies": { 14 | "typescript": "3.3.3" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | }, 22 | "browserslist": [ 23 | ">0.2%", 24 | "not dead", 25 | "not ie <= 11", 26 | "not op_mini all" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /06 Exercise 1/03-implemented/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /06 Exercise 1/03-implemented/src/api.js: -------------------------------------------------------------------------------- 1 | export const fetchUserLookups = () => 2 | fetch(`https://jsonplaceholder.typicode.com/users`) 3 | .then(response => response.json()) 4 | .then(data => { 5 | console.log('User lookups fetched from server'); 6 | console.log({ data }); 7 | return data; 8 | }); 9 | -------------------------------------------------------------------------------- /06 Exercise 1/03-implemented/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Router } from "./router"; 4 | import { UserProvider } from "./user.context"; 5 | import "./styles.css"; 6 | 7 | const App = () => { 8 | return ( 9 |
10 | 11 | 12 | 13 |
14 | ); 15 | }; 16 | 17 | const rootElement = document.getElementById("root"); 18 | ReactDOM.render(, rootElement); 19 | -------------------------------------------------------------------------------- /06 Exercise 1/03-implemented/src/page-a.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { useUserCollection } from "./user.hooks"; 4 | 5 | export const PageA = () => { 6 | const { lookupUserCollection, userCollection } = useUserCollection(); 7 | 8 | return ( 9 | <> 10 |

Page A

11 |
    12 | {userCollection.map((user, index) => ( 13 |
  • {user.name}
  • 14 | ))} 15 |
16 | 17 | 18 | Navigate Page B → 19 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /06 Exercise 1/03-implemented/src/page-b.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { useUserCollection } from "./user.hooks"; 4 | 5 | export const PageB = () => { 6 | const { lookupUserCollection, userCollection } = useUserCollection(); 7 | 8 | return ( 9 | <> 10 |

Page B

11 |
    12 | {userCollection.map((user, index) => ( 13 |
  • {user.name}
  • 14 | ))} 15 |
16 | 17 | 18 | ← Navigate Page A 19 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /06 Exercise 1/03-implemented/src/router.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { HashRouter, Switch, Route } from "react-router-dom"; 3 | import { PageA } from "./page-a"; 4 | import { PageB } from "./page-b"; 5 | 6 | export const Router = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /06 Exercise 1/03-implemented/src/styles.css: -------------------------------------------------------------------------------- 1 | .app { 2 | font-family: sans-serif; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | 8 | .buttons-container { 9 | width: 50%; 10 | display: flex; 11 | justify-content: center; 12 | } 13 | 14 | .link-button { 15 | margin-top: 5rem; 16 | text-decoration: none; 17 | } 18 | -------------------------------------------------------------------------------- /06 Exercise 1/03-implemented/src/user.context.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export const UserContext = React.createContext(); 4 | export const DispatchUserContext = React.createContext(); 5 | 6 | export const UserProvider = ({ children }) => { 7 | const [userCollection, setUserCollection] = React.useState([]); 8 | return ( 9 | 10 | {children} 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /06 Exercise 1/03-implemented/src/user.hooks.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as api from "./api"; 3 | import { UserContext, DispatchUserContext } from "./user.context"; 4 | 5 | export const useUserCollection = () => { 6 | const userCollection = React.useContext(UserContext); 7 | const setUserCollection = React.useContext(DispatchUserContext); 8 | 9 | const lookupUserCollection = React.useCallback(() => { 10 | if (userCollection && userCollection.length === 0) { 11 | api.fetchUserLookups().then(userCollection => { 12 | setUserCollection(userCollection); 13 | }); 14 | } else { 15 | console.log("User collection consumed from context"); 16 | } 17 | }, [userCollection]); 18 | 19 | return { lookupUserCollection, userCollection }; 20 | }; 21 | -------------------------------------------------------------------------------- /06 Exercise 1/Readme.md: -------------------------------------------------------------------------------- 1 | # Challenge 1 2 | 3 | ## Starting material: 4 | 5 | In our application we need to load tons of collections from servers (usually lookups), and the server rest-api comes from a third party. For the sake of performance: 6 | 7 | - We don't want to load all the enums at application startup. 8 | - We dont' want to load the enum on every request. 9 | - We want to be lazy, load it one time and keep it globally in context 10 | 11 | ## Startup 12 | 13 | - Copy `00-start` project folder. 14 | - Run `npm install`. 15 | - Run `npm start` and implement the challenge. 16 | 17 | ## Tips 18 | 19 | A. Make it global: 20 | 21 | - Move the lookup data to context. 22 | - Create a hook that has Access to this context and exposes a loadMethod. 23 | - Use the hook on PageA and PageB. 24 | 25 | B. Make it lazy: 26 | 27 | - Just add a state on the custom hook you have created to check if the lookup has been already loaded, and depending on the result just return the promise resolved with the data if not fire the async request. 28 | 29 | C. Extra: 30 | 31 | - what if we can launch in parallel several request at the same for the same lookup? Could we just store the first promise launched and return that? 32 | 33 | # About Basefactor + Lemoncode 34 | 35 | We are an innovating team of Front End Developers, passionate about turning your ideas into robust products. 36 | 37 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 38 | 39 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 40 | 41 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 42 | -------------------------------------------------------------------------------- /07 Exercise 2/00-start/Readme.md: -------------------------------------------------------------------------------- 1 | # Challenge 2 - 00-start 2 | 3 | ## Steps 4 | 5 | - Copy `00-start` project folder. 6 | - Run `npm install`. 7 | - Run `npm start` and implement the challenge. 8 | 9 | ## Tips 10 | 11 | A. You can try to build something like that by yourself (nice exercise). 12 | 13 | B. Read from blog post and check if the solutions was similar like your proposal: [https://dev.to/gabe_ragland/debouncing-with-react-hooks-jci](https://dev.to/gabe_ragland/debouncing-with-react-hooks-jci) 14 | 15 | C. Use an already made hook: [https://github.com/xnimorz/use-debounce](https://github.com/xnimorz/use-debounce) 16 | 17 | # About Basefactor + Lemoncode 18 | 19 | We are an innovating team of Front End Developers, passionate about turning your ideas into robust products. 20 | 21 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 22 | 23 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 24 | 25 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 26 | -------------------------------------------------------------------------------- /07 Exercise 2/00-start/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.9.0", 7 | "react-dom": "^16.9.0", 8 | "react-scripts": "3.1.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /07 Exercise 2/00-start/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/07 Exercise 2/00-start/public/favicon.ico -------------------------------------------------------------------------------- /07 Exercise 2/00-start/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /07 Exercise 2/00-start/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/07 Exercise 2/00-start/public/logo192.png -------------------------------------------------------------------------------- /07 Exercise 2/00-start/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/07 Exercise 2/00-start/public/logo512.png -------------------------------------------------------------------------------- /07 Exercise 2/00-start/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /07 Exercise 2/00-start/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /07 Exercise 2/00-start/src/demo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const useUserCollection = () => { 4 | const [filter, setFilter] = React.useState(""); 5 | const [userCollection, setUserCollection] = React.useState([]); 6 | 7 | // Load full list when the component gets mounted and filter gets updated 8 | const loadUsers = () => { 9 | fetch(`https://jsonplaceholder.typicode.com/users?name_like=${filter}`) 10 | .then(response => response.json()) 11 | .then(json => setUserCollection(json)); 12 | }; 13 | 14 | return { userCollection, loadUsers, filter, setFilter }; 15 | }; 16 | 17 | export const MyComponent = () => { 18 | const { userCollection, loadUsers, filter, setFilter } = useUserCollection(); 19 | 20 | React.useEffect(() => { 21 | loadUsers(); 22 | }, [filter]); 23 | 24 | return ( 25 |
26 | setFilter(e.target.value)} /> 27 |
    28 | {userCollection.map((user, index) => ( 29 |
  • {user.name}
  • 30 | ))} 31 |
32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /07 Exercise 2/00-start/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { MyComponent } from "./demo"; 4 | import "./styles.css"; 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | const rootElement = document.getElementById("root"); 15 | ReactDOM.render(, rootElement); 16 | -------------------------------------------------------------------------------- /07 Exercise 2/00-start/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /07 Exercise 2/01-implemented/Readme.md: -------------------------------------------------------------------------------- 1 | # Challenge 2 - 01-implemented 2 | 3 | ## Steps 4 | 5 | - Copy `00-start` project folder. 6 | 7 | - Run `npm install`. 8 | 9 | - First, we will create the `useDebounce` hook. The goal is getting a debounced `filter` value: 10 | 11 | _./src/use-debounce.js_ 12 | 13 | ```javascript 14 | import React from 'react'; 15 | 16 | export const useDebounce = (value, timeout) => { 17 | const [debouncedValue, setDebouncedValue] = React.useState(value); 18 | 19 | React.useEffect(() => { 20 | const timerId = setTimeout(() => { 21 | setDebouncedValue(value); 22 | }, timeout); 23 | return () => { 24 | clearTimeout(timerId); 25 | }; 26 | }, [value, timeout]); 27 | 28 | return debouncedValue; 29 | }; 30 | 31 | ``` 32 | 33 | > NOTE: It's important the `clearTimeout` due to we want to remove old filter values, we only want the last one. 34 | 35 | - Update the component: 36 | 37 | _./src/demo.js_ 38 | 39 | ```diff 40 | import React from "react"; 41 | 42 | const useUserCollection = () => { 43 | const [filter, setFilter] = React.useState(""); 44 | const [userCollection, setUserCollection] = React.useState([]); 45 | 46 | // Load full list when the component gets mounted and filter gets updated 47 | - const loadUsers = () => { 48 | + const loadUsers = userName => { 49 | - fetch(`https://jsonplaceholder.typicode.com/users?name_like=${filter}`) 50 | + fetch(`https://jsonplaceholder.typicode.com/users?name_like=${userName}`) 51 | .then(response => response.json()) 52 | .then(json => setUserCollection(json)); 53 | }; 54 | 55 | return { userCollection, loadUsers, filter, setFilter }; 56 | }; 57 | 58 | export const MyComponent = () => { 59 | const { userCollection, loadUsers, filter, setFilter } = useUserCollection(); 60 | + const debouncedFilter = useDebounce(filter, 500); 61 | 62 | - React.useEffect(() => { 63 | - loadUsers(); 64 | - }, [filter]); 65 | + React.useEffect(() => { 66 | + loadUsers(debouncedFilter); 67 | + }, [debouncedFilter]); 68 | ... 69 | ``` 70 | 71 | # About Basefactor + Lemoncode 72 | 73 | We are an innovating team of Front End Developers, passionate about turning your ideas into robust products. 74 | 75 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 76 | 77 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 78 | 79 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 80 | -------------------------------------------------------------------------------- /07 Exercise 2/01-implemented/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.9.0", 7 | "react-dom": "^16.9.0", 8 | "react-scripts": "3.1.1" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /07 Exercise 2/01-implemented/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/07 Exercise 2/01-implemented/public/favicon.ico -------------------------------------------------------------------------------- /07 Exercise 2/01-implemented/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /07 Exercise 2/01-implemented/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/07 Exercise 2/01-implemented/public/logo192.png -------------------------------------------------------------------------------- /07 Exercise 2/01-implemented/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/07 Exercise 2/01-implemented/public/logo512.png -------------------------------------------------------------------------------- /07 Exercise 2/01-implemented/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /07 Exercise 2/01-implemented/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /07 Exercise 2/01-implemented/src/demo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDebounce } from './use-debounce'; 3 | 4 | const useUserCollection = () => { 5 | const [filter, setFilter] = React.useState(''); 6 | const [userCollection, setUserCollection] = React.useState([]); 7 | 8 | // Load full list when the component gets mounted and filter gets updated 9 | const loadUsers = userName => { 10 | fetch(`https://jsonplaceholder.typicode.com/users?name_like=${userName}`) 11 | .then(response => response.json()) 12 | .then(json => setUserCollection(json)); 13 | }; 14 | 15 | return { userCollection, loadUsers, filter, setFilter }; 16 | }; 17 | 18 | export const MyComponent = () => { 19 | const { userCollection, loadUsers, filter, setFilter } = useUserCollection(); 20 | const debouncedFilter = useDebounce(filter, 500); 21 | 22 | React.useEffect(() => { 23 | loadUsers(debouncedFilter); 24 | }, [debouncedFilter]); 25 | 26 | return ( 27 |
28 | setFilter(e.target.value)} /> 29 |
    30 | {userCollection.map((user, index) => ( 31 |
  • {user.name}
  • 32 | ))} 33 |
34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /07 Exercise 2/01-implemented/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { MyComponent } from "./demo"; 4 | import "./styles.css"; 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | 14 | const rootElement = document.getElementById("root"); 15 | ReactDOM.render(, rootElement); 16 | -------------------------------------------------------------------------------- /07 Exercise 2/01-implemented/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /07 Exercise 2/01-implemented/src/use-debounce.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const useDebounce = (value, timeout) => { 4 | const [debouncedValue, setDebouncedValue] = React.useState(value); 5 | 6 | React.useEffect(() => { 7 | const timerId = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, timeout); 10 | return () => { 11 | clearTimeout(timerId); 12 | }; 13 | }, [value, timeout]); 14 | 15 | return debouncedValue; 16 | }; 17 | -------------------------------------------------------------------------------- /07 Exercise 2/Readme.md: -------------------------------------------------------------------------------- 1 | # Challenge 2 2 | 3 | ## Starting material: 4 | 5 | In a previous example, we just saw how to filter a list of users server from a rest-api listening to an input onChange event. 6 | 7 | This is not optimal, shouldn't it be cool to wait a bit til the user is not typing for some milisseconds in order to fire the server request? 8 | 9 | ## Startup 10 | 11 | - Copy `00-start` project folder. 12 | - Run `npm install`. 13 | - Run `npm start` and implement the challenge. 14 | 15 | ## Tips 16 | 17 | A. You can try to build something like that by yourself (nice exercise). 18 | 19 | B. Read from blog post and check if the solutions was similar like your proposal: [https://dev.to/gabe_ragland/debouncing-with-react-hooks-jci](https://dev.to/gabe_ragland/debouncing-with-react-hooks-jci) 20 | 21 | C. Use an already made hook: [https://github.com/xnimorz/use-debounce](https://github.com/xnimorz/use-debounce) 22 | 23 | # About Basefactor + Lemoncode 24 | 25 | We are an innovating team of Front End Developers, passionate about turning your ideas into robust products. 26 | 27 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 28 | 29 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 30 | 31 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 32 | -------------------------------------------------------------------------------- /08 Exercise 3/00-start/Readme.md: -------------------------------------------------------------------------------- 1 | # Challenge 3 - 00-start 2 | 3 | ## Steps 4 | 5 | - Copy `00-start` project folder. 6 | - Run `npm install`. 7 | - Run `npm start` and implement the challenge. 8 | 9 | ## Tips 10 | 11 | A. Play with component feeding `showMenu` props equals true / false. 12 | 13 | B. Create `useMediaQuery` hook. You can use [matchMedia](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) Javascript method to check if the document matches the media query. 14 | 15 | # About Basefactor + Lemoncode 16 | 17 | We are an innovating team of Front End Developers, passionate about turning your ideas into robust products. 18 | 19 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 20 | 21 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 22 | 23 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 24 | -------------------------------------------------------------------------------- /08 Exercise 3/00-start/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "@material-ui/core": "^4.4.3", 9 | "@material-ui/icons": "^4.4.3", 10 | "react": "16.8.6", 11 | "react-dom": "16.8.6", 12 | "react-scripts": "2.1.8" 13 | }, 14 | "devDependencies": { 15 | "typescript": "3.3.3" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test --env=jsdom", 21 | "eject": "react-scripts eject" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /08 Exercise 3/00-start/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /08 Exercise 3/00-start/src/components/header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AppBar from '@material-ui/core/AppBar'; 3 | import Toolbar from '@material-ui/core/Toolbar'; 4 | import Button from '@material-ui/core/Button'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import { useStyles } from './header.styles'; 7 | import { Menu } from './menu'; 8 | 9 | export const Header = props => { 10 | const { showMenu } = props; 11 | const classes = useStyles(); 12 | 13 | return ( 14 | 15 | 16 | 17 | My App 18 | 19 | {showMenu ? ( 20 | 21 | ) : ( 22 | <> 23 | 24 | 25 | 26 | 27 | )} 28 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /08 Exercise 3/00-start/src/components/header.styles.js: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core'; 2 | 3 | export const useStyles = makeStyles({ 4 | title: { 5 | flexGrow: 1, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /08 Exercise 3/00-start/src/components/index.js: -------------------------------------------------------------------------------- 1 | export * from './header'; 2 | -------------------------------------------------------------------------------- /08 Exercise 3/00-start/src/components/menu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MaterialMenu from '@material-ui/core/Menu'; 3 | import MenuItem from '@material-ui/core/MenuItem'; 4 | import IconButton from '@material-ui/core/IconButton'; 5 | import MenuIcon from '@material-ui/icons/Menu'; 6 | 7 | export const Menu = props => { 8 | const [isOpen, setIsOpen] = React.useState(false); 9 | 10 | return ( 11 | <> 12 | setIsOpen(true)}> 13 | 14 | 15 | setIsOpen(false)} 18 | anchorOrigin={{ vertical: 'top', horizontal: 'right' }} 19 | > 20 | Home 21 | Contact 22 | About 23 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /08 Exercise 3/00-start/src/demo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Header } from './components'; 3 | 4 | export const MyComponent = () => { 5 | return
; 6 | }; 7 | -------------------------------------------------------------------------------- /08 Exercise 3/00-start/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { MyComponent } from './demo'; 4 | 5 | function App() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | 13 | const rootElement = document.getElementById('root'); 14 | ReactDOM.render(, rootElement); 15 | -------------------------------------------------------------------------------- /08 Exercise 3/01-implemented/Readme.md: -------------------------------------------------------------------------------- 1 | # Challenge 3 - 01-implemented 2 | 3 | ## Steps 4 | 5 | - Copy `00-start` project folder. 6 | 7 | - Run `npm install`. 8 | 9 | - Create `useMediaQuery` hook: 10 | 11 | _./src/use-media-query.js_ 12 | 13 | ```javascript 14 | import React from 'react'; 15 | 16 | export const useMediaQuery = mediaQuery => { 17 | const [matches, setMatches] = React.useState(window.matchMedia(mediaQuery)); 18 | 19 | React.useEffect(() => { 20 | const listener = event => setMatches(event.matches); 21 | window.matchMedia(mediaQuery).addListener(listener); 22 | 23 | return () => { 24 | window.matchMedia(mediaQuery).removeListener(listener); 25 | }; 26 | }, [mediaQuery]); 27 | 28 | return matches; 29 | }; 30 | 31 | ``` 32 | 33 | - Use it: 34 | 35 | _./src/demo.js_ 36 | 37 | ```diff 38 | import React from 'react'; 39 | import { Header } from './components'; 40 | + import { useMediaQuery } from './use-media-query'; 41 | 42 | export const MyComponent = () => { 43 | + const isTablet = useMediaQuery('(max-width: 1024px)'); 44 | - return
; 45 | + return
; 46 | }; 47 | 48 | ``` 49 | 50 | # About Basefactor + Lemoncode 51 | 52 | We are an innovating team of Front End Developers, passionate about turning your ideas into robust products. 53 | 54 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 55 | 56 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 57 | 58 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 59 | -------------------------------------------------------------------------------- /08 Exercise 3/01-implemented/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "@material-ui/core": "^4.4.3", 9 | "@material-ui/icons": "^4.4.3", 10 | "react": "16.8.6", 11 | "react-dom": "16.8.6", 12 | "react-scripts": "2.1.8" 13 | }, 14 | "devDependencies": { 15 | "typescript": "3.3.3" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test --env=jsdom", 21 | "eject": "react-scripts eject" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /08 Exercise 3/01-implemented/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /08 Exercise 3/01-implemented/src/components/header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AppBar from '@material-ui/core/AppBar'; 3 | import Toolbar from '@material-ui/core/Toolbar'; 4 | import Button from '@material-ui/core/Button'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import { useStyles } from './header.styles'; 7 | import { Menu } from './menu'; 8 | 9 | export const Header = props => { 10 | const { showMenu } = props; 11 | const classes = useStyles(); 12 | 13 | return ( 14 | 15 | 16 | 17 | My App 18 | 19 | {showMenu ? ( 20 | 21 | ) : ( 22 | <> 23 | 24 | 25 | 26 | 27 | )} 28 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /08 Exercise 3/01-implemented/src/components/header.styles.js: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core'; 2 | 3 | export const useStyles = makeStyles({ 4 | title: { 5 | flexGrow: 1, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /08 Exercise 3/01-implemented/src/components/index.js: -------------------------------------------------------------------------------- 1 | export * from './header'; 2 | -------------------------------------------------------------------------------- /08 Exercise 3/01-implemented/src/components/menu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MaterialMenu from '@material-ui/core/Menu'; 3 | import MenuItem from '@material-ui/core/MenuItem'; 4 | import IconButton from '@material-ui/core/IconButton'; 5 | import MenuIcon from '@material-ui/icons/Menu'; 6 | 7 | export const Menu = props => { 8 | const [isOpen, setIsOpen] = React.useState(false); 9 | 10 | return ( 11 | <> 12 | setIsOpen(true)}> 13 | 14 | 15 | setIsOpen(false)} 18 | anchorOrigin={{ vertical: 'top', horizontal: 'right' }} 19 | > 20 | Home 21 | Contact 22 | About 23 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /08 Exercise 3/01-implemented/src/demo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Header } from './components'; 3 | import { useMediaQuery } from './use-media-query'; 4 | 5 | export const MyComponent = () => { 6 | const isTablet = useMediaQuery('(max-width: 1024px)'); 7 | return
; 8 | }; 9 | -------------------------------------------------------------------------------- /08 Exercise 3/01-implemented/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { MyComponent } from './demo'; 4 | 5 | function App() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | 13 | const rootElement = document.getElementById('root'); 14 | ReactDOM.render(, rootElement); 15 | -------------------------------------------------------------------------------- /08 Exercise 3/01-implemented/src/use-media-query.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const useMediaQuery = mediaQuery => { 4 | const [matches, setMatches] = React.useState(window.matchMedia(mediaQuery)); 5 | 6 | React.useEffect(() => { 7 | const listener = event => setMatches(event.matches); 8 | window.matchMedia(mediaQuery).addListener(listener); 9 | 10 | return () => { 11 | window.matchMedia(mediaQuery).removeListener(listener); 12 | }; 13 | }, [mediaQuery]); 14 | 15 | return matches; 16 | }; 17 | -------------------------------------------------------------------------------- /08 Exercise 3/Readme.md: -------------------------------------------------------------------------------- 1 | # Challenge 3 2 | 3 | ## Starting material: 4 | 5 | CSS media queries are great when we are creating a responsive web design but sometimes, we need to apply same media queries in the Javascript side like "isDesktop ? use Header component else use HamburgerMenu" 6 | 7 | ## Startup 8 | 9 | - Copy `00-start` project folder. 10 | - Run `npm install`. 11 | - Run `npm start` and implement the challenge. 12 | 13 | ## Tips 14 | 15 | A. Play with component feeding `showMenu` props equals true / false. 16 | 17 | B. Create `useMediaQuery` hook. You can use [matchMedia](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) Javascript method to check if the document matches the media query. 18 | 19 | # About Basefactor + Lemoncode 20 | 21 | We are an innovating team of Front End Developers, passionate about turning your ideas into robust products. 22 | 23 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 24 | 25 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 26 | 27 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Lemoncode 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-alicante-hooks-workshop 2 | 3 | [React Alicante 2019 Workshop](https://reactalicante.es/). 4 | 5 | # About Basefactor + Lemoncode 6 | 7 | We are an innovating team of Front End Developers, passionate about turning your ideas into robust products. 8 | 9 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 10 | 11 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 12 | 13 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 14 | 15 | -------------------------------------------------------------------------------- /[React Alicante 19] [Workshop] Hooked on Hooks.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-alicante-hooks-workshop/3770c81812b9e19bdd46f65de969bba686f6f1f4/[React Alicante 19] [Workshop] Hooked on Hooks.pdf --------------------------------------------------------------------------------