├── .firebaserc ├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── README.md ├── SECURITY.md ├── firebase.json ├── package.json ├── public ├── 404.html ├── discount-code.png ├── dog.jpg ├── favicon.ico ├── index.html ├── logo.png └── manifest.json ├── src ├── components │ ├── App.jsx │ ├── FloatingMenuBtn.jsx │ ├── RGALogo.jsx │ ├── Root.jsx │ ├── functional-programming │ │ ├── closure │ │ │ ├── Page.jsx │ │ │ └── exercise.js │ │ ├── composition │ │ │ ├── Page.jsx │ │ │ ├── bonus │ │ │ │ ├── index.js │ │ │ │ └── validators.js │ │ │ └── exercise │ │ │ │ └── index.js │ │ └── memoization │ │ │ ├── Page.jsx │ │ │ └── exercise.js │ └── patterns │ │ ├── CompoundComponents │ │ ├── Page.jsx │ │ ├── example │ │ │ ├── RadioGroup.jsx │ │ │ └── RadioOption.jsx │ │ └── exercise │ │ │ ├── Menu.jsx │ │ │ └── MenuItem.jsx │ │ ├── Context │ │ ├── Page.jsx │ │ ├── example │ │ │ ├── Modal.jsx │ │ │ └── index.jsx │ │ └── exercise │ │ │ ├── GraphQLProvider.jsx │ │ │ ├── index.jsx │ │ │ └── utils.js │ │ ├── CustomHooks │ │ ├── Page.jsx │ │ ├── example │ │ │ ├── UseCallbackOrMemo.js │ │ │ └── index.jsx │ │ └── exercise │ │ │ ├── index.jsx │ │ │ └── useWidth.js │ │ ├── HigherOrderComponents │ │ ├── Page.jsx │ │ ├── example │ │ │ └── withMousePosition.jsx │ │ ├── exercise_1 │ │ │ ├── List.jsx │ │ │ ├── index.js │ │ │ └── withData.jsx │ │ ├── exercise_2 │ │ │ ├── appWithWidth.js │ │ │ └── withWidth.jsx │ │ └── exercise_bonus │ │ │ ├── connect.js │ │ │ └── utils │ │ │ └── redux-counter.js │ │ ├── HookReducer │ │ ├── Page.jsx │ │ ├── example │ │ │ └── index.jsx │ │ └── exercise │ │ │ └── index.jsx │ │ ├── Hooks │ │ ├── Page.jsx │ │ ├── examples │ │ │ ├── UseEffect.jsx │ │ │ ├── UseState.jsx │ │ │ └── index.jsx │ │ └── exercise │ │ │ ├── RandomImage.jsx │ │ │ └── index.jsx │ │ ├── RenderProps │ │ ├── Page.jsx │ │ ├── example │ │ │ ├── Input.jsx │ │ │ └── index.jsx │ │ ├── exercise │ │ │ ├── Field.jsx │ │ │ ├── Measure.jsx │ │ │ └── index.jsx │ │ └── exercise_bonus │ │ │ ├── BadImplementation │ │ │ └── index.jsx │ │ │ ├── HoCs │ │ │ ├── GitHubIssueSearchInfiniteScroller.jsx │ │ │ └── withGitHubIssueSearch.jsx │ │ │ ├── RenderCallback │ │ │ ├── GitHubIssueSearcher.jsx │ │ │ └── index.jsx │ │ │ ├── ScrollNotifier.jsx │ │ │ └── utils │ │ │ ├── config.js │ │ │ └── searchGitHubIssues.js │ │ ├── StateReducer │ │ ├── Page.jsx │ │ ├── example │ │ │ ├── Field.jsx │ │ │ └── index.jsx │ │ ├── exercise_1 │ │ │ ├── Field.jsx │ │ │ └── index.jsx │ │ ├── exercise_2 │ │ │ ├── Field.jsx │ │ │ └── index.jsx │ │ ├── exercise_3 │ │ │ ├── Dropdown.jsx │ │ │ └── index.js │ │ └── exercise_bonus │ │ │ ├── Dropdown.jsx │ │ │ └── index.js │ │ ├── Theming │ │ ├── Page.jsx │ │ ├── example │ │ │ ├── index.jsx │ │ │ └── theme.js │ │ └── exercise │ │ │ ├── index.jsx │ │ │ └── theme.js │ │ └── Variants │ │ ├── Page.jsx │ │ ├── example │ │ ├── index.js │ │ └── theme.js │ │ └── exercise │ │ ├── index.js │ │ └── theme.js ├── index.js └── main.css └── yarn.lock /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "advanced-react-patterns" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '41 12 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | .firebase 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Advanced Cocoa Patterns 2 | 3 | ## Teaching method 4 | 5 | 1. Collaborative learning environment & pair programming. 6 | - Rooms with small groups 7 | - Work together, discuss, help each other. 8 | 2. We try to foster critical thinking. 9 | - ⬆️ Discovery ⬇️ Instruction 10 | 3. We don’t explain everything you need to know before the exercise: 11 | - Learn by doing (and teaching ;) 12 | - The exercise is meant to help you build a mental model 13 | 14 | More on the [teaching method](https://reactgraphql.academy/blog/react-graphql-academy-teaching-method/) 15 | 16 | ## Target audience: 17 | 18 | reactDevs >= 6 months && reactDevs <= 2 years 19 | 20 | ## Advanced React patterns 21 | 22 | This workshop doesn't define "advanced" as doing complicated things that you haven't done or seen before, but: 23 | 24 | 1. Understanding how things work, not just "doing". 25 | 2. Using those things properly and when necessary. 26 | 3. Making complicated things simple, not the other way around. 27 | 28 | ## How to install 29 | 30 | - `git clone https://github.com/reactgraphqlacademy/advanced-react-patterns.git` 31 | - `cd advanced-react-patterns` 32 | - `npm i` 33 | - `npm start` 34 | 35 | # Links 36 | 37 | - [Lecture: React Patterns](https://reactgraphql.academy/react/react-is-all-about-composition-react-hooks-render-props-hocs/) 38 | - [https://github.com/reactjs/react-basic#composition](https://github.com/reactjs/react-basic#composition) 39 | - [https://github.com/reactjs/react-basic#memoization](https://github.com/reactjs/react-basic#memoization) 40 | 41 | ## License 42 | 43 | This material is available for private, non-commercial use under the [GPL version 3](http://www.gnu.org/licenses/gpl-3.0-standalone.html). 44 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 5 | "rewrites": [ 6 | { 7 | "source": "**", 8 | "destination": "/index.html" 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "advanced-react-patterns-new", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "final-form": "^4.11.0", 7 | "jsonp": "^0.2.1", 8 | "polished": "^2.3.0", 9 | "prop-types": "^15.6.2", 10 | "react": "^16.8.0", 11 | "react-bootstrap": "^0.32.1", 12 | "react-burger-menu": "^2.5.2", 13 | "react-dom": "^16.8.0", 14 | "react-final-form": "^4.0.2", 15 | "react-router-dom": "^4.3.1", 16 | "react-scripts": "1.1.4", 17 | "redux": "^4.0.1", 18 | "styled-components": "^4.1.2", 19 | "styled-system": "^3.1.11" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test --env=jsdom", 25 | "eject": "react-scripts eject" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /public/discount-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bounceru/Cocoa-pattern/887b5a1b3aab3bd1104c72ea311aac87687ca6bb/public/discount-code.png -------------------------------------------------------------------------------- /public/dog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bounceru/Cocoa-pattern/887b5a1b3aab3bd1104c72ea311aac87687ca6bb/public/dog.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bounceru/Cocoa-pattern/887b5a1b3aab3bd1104c72ea311aac87687ca6bb/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 24 | Advanced React Patterns 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bounceru/Cocoa-pattern/887b5a1b3aab3bd1104c72ea311aac87687ca6bb/public/logo.png -------------------------------------------------------------------------------- /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 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Redirect } from "react-router-dom"; 3 | 4 | import Root from "./Root"; 5 | import withWidth, { 6 | LARGE, 7 | } from "./patterns/HigherOrderComponents/exercise_2/withWidth"; 8 | import HigherOrderComponentsPage from "./patterns/HigherOrderComponents/Page"; 9 | import RenderPropsPage from "./patterns/RenderProps/Page"; 10 | import CompoundComponentsPage from "./patterns/CompoundComponents/Page"; 11 | import ContextPage from "./patterns/Context/Page"; 12 | import MenuItem from "./patterns/CompoundComponents/exercise/MenuItem"; 13 | import Menu from "./patterns/CompoundComponents/exercise/Menu"; 14 | import CompositionPage from "./functional-programming/composition/Page"; 15 | import ClosurePage from "./functional-programming/closure/Page"; 16 | import MemoizationPage from "./functional-programming/memoization/Page"; 17 | import HookReducerPage from "./patterns/HookReducer/Page"; 18 | import CustomHooksPage from "./patterns/CustomHooks/Page"; 19 | import ThemingPage from "./patterns/Theming/Page"; 20 | import VariantsPage from "./patterns/Variants/Page"; 21 | import HooksPage from "./patterns/Hooks/Page"; 22 | import RGALogo from "./RGALogo"; 23 | 24 | class App extends React.Component { 25 | constructor() { 26 | super(); 27 | this.state = { 28 | menu: { open: false }, 29 | }; 30 | } 31 | 32 | toggleMenu = () => { 33 | if (this.props.width !== LARGE) { 34 | this.setState({ menu: { open: !this.state.menu.open } }); 35 | } 36 | }; 37 | 38 | render() { 39 | let isMenuOpen = this.state.menu.open; 40 | const styles = { 41 | paddingRight: "20px", 42 | paddingLeft: "20px", 43 | }; 44 | 45 | if (this.props.width === LARGE) { 46 | isMenuOpen = true; 47 | styles.paddingLeft = 320; 48 | } 49 | 50 | return ( 51 | 52 |
53 | 54 |

55 | 60 | 61 | 62 |

63 |
64 | 65 | 1. Closure 66 | 67 | 68 | 2. Memoization 69 | 70 | 71 | 3. Function composition 72 | 73 |
74 | 78 | 4. Higher-Order Components 79 | 80 | 81 | 5. Render Props 82 | 83 | 84 | 6. Hooks 85 | 86 | 87 | 7. Custom Hooks 88 | 89 | 90 | 8. Hook Reducer 91 | 92 | 93 | 9. Context 94 | 95 |
96 | 118 |
119 |
120 | } /> 121 | 122 | 123 | 124 | 128 | 129 | 130 | 131 | 132 | 136 | 137 | 138 | 139 |
140 |
141 |
142 | ); 143 | } 144 | } 145 | 146 | export default withWidth(App); 147 | -------------------------------------------------------------------------------- /src/components/FloatingMenuBtn.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const FloatingMenuBtn = ({ toggleMenu }) => ( 4 | 5 | 6 | 7 | 8 | ) 9 | 10 | export default FloatingMenuBtn 11 | -------------------------------------------------------------------------------- /src/components/RGALogo.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Icon({ width = 260 }) { 4 | return ( 5 | 6 | 7 | 11 | 16 | 21 | 22 | 23 | ); 24 | } 25 | 26 | export default Icon; 27 | -------------------------------------------------------------------------------- /src/components/Root.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter as Router } from "react-router-dom"; 3 | import Modal from "./patterns/Context/example/Modal"; 4 | import { GraphQLProvider } from "./patterns/Context/exercise/GraphQLProvider"; 5 | import { createClient } from "./patterns/Context/exercise/utils"; 6 | 7 | const client = createClient({ url: "https://rickandmortyapi.com/graphql/" }); 8 | 9 | const Root = ({ children }) => ( 10 | 11 | 12 | {children} 13 | 14 | 15 | ); 16 | 17 | export default Root; 18 | -------------------------------------------------------------------------------- /src/components/functional-programming/closure/Page.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/accessible-emoji */ 2 | import React from "react"; 3 | import "./exercise"; 4 | 5 | const Page = () => ( 6 | 7 |

Closure

8 |
9 | Closure is when a function is able to remember and access its lexical 10 | scope even when that function is executing outside its lexical scope. Kyle 11 | Simpson 12 |
13 |
14 | “[Lexical scoping] how a parser resolves variable names when functions are 15 | nested”. Mozilla Dev Net 16 |
17 |

🏋️‍♀️Exercise

18 |

19 | Open the console on your browser and type [closure exercise] in the 20 | console filter. You should see on the console the console.log for this 21 | exercise. 22 |

23 |

24 | 1. Go to{" "} 25 | src/components/functional-programming/closure/exercise.js and 26 | uncomment line 11. You should get the following error "TypeError: addFive 27 | is not a function" 28 |

29 |

30 | 2. To fix it you are only allowed to change the{" "} 31 | function add(). The function add() should be 32 | implemented in a way that 33 | addFive(7) outputs 12 34 |
35 | 🚨 YOU CAN ONLY EDIT THE function add() IN THAT FILE 🚨 36 |

37 | 38 |

39 | You know your implementation works because you'll see on the console: 40 | [closure exercise] addFive(7) is 12 41 |

42 |

Bonus, discuss about the solution with your peers

43 |

1- Is the inner function pure?

44 |

2- What's executed first, the inner function or the outer function?

45 |
46 | ); 47 | 48 | export default Page; 49 | -------------------------------------------------------------------------------- /src/components/functional-programming/closure/exercise.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | // Open the console on your browser and type [closure exercise] in the console filter. 4 | // You should see on the console the console.log() for this exercise. 5 | 6 | function add() {} 7 | 8 | const addFive = add(5); 9 | 10 | let result; 11 | // result = addFive(7); // should output 12 12 | 13 | console.log(`[closure exercise] addFive(7) is ${result}`); 14 | -------------------------------------------------------------------------------- /src/components/functional-programming/composition/Page.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/accessible-emoji */ 2 | import React from "react"; 3 | 4 | import { transformText } from "./exercise"; 5 | import FormExercise from "./bonus"; 6 | 7 | const exampleText = "1 2 3 React GraphQL Academy is a m a z i n g"; 8 | 9 | const Page = () => ( 10 | 11 |

