├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.js ├── App.test.js ├── index.js └── patterns ├── compound-component ├── Counter.js ├── Usage.js ├── components │ ├── Count.js │ ├── Decrement.js │ ├── Increment.js │ ├── Label.js │ ├── index.js │ └── styles.js └── useCounterContext.js ├── control-props ├── Counter.js ├── Usage.js ├── components │ ├── Count.js │ ├── Decrement.js │ ├── Increment.js │ ├── Label.js │ ├── index.js │ └── styles.js └── useCounterContext.js ├── custom-hooks ├── Counter.js ├── Usage.js ├── components │ ├── Count.js │ ├── Decrement.js │ ├── Increment.js │ ├── Label.js │ ├── index.js │ └── styles.js ├── useCounter.js └── useCounterContext.js ├── props-getters ├── Counter.js ├── Usage.js ├── components │ ├── Count.js │ ├── Decrement.js │ ├── Increment.js │ ├── Label.js │ ├── index.js │ └── styles.js ├── useCounter.js └── useCounterContext.js └── state-reducer ├── Counter.js ├── Usage.js ├── components ├── Count.js ├── Decrement.js ├── Increment.js ├── Label.js ├── index.js └── styles.js ├── useCounter.js └── useCounterContext.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Advanced React Patterns 2 | 3 | Repository used within the article : https://javascript.plainenglish.io/5-advanced-react-patterns-a6b7624267a6 4 | 5 | 6 | ## Available Scripts 7 | 8 | In the project directory, you can run: 9 | 10 | ### `npm start` 11 | 12 | Runs the app in the development mode.\ 13 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 14 | 15 | The page will reload if you make edits.\ 16 | You will also see any lint errors in the console. 17 | 18 | ### `npm run build` 19 | 20 | Builds the app for production to the `build` folder.\ 21 | It correctly bundles React in production mode and optimizes the build for the best performance. 22 | 23 | The build is minified and the filenames include the hashes.\ 24 | Your app is ready to be deployed! 25 | 26 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 27 | 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "advanced-react-patterns", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-svg-core": "^1.2.35", 7 | "@fortawesome/free-solid-svg-icons": "^5.15.3", 8 | "@fortawesome/react-fontawesome": "^0.1.14", 9 | "react": "^17.0.1", 10 | "react-dom": "^17.0.1", 11 | "react-scripts": "4.0.3", 12 | "styled-components": "^5.2.1" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build" 17 | }, 18 | "eslintConfig": { 19 | "extends": [ 20 | "react-app", 21 | "react-app/jest" 22 | ] 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexis-regnaud/advanced-react-patterns/72fa7786f2e5593d8ef51774ffe35f9d6c229b6c/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexis-regnaud/advanced-react-patterns/72fa7786f2e5593d8ef51774ffe35f9d6c229b6c/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexis-regnaud/advanced-react-patterns/72fa7786f2e5593d8ef51774ffe35f9d6c229b6c/public/logo512.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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Usage as CompoundComponent } from "./patterns/compound-component/Usage"; 4 | import { Usage as ControlProps } from "./patterns/control-props/Usage"; 5 | import { Usage as CustomHooks } from "./patterns/custom-hooks/Usage"; 6 | import { Usage as PropsGetters } from "./patterns/props-getters/Usage"; 7 | import { Usage as StateReducer } from "./patterns/state-reducer/Usage"; 8 | 9 | import { library } from "@fortawesome/fontawesome-svg-core"; 10 | import { 11 | faPlus, 12 | faPlusCircle, 13 | faPlusSquare, 14 | faMinus, 15 | faMinusCircle, 16 | faMinusSquare 17 | } from "@fortawesome/free-solid-svg-icons"; 18 | library.add( 19 | faPlus, 20 | faPlusCircle, 21 | faPlusSquare, 22 | faMinus, 23 | faMinusCircle, 24 | faMinusSquare 25 | ); 26 | 27 | export default function App() { 28 | return ( 29 | 30 | 31 |

