├── .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 |
--------------------------------------------------------------------------------