Function composition

12 |

Exercise

13 | With the transformText function we can transform{" "} 14 | "{exampleText}" into 15 | "{transformText(exampleText)}" 16 |

17 | Let's make that composition more declarative using a compose{" "} 18 | function: 19 |

20 |

21 | 1. Your first task is to implement the compose 22 | function. Go to{" "} 23 | 24 | {" "} 25 | src/components/functional-programming/composition/exercise/index.js 26 | {" "} 27 | and follow the hints. 28 |

29 |

30 | 2. Can you use your compose{" "} 31 | function to compose HoCs? You can try to use it along with the{" "} 32 | withWidth at the bottom of the file{" "} 33 | src/components/App.jsx 34 |

35 |

Bonus Exercise

36 |

37 | Validate the following form composing the validators defined in 38 | 39 | src/components/functional-programming/composition/bonus/valiators 40 | 41 | . To do that you'll need to finish the implementation of the 42 | composeValidators function defined in 43 | 44 | src/components/functional-programming/composition/bonus/index.js 45 | 46 |

47 | 48 |

Notes

49 |

50 | You can see a real-world example of this technique to compose validators 51 | in the{" "} 52 | 57 | source code 58 | {" "} 59 | of the checkout of the{" "} 60 | 65 | React GraphQL Academy website 66 | 67 |

68 |
69 | ); 70 | 71 | export default Page; 72 | -------------------------------------------------------------------------------- /src/components/functional-programming/composition/bonus/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Form, Field } from "react-final-form"; 3 | import { required, mustBeEmail, atLeastFiveCharacters } from "./validators"; 4 | 5 | // 🚧 Task 1, implement the composeValidators function 6 | // each validator has a value as input and returns undefined or the error message 7 | export const composeValidators = (...validators) => (value) => 8 | validators.reduceRight((error, validator) => undefined, undefined); 9 | 10 | // 🚧 Task 2, you need to use the composeValidators so 11 | // - Email is validated with required and mustBeEmail 12 | // - Password is validatie with required and atLeastFiveCharacters 13 | const FormExercise = () => ( 14 |
( 17 | 18 |

19 | 26 |
27 | Task: validate with required and must be an email 28 |

29 |

30 | 37 |
38 | Task: validate with required and min length 5 characters 39 |

40 | 43 |
44 | )} 45 | /> 46 | ); 47 | 48 | const onSubmit = () => {}; 49 | 50 | const Input = ({ input, meta, placeholder, type }) => ( 51 | 52 | 53 | {meta.error && meta.touched && ( 54 | {meta.error} 55 | )} 56 | 57 | ); 58 | 59 | export default FormExercise; 60 | -------------------------------------------------------------------------------- /src/components/functional-programming/composition/bonus/validators.js: -------------------------------------------------------------------------------- 1 | export const required = (value) => (value ? undefined : "Required"); 2 | 3 | export const mustBeEmail = (value) => { 4 | const reEmail = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; // eslint-disable-line 5 | return reEmail.test(value) ? undefined : "Email format is not correct"; 6 | }; 7 | 8 | export const atLeastFiveCharacters = (value) => 9 | value && value.length >= 5 10 | ? undefined 11 | : "You need to type at least 5 characters"; 12 | -------------------------------------------------------------------------------- /src/components/functional-programming/composition/exercise/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | const toUpperCase = (text) => text.toUpperCase(); 4 | 5 | const removeSpaces = (text) => text.replace(/\s/g, ""); 6 | 7 | const removeNumbers = (text) => text.replace(/[0-9]/g, ""); 8 | 9 | // 🚧 Task 0: comment out the following transformText function and uncomment the one bellow 10 | export const transformText = (text) => 11 | toUpperCase(removeSpaces(removeNumbers(text))); 12 | 13 | // 🚧 Task 1: implement the following compose function 14 | // export const transformText = compose( 15 | // toUpperCase, 16 | // removeNumbers, 17 | // removeSpaces 18 | // ); 19 | // 🕵️‍♀️Hints: 20 | // - The compose function should return another function (think of the previous addFive, same idea) 21 | // - Spread the arguments of the compose function 22 | // - Use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduceRight 23 | -------------------------------------------------------------------------------- /src/components/functional-programming/memoization/Page.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/accessible-emoji */ 2 | import React from "react"; 3 | import "./exercise"; 4 | 5 | const Page = () => ( 6 | 7 |

Memoization

8 |

9 | Memoization is an optimization technique that stores the results of a 10 | function call and returns the cached result when the same inputs are 11 | supplied again. 12 |

13 |

🏋️‍♀️ Exercise

14 |

15 | Open the console on your browser and type [memoization exercise] in the 16 | console filter. You should see on the console the console.log() for this 17 | exercise.{" "} 18 |

19 |

20 | Given the function memoize() in 21 | `src/components/functional-programming/memoization/exercise`: 22 |

23 |

24 | 25 | 1. Pair up and explain to each other how the memoize function works with 26 | the doEasyWork function. 27 |

28 |
    29 |
  • Where is the closure? Between line X and Y
  • 30 |
  • What variable/s are captured in the closure?
  • 31 |
32 | 33 |

34 | 35 | 2. Explain to each other how the memoize function works with doHardWork. 36 | Does the memoize function work differently? 37 |

38 | 39 |

Bonus exercise

40 |

41 | 42 | b.1, Refactor the memoize function so it can memoize functions with any 43 | number of arguments. You can use the function doAnyWork to test your 44 | refactored memoize function. 45 |

46 |

47 | 48 | b.2, Extract the key cache functionality to a "resolver" function. 🕵️‍♂️ 49 | hint: see how{" "} 50 | 55 | lodash implements it 56 | 57 |

58 |
59 | ); 60 | 61 | export default Page; 62 | -------------------------------------------------------------------------------- /src/components/functional-programming/memoization/exercise.js: -------------------------------------------------------------------------------- 1 | /* 2 | Exercise 3 | 4 | Open the console on your browser and type [memoization exercise] in the console filter. 5 | You should see on the console the console.log() for this exercise. 6 | 7 | 1. Pair up and explain to each other how the memoize function works with the doEasyWork function. 8 | 9 | 2. Where is the closure? 10 | 11 | 3. Explain to each other how the memoize function works with doHardWork. 12 | Does the memoize function work differently? 13 | 14 | 4. Bonus, refactor the memoize function so it can memoize functions with any number of arguments- 15 | You can use the function doAnyWork to test your refactored memoize function 16 | */ 17 | 18 | export async function doEasyWork(amount) { 19 | console.log(`[memoization exercise] ${amount} easy units produced`); 20 | } 21 | 22 | export async function doHardWork(amount) { 23 | console.log("[memoization exercise] doing work"); 24 | await new Promise((resolve) => setTimeout(resolve, amount)); 25 | console.log(`[memoization exercise] ${amount} units of hard work produced!`); 26 | 27 | return amount; 28 | } 29 | 30 | export function doAnyWork(amount = 1, amount2 = 1, amount3 = 1) { 31 | return amount + amount2 + amount3; 32 | } 33 | 34 | function memoize(fn) { 35 | let cache = {}; 36 | return (amount) => { 37 | if (amount in cache) { 38 | console.log("[memoization exercise] output from cache"); 39 | return cache[amount]; 40 | } else { 41 | let result = fn(amount); 42 | cache[amount] = result; 43 | return result; 44 | } 45 | }; 46 | } 47 | 48 | const memoizedDoWork = memoize(doEasyWork); 49 | memoizedDoWork(4000); 50 | memoizedDoWork(4000); 51 | 52 | // Bounus 53 | // const memoizedDoWork = memoize(doAnyWork); 54 | // console.log(`[memoization exercise] ${memoizedDoWork(1, 2, 3)} === 6 ?`); 55 | // console.log(`[memoization exercise] ${memoizedDoWork(1, 50, 104)} === 155 ?`); 56 | 57 | // Bonus 2, extract the key cache functionality to a "resolver" function 58 | -------------------------------------------------------------------------------- /src/components/patterns/CompoundComponents/Page.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import RadioGroup from "./example/RadioGroup"; 4 | import RadioOption from "./example/RadioOption"; 5 | 6 | const Page = () => ( 7 |
8 |

Compound Components

9 |

Example

10 |

11 | 16 | source code exercise branch » 17 | 18 |

19 |

The following components are a compound component:

20 |

21 | 22 | Folder: patterns/CompoundComponents/example/RadioGroup.jsx and 23 | RadioOption.jsx 24 | 25 |

26 |
27 |

What is a compound component?

28 | { 31 | console.log("radioValue", radioValue); 32 | }} 33 | > 34 | 35 | A component that returns another component 36 | 37 | A function that returns a component 38 | 39 | A component that passes props dinamically to its children 40 | 41 | 42 | I have no idea so I'll wait for my pair to answer 43 | 44 | 45 | ... still waiting for my pair to answer, I think neither of us have 46 | any clue 47 | 48 | 49 |
50 |

Exercise

51 |

52 | 57 | source code exercise branch » 58 | 59 |

60 |

61 | Refactor the components in patterns/CompoundComponents/exercise/ so we 62 | don't have to pass explicitly the toggleMenu property on every MenuItem. 63 |

64 |

65 | Good: <MenuItem link="#1">Higher-Order Components</MenuItem> 66 |

67 |

68 | Not good: <MenuItem link="#1" toggleMenu={ this.toggleMenu 69 | }<Higher-Order Components>/MenuItem> 70 |

71 |
72 |
73 | ); 74 | 75 | export default Page; 76 | -------------------------------------------------------------------------------- /src/components/patterns/CompoundComponents/example/RadioGroup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | class RadioGroup extends React.Component { 5 | constructor(props) { 6 | super() 7 | this.state = { 8 | value: props.defaultValue 9 | } 10 | } 11 | 12 | select(value) { 13 | this.setState({ value }, () => { 14 | this.props.onChange(this.state.value) 15 | }) 16 | } 17 | 18 | render() { 19 | const children = React.Children.map(this.props.children, (child) => ( 20 | React.cloneElement(child, { 21 | isSelected: child.props.value === this.state.value, 22 | onClick: () => this.select(child.props.value) 23 | }) 24 | )) 25 | 26 | return
{ children }
27 | } 28 | } 29 | 30 | RadioGroup.propTypes = { 31 | defaultValue: PropTypes.string 32 | } 33 | 34 | export default RadioGroup 35 | -------------------------------------------------------------------------------- /src/components/patterns/CompoundComponents/example/RadioOption.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const RadioOption = ({ onClick, isSelected, children }) => ( 5 |
6 | 10 | { children } 11 |
12 | ) 13 | 14 | RadioOption.propTypes = { 15 | value: PropTypes.string 16 | } 17 | 18 | export default RadioOption 19 | -------------------------------------------------------------------------------- /src/components/patterns/CompoundComponents/exercise/Menu.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SideMenu from "react-burger-menu"; 3 | 4 | import withWidth, { 5 | LARGE 6 | } from "../../HigherOrderComponents/exercise_2/withWidth"; 7 | import FloatingMenuBtn from "../../../FloatingMenuBtn"; 8 | 9 | const Menu = ({ isOpen, children, pageWrapId, width, toggleMenu }) => ( 10 |
11 | {width === LARGE ? "" : } 12 | 17 | {children} 18 | 19 |
20 | ); 21 | 22 | export default withWidth(Menu); 23 | -------------------------------------------------------------------------------- /src/components/patterns/CompoundComponents/exercise/MenuItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | 4 | const MenuItem = ({ children, toggleMenu, link }) => ( 5 |

6 | toggleMenu() }> 7 | { children } 8 | 9 |