Advanced React Pattern

32 |
33 | 34 | 35 |

Compound component pattern

36 | 37 |
38 | 39 | 40 |

Control props pattern

41 | 42 |
43 | 44 | 45 |

Custom hooks pattern

46 | 47 |
48 | 49 | 50 |

Props PropsGetters pattern

51 | 52 |
53 | 54 | 55 |

State reducer Pattern

56 | 57 |
58 |
59 | ); 60 | } 61 | 62 | const StyledContainer = styled.div` 63 | text-align: center; 64 | font-family: sans-serif; 65 | `; 66 | 67 | const StyledTitleContainer = styled.div` 68 | background-color: #1428a0; 69 | color: white; 70 | padding: 35px; 71 | 72 | > h1 { 73 | margin: 0; 74 | } 75 | `; 76 | 77 | const StyledPatternContainer = styled.div` 78 | padding: 30px; 79 | border-bottom: 2px solid #d3d3d3; 80 | 81 | &:last-child { 82 | border: none; 83 | } 84 | 85 | > h2 { 86 | margin-top: 0; 87 | } 88 | `; 89 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('root') 10 | ); 11 | 12 | -------------------------------------------------------------------------------- /src/patterns/compound-component/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import styled from "styled-components"; 3 | import { CounterProvider } from "./useCounterContext"; 4 | import { Count, Label, Decrement, Increment } from "./components"; 5 | 6 | function Counter({ children, onChange, initialValue = 0 }) { 7 | const [count, setCount] = useState(initialValue); 8 | 9 | const firstMounded = useRef(true); 10 | useEffect(() => { 11 | if (!firstMounded.current) { 12 | onChange && onChange(count); 13 | } 14 | firstMounded.current = false; 15 | }, [count, onChange]); 16 | 17 | const handleIncrement = () => { 18 | setCount(count + 1); 19 | }; 20 | 21 | const handleDecrement = () => { 22 | setCount(Math.max(0, count - 1)); 23 | }; 24 | 25 | return ( 26 | 27 | {children} 28 | 29 | ); 30 | } 31 | 32 | const StyledCounter = styled.div` 33 | display: inline-flex; 34 | border: 1px solid #17a2b8; 35 | line-height: 1.5; 36 | border-radius: 0.25rem; 37 | overflow: hidden; 38 | `; 39 | 40 | Counter.Count = Count; 41 | Counter.Label = Label; 42 | Counter.Increment = Increment; 43 | Counter.Decrement = Decrement; 44 | 45 | export { Counter }; 46 | -------------------------------------------------------------------------------- /src/patterns/compound-component/Usage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Counter } from "./Counter"; 3 | 4 | function Usage() { 5 | const handleChangeCounter = (count) => { 6 | console.log("count", count); 7 | }; 8 | 9 | return ( 10 | 11 | 12 | Counter 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export { Usage }; 20 | -------------------------------------------------------------------------------- /src/patterns/compound-component/components/Count.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { useCounterContext } from "../useCounterContext"; 4 | 5 | function Count({ max }) { 6 | const { count } = useCounterContext(); 7 | 8 | const hasError = max ? count >= max : false; 9 | 10 | return {count}; 11 | } 12 | 13 | const StyledCount = styled.div` 14 | background-color: ${({ hasError }) => (hasError ? "#bd2130" : "#17a2b8")}; 15 | color: white; 16 | padding: 5px 7px; 17 | `; 18 | 19 | export { Count }; 20 | -------------------------------------------------------------------------------- /src/patterns/compound-component/components/Decrement.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StyledButton } from "./styles.js"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import { useCounterContext } from "../useCounterContext"; 5 | 6 | function Decrement({ icon = "minus" }) { 7 | const { handleDecrement } = useCounterContext(); 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export { Decrement }; 16 | -------------------------------------------------------------------------------- /src/patterns/compound-component/components/Increment.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { StyledButton } from "./styles.js"; 4 | import { useCounterContext } from "../useCounterContext"; 5 | 6 | function Increment({ icon = "plus" }) { 7 | const { handleIncrement } = useCounterContext(); 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export { Increment }; 16 | -------------------------------------------------------------------------------- /src/patterns/compound-component/components/Label.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | function Label({ children }) { 5 | return {children}; 6 | } 7 | 8 | const StyledLabel = styled.div` 9 | background-color: #e9ecef; 10 | color: #495057; 11 | padding: 5px 7px; 12 | `; 13 | 14 | export { Label }; 15 | -------------------------------------------------------------------------------- /src/patterns/compound-component/components/index.js: -------------------------------------------------------------------------------- 1 | export * from "./Count"; 2 | export * from "./Decrement"; 3 | export * from "./Increment"; 4 | export * from "./Label"; 5 | -------------------------------------------------------------------------------- /src/patterns/compound-component/components/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledButton = styled.button` 4 | background-color: white; 5 | border: none; 6 | &:hover { 7 | cursor: pointer; 8 | } 9 | &:active, 10 | &:focus { 11 | outline: none; 12 | } 13 | `; 14 | 15 | export { StyledButton }; 16 | -------------------------------------------------------------------------------- /src/patterns/compound-component/useCounterContext.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CounterContext = React.createContext(undefined); 4 | 5 | function CounterProvider({ children, value }) { 6 | return ( 7 | {children} 8 | ); 9 | } 10 | 11 | function useCounterContext() { 12 | const context = React.useContext(CounterContext); 13 | if (context === undefined) { 14 | throw new Error("useCounterContext must be used within a CounterProvider"); 15 | } 16 | return context; 17 | } 18 | 19 | export { CounterProvider, useCounterContext }; 20 | -------------------------------------------------------------------------------- /src/patterns/control-props/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import { CounterProvider } from "./useCounterContext"; 4 | import { Count, Label, Decrement, Increment } from "./components"; 5 | 6 | function Counter({ children, value = null, initialValue = 0, onChange }) { 7 | const [count, setCount] = useState(initialValue); 8 | 9 | const isControlled = value !== null && !!onChange; 10 | 11 | const getCount = () => (isControlled ? value : count); 12 | 13 | const firstMounded = useRef(true); 14 | useEffect(() => { 15 | if (!firstMounded.current && !isControlled) { 16 | onChange && onChange(count); 17 | } 18 | firstMounded.current = false; 19 | }, [count, onChange, isControlled]); 20 | 21 | const handleIncrement = () => { 22 | handleCountChange(getCount() + 1); 23 | }; 24 | 25 | const handleDecrement = () => { 26 | handleCountChange(Math.max(0, getCount() - 1)); 27 | }; 28 | 29 | const handleCountChange = (newValue) => { 30 | isControlled ? onChange(newValue) : setCount(newValue); 31 | }; 32 | 33 | return ( 34 | 37 | {children} 38 | 39 | ); 40 | } 41 | 42 | const StyledCounter = styled.div` 43 | display: inline-flex; 44 | border: 1px solid #17a2b8; 45 | line-height: 1.5; 46 | border-radius: 0.25rem; 47 | overflow: hidden; 48 | `; 49 | 50 | Counter.Count = Count; 51 | Counter.Label = Label; 52 | Counter.Increment = Increment; 53 | Counter.Decrement = Decrement; 54 | 55 | export { Counter }; 56 | -------------------------------------------------------------------------------- /src/patterns/control-props/Usage.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Counter } from "./Counter"; 3 | 4 | function Usage() { 5 | const [count, setCount] = useState(0); 6 | 7 | const handleChangeCounter = (newCount) => { 8 | setCount(newCount); 9 | }; 10 | return ( 11 | 12 | 13 | Counter 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | export { Usage }; 21 | -------------------------------------------------------------------------------- /src/patterns/control-props/components/Count.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { useCounterContext } from "../useCounterContext"; 4 | 5 | function Count({ max }) { 6 | const { count } = useCounterContext(); 7 | 8 | const hasError = max ? count >= max : false; 9 | 10 | return {count}; 11 | } 12 | 13 | const StyledCount = styled.div` 14 | background-color: ${({ hasError }) => (hasError ? "#bd2130" : "#17a2b8")}; 15 | color: white; 16 | padding: 5px 7px; 17 | `; 18 | 19 | export { Count }; 20 | -------------------------------------------------------------------------------- /src/patterns/control-props/components/Decrement.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StyledButton } from "./styles.js"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import { useCounterContext } from "../useCounterContext"; 5 | 6 | function Decrement({ icon = "minus" }) { 7 | const { handleDecrement } = useCounterContext(); 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export { Decrement }; 16 | -------------------------------------------------------------------------------- /src/patterns/control-props/components/Increment.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { StyledButton } from "./styles.js"; 4 | import { useCounterContext } from "../useCounterContext"; 5 | 6 | function Increment({ icon = "plus" }) { 7 | const { handleIncrement } = useCounterContext(); 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export { Increment }; 16 | -------------------------------------------------------------------------------- /src/patterns/control-props/components/Label.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | function Label({ children }) { 5 | return {children}; 6 | } 7 | 8 | const StyledLabel = styled.div` 9 | background-color: #e9ecef; 10 | color: #495057; 11 | padding: 5px 7px; 12 | `; 13 | 14 | export { Label }; 15 | -------------------------------------------------------------------------------- /src/patterns/control-props/components/index.js: -------------------------------------------------------------------------------- 1 | export * from "./Count"; 2 | export * from "./Decrement"; 3 | export * from "./Increment"; 4 | export * from "./Label"; 5 | -------------------------------------------------------------------------------- /src/patterns/control-props/components/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledButton = styled.button` 4 | background-color: white; 5 | border: none; 6 | &:hover { 7 | cursor: pointer; 8 | } 9 | &:active, 10 | &:focus { 11 | outline: none; 12 | } 13 | `; 14 | 15 | export { StyledButton }; 16 | -------------------------------------------------------------------------------- /src/patterns/control-props/useCounterContext.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CounterContext = React.createContext(undefined); 4 | 5 | function CounterProvider({ children, value }) { 6 | return ( 7 | {children} 8 | ); 9 | } 10 | 11 | function useCounterContext() { 12 | const context = React.useContext(CounterContext); 13 | if (context === undefined) { 14 | throw new Error("useCounterContext must be used within a CounterProvider"); 15 | } 16 | return context; 17 | } 18 | 19 | export { CounterProvider, useCounterContext }; 20 | -------------------------------------------------------------------------------- /src/patterns/custom-hooks/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import { CounterProvider } from "./useCounterContext"; 4 | import { Count, Label, Decrement, Increment } from "./components"; 5 | 6 | function Counter({ children, value: count, onChange }) { 7 | const firstMounded = useRef(true); 8 | useEffect(() => { 9 | if (!firstMounded.current) { 10 | onChange && onChange(count); 11 | } 12 | firstMounded.current = false; 13 | }, [count, onChange]); 14 | 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | } 21 | 22 | const StyledCounter = styled.div` 23 | display: inline-flex; 24 | border: 1px solid #17a2b8; 25 | line-height: 1.5; 26 | border-radius: 0.25rem; 27 | overflow: hidden; 28 | `; 29 | 30 | Counter.Count = Count; 31 | Counter.Label = Label; 32 | Counter.Increment = Increment; 33 | Counter.Decrement = Decrement; 34 | 35 | export { Counter }; 36 | -------------------------------------------------------------------------------- /src/patterns/custom-hooks/Usage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Counter } from "./Counter"; 4 | import { useCounter } from "./useCounter"; 5 | 6 | function Usage() { 7 | const { count, handleIncrement, handleDecrement } = useCounter(0); 8 | const MAX_COUNT = 10; 9 | 10 | const handleClickIncrement = () => { 11 | //Put your custom logic 12 | if (count < MAX_COUNT) { 13 | handleIncrement(); 14 | } 15 | }; 16 | 17 | return ( 18 | <> 19 | 20 | 25 | Counter 26 | 27 | 32 | 33 | 34 | 37 | 38 | 39 | ); 40 | } 41 | 42 | export { Usage }; 43 | 44 | const StyledContainer = styled.div` 45 | margin-top: 20px; 46 | `; 47 | -------------------------------------------------------------------------------- /src/patterns/custom-hooks/components/Count.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { useCounterContext } from "../useCounterContext"; 4 | 5 | function Count({ max }) { 6 | const { count } = useCounterContext(); 7 | 8 | const hasError = max ? count > max : false; 9 | 10 | return {count}; 11 | } 12 | 13 | const StyledCount = styled.div` 14 | background-color: ${({ hasError }) => (hasError ? "#bd2130" : "#17a2b8")}; 15 | color: white; 16 | padding: 5px 7px; 17 | `; 18 | 19 | export { Count }; 20 | -------------------------------------------------------------------------------- /src/patterns/custom-hooks/components/Decrement.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StyledButton } from "./styles.js"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | 5 | function Decrement({ icon = "minus", onClick }) { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export { Decrement }; 14 | -------------------------------------------------------------------------------- /src/patterns/custom-hooks/components/Increment.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { StyledButton } from "./styles.js"; 4 | 5 | function Increment({ icon = "plus", onClick }) { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export { Increment }; 14 | -------------------------------------------------------------------------------- /src/patterns/custom-hooks/components/Label.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | function Label({ children }) { 5 | return {children}; 6 | } 7 | 8 | const StyledLabel = styled.div` 9 | background-color: #e9ecef; 10 | color: #495057; 11 | padding: 5px 7px; 12 | `; 13 | 14 | export { Label }; 15 | -------------------------------------------------------------------------------- /src/patterns/custom-hooks/components/index.js: -------------------------------------------------------------------------------- 1 | export * from "./Count"; 2 | export * from "./Decrement"; 3 | export * from "./Increment"; 4 | export * from "./Label"; 5 | -------------------------------------------------------------------------------- /src/patterns/custom-hooks/components/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledButton = styled.button` 4 | background-color: white; 5 | border: none; 6 | &:hover { 7 | cursor: pointer; 8 | } 9 | &:active, 10 | &:focus { 11 | outline: none; 12 | } 13 | `; 14 | 15 | export { StyledButton }; 16 | -------------------------------------------------------------------------------- /src/patterns/custom-hooks/useCounter.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | function useCounter(intialeCount) { 4 | const [count, setCount] = useState(intialeCount); 5 | 6 | const handleIncrement = () => { 7 | setCount((prevCount) => prevCount + 1); 8 | }; 9 | 10 | const handleDecrement = () => { 11 | setCount((prevCount) => Math.max(0, prevCount - 1)); 12 | }; 13 | 14 | return { count, handleIncrement, handleDecrement }; 15 | } 16 | 17 | export { useCounter }; 18 | -------------------------------------------------------------------------------- /src/patterns/custom-hooks/useCounterContext.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CounterContext = React.createContext(undefined); 4 | 5 | function CounterProvider({ children, value }) { 6 | return ( 7 | {children} 8 | ); 9 | } 10 | 11 | function useCounterContext() { 12 | const context = React.useContext(CounterContext); 13 | if (context === undefined) { 14 | throw new Error("useCounterContext must be used within a CounterProvider"); 15 | } 16 | return context; 17 | } 18 | 19 | export { CounterProvider, useCounterContext }; 20 | -------------------------------------------------------------------------------- /src/patterns/props-getters/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import { CounterProvider } from "./useCounterContext"; 4 | import { Count, Label, Decrement, Increment } from "./components"; 5 | 6 | function Counter({ children, value: count, onChange }) { 7 | const firstMounded = useRef(true); 8 | useEffect(() => { 9 | if (!firstMounded.current) { 10 | onChange && onChange(count); 11 | } 12 | firstMounded.current = false; 13 | }, [count, onChange]); 14 | 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | } 21 | 22 | const StyledCounter = styled.div` 23 | display: inline-flex; 24 | border: 1px solid #17a2b8; 25 | line-height: 1.5; 26 | border-radius: 0.25rem; 27 | overflow: hidden; 28 | `; 29 | 30 | Counter.Count = Count; 31 | Counter.Label = Label; 32 | Counter.Increment = Increment; 33 | Counter.Decrement = Decrement; 34 | 35 | export { Counter }; 36 | -------------------------------------------------------------------------------- /src/patterns/props-getters/Usage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Counter } from "./Counter"; 4 | import { useCounter } from "./useCounter"; 5 | 6 | const MAX_COUNT = 10; 7 | 8 | function Usage() { 9 | const { 10 | count, 11 | getCounterProps, 12 | getIncrementProps, 13 | getDecrementProps 14 | } = useCounter({ 15 | initial: 0, 16 | max: MAX_COUNT 17 | }); 18 | 19 | const handleBtn1Clicked = () => { 20 | console.log("btn 1 clicked"); 21 | }; 22 | 23 | return ( 24 | <> 25 | 26 | 27 | Counter 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 40 | 41 | 42 | ); 43 | } 44 | 45 | export { Usage }; 46 | 47 | const StyledContainer = styled.div` 48 | margin-top: 20px; 49 | `; 50 | -------------------------------------------------------------------------------- /src/patterns/props-getters/components/Count.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { useCounterContext } from "../useCounterContext"; 4 | 5 | function Count({ max }) { 6 | const { count } = useCounterContext(); 7 | 8 | const hasError = max ? count >= max : false; 9 | 10 | return {count}; 11 | } 12 | 13 | const StyledCount = styled.div` 14 | background-color: ${({ hasError }) => (hasError ? "#bd2130" : "#17a2b8")}; 15 | color: white; 16 | padding: 5px 7px; 17 | `; 18 | 19 | export { Count }; 20 | -------------------------------------------------------------------------------- /src/patterns/props-getters/components/Decrement.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StyledButton } from "./styles.js"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | 5 | function Decrement({ icon = "minus", onClick, ...props }) { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export { Decrement }; 14 | -------------------------------------------------------------------------------- /src/patterns/props-getters/components/Increment.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { StyledButton } from "./styles.js"; 4 | 5 | function Increment({ icon = "plus", onClick, ...props }) { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export { Increment }; 14 | -------------------------------------------------------------------------------- /src/patterns/props-getters/components/Label.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | function Label({ children }) { 5 | return {children}; 6 | } 7 | 8 | const StyledLabel = styled.div` 9 | background-color: #e9ecef; 10 | color: #495057; 11 | padding: 5px 7px; 12 | `; 13 | 14 | export { Label }; 15 | -------------------------------------------------------------------------------- /src/patterns/props-getters/components/index.js: -------------------------------------------------------------------------------- 1 | export * from "./Count"; 2 | export * from "./Decrement"; 3 | export * from "./Increment"; 4 | export * from "./Label"; 5 | -------------------------------------------------------------------------------- /src/patterns/props-getters/components/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledButton = styled.button` 4 | background-color: white; 5 | border: none; 6 | &:hover { 7 | cursor: pointer; 8 | } 9 | &:active, 10 | &:focus { 11 | outline: none; 12 | } 13 | `; 14 | 15 | export { StyledButton }; 16 | -------------------------------------------------------------------------------- /src/patterns/props-getters/useCounter.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | //Function which concat all functions together 4 | const callFnsInSequence = (...fns) => (...args) => 5 | fns.forEach((fn) => fn && fn(...args)); 6 | 7 | function useCounter({ initial, max }) { 8 | const [count, setCount] = useState(initial); 9 | 10 | const handleIncrement = () => { 11 | setCount((prevCount) => Math.min(prevCount + 1, max)); 12 | }; 13 | 14 | const handleDecrement = () => { 15 | setCount((prevCount) => Math.max(0, prevCount - 1)); 16 | }; 17 | 18 | //props getter for 'Counter' 19 | const getCounterProps = ({ ...otherProps } = {}) => ({ 20 | value: count, 21 | "aria-valuemax": max, 22 | "aria-valuemin": 0, 23 | "aria-valuenow": count, 24 | ...otherProps 25 | }); 26 | 27 | //props getter for 'Decrement' 28 | const getDecrementProps = ({ onClick, ...otherProps } = {}) => ({ 29 | onClick: callFnsInSequence(handleDecrement, onClick), 30 | disabled: count === 0, 31 | ...otherProps 32 | }); 33 | 34 | //props getter for 'Increment' 35 | const getIncrementProps = ({ onClick, ...otherProps } = {}) => ({ 36 | onClick: callFnsInSequence(handleIncrement, onClick), 37 | disabled: count === max, 38 | ...otherProps 39 | }); 40 | 41 | return { 42 | count, 43 | handleIncrement, 44 | handleDecrement, 45 | getCounterProps, 46 | getDecrementProps, 47 | getIncrementProps 48 | }; 49 | } 50 | 51 | export { useCounter }; 52 | -------------------------------------------------------------------------------- /src/patterns/props-getters/useCounterContext.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CounterContext = React.createContext(undefined); 4 | 5 | function CounterProvider({ children, value }) { 6 | return ( 7 | {children} 8 | ); 9 | } 10 | 11 | function useCounterContext() { 12 | const context = React.useContext(CounterContext); 13 | if (context === undefined) { 14 | throw new Error("useCounterContext must be used within a CounterProvider"); 15 | } 16 | return context; 17 | } 18 | 19 | export { CounterProvider, useCounterContext }; 20 | -------------------------------------------------------------------------------- /src/patterns/state-reducer/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import { CounterProvider } from "./useCounterContext"; 4 | import { Count, Label, Decrement, Increment } from "./components"; 5 | 6 | function Counter({ children, value: count, onChange }) { 7 | const firstMounded = useRef(true); 8 | useEffect(() => { 9 | if (!firstMounded.current) { 10 | onChange && onChange(count); 11 | } 12 | firstMounded.current = false; 13 | }, [count, onChange]); 14 | 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | } 21 | 22 | const StyledCounter = styled.div` 23 | display: inline-flex; 24 | border: 1px solid #17a2b8; 25 | line-height: 1.5; 26 | border-radius: 0.25rem; 27 | overflow: hidden; 28 | `; 29 | 30 | Counter.Count = Count; 31 | Counter.Label = Label; 32 | Counter.Increment = Increment; 33 | Counter.Decrement = Decrement; 34 | 35 | export { Counter }; 36 | -------------------------------------------------------------------------------- /src/patterns/state-reducer/Usage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Counter } from "./Counter"; 4 | import { useCounter } from "./useCounter"; 5 | 6 | const MAX_COUNT = 10; 7 | function Usage() { 8 | const reducer = (state, action) => { 9 | switch (action.type) { 10 | case "decrement": 11 | return { 12 | count: Math.max(0, state.count - 2) //The decrement delta was changed for 2 (Default is 1) 13 | }; 14 | default: 15 | return useCounter.reducer(state, action); 16 | } 17 | }; 18 | 19 | const { count, handleDecrement, handleIncrement } = useCounter( 20 | { initial: 0, max: 10 }, 21 | reducer 22 | ); 23 | 24 | return ( 25 | <> 26 | 27 | 28 | Counter 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | ); 39 | } 40 | 41 | 42 | 43 | export { Usage }; 44 | 45 | const StyledContainer = styled.div` 46 | margin-top: 20px; 47 | `; 48 | -------------------------------------------------------------------------------- /src/patterns/state-reducer/components/Count.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { useCounterContext } from "../useCounterContext"; 4 | 5 | function Count({ max }) { 6 | const { count } = useCounterContext(); 7 | 8 | const hasError = max ? count >= max : false; 9 | 10 | return {count}; 11 | } 12 | 13 | const StyledCount = styled.div` 14 | background-color: ${({ hasError }) => (hasError ? "#bd2130" : "#17a2b8")}; 15 | color: white; 16 | padding: 5px 7px; 17 | `; 18 | 19 | export { Count }; 20 | -------------------------------------------------------------------------------- /src/patterns/state-reducer/components/Decrement.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StyledButton } from "./styles.js"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | 5 | function Decrement({ icon = "minus", onClick, ...props }) { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export { Decrement }; 14 | -------------------------------------------------------------------------------- /src/patterns/state-reducer/components/Increment.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { StyledButton } from "./styles.js"; 4 | 5 | function Increment({ icon = "plus", onClick, ...props }) { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export { Increment }; 14 | -------------------------------------------------------------------------------- /src/patterns/state-reducer/components/Label.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | function Label({ children }) { 5 | return {children}; 6 | } 7 | 8 | const StyledLabel = styled.div` 9 | background-color: #e9ecef; 10 | color: #495057; 11 | padding: 5px 7px; 12 | `; 13 | 14 | export { Label }; 15 | -------------------------------------------------------------------------------- /src/patterns/state-reducer/components/index.js: -------------------------------------------------------------------------------- 1 | export * from "./Count"; 2 | export * from "./Decrement"; 3 | export * from "./Increment"; 4 | export * from "./Label"; 5 | -------------------------------------------------------------------------------- /src/patterns/state-reducer/components/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const StyledButton = styled.button` 4 | background-color: white; 5 | border: none; 6 | &:hover { 7 | cursor: pointer; 8 | } 9 | &:active, 10 | &:focus { 11 | outline: none; 12 | } 13 | `; 14 | 15 | export { StyledButton }; 16 | -------------------------------------------------------------------------------- /src/patterns/state-reducer/useCounter.js: -------------------------------------------------------------------------------- 1 | import { useReducer } from "react"; 2 | 3 | const internalReducer = ({ count }, { type, payload }) => { 4 | switch (type) { 5 | case "increment": 6 | return { 7 | count: Math.min(count + 1, payload.max) 8 | }; 9 | case "decrement": 10 | return { 11 | count: Math.max(0, count - 1) 12 | }; 13 | default: 14 | throw new Error(`Unhandled action type: ${type}`); 15 | } 16 | }; 17 | 18 | function useCounter({ initial, max }, reducer = internalReducer) { 19 | const [{ count }, dispatch] = useReducer(reducer, { count: initial }); 20 | 21 | const handleIncrement = () => { 22 | dispatch({ type: "increment", payload: { max } }); 23 | }; 24 | 25 | const handleDecrement = () => { 26 | dispatch({ type: "decrement" }); 27 | }; 28 | 29 | return { 30 | count, 31 | handleIncrement, 32 | handleDecrement 33 | }; 34 | } 35 | 36 | useCounter.reducer = internalReducer; 37 | useCounter.types = { 38 | increment: "increment", 39 | decrement: "decrement" 40 | }; 41 | 42 | export { useCounter }; 43 | -------------------------------------------------------------------------------- /src/patterns/state-reducer/useCounterContext.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CounterContext = React.createContext(undefined); 4 | 5 | function CounterProvider({ children, value }) { 6 | return ( 7 | {children} 8 | ); 9 | } 10 | 11 | function useCounterContext() { 12 | const context = React.useContext(CounterContext); 13 | if (context === undefined) { 14 | throw new Error("useCounterContext must be used within a CounterProvider"); 15 | } 16 | return context; 17 | } 18 | 19 | export { CounterProvider, useCounterContext }; 20 | --------------------------------------------------------------------------------