10 | ) 11 | 12 | export default MenuItem 13 | -------------------------------------------------------------------------------- /src/components/patterns/Context/Page.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Example from "./example"; 3 | import Exercise from "./exercise"; 4 | 5 | const Page = () => ( 6 | 7 |

Context

8 |
9 | Accepts a context object (the value returned from React.createContext) and 10 | returns the current context value for that context. The current context 11 | value is determined by the value prop of the nearest 12 | <MyContext.Provider> above the calling component in the tree.{" "} 13 | 18 | React docs 19 | 20 |
21 |

Example

22 | 23 |
24 | 25 | 26 |

Bonus Exercise 1

27 |

Now that you know how the React Context works:

28 |
    29 |
  • 30 | Would use the React Context for the form in 31 | the previous React Reducer Exercise? What are the pros and cons? 32 |
  • 33 |
  • 34 | If you use the React Context for the form, 35 | would you pass the value of the field and other props to the Field 36 | component using context or props? 37 |
  • 38 |
  • 39 | If you pass the value of the "input" and the 40 | other required props to the Field component using the React Context, do 41 | you think it still makes sense to use the React.memo HoC in the Field 42 | component? 43 |
  • 44 |
45 | 46 |

Bonus Exercise 2

47 |

48 | In our current implementation the cache (data 49 | key in our reducer) for each pair query & variables, we can only send 1 50 | query at a time. How would you make it possible to send requests 51 | concurrently? 52 |

53 |
54 | ); 55 | 56 | export default Page; 57 | -------------------------------------------------------------------------------- /src/components/patterns/Context/example/Modal.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Modal as BootstrapModal } from "react-bootstrap"; 3 | 4 | const ModalContext = React.createContext(); 5 | 6 | const Modal = ({ children }) => { 7 | const [modalChildren, showModal] = React.useState(null); 8 | const hideModal = () => showModal(null); 9 | const isOpen = !!modalChildren; 10 | 11 | return ( 12 | 13 | 14 | {modalChildren} 15 | 16 | {children} 17 | 18 | ); 19 | }; 20 | 21 | export const useModal = () => { 22 | const context = React.useContext(ModalContext); 23 | 24 | if (context === undefined) { 25 | throw new Error("useModal must be used within a ModalProvider"); 26 | } 27 | 28 | return context; 29 | }; 30 | 31 | export default Modal; 32 | -------------------------------------------------------------------------------- /src/components/patterns/Context/example/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button } from "react-bootstrap"; 3 | import { useModal } from "./Modal"; 4 | 5 | const Example = () => { 6 | const { showModal } = useModal(); 7 | 8 | return ( 9 | 10 |

11 | I'm a button using context {` `} 12 | 20 |

21 |

22 | File: src/components/patterns/Context/example/Modal.jsx 23 |

24 |
25 | When the nearest <MyContext.Provider> above the component updates, 26 | this Hook will trigger a rerender with the latest context value passed 27 | to that MyContext provider.{" "} 28 | 33 | React docs 34 | 35 |
36 |

Let me show you on the React Profiler what that quote means

37 |
38 | ); 39 | }; 40 | 41 | export default Example; 42 | -------------------------------------------------------------------------------- /src/components/patterns/Context/exercise/GraphQLProvider.jsx: -------------------------------------------------------------------------------- 1 | import React, { useReducer, useContext, useEffect } from "react"; 2 | import { memoize, hashGql, createClient } from "./utils"; 3 | 4 | const RECEIVE_DATA = "RECEIVE_DATA"; 5 | const SET_ERROR = "SET_ERROR"; 6 | 7 | export const StoreContext = React.createContext(); 8 | // 🚧 1.1 Create a context for the data fetching client 9 | 10 | const reducer = (state, action) => { 11 | switch (action.type) { 12 | case RECEIVE_DATA: 13 | return { 14 | ...state, 15 | loading: false, 16 | error: action.error, 17 | data: { ...state.data, ...action.payload }, 18 | }; 19 | case SET_ERROR: 20 | return { ...state, loading: false, error: action.error }; 21 | default: 22 | return state; 23 | } 24 | }; 25 | 26 | export const GraphQLProvider = ({ 27 | children, 28 | initialState = { 29 | data: {}, 30 | error: null, 31 | loading: true, 32 | }, 33 | }) => { 34 | const [state, dispatch] = useReducer(reducer, initialState); 35 | 36 | // 🚧 Part 1.2. Add your ClientProvider inside the return 37 | return ( 38 | 39 | {children} 40 | 41 | ); 42 | }; 43 | 44 | // 🚧 Bonus exercise, should we use useMemo for this memoized function? Why? 45 | const memoizedHashGql = memoize(hashGql); 46 | 47 | export const useQuery = (query, { variables }) => { 48 | // 🚧 1.3. Use the client from the context, instead of this hardcoded implementation. You can create a handy useClient custom hook (almost implemented at the end of the file). 49 | // Why moving the client to the context? For testing. E.g. https://www.apollographql.com/docs/react/development-testing/testing/#mockedprovider 50 | const client = createClient({ url: "https://rickandmortyapi.com/graphql/" }); 51 | const [state, dispatch] = useContext(StoreContext); 52 | const { loading, error, data: cache } = state; 53 | const cacheKey = memoizedHashGql(query, variables); 54 | const data = cache && cache[cacheKey]; 55 | 56 | useEffect(() => { 57 | if (data) { 58 | return; 59 | } 60 | 61 | client 62 | .query({ query, variables }) 63 | .then(({ data, error }) => { 64 | dispatch({ 65 | type: RECEIVE_DATA, 66 | payload: { [cacheKey]: data, error }, 67 | }); 68 | }) 69 | .catch((error) => 70 | dispatch({ 71 | type: SET_ERROR, 72 | error, 73 | }) 74 | ); 75 | }, [query, cacheKey, variables, dispatch, data]); 76 | 77 | return { data, loading, error }; 78 | }; 79 | 80 | export const useClient = () => { 81 | const client = null; // 🚧 get the client from the context here 82 | if (!client) { 83 | throw new Error( 84 | "No GraphQL client found, please make sure that you are providing a client prop to the GraphQL Provider" 85 | ); 86 | } 87 | 88 | return { client }; 89 | }; 90 | -------------------------------------------------------------------------------- /src/components/patterns/Context/exercise/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/accessible-emoji */ 2 | import React from "react"; 3 | import { useQuery } from "./GraphQLProvider"; 4 | 5 | const Root = () => { 6 | const { data, loading, error } = useQuery( 7 | `query character($id: ID! = 1) { 8 | character(id: $id) { 9 | image 10 | name 11 | } 12 | }`, 13 | { variables: { id: 2 } } 14 | ); 15 | if (loading) { 16 | return "loading"; 17 | } else if (error) { 18 | return "Oops, something went wrong"; 19 | } 20 | 21 | return ( 22 |
23 |

🏋️‍♀️Exercise

24 |

25 | 🎯 The goal of this exercise is to use context in some real-world use 26 | cases, along with custom hooks and some other React additional Hooks 27 |

28 | {data.character.name} 29 |

Exercise part 1, using context for the GraphQL client

30 |

31 | Our current implementation works, but we won't be able to easly write 32 | tests for our GraphQL queries because the fetching to the API is 33 | hardcoded. 34 |

35 |

36 | We can solve that by moving the GraphQL data fetching to the context. 37 | This way we can "inject" the data fetching dependency via context. Just 38 | like the{" "} 39 | 44 | MockedProvider 45 | {" "} 46 | from Apollo 47 |

48 |

Tasks part 1:

49 |

50 | 1.1. Go to{" "} 51 | 52 | src/components/patterns/Context/exercise/GraphQLProvider.jsx 53 | {" "} 54 | and create a context for the data fetching client. You can call it 55 | ClientContext. 56 |

57 |

58 | 1.2. Add the ClientContext Provider to the 59 | GraphQLProvider. Should the ClientContext wrap StoreContext? The other 60 | way around? Does it matter in this case? 61 |

62 |

63 | 1.3. Use the client from the context inside 64 | your useQuery. You can create a handy useClient custom hook like we did 65 | in the example{" "} 66 | 67 | src/components/patterns/Context/example/modal.jsx : useModal function{" "} 68 | 69 |

70 | 71 |

Tasks part 2:

72 |
73 | In large component trees, an alternative we recommend is to pass down a 74 | dispatch function from useReducer via context... 75 |
76 |
77 | ...If you use context to pass down the state too,{" "} 78 | use two different context types — the dispatch context 79 | never changes, so components that read it don’t need to rerender unless 80 | they also need the application state.{" "} 81 | 86 | React docs 87 | 88 |
89 |

🤔 React docs say "use two different context types". Let's do it!

90 |

91 | 2.1. Create two different context types for 92 | our StoreContext. One context for the dispatch, and another context for 93 | the state. 94 |

95 |

96 | 2.2. Great! We've implemented task 2.1. but, 97 | wait 🤔... does it make any difference in our use case? Why? Discuss 98 | with your peers. 99 |

100 |

Tasks part 3:

101 |

102 | 3. In{" "} 103 | 104 | src/components/patterns/Context/exercise/GraphQLProvider.jsx 105 | {" "} 106 | we are using const memoizedHashGql = memoize(hashGql);. 107 | Should we use useMemo instead? Why? 108 |

109 |
110 | ); 111 | }; 112 | 113 | export default Root; 114 | -------------------------------------------------------------------------------- /src/components/patterns/Context/exercise/utils.js: -------------------------------------------------------------------------------- 1 | export function hashGql(query, variables) { 2 | const body = JSON.stringify({ query, variables }); 3 | 4 | return body.split("").reduce(function (a, b) { 5 | a = (a << 5) - a + b.charCodeAt(0); 6 | return a & a; 7 | }, 0); 8 | } 9 | 10 | export function memoize(fn) { 11 | const cache = new Map(); 12 | return (...args) => { 13 | const hit = args.reduce((acc, arg) => acc && acc.get(arg), cache); 14 | if (hit) { 15 | return hit; 16 | } else { 17 | let result = fn(...args); 18 | const lastArgIndex = args.length - 1; 19 | args.reduce((accCache, arg, i) => { 20 | const resultOrCache = 21 | i === lastArgIndex ? result : accCache ? accCache : new Map(); 22 | accCache.set(arg, resultOrCache); 23 | return resultOrCache; 24 | }, cache); 25 | 26 | return result; 27 | } 28 | }; 29 | } 30 | 31 | export const createClient = ({ url }) => { 32 | return { 33 | query: async ({ query, variables }) => { 34 | const body = JSON.stringify({ query, variables }); 35 | const response = await fetch(url, { 36 | method: "POST", 37 | headers: { "Content-Type": "application/json" }, 38 | body, 39 | }); 40 | 41 | return response.json(); 42 | }, 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/patterns/CustomHooks/Page.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Example from "./example"; 3 | import Exercise from "./exercise"; 4 | 5 | const Page = () => ( 6 |
7 |

Custom Hooks

8 | 9 | 10 |
11 | ); 12 | 13 | export default Page; 14 | -------------------------------------------------------------------------------- /src/components/patterns/CustomHooks/example/UseCallbackOrMemo.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/accessible-emoji */ 2 | import React, { useMemo, useCallback, useEffect } from "react"; 3 | import { useIncrement } from "./index"; 4 | 5 | const computeExpensiveValue = (a, b) => { 6 | console.log("[custom hooks exercise] computing expensive value"); 7 | 8 | return a * b; 9 | }; 10 | 11 | const UseCallbackOrMemo = ({ a = 1, b = 2 }) => { 12 | const { count, increment } = useIncrement(); 13 | // useMemo returns a value 14 | const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); 15 | // useCallback returns a function 16 | const memoizedCallback = useCallback(increment, []); 17 | 18 | useEffect(() => { 19 | memoizedCallback(); 20 | // 👩‍🏫 increment instead of memoizedCallback and you'll get an infinite loop 21 | // increment(); 22 | }, [memoizedCallback]); 23 | 24 | /* 25 | 👩‍🏫 Do: 26 | - Go to http://localhost:3000/custom-hooks 27 | - Open the console on the dev tools 28 | - Click on the "Increment 🍄" button 29 | - You should only see one "[custom hooks exercise] computing expensive value" 30 | */ 31 | 32 | return ( 33 | 34 |

🥑 Before the exercise 🏋️‍♀️

35 |

useMemo

36 |
37 | Returns a memoized value. 38 | 43 | {` `} The docs 44 | 45 |
46 |

useCallback

47 |
48 | Returns a memoized callback (function). 49 | 54 | {` `} The docs 55 | 56 |
57 |

Last render on {new Date().toString()}

58 |

59 | memoized value: {memoizedValue} 60 |
61 | 62 | If you look at the console, you should not see "[custom hooks 63 | exercise] computing expensive value" on every render 64 | 65 |

66 |

67 | 68 | current count: {count} 69 |

70 |

71 | File: 72 | 73 | src/components/patterns/CustomHooks/example/UseCallbackOrMemo.jsx 74 | 75 |

76 |
77 | ); 78 | }; 79 | 80 | export default UseCallbackOrMemo; 81 | -------------------------------------------------------------------------------- /src/components/patterns/CustomHooks/example/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/accessible-emoji */ 2 | import React, { useState } from "react"; 3 | import UseCallbackOrMemo from "./UseCallbackOrMemo"; 4 | 5 | // const calculateValue = (a, b) => a + b; 6 | 7 | // export const useIncrement = (initialValue, calculateValue) => { 8 | export const useIncrement = (initialValue) => { 9 | const [count, setCount] = useState(initialValue || 0); 10 | // 👩‍🏫 the value of the increment could be resolved by a function that is passed to the useIncrement (e.g. inversion of control) 11 | // const increment = (value = 1) => setCount(calculateValue(count, value)); 12 | const increment = (value = 1) => setCount(count + value); 13 | 14 | return { count, increment }; 15 | }; 16 | 17 | // Using a custom hook 18 | // const ExampleComponent = (props) => { 19 | // const { count, increment } = useIncrement(props.initialValue); 20 | 21 | // return ( 22 | //

23 | // 24 | // current count: {count} 25 | //

26 | // ); 27 | // }; 28 | 29 | const ExampleComponent = (props) => { 30 | const { count, increment } = useIncrement(props.initialValue); 31 | 32 | return ( 33 |

34 | 35 | Current count: {count} 36 |

37 | ); 38 | }; 39 | 40 | const Example = () => ( 41 | 42 |

👩‍🏫 Example

43 |

44 | A React Hook is a function that lets us reuse component logic across 45 | different function components. Component logic is: 46 |

47 |
    48 |
  • State
  • 49 |
  • Lifecyle
  • 50 |
  • Context
  • 51 |
52 |

A custom Hook is a function that calls another Hook

53 | 54 |

55 | Custom Hooks documentation{" "} 56 | 61 | https://reactjs.org/docs/hooks-custom.html 62 | 63 |

64 | 65 |

66 | File: src/components/patterns/CustomHooks/example/index.jsx 67 |

68 |
69 | 70 |
71 | ); 72 | 73 | export default Example; 74 | -------------------------------------------------------------------------------- /src/components/patterns/CustomHooks/exercise/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/accessible-emoji */ 2 | import React from "react"; 3 | // import useWidth, { LARGE, MEDIUM } from "./useWidth"; 4 | // remove the following import after refactoring the Width component to a custom hook 5 | import Width from "./useWidth"; 6 | 7 | const Bonus = () => { 8 | return ( 9 | 10 |
11 |

🏋️‍♀️Exercise

12 |

13 | 🎯 The goal is to extract some component logic into a reusable function 14 | avoiding common pitfalls 15 |

16 | 17 |

18 | {/* Comment out the following after implementing part 1 */} 19 | The width is: 20 | {/* Use the width value from your custom hook in the next line after you implment part 1 */} 21 | {/* {width} */} 22 |

23 | 24 |

Part 1, refactoring

25 | 26 |

27 | 1. Refactor the{" "} 28 | src/components/patterns/CustomHooks/exercise/useWidth.js{" "} 29 | class to a function component using the useState and{" "} 30 | useEffect Hooks. 31 |

32 |

33 | Tip: to remove the event listeners we need to use the{" "} 34 | 39 | {" "} 40 | cleanup phase 41 | {" "} 42 | of the effect. To clean up the side effects you must return a function. 43 |

44 |

45 | You'll know it works because the "The width is: X" will change properly 46 | when you resize the screen. 47 |

48 |

Part 2, reusing

49 |

50 | 2.1. Import your 51 | src/components/patterns/CustomHooks/exercise/useWidth.js custom hook in{" "} 52 | src/components/patterns/CustomHooks/exercise/index.jsx and 53 | replace the <Width /> component using: 54 | const width = useWidth() 55 |

56 |

57 | You'll know it works because the "The width is: X" will change properly 58 | when you resize the screen. 🤔wait... then (discuss with your peer in 59 | the group): 60 |

61 |

62 | 2.2. What's the difference between <Width 63 | /> and useWidth if I can also do{" "} 64 | import Width from "./useWidth" and do <Width />? 65 |

66 |

Part 3, pitfalls

67 |

68 | 3.1. We have dissabled the{" "} 69 | 74 | 'exhaustive-deps' lint rule 75 | 76 | , and you might have a memory leak in your useWidth. Your 77 | task is to identify it and fix it using useMemo or useCallback (you'll 78 | have to decide). Notice, you might be following good practices and 79 | already fixed the problem without realizing it. Double check with your 80 | peers in your group. 81 |

82 |

83 | ⚠️ Warning, you should use eslint to help you identify potential bugs. 84 | It's not enable in this exercise to help you understand the problem. 85 |

86 | 87 |

🤸‍♀️Bonus exercise, legacy

88 |

89 | Replace the HoC withWidth in 90 | 91 | src/components/patterns/CompoundComponents/exercise/Menu.jsx 92 | {" "} 93 | with your custom hook useWidth. 94 |

95 | 96 |

97 | We want to replace the HoC{" "} 98 | withWidth in 99 | src/components/App.jsx with your custom hook but we don't 100 | have much time and confidence (ohh we don't have tests!). Create a HoC 101 | that uses the useWidth hook and injects the width value via props to the 102 | App component. 103 |

104 |
105 | ); 106 | }; 107 | 108 | export default Bonus; 109 | -------------------------------------------------------------------------------- /src/components/patterns/CustomHooks/exercise/useWidth.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const SMALL = 1; 4 | export const MEDIUM = 2; 5 | export const LARGE = 3; 6 | 7 | const largeWidth = 992, 8 | mediumWidth = 768; 9 | 10 | class WithWidth extends React.Component { 11 | constructor() { 12 | super(); 13 | this.state = { width: this.windowWidth() }; 14 | } 15 | 16 | componentDidMount() { 17 | if (typeof window !== "undefined") { 18 | window.addEventListener("resize", this.handleResize); 19 | this.handleResize(); 20 | } 21 | } 22 | 23 | componentWillUnmount() { 24 | if (typeof window !== "undefined") 25 | window.removeEventListener("resize", this.handleResize); 26 | } 27 | 28 | handleResize = () => { 29 | let width = this.windowWidth(); 30 | if (width !== this.state.width) this.setState({ width }); 31 | }; 32 | 33 | windowWidth() { 34 | let innerWidth = 0; 35 | let width; 36 | if (window) innerWidth = window.innerWidth; 37 | 38 | if (innerWidth >= largeWidth) { 39 | width = LARGE; 40 | } else if (innerWidth >= mediumWidth) { 41 | width = MEDIUM; 42 | } else { 43 | // innerWidth < 768 44 | width = SMALL; 45 | } 46 | 47 | return width; 48 | } 49 | 50 | render() { 51 | return this.state.width; 52 | } 53 | } 54 | 55 | export default WithWidth; 56 | -------------------------------------------------------------------------------- /src/components/patterns/HigherOrderComponents/Page.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import withMousePosition from "./example/withMousePosition"; 3 | import Exercise1 from "./exercise_1"; 4 | // import { connect } from "./exercise_bonus/connect"; 5 | import { 6 | incrementAction, 7 | decrementAction, 8 | } from "./exercise_bonus/utils/redux-counter"; 9 | 10 | const Page = ({ dispatch, ...props }) => ( 11 |
12 |

Higher-Order Components

13 |

Example

14 |

15 | 20 | source code exercise branch » 21 | 22 |

23 |

24 | The following component is a higher-order component called 25 | withMousePosition, it prints the position of the mouse when you move the 26 | mouse over the component using props. 27 |

28 |

29 | 30 | File: patterns/HigherOrderComponents/example/withMousePosition.jsx 31 | 32 |

33 |
34 |
 35 |         {JSON.stringify(props.mouse, null, 2)}
 36 |       
37 |
38 |

Exercise withData

39 |

40 | 45 | source code exercise branch » 46 | 47 |

48 |

A typical use case of a HoC is to fetch data into a component.

49 |

50 | Task: Can you compose the List component displayed below 51 | using{" "} 52 | 53 | src/components/patterns/HigherOrderComponents/exercise/withData 54 | 55 | ?. Hint: the composition happens in List component not in 56 | Question1/index.js. For the task you'll have to edit 2 files: 57 |

58 |
    59 |
  • 60 | 61 | src/components/patterns/HigherOrderComponents/exercise/withData 62 | 63 |
  • 64 |
  • 65 | src/components/questions/Question1/List.jsx 66 |
  • 67 |
68 | 69 |
70 |

Pro-tip

71 |

72 | Why do you think we need to use a HoC to fetch the data? why not just 73 | having a "helper" function that fetches data and we call it from List 74 | component to fetch data? 75 |

76 |

77 | In this particular case there are two reasons we need to encapsulate 78 | that logic in a component 79 |

80 |
    81 |
  • We are tracking some state
  • 82 |
  • 83 | We are triggering the logic in a life cycle method (componentDidMount) 84 |
  • 85 |
86 |
87 |
88 |

Exercise withWidth

89 |

90 | 95 | source code exercise branch » 96 | 97 |

98 |

99 | A good implementation of a HoC lets the developer that uses the HoC 100 | configure it easily without having to reimplement anything. 101 |

102 |

103 | The{" "} 104 | 105 | src/components/patterns/HigherOrderComponents/exercise/withWidth 106 | {" "} 107 | implementation we use has the largeWidth and mediumWidth hardcoded. It 108 | makes it more difficult to reuse this HoC in different projects where we 109 | might consider different screen sizes. 110 |

111 |

112 | Task: Refactor the higher-order component withWidth so it 113 | accepts the sizes as a parameter. You can implement it in different ways: 114 |

115 |
    116 |
  • 117 |

    118 | The first way, and more naive, is by adding an extra parameter/s to 119 | the HoC functions. Example: 120 |

    121 |

    122 | 123 | withWidth(compnent, { largeWidth : 992, mediumWidth : 768, 124 | resizeInterval : 166 } 125 | 126 | ) 127 |

    128 |
  • 129 |
  • 130 |

    131 | The second one is using currying. Try to implement a 132 | HoC so you can call it as follows 133 |

    134 |

    135 | 136 | withWidth({ largeWidth : 992, mediumWidth : 768, resizeInterval 137 | : 166 })(component) 138 | 139 |

    140 |
  • 141 |
142 |

143 | Tip. What you are trying to implement is similar to the{" "} 144 | connect function from react-redux. 145 |

146 |
147 |

Heads-up

148 |

149 | There is more than one place where the HoC withWidth is being used. 150 | You'll need to update any call to withWidth using the new implemention 151 |

152 |
153 |
154 |

Bonus exercise

155 |

156 | 161 | source code exercise branch » 162 | 163 |

164 |

165 | Implement the connect from react-redux. Look at the comments in 166 | src/components/patterns/HigherOrderComponents/xbonus/connect.js 167 |

168 |

169 | 172 | {props.counter} 173 | 176 |

177 |
178 | ); 179 | 180 | // Uncomment the following line for the HoCs bonus exercise 181 | // export default connect(mapStateToProps, mapDispatchToProps)(withMousePosition(Question1)) 182 | export default withMousePosition(Page); 183 | -------------------------------------------------------------------------------- /src/components/patterns/HigherOrderComponents/example/withMousePosition.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const withMousePosition = Component => { 4 | return class ComponentWithMPosition extends React.Component { 5 | constructor() { 6 | super() 7 | this.state = { x: 0, y: 0 } 8 | } 9 | 10 | handleMouseMove = (event) => { 11 | this.setState({ 12 | x: event.clientX, 13 | y: event.clientY 14 | }) 15 | } 16 | 17 | render() { 18 | return ( 19 |
20 | 21 |
22 | ) 23 | } 24 | } 25 | } 26 | 27 | export default withMousePosition 28 | -------------------------------------------------------------------------------- /src/components/patterns/HigherOrderComponents/exercise_1/List.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // import withData from './withData' 3 | 4 | const List = ({ data = [], error, loading }) => { 5 | if (error) { 6 | return

Error: {error}

; 7 | } else if (loading) { 8 | return

Loading...

; 9 | } else if (!data.length) { 10 | return ( 11 |

12 | Empty list :( 13 |

14 | ); 15 | } else { 16 | return ( 17 | 31 | ); 32 | } 33 | }; 34 | 35 | export default List; 36 | -------------------------------------------------------------------------------- /src/components/patterns/HigherOrderComponents/exercise_1/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import List from "./List"; 3 | 4 | const Exercise1 = () => ( 5 | 6 |

Display a list of gists from https://api.github.com/gists/public

7 | 8 |
9 | ); 10 | 11 | export default Exercise1; 12 | -------------------------------------------------------------------------------- /src/components/patterns/HigherOrderComponents/exercise_1/withData.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | /* 4 | A higher-order component (HoC) is a function that: 5 | 1- receives a COMPONENT as a parameter (hint: is this function receving a component?) 6 | 2- and returns another component 7 | */ 8 | const withData = () => { 9 | class OuterComponent extends React.Component { 10 | state = { 11 | data: undefined, 12 | loading: false, 13 | error: undefined 14 | }; 15 | 16 | componentDidMount = async () => { 17 | this.setState({ loading: true }); 18 | try { 19 | const response = await fetch(this.props.url); 20 | const data = await response.json(); 21 | this.setState({ data }); 22 | } catch (error) { 23 | this.setState({ error: error.message }); 24 | } finally { 25 | this.setState({ loading: false }); 26 | } 27 | }; 28 | 29 | render() { 30 | // you should return something here 31 | } 32 | } 33 | 34 | return OuterComponent; 35 | }; 36 | 37 | export default withData; 38 | -------------------------------------------------------------------------------- /src/components/patterns/HigherOrderComponents/exercise_2/appWithWidth.js: -------------------------------------------------------------------------------- 1 | import withWidth from "./withWidth"; 2 | export { LARGE, MEDIUM, SMALL } from "./withWidth"; 3 | 4 | export default withWidth({ largeWidth: 1000 }); 5 | -------------------------------------------------------------------------------- /src/components/patterns/HigherOrderComponents/exercise_2/withWidth.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | export const SMALL = 1 4 | export const MEDIUM = 2 5 | export const LARGE = 3 6 | 7 | const withWidth = (MyComponent) => { 8 | const largeWidth = 992, 9 | mediumWidth = 768 10 | 11 | return class WithWidth extends Component { 12 | constructor() { 13 | super() 14 | this.state = { width: this.windowWidth() } 15 | this.handleResize = this.handleResize.bind(this) 16 | } 17 | 18 | componentDidMount() { 19 | if (window) { 20 | window.addEventListener('resize', this.handleResize) 21 | this.handleResize() 22 | } 23 | } 24 | 25 | componentWillUnmount() { 26 | if (window) window.removeEventListener('resize', this.handleResize) 27 | } 28 | 29 | handleResize() { 30 | let width = this.windowWidth() 31 | if (width !== this.state.width) this.setState({ width }) 32 | } 33 | 34 | windowWidth() { 35 | let innerWidth = 0 36 | let width 37 | if (window) innerWidth = window.innerWidth 38 | 39 | if (innerWidth >= largeWidth) { 40 | width = LARGE 41 | } else if (innerWidth >= mediumWidth) { 42 | width = MEDIUM 43 | } else { // innerWidth < 768 44 | width = SMALL 45 | } 46 | 47 | return width 48 | } 49 | 50 | render() { 51 | return ( 52 | 56 | ) 57 | } 58 | } 59 | } 60 | 61 | 62 | export default withWidth 63 | -------------------------------------------------------------------------------- /src/components/patterns/HigherOrderComponents/exercise_bonus/connect.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // Don't do window.__store ever!! this is only to avoid using a provider until the context exercise 4 | const store = window.__store 5 | 6 | export const connect = () => { 7 | class ConnectedComponent extends React.Component { 8 | componentDidMount() { 9 | // We subscribe to the store just once, when the component is mounted. https://reactjs.org/docs/react-component.html#componentdidmount 10 | this.unsubscribe = store.subscribe(() => { 11 | this.forceUpdate() 12 | }) 13 | } 14 | 15 | componentWillUnmount() { 16 | // We unsubscribe from the store when the component is unmounted. https://reactjs.org/docs/react-component.html#componentwillunmount 17 | this.unsubscribe() 18 | } 19 | 20 | render() { 21 | // TODO: implement the render method 22 | return null 23 | } 24 | } 25 | 26 | return ConnectedComponent 27 | } -------------------------------------------------------------------------------- /src/components/patterns/HigherOrderComponents/exercise_bonus/utils/redux-counter.js: -------------------------------------------------------------------------------- 1 | const INCREMENT = 'INCREMENT' 2 | const DECREMENT = 'DECREMENT' 3 | 4 | export const counter = (state = 0, action) => { 5 | switch (action.type) { 6 | case INCREMENT: 7 | return state + 1 8 | case DECREMENT: 9 | return state - 1 10 | default: 11 | return state 12 | } 13 | } 14 | 15 | export const incrementAction = () => ({ 16 | type: INCREMENT 17 | }) 18 | 19 | export const decrementAction = () => ({ 20 | type: DECREMENT 21 | }) -------------------------------------------------------------------------------- /src/components/patterns/HookReducer/Page.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/accessible-emoji */ 2 | import React from "react"; 3 | import Example from "./example"; 4 | import Exercise from "./exercise"; 5 | 6 | const Page = () => ( 7 | 8 |

Hook Reducer

9 |

10 | From the{" "} 11 | 16 | React docs 17 | 18 |

19 |
20 | React.useReducer is usually preferable to useState when you have complex 21 | state logic that involves multiple sub-values or when the next state 22 | depends on the previous one.{" "} 23 | (We'll practice this in this exercise) 24 |
25 |
26 | React.useReducer also lets you optimize performance for components that 27 | trigger deep updates because you can pass dispatch down instead of 28 | callbacks.{" "} 29 | (We'll practice this in the next exercise about Context) 30 |
31 |

Example

32 |

Basic login form

33 | 34 |
35 |

36 | File: src/components/patterns/HookReducer/example/index.jsx 37 |

38 |
39 |

🥑 Before the exercise 🏋️‍♀️

40 |

41 | The code of the example is very similar to the code of the exercise. The 42 | example contains explanations 👩‍🏫, the exercise contains tasks 🚧 and hints 43 | 🕵️‍♀️. If there are things that you don't understand about the code it's 44 | better to look at the example. If there are things that are not clear 45 | about what needs to done in the exercise after checking the tasks, let me 46 | know. 47 |

48 | 49 |

Exercise part 1

50 | 51 |

52 | 🎯 The goal is to understand how to handle complex state logic in our 53 | components that involves multiple sub-values 54 |

55 | 56 |

57 | Refactor the LoginForm component in{" "} 58 | src/components/patterns/HookReducer/exercise/index.jsx so it 59 | implements the following: 60 |

61 |

62 | 1. Handles the SET_ERRORS action in the reducer. 63 | It should add an errors object to the the state using the action.payload 64 |

65 |

66 | 2. dispatch a SET_ERRORS action 67 | with the errors (output from the 68 | validate(state.values) invocation) as payload. 69 |

70 |

71 | 3.dispatch the SET_ERRORS action 72 | only when the state of the input fields change. Hint, you need to use the 73 | useEffect second argument. 74 |

75 | 76 | 77 |
78 |

Exercise part 2

79 |

80 | Create a custom hook from your Login Form. You 81 | can call it useForm. 82 |

83 |

🕵️‍♀️ Hints :

84 |
    85 |
  • Extract the useReducer outside the Login Form
  • 86 |
  • 87 | Don't think of state only, but also functions that create "props". 88 |
  • 89 |
90 | 91 |

Exercise part 3

92 |

93 | By default we are displaying the error message to the user even if the 94 | user did not use the form. That's not a great user experience. To improve 95 | that we are going to add two more states in our form to identify which 96 | fields are `dirty` and if the form is `submitted`. 97 |

98 |

99 | A field is dirty when the value of the field is not equal to the initial 100 | value, false if the values are equal. 101 |

102 | 103 |

A field is submitted if the form is submitted :D

104 | 105 |

Bonus exercise part 1

106 |

107 | We are going to add some state to our form to know when the form is being 108 | submitted. 109 |

110 |

111 | If the form is being submitted then we'll display the text "submitting" 112 | instead of "submit" in the submit button. 113 |

114 | 115 |

Bonus exercise part 2

116 |

117 | Use the{" "} 118 | 122 | React Profiler 123 | {" "} 124 | in the React Dev Tools to record what happens when you type in the user 125 | id. Is the password being rendered as well? Why? 126 |

127 |

128 | To avoid unnecessary renders you can create another component called 129 | "Field" and use{" "} 130 | 134 | React.memo 135 | 136 | . 137 |

138 |
139 | ); 140 | 141 | export default Page; 142 | -------------------------------------------------------------------------------- /src/components/patterns/HookReducer/example/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const SET_FIELD_VALUE = "SET_FIELD_VALUE"; 4 | 5 | // 👩‍🏫The state of a form is a relatively complex state logic. 6 | // Using a reducer also helps separate reads, from writes. 7 | function reducer(state, action) { 8 | switch (action.type) { 9 | // 👩‍🏫reducers should respond to a given action 10 | case SET_FIELD_VALUE: 11 | // 👩‍🏫reducers should not mutate the current state but create a new one 12 | // (If you’re familiar with Redux, you already know how this works, but we have to mention it :) 13 | return { 14 | ...state, 15 | values: { 16 | ...state.values, 17 | ...action.payload, 18 | }, 19 | }; 20 | default: 21 | // 👩‍🏫reducers should return the current state if the received action is not a case to be handled 22 | // (If you’re familiar with Redux, you already know all this, but we have to mention it anyway :) 23 | return state; 24 | } 25 | } 26 | 27 | const LoginForm = (props) => { 28 | const initialState = { 29 | values: props.initialValues, 30 | }; 31 | 32 | // 👩‍🏫 useReducer accepts a reducer of type (state, action) => newState, 33 | // and returns the current state paired with a dispatch method 34 | const [state, dispatch] = React.useReducer(reducer, initialState); 35 | 36 | // 👩‍🏫 Notice we are using a closure here. As a mental model from the closure exercise we did before: 37 | // add = (a) => (b) => 38 | const handleChange = (fieldName) => (event) => { 39 | event.preventDefault(); 40 | dispatch({ 41 | type: SET_FIELD_VALUE, 42 | payload: { [fieldName]: event.target.value }, 43 | }); 44 | }; 45 | 46 | const getFieldProps = (fieldName) => ({ 47 | value: state.values[fieldName], 48 | onChange: handleChange(fieldName), // 👩‍🏫 fieldName gets "captured" in the handleChange closure 49 | }); 50 | 51 | const handleSubmit = (e) => { 52 | e.preventDefault(); 53 | alert(JSON.stringify(state.values)); 54 | }; 55 | 56 | return ( 57 |
58 | 63 |
64 | 69 |
70 | 71 |
72 | ); 73 | }; 74 | 75 | const Example = () => ( 76 | 82 | ); 83 | 84 | export default Example; 85 | -------------------------------------------------------------------------------- /src/components/patterns/HookReducer/exercise/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from "react"; 3 | 4 | function reducer(state, action) { 5 | switch (action.type) { 6 | // 🚧 Add a SET_ERRORS case that adds an errors key to the state with the action.payload 7 | // 🕵️‍♀️ You probably want to clear previous errors every time you do SET_ERRORS 8 | case "SET_FIELD_VALUE": 9 | return { 10 | ...state, 11 | values: { 12 | ...state.values, 13 | ...action.payload, 14 | }, 15 | }; 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | function LoginForm(props) { 22 | const { initialValues, onSubmit } = props; 23 | // 👮‍♀you don't have to edit this validate function 24 | const validate = (values) => { 25 | let errors = {}; 26 | if (!values.password) { 27 | errors.password = "Password is required"; 28 | } 29 | if (!values.userId) { 30 | errors.userId = "User Id is required"; 31 | } 32 | return errors; 33 | }; 34 | 35 | const [state, dispatch] = React.useReducer(reducer, { 36 | values: initialValues, 37 | errors: {}, 38 | }); 39 | 40 | React.useEffect(() => { 41 | if (validate) { 42 | const errors = validate(state.values); 43 | // 🚧 dispatch a SET_ERRORS action with the errors as payload 44 | } 45 | }, []); // 🚧 dispatch the SET_ERRORS action only when the state of the input fields change. 46 | 47 | const handleChange = (fieldName) => (event) => { 48 | event.preventDefault(); 49 | dispatch({ 50 | type: "SET_FIELD_VALUE", 51 | payload: { [fieldName]: event.target.value }, 52 | }); 53 | }; 54 | 55 | const handleSubmit = (event) => { 56 | event.preventDefault(); 57 | const errors = validate(state.values); 58 | if (!Object.keys(errors).length) { 59 | onSubmit(state.values); 60 | } 61 | }; 62 | 63 | const getFieldProps = (fieldName) => ({ 64 | value: state.values[fieldName], 65 | onChange: handleChange(fieldName), 66 | }); 67 | 68 | const { errors } = state; 69 | 70 | return ( 71 |
72 | 78 |
79 | 87 |
88 | 89 |
90 | ); 91 | } 92 | 93 | const Exercise = () => ( 94 | 95 |

Custom Login Form with validation

96 | { 102 | alert(JSON.stringify(values, null, 2)); 103 | }} 104 | /> 105 |
106 | ); 107 | 108 | export default Exercise; 109 | -------------------------------------------------------------------------------- /src/components/patterns/Hooks/Page.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Examples from "./examples"; 3 | import Exercise from "./exercise"; 4 | 5 | const Page = () => ( 6 |
7 |

Hooks

8 | 9 | 10 |
11 | ); 12 | 13 | export default Page; 14 | -------------------------------------------------------------------------------- /src/components/patterns/Hooks/examples/UseEffect.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | 3 | const DocumentTitleEffect = () => { 4 | const [count, setCount] = useState(0); 5 | 6 | // Similar to componentDidMount and componentDidUpdate: 7 | useEffect(() => { 8 | // Update the document title using the browser API 9 | document.title = `${count} clicks`; 10 | }); 11 | 12 | return ( 13 |
14 |

Click counter on the document title

15 | 16 |
17 | ); 18 | }; 19 | 20 | export default DocumentTitleEffect; 21 | -------------------------------------------------------------------------------- /src/components/patterns/Hooks/examples/UseState.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | const Input = () => { 4 | const [value, setValue] = useState("default value"); 5 | 6 | return setValue(e.target.value)} />; 7 | }; 8 | 9 | // class Input extends React.Component { 10 | // state = { 11 | // value: "" 12 | // }; 13 | 14 | // onChange = e => { 15 | // this.setState({ value: e.target.value }, () => { 16 | // console.log("Current Input state:", this.state.value); 17 | // }); 18 | // }; 19 | 20 | // render() { 21 | // const { value } = this.state; 22 | // const { onChange } = this; 23 | 24 | // return ; 25 | // } 26 | // } 27 | 28 | export default Input; 29 | -------------------------------------------------------------------------------- /src/components/patterns/Hooks/examples/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import UseState from "./UseState"; 3 | import UseEffect from "./UseEffect"; 4 | 5 | const Examples = () => ( 6 | 7 |

Examples

8 |
9 |

State Hook

10 | 11 |
12 |

Effect Hook Example

13 | 14 |
15 |
16 | ); 17 | 18 | export default Examples; 19 | -------------------------------------------------------------------------------- /src/components/patterns/Hooks/exercise/RandomImage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class RandomImage extends React.Component { 4 | state = { 5 | imageUrl: undefined, 6 | }; 7 | 8 | componentDidMount = () => { 9 | this.fetchRandomImage(); 10 | }; 11 | 12 | fetchRandomImage = async () => { 13 | const response = await fetch( 14 | "https://jsonplaceholder.typicode.com/photos?_limit=100" 15 | ); 16 | const data = await response.json(); 17 | const image = data[Math.floor(Math.random() * data.length)]; 18 | this.setState({ imageUrl: image.thumbnailUrl }); 19 | }; 20 | 21 | render() { 22 | const { imageUrl } = this.state; 23 | 24 | return {imageUrl}; 25 | } 26 | } 27 | 28 | export default RandomImage; 29 | -------------------------------------------------------------------------------- /src/components/patterns/Hooks/exercise/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RandomImage from "./RandomImage"; 3 | 4 | const Exercise = () => ( 5 | 6 |

Exercise

7 |

8 | Refactor the src/patterns/Hooks/exercise/RandomImage.jsx{" "} 9 | using the useState and useEffect Hooks. 10 |

11 |

12 | Tip: The fetchRandomImage function image should be executed only on the 13 | first render. 14 |

15 | 16 |
17 |
18 | ); 19 | 20 | export default Exercise; 21 | -------------------------------------------------------------------------------- /src/components/patterns/RenderProps/Page.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Example from "./example"; 3 | import Exercise from "./exercise"; 4 | //import BonusExercise from '../exercise_bonus/HoCs/GitHubIssueSearchInfiniteScroller' 5 | import BonusExercise from "./exercise_bonus/BadImplementation"; 6 | 7 | const Page = props => ( 8 |
9 |

Render Props

10 | 11 |
12 | 13 |

Bonus exercise

14 |

15 | 20 | source code exercise branch » 21 | 22 |

23 | Refactor the HoCs GitHubIssueSearchInfiniteScroller to be a Render Props 24 | 25 |
26 |
27 | ); 28 | 29 | export default Page; 30 | -------------------------------------------------------------------------------- /src/components/patterns/RenderProps/example/Input.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export class Input extends React.Component { 4 | state = { 5 | value: "" 6 | }; 7 | 8 | onChange = e => { 9 | this.setState({ value: e.target.value }, () => { 10 | console.log("Current Input state:", this.state.value); 11 | }); 12 | }; 13 | 14 | render() { 15 | const { value } = this.state; 16 | const { onChange } = this; 17 | 18 | return this.props.children({ value, onChange }); 19 | } 20 | } 21 | 22 | export default Input; 23 | -------------------------------------------------------------------------------- /src/components/patterns/RenderProps/example/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Input from "./Input"; 3 | 4 | const Example = () => ( 5 |
6 |
7 |

Example

8 |

Input

9 |

10 | 15 | source code exercise branch » 16 | 17 |

18 |

19 | In the example the state of an input is managed by an Input (notice the 20 | capital I) component. We can say that the input is a controlled 21 | component, and it's controlled by the Input. The Input owns the state of 22 | the input. 23 |

24 |

25 | File: patterns/RenderProps/example/Input.jsx 26 |

27 |

28 | 29 | {({ value, onChange }) => ( 30 | 35 | )} 36 | 37 |

38 |

39 | This technique gives us a lot of control on how the consumer of the 40 | Input wants to render the input 41 |

42 |

43 | Uncomment the following code 44 | {/* 45 | {({ value, onChange }) => ( 46 | 47 | {" "} 54 | Red 55 |
56 | {" "} 63 | Blue 64 |
65 |
66 | )} 67 | */} 68 |

69 |
70 |
71 | ); 72 | 73 | export default Example; 74 | -------------------------------------------------------------------------------- /src/components/patterns/RenderProps/exercise/Field.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export class Field extends React.Component { 4 | state = { 5 | value: "" 6 | }; 7 | 8 | onChange = e => { 9 | this.setState({ value: e.target.value }, () => { 10 | console.log("Current Field state:", this.state.value); 11 | }); 12 | }; 13 | 14 | render() { 15 | const { value } = this.state; 16 | const { onChange } = this; 17 | 18 | return this.props.children({ value, onChange }); 19 | } 20 | } 21 | 22 | export default Field; 23 | -------------------------------------------------------------------------------- /src/components/patterns/RenderProps/exercise/Measure.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class Measure extends React.Component { 5 | constructor() { 6 | super(); 7 | this.state = { 8 | width: 0 9 | }; 10 | this.handleResize = this.handleResize.bind(this); 11 | } 12 | 13 | componentDidMount() { 14 | this.handleResize(); 15 | window.addEventListener("resize", this.handleResize, false); 16 | } 17 | 18 | componentWillUnmount() { 19 | window.removeEventListener("resize", this.handleResize, false); 20 | } 21 | 22 | handleResize() { 23 | const { clientWidth } = this.wrapper; 24 | this.setState({ 25 | width: clientWidth 26 | }); 27 | } 28 | 29 | render() { 30 | const { width } = this.state; 31 | 32 | return ( 33 |
(this.wrapper = ref)}> 34 | You need to add something here. Don't change the wrapping div. 35 |
36 | ); 37 | } 38 | } 39 | 40 | Measure.propTypes = { 41 | children: PropTypes.func.isRequired 42 | }; 43 | 44 | export default Measure; 45 | -------------------------------------------------------------------------------- /src/components/patterns/RenderProps/exercise/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // import Field from "./Field"; 3 | // import Measure from "./Measure"; 4 | 5 | const Example = () => ( 6 |
7 |
8 |

Exercise 1

9 |

Introduction

10 |

11 | The advantage of separating the previous code into an Input to handle 12 | the state and an input (notice it's not capital I) to handle the 13 | appearance is that the Input functionality can also be applied to other 14 | form controls like radio buttons, or select fields, etc. In the end, al 15 | the form fields have some state that changes. Threfore, it probably 16 | makes more sense to call it Field instead of Input. 17 |

18 |

Task

19 |

20 | You need to edit this file{" "} 21 | patterns/RenderProps/exercise/index.jsx so: 22 |

23 |
    24 |
  • 25 | {" "} 26 | The <select> (there is only one select in that 27 | file) should be composed with a <Field>{" "} 28 | component (the Field.jsx component is already imported 29 | ). 30 |
  • 31 |
  • 32 | The Field component will provide{" "} 33 | 1) the value and 2) the onChange function to the 34 | <select> using a function as a children (AKA Render Props 35 | pattern) 36 |
  • 37 |
  • 38 | You will know it works because the value of the <select> will be 39 | displayed on the console every time the <select> changes. 40 |
  • 41 |
  • 42 | You don't have to edit this file{" "} 43 | 44 | patterns/RenderProps/exercise/Field.jsx 45 | 46 |
  • 47 |
48 |

49 | Hint: it's the same we do when using the Input component in the previous 50 | example. Check this file patterns/RenderProps/example/index.jsx 51 |

52 |

53 | 57 |

58 |
59 |
60 |

Exercise 2

61 |

62 | 67 | source code exercise branch » 68 | 69 |

70 |

71 | In this case we want to implement a Measure component that can compose 72 | other components (they'll be the Measure's children) and provide them 73 | with its width (width of Measure's children). 74 |

75 |

Tasks

76 |
    77 |
  • 78 | Edit this file patterns/RenderProps/exercise/Measure.jsx so 79 | the render method invokes the this.props.children and passes the width 80 | as an argument 81 |
  • 82 | 83 |
  • 84 | Edit this file patterns/RenderProps/exercise/index.jsx so the 85 | <figure> (there is only one <figure> on that file) is 86 | composed with the <Measure> 87 |
  • 88 |
  • 89 | Inside the <figcaption> render the width provided by the Measure 90 | component. 91 |
  • 92 |
93 |
94 |
95 |
96 | dog 97 |
98 | My width is {"REPLACE THIS WITH THE ACTUAL width"} px 99 |
100 |
101 |
102 |
103 | ); 104 | 105 | export default Example; 106 | -------------------------------------------------------------------------------- /src/components/patterns/RenderProps/exercise_bonus/BadImplementation/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import searchGitHubIssues from "../utils/searchGitHubIssues"; 3 | import ScrollNotifier from "../ScrollNotifier"; 4 | import { GITHUB_ISSUES_API_URL } from "../utils/config"; 5 | 6 | class GitHubIssueSearcher extends React.Component { 7 | constructor() { 8 | super(); 9 | this.state = { 10 | isFetching: false, 11 | issues: [], 12 | links: {}, 13 | }; 14 | } 15 | 16 | componentDidMount() { 17 | const { 18 | query = "react", 19 | label = "bug", 20 | language = "javascript", 21 | state = "open", 22 | sort = "sort", 23 | order = "asc", 24 | } = this.props; 25 | 26 | this.fetch( 27 | `${GITHUB_ISSUES_API_URL}?q=${query}+label:${label}+language:${language}+state:${state}&sort=${sort}&order=${order}` 28 | ); 29 | } 30 | 31 | fetchNextPage = () => { 32 | this.fetch(this.state.links.next); 33 | }; 34 | 35 | fetch = (url) => { 36 | this.setState({ isFetching: true }); 37 | searchGitHubIssues(url, (issues, links) => { 38 | this.setState({ 39 | isFetching: false, 40 | issues: [...this.state.issues, ...issues], 41 | links, 42 | }); 43 | }); 44 | }; 45 | 46 | render() { 47 | const fetchNextPage = 48 | !this.state.isFetching && this.state.links.next 49 | ? this.fetchNextPage 50 | : null; 51 | 52 | const fetchingComponent = !this.state.isFetching ? null : ( 53 |
  • 54 |

    55 | 56 |

    57 |
  • 58 | ); 59 | 60 | return ( 61 | 65 | 80 | 81 | ); 82 | } 83 | } 84 | 85 | export default GitHubIssueSearcher; 86 | -------------------------------------------------------------------------------- /src/components/patterns/RenderProps/exercise_bonus/HoCs/GitHubIssueSearchInfiniteScroller.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import withGitHubIssueSearch from "./withGitHubIssueSearch"; 3 | import ScrollNotifier from "../ScrollNotifier"; 4 | 5 | const IssueInfiniteScroller = ({ issues, fetchNextPage, isFetching }) => ( 6 | 7 | 24 | 25 | ); 26 | 27 | export default withGitHubIssueSearch(IssueInfiniteScroller); 28 | -------------------------------------------------------------------------------- /src/components/patterns/RenderProps/exercise_bonus/HoCs/withGitHubIssueSearch.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import searchGitHubIssues from '../utils/searchGitHubIssues' 3 | import { GITHUB_ISSUES_API_URL } from '../utils/config' 4 | 5 | const withGitHubIssueSearch = MyComponent => ( 6 | class ComponentWithGitHubIssueSearch extends React.Component { 7 | constructor() { 8 | super() 9 | this.state = { 10 | isFetching: false, 11 | issues: [], 12 | links: {} 13 | } 14 | } 15 | 16 | componentDidMount() { 17 | const { 18 | query = 'react', 19 | label = 'bug', 20 | language = 'javascript', 21 | state = 'open', 22 | sort = 'sort', 23 | order = 'asc' 24 | } = this.props 25 | 26 | this.fetch(`${GITHUB_ISSUES_API_URL}?q=${query}+label:${label}+language:${language}+state:${state}&sort=${sort}&order=${order}`) 27 | } 28 | 29 | fetchNextPage = () => { 30 | this.fetch(this.state.links.next) 31 | } 32 | 33 | fetch = (url) => { 34 | this.setState({ isFetching: true }) 35 | searchGitHubIssues(url, (issues, links) => { 36 | this.setState({ 37 | isFetching: false, 38 | issues: [...this.state.issues, ...issues], 39 | links 40 | }) 41 | }) 42 | } 43 | 44 | render() { 45 | const fetchNextPage = !this.state.isFetching && this.state.links.next ? 46 | this.fetchNextPage : 47 | null 48 | const { issues, isFetching } = this.state 49 | 50 | return 56 | } 57 | } 58 | ) 59 | 60 | export default withGitHubIssueSearch 61 | -------------------------------------------------------------------------------- /src/components/patterns/RenderProps/exercise_bonus/RenderCallback/GitHubIssueSearcher.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Hint: You must move the logic from the Higher-Order Component here. 4 | Where is the logic in withGitHubIssueSearch or in GitHubIssueSearchInfiniteScroller? 5 | 6 | Steps: 7 | 1. Copy the logic from the HoC into this file 8 | 2. The Render Props is a component and the HoC is a function. Once you copy 9 | the logic from the HoC here you will have to remove the main function 10 | 3. Refactor the return of the render method of the HoC code in this file to 11 | "execute" the children and pass the right arguments 12 | 13 | */ 14 | -------------------------------------------------------------------------------- /src/components/patterns/RenderProps/exercise_bonus/RenderCallback/index.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Hint: 4 | - Move the composed component into this file 5 | - GitHubIssueSearch is a component, not a function. You should import ./GitHubIssueSearcher 6 | here and receive the arguments from GitHubIssueSearch using an arrow function 7 | 8 | */ 9 | -------------------------------------------------------------------------------- /src/components/patterns/RenderProps/exercise_bonus/ScrollNotifier.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const ScrollNotifier = ({ buffer, onScrollAtTheBottom, style, children }) => { 5 | const handleScroll = event => { 6 | const { scrollTop, scrollHeight, clientHeight } = event.target 7 | const isAtTheBottom = scrollTop + clientHeight >= scrollHeight - buffer 8 | 9 | if (isAtTheBottom && onScrollAtTheBottom) 10 | onScrollAtTheBottom() 11 | } 12 | 13 | return ( 14 |
    19 | {children} 20 |
    21 | ) 22 | } 23 | 24 | ScrollNotifier.propTypes = { 25 | buffer: PropTypes.number.isRequired, 26 | onScrollAtTheBottom: PropTypes.func 27 | } 28 | 29 | ScrollNotifier.defaultProps = { 30 | buffer: 300 31 | } 32 | 33 | export default ScrollNotifier 34 | -------------------------------------------------------------------------------- /src/components/patterns/RenderProps/exercise_bonus/utils/config.js: -------------------------------------------------------------------------------- 1 | export const GITHUB_ISSUES_API_URL = "https://api.github.com/search/issues"; 2 | -------------------------------------------------------------------------------- /src/components/patterns/RenderProps/exercise_bonus/utils/searchGitHubIssues.js: -------------------------------------------------------------------------------- 1 | import jsonp from "jsonp"; 2 | 3 | const searchGitHubIssues = (url, callback) => { 4 | jsonp(url, (error, response) => { 5 | const links = response.meta.Link.reduce((links, link) => { 6 | links[link[1].rel] = link[0]; 7 | return links; 8 | }, {}); 9 | callback(response.data.items, links); 10 | }); 11 | }; 12 | 13 | export default searchGitHubIssues; 14 | -------------------------------------------------------------------------------- /src/components/patterns/StateReducer/Page.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Example from "./example"; 3 | import ExerciseOne from "./exercise_1"; 4 | import ExerciseTwo from "./exercise_2"; 5 | import ExerciseThree from "./exercise_3/"; 6 | import BonusExercise from "./exercise_bonus/"; 7 | 8 | const Page = () => ( 9 | 10 |

    State Reducer

    11 |

    12 | State reducer allows consumers to control how the state is managed. This 13 | means the consumer has control over some logic in the parent. This is very 14 | useful in combination with the Render Props. This is called inversion of 15 | control. Heads up, the State Reducer pattern exposes implementation 16 | details by exposing component state logic. Think twice before applying 17 | this pattern. 18 |

    19 |

    Example

    20 |

    21 | 26 | source code exercise branch » 27 | 28 |

    29 | 30 | 31 |

    Exercise 1

    32 |

    33 | 38 | source code exercise branch » 39 | 40 |

    41 | 42 | 43 |

    Exercise 2

    44 |

    45 | 50 | source code exercise branch » 51 | 52 |

    53 | 54 | 55 |

    Exercise 3

    56 |

    57 | 62 | source code exercise branch » 63 | 64 |

    65 | 66 | 67 |

    Bonus exercise

    68 |

    69 | 74 | source code exercise branch » 75 | 76 |

    77 | 78 |
    79 | ); 80 | 81 | export default Page; 82 | -------------------------------------------------------------------------------- /src/components/patterns/StateReducer/example/Field.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export class Field extends React.Component { 4 | initialState = { 5 | value: "" 6 | }; 7 | state = this.initialState; 8 | static defaultProps = { 9 | stateReducer: (state, change) => change 10 | }; 11 | 12 | onChange = e => { 13 | const { stateReducer } = this.props; 14 | const { value } = e.target; 15 | 16 | this.setState( 17 | state => stateReducer(state, { ...state, value }), 18 | () => { 19 | console.log("Current Field state:", this.state.value); 20 | } 21 | ); 22 | }; 23 | 24 | render() { 25 | const { value } = this.state; 26 | const { onChange } = this; 27 | 28 | return this.props.children({ value, onChange }); 29 | } 30 | } 31 | 32 | export default Field; 33 | -------------------------------------------------------------------------------- /src/components/patterns/StateReducer/example/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Field from "./Field"; 3 | 4 | const Example = () => ( 5 | 6 |

    The following input gets the state from the parent Field component.

    7 |

    8 | 9 | {({ value, onChange }) => ( 10 | 15 | )} 16 | 17 |

    18 |

    19 | The following input only lets the user type numbers by implementing a new 20 | stateReducer that only accepts numbers. 21 |

    22 |

    23 | (isNaN(change.value) ? state : change)} 25 | > 26 | {({ value, onChange }) => ( 27 | 28 | )} 29 | 30 |

    31 |
    32 | ); 33 | 34 | export default Example; 35 | -------------------------------------------------------------------------------- /src/components/patterns/StateReducer/exercise_1/Field.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export class Field extends React.Component { 4 | initialState = { 5 | value: "😄" 6 | }; 7 | state = this.initialState; 8 | static defaultProps = { 9 | stateReducer: (state, { type, ...change }) => change 10 | }; 11 | 12 | onChange = e => { 13 | const { stateReducer } = this.props; 14 | const { value } = e.target; 15 | 16 | this.setState(state => stateReducer(state, { ...state, value })); 17 | }; 18 | 19 | onReset = e => { 20 | // TODO finish the implementation of this method so when it's 21 | // executed it invokes the state reducer with the initial state 22 | }; 23 | 24 | render() { 25 | const { value } = this.state; 26 | const { onChange, onReset } = this; 27 | 28 | return this.props.children({ value, onChange, onReset }); 29 | } 30 | } 31 | 32 | export default Field; 33 | -------------------------------------------------------------------------------- /src/components/patterns/StateReducer/exercise_1/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Field from "./Field"; 3 | 4 | const Example = () => ( 5 | 6 |

    7 | Implement the onReset method on 8 | `src/components/patterns/StateReducer/exercise_1/Field.jsx`, so when the 9 | input executes onReset the state reducer is invoked with the initial state 10 |

    11 |

    12 | 13 | {({ value, onChange, onReset }) => ( 14 | 15 | 20 | 21 | 22 | )} 23 | 24 |

    25 |
    26 | ); 27 | 28 | export default Example; 29 | -------------------------------------------------------------------------------- /src/components/patterns/StateReducer/exercise_2/Field.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const ON_FIELD_CHANGE = "ON_FIELD_CHANGE"; 4 | export const ON_FIELD_RESET = "ON_FIELD_RESET"; 5 | 6 | export class Field extends React.Component { 7 | initialState = { 8 | value: "😄" 9 | }; 10 | state = this.initialState; 11 | static defaultProps = { 12 | stateReducer: (state, { type, ...change }) => change 13 | }; 14 | 15 | onChange = e => { 16 | const { stateReducer } = this.props; 17 | const { value } = e.target; 18 | 19 | this.setState(state => 20 | stateReducer(state, { type: ON_FIELD_CHANGE, value }) 21 | ); 22 | }; 23 | 24 | onReset = e => { 25 | // TODO finish the implementation of this method. You need to use the ON_FIELD_RESET type. 26 | }; 27 | 28 | render() { 29 | const { value } = this.state; 30 | const { onChange, onReset } = this; 31 | 32 | return this.props.children({ value, onChange, onReset }); 33 | } 34 | } 35 | 36 | export default Field; 37 | -------------------------------------------------------------------------------- /src/components/patterns/StateReducer/exercise_2/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Field, { ON_FIELD_RESET } from "./Field"; 3 | 4 | const Example = () => ( 5 | 6 |

    7 | We are adding a type key to the change object. By doing so we get more 8 | flexibility on how to handle the changes in the state reducer based on the 9 | action that is executed. 10 |

    11 |

    12 | Extend `src/components/patterns/StateReducer/exercise_2/Field.jsx` so the 13 | intitial value in the state is a emoji smiley face but when the user 14 | resets the state it's an empty string. To achieve this you'll need to edit 15 | two different files: 16 |

    17 |
      18 |
    • 19 | In `src/components/patterns/StateReducer/exercise_2/Field.jsx` you will 20 | have to refactor the onReset method and add an{" "} 21 | ON_FIELD_RESET type to the change. 22 |
    • 23 |
    • 24 | In `src/components/patterns/StateReducer/exercise_2/index.js` you will 25 | have to add a stateReducer prop to the Field component. Hint, use a 26 | ternary operator based on the type ON_FIELD_RESET. 27 |
    • 28 |
    29 |

    30 | 31 | {({ value, onChange, onReset }) => ( 32 | 33 | 38 | 39 | 40 | )} 41 | 42 |

    43 |
    44 | ); 45 | 46 | export default Example; 47 | -------------------------------------------------------------------------------- /src/components/patterns/StateReducer/exercise_3/Dropdown.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const CLICKED_OUTSIDE_ACTION = "CLICKED_OUTSIDE_ACTION"; 4 | export const TOGGLE_DROPDOWN_ACTION = "TOGGLE_DROPDOWN_ACTION"; 5 | 6 | class Dropdown extends React.Component { 7 | state = { 8 | isOpen: false 9 | }; 10 | 11 | wrapperRef = React.createRef(); 12 | 13 | static defaultProps = { 14 | stateReducer: (state, change) => change 15 | }; 16 | 17 | componentWillUnmount() { 18 | this.removeMousedownListener(); 19 | } 20 | 21 | addMousedownListener = () => { 22 | document.addEventListener("mousedown", this.handleClickOutside); 23 | }; 24 | 25 | removeMousedownListener = () => { 26 | document.removeEventListener("mousedown", this.handleClickOutside); 27 | }; 28 | 29 | handleClickOutside = event => { 30 | if ( 31 | this.wrapperRef && 32 | !this.wrapperRef.current.contains(event.target) && 33 | this.state.isOpen 34 | ) { 35 | this.toggleIsOpen(CLICKED_OUTSIDE_ACTION); 36 | this.removeMousedownListener(); 37 | } 38 | }; 39 | 40 | toggleIsOpen = actionType => { 41 | if (!this.state.isOpen) { 42 | this.addMousedownListener(); 43 | } 44 | 45 | const { stateReducer } = this.props; 46 | 47 | this.setState(state => { 48 | const change = { 49 | type: actionType, 50 | isOpen: !state.isOpen 51 | }; 52 | return stateReducer(state, change); 53 | }); 54 | }; 55 | 56 | onToggleDropdown = event => { 57 | event.preventDefault(); 58 | this.toggleIsOpen(TOGGLE_DROPDOWN_ACTION); 59 | }; 60 | 61 | render() { 62 | const { isOpen } = this.state; 63 | const { onToggleDropdown } = this; 64 | 65 | return ( 66 |
    70 | {this.props.children({ onToggleDropdown, isOpen })} 71 |
    72 | ); 73 | } 74 | } 75 | 76 | export default Dropdown; 77 | -------------------------------------------------------------------------------- /src/components/patterns/StateReducer/exercise_3/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Dropdown, { CLICKED_OUTSIDE_ACTION } from "./Dropdown"; 3 | import styled from "styled-components"; 4 | 5 | export const Ul = styled.ul` 6 | position: absolute; 7 | list-style: none; 8 | margin: 0px 0 0 -18px; 9 | padding: 18px 18px 36px 18px; 10 | background-color: white; 11 | li { 12 | padding-left: 0; 13 | list-style-type: none; 14 | } 15 | `; 16 | 17 | const Button = styled.i` 18 | cursor: pointer; 19 | width: 0; 20 | height: 0; 21 | border-left: 8px solid transparent; 22 | border-right: 8px solid transparent; 23 | display: inline-block; 24 | margin-left: 9px; 25 | padding-bottom: 2px; 26 | ${props => 27 | props.open 28 | ? `border-bottom: 8px solid grey;` 29 | : `border-top: 8px solid grey;`}; 30 | `; 31 | 32 | const Example = () => ( 33 | 34 |

    35 | Extend the Dropdown so it doesn't close when the user clicks outside the 36 | dropdown. To achieve this you'll need to edit only one file: 37 |

    38 |
      39 |
    • 40 | In `src/components/patterns/StateReducer/exercise_3/index.js` you will 41 | have to add a stateReducer prop to the Dropdown component. Hint, use a 42 | ternary operator based on the type CLICKED_OUTSIDE_ACTION. 43 |
    • 44 |
    45 | 46 | {({ onToggleDropdown, isOpen }) => ( 47 | 48 | 49 | Select a color 50 |
    89 | ); 90 | 91 | export default Example; 92 | -------------------------------------------------------------------------------- /src/components/patterns/Theming/Page.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Example from "./example"; 3 | import Exercise from "./exercise"; 4 | 5 | const Page = props => ( 6 |
    7 |

    Theming

    8 |

    9 | styled-components has full theming support by exporting a{" "} 10 | ThemeProvider wrapper component. This component provides a 11 | theme to all React components underneath itself via the context API. In 12 | the render tree all styled-components will have access to the provided 13 | theme, even when they are multiple levels deep. 14 |

    15 |

    16 | Using a theme help us to share values and styles through out all styled 17 | components. you can see the theme we are using in 18 | src/components/patterns/Theming/example/theme.js. On every 19 | styled component you will have access to a theme prop 20 | attached to component props. 21 |

    22 |

    Example

    23 |

    24 | 29 | source code exercise branch » 30 | 31 |

    32 | 33 |
    34 |

    Exercise

    35 |

    36 | 41 | source code exercise branch » 42 | 43 |

    44 |
    45 | 46 |
    47 |
    48 | ); 49 | 50 | export default Page; 51 | -------------------------------------------------------------------------------- /src/components/patterns/Theming/example/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled, { ThemeProvider } from "styled-components"; 3 | import theme from "./theme"; 4 | 5 | const Button = styled("button")` 6 | background: white; 7 | border-radius: 8px; 8 | font-size: 24px; 9 | font-weight: 800; 10 | padding: 8px 16px; 11 | transition: all 0.2s ease; 12 | color: ${props => props.theme.colors.blue}; 13 | border: 2px solid ${props => props.theme.colors.blue}; 14 | 15 | &:hover { 16 | color: ${props => props.theme.colors.green}; 17 | border: 2px solid ${props => props.theme.colors.green}; 18 | } 19 | `; 20 | 21 | const Wrapper = styled("div")` 22 | padding: 40px; 23 | background: ${props => props.theme.colors.background}; 24 | `; 25 | 26 | const ThemingExample = () => ( 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | 34 | export default ThemingExample; 35 | -------------------------------------------------------------------------------- /src/components/patterns/Theming/example/theme.js: -------------------------------------------------------------------------------- 1 | const theme = { 2 | colors: { 3 | blue: '#07c', 4 | green: '#3f714c', 5 | background: '#f6f6ff' 6 | }, 7 | } 8 | 9 | export default theme; 10 | -------------------------------------------------------------------------------- /src/components/patterns/Theming/exercise/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | // import theme from "./theme"; 4 | 5 | /* 6 | Exercise TODO: 7 | - wrap the whole component with the `ThemeProvider` from "styled-components". 8 | - create a simple theme in `theme.js` and import it. (`import theme from './theme'`) 9 | - this theme should have the base colors for your app. 10 | */ 11 | 12 | /* 13 | remember that you can call a function inside styled-components: 14 | color: ${props => YOUR EXPRESSION}; 15 | */ 16 | const Card = styled("div")` 17 | border-radius: 8px; 18 | padding: 16px; 19 | box-shadow: 0 0 8px 4px rgba(0, 0, 0, 0.1); 20 | font-size: 18px; 21 | font-weight: 800; 22 | text-align: center; 23 | background-color: white; 24 | `; 25 | 26 | /* 27 | in this component just use the background color from the theme and asign it to this component 28 | */ 29 | const Wrapper = styled("div")` 30 | padding: 40px; 31 | `; 32 | 33 | /* 34 | TODO: you need to "wrap" your component with the `ThemeProvider` component 35 | - remember that you need to pass a `theme` to this provider. 36 | - documentation: 37 | */ 38 | 39 | const ThemingExercise = () => ( 40 | 41 | Hallo I'm a Card 42 | 43 | ); 44 | 45 | export default ThemingExercise; 46 | -------------------------------------------------------------------------------- /src/components/patterns/Theming/exercise/theme.js: -------------------------------------------------------------------------------- 1 | const theme = { 2 | // add some `colors` here maybe?? 3 | } 4 | 5 | export default theme; 6 | -------------------------------------------------------------------------------- /src/components/patterns/Variants/Page.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Example from "./example"; 3 | import Exercise from "./exercise"; 4 | 5 | const Page = props => ( 6 |
    7 |

    Variants

    8 |

    9 | The variant style utilities allow you to define reusable style objects in 10 | your theme for things like text styles and color combinations. 11 |

    12 | 13 |

    Example

    14 |

    15 | 20 | source code exercise branch » 21 | 22 |

    23 |

    24 | In this example, you will see how we can define Button Variants, and 25 | effectively apply it to our components. 26 |

    27 | 28 |
    29 |

    Exercise

    30 |

    31 | 36 | source code exercise branch » 37 | 38 |

    39 |

    40 | The goal of this exercise is to create variants for some alert components. 41 | In this simple example, we can focus on changing the{" "} 42 | background-color & color of each variant. 43 |

    44 |

    The variants should be:

    45 |
      46 |
    • default
    • 47 |
    • success
    • 48 |
    • warning
    • 49 |
    • error
    • 50 |
    51 | 52 |
    53 |

    Bonus

    54 |

    55 | now let's try to embrace more tools from styled-system. the 56 | new things we are going to cover as a demo are: 57 |

    58 |
      59 |
    • 60 | styled-system utilities like space 61 |
    • 62 |
    • 63 | the use of the prop as from styled-components 64 |
    • 65 |
    • mix all with your theme
    • 66 |
    67 |
    68 | ); 69 | 70 | export default Page; 71 | -------------------------------------------------------------------------------- /src/components/patterns/Variants/example/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled, { ThemeProvider } from "styled-components"; 3 | import theme from "./theme"; 4 | import { variant } from "styled-system"; 5 | 6 | const buttonStyle = variant({ 7 | key: "buttons", 8 | }); 9 | 10 | const Button = styled("button")` 11 | background: white; 12 | border-radius: 8px; 13 | font-size: 18px; 14 | font-weight: 800; 15 | padding: 16px; 16 | margin: 16px; 17 | transition: all 0.2s ease; 18 | ${buttonStyle}; 19 | `; 20 | 21 | Button.defaultProps = { 22 | variant: "primary", 23 | }; 24 | 25 | const Wrapper = styled("div")` 26 | padding: 40px; 27 | background: ${(props) => props.theme.colors.background}; 28 | `; 29 | 30 | const VariantsExample = () => ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | 39 | export default VariantsExample; 40 | -------------------------------------------------------------------------------- /src/components/patterns/Variants/example/theme.js: -------------------------------------------------------------------------------- 1 | const COLOR_BLUE = '#07c'; 2 | const COLOR_GREEN = '#3f714c'; 3 | const COLOR_BACKGROUND = '#f6f6ff'; 4 | 5 | const theme = { 6 | colors: { 7 | blue: COLOR_BLUE, 8 | green: COLOR_GREEN, 9 | background: COLOR_BACKGROUND 10 | }, 11 | buttons: { 12 | primary: { 13 | color: COLOR_BLUE, 14 | border: `3px solid ${COLOR_BLUE}`, 15 | ':hover': { 16 | backgroundColor: COLOR_BLUE, 17 | color: "white" 18 | } 19 | }, 20 | secondary: { 21 | color: COLOR_GREEN, 22 | border: `3px solid ${COLOR_GREEN}`, 23 | ':hover': { 24 | backgroundColor: COLOR_GREEN, 25 | color: "white" 26 | } 27 | } 28 | } 29 | } 30 | 31 | export default theme; 32 | -------------------------------------------------------------------------------- /src/components/patterns/Variants/exercise/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled, { ThemeProvider } from "styled-components"; 3 | import theme from "./theme"; 4 | // import { variant } from 'styled-system' 5 | 6 | // TODO: create a variant called `alertStyle` 7 | /* 8 | in order to add these new variant styles to your component, 9 | you need to add it to your styles AT THE END. 10 | 11 | *if you have questions why at the end ask us! 12 | */ 13 | 14 | const Alert = styled("div")` 15 | border-radius: 8px; 16 | padding: 8px; 17 | box-shadow: 0 2px 1px -1px rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.14), 18 | 0 1px 3px 0 rgba(0, 0, 0, 0.12); 19 | font-size: 18px; 20 | margin: 24px; 21 | font-weight: 800; 22 | text-align: center; 23 | `; 24 | 25 | const Wrapper = styled("div")` 26 | background-color: ${props => props.theme.colors.background}; 27 | padding: 40px; 28 | `; 29 | 30 | // TODO: here you have hints on how the variants should be named... 31 | const VariantsExercise = () => ( 32 | 33 | 34 | default alert 35 | Success alert 36 | Error alert 37 | Warning alert 38 | 39 | 40 | ); 41 | 42 | export default VariantsExercise; 43 | -------------------------------------------------------------------------------- /src/components/patterns/Variants/exercise/theme.js: -------------------------------------------------------------------------------- 1 | const theme = { 2 | colors: { 3 | primary: "peru", 4 | secondary: "#f6f6ff", 5 | background: "papayawhip" 6 | }, 7 | // add some variants for your alerts here 8 | } 9 | 10 | export default theme; 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import App from "./components/App"; 5 | import "./main.css"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | -------------------------------------------------------------------------------- /src/main.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Barlow:wght@400;800&display=swap"); 2 | 3 | html, 4 | body { 5 | font-family: "Barlow", sans-serif; 6 | font-size: 16px; 7 | padding-bottom: 25px; 8 | } 9 | h1, 10 | h2, 11 | h3, 12 | h4, 13 | h5, 14 | h6 { 15 | font-weight: 800; 16 | } 17 | h3, 18 | h4 { 19 | padding: 10px 0; 20 | } 21 | 22 | #page-wrap > div:first-child { 23 | position: absolute; 24 | height: 100%; 25 | z-index: 1000; 26 | top: 0; 27 | } 28 | 29 | .floating-menu-btn { 30 | z-index: 9999; 31 | position: fixed; 32 | top: 10px; 33 | right: 10px; 34 | font-size: 2em; 35 | cursor: pointer; 36 | } 37 | 38 | .view-container { 39 | -webkit-transition: padding 0.5s; 40 | -moz-transition: padding 0.5s; 41 | -o-transition: padding 0.5s; 42 | transition: padding 0.5s; 43 | } 44 | 45 | .bm-cross-button { 46 | display: none; 47 | } 48 | 49 | /* Position and sizing of burger button */ 50 | .bm-burger-button { 51 | position: fixed; 52 | width: 36px; 53 | height: 30px; 54 | left: 36px; 55 | top: 36px; 56 | } 57 | 58 | /* Color/shape of burger icon bars */ 59 | .bm-burger-bars { 60 | background: #373a47; 61 | } 62 | 63 | /* Position and sizing of clickable cross button */ 64 | .bm-cross-button { 65 | height: 24px; 66 | width: 24px; 67 | right: 4px !important; 68 | } 69 | 70 | /* Color/shape of close button cross */ 71 | .bm-cross { 72 | background: #bdc3c7; 73 | } 74 | 75 | /* General sidebar styles */ 76 | .bm-menu { 77 | background: #1f4653; 78 | padding: 2.5em 2em 0 1.5em; 79 | font-size: 1.15em; 80 | } 81 | 82 | .bm-menu a, 83 | .bm-menu a:link, 84 | .bm-menu a:visited, 85 | .bm-menu a:hover, 86 | .bm-menu a:active { 87 | color: white; 88 | } 89 | 90 | .bm-menu h3 { 91 | margin-top: 0; 92 | } 93 | 94 | #outer-container { 95 | position: relative; 96 | } 97 | 98 | .bm-menu-wrap { 99 | z-index: 1499 !important; 100 | } 101 | 102 | /* Morph shape necessary with bubble or elastic */ 103 | .bm-morph-shape { 104 | fill: #373a47; 105 | } 106 | 107 | /* Wrapper for item list */ 108 | .bm-item-list { 109 | color: #b8b7ad; 110 | padding: 0; 111 | } 112 | 113 | /* Styling of overlay */ 114 | .bm-overlay { 115 | background: rgba(0, 0, 0, 0.3); 116 | position: absolute !important; 117 | } 118 | 119 | .bm-burger-button { 120 | display: none; 121 | } 122 | 123 | .scroll-notifier { 124 | overflow-y: auto; 125 | overflow-x: hidden; 126 | border: solid 1px; 127 | } 128 | 129 | .issue-list { 130 | padding: 0; 131 | margin: 0; 132 | } 133 | 134 | .issue-list li { 135 | display: block; 136 | text-decoration: none; 137 | border-top: "1px solid #aaa"; 138 | padding: 10; 139 | } 140 | 141 | a { 142 | cursor: pointer; 143 | } 144 | --------------------------------------------------------------------------------