├── .gitignore ├── README.md ├── lerna.json ├── package-lock.json ├── package.json └── packages ├── gatsby-starter ├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── .travis.yml ├── LICENSE ├── README.md ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── package-lock.json ├── package.json ├── src │ ├── components │ │ ├── header.test.tsx │ │ ├── header.tsx │ │ ├── image.tsx │ │ ├── layout.css │ │ ├── layout.tsx │ │ └── seo.tsx │ ├── images │ │ ├── gatsby-astronaut.png │ │ └── gatsby-icon.png │ └── pages │ │ ├── 404.tsx │ │ └── index.tsx └── tsconfig.json ├── jest-enzyme ├── .babelrc ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── setupTests.js ├── src │ ├── App.js │ ├── App.test.js │ └── index.js └── webpack.config.js ├── react-basic ├── .babelrc ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src │ └── index.js └── webpack.config.js ├── react-hooks ├── .babelrc ├── README.md ├── index.html ├── mocks │ └── comments.json ├── package-lock.json ├── package.json ├── server.js ├── setup-tests.js ├── src │ ├── callback-hook │ │ └── index.js │ ├── context-hook │ │ ├── index.enzyme.test.js │ │ ├── index.js │ │ ├── index.react-test-utils.test.js │ │ └── index.react-testing-library.test.js │ ├── effect-hook │ │ ├── data-fetching │ │ │ ├── index.enzyme.test.js │ │ │ ├── index.js │ │ │ ├── index.react-test-utils.test.js │ │ │ └── index.react-testing-library.test.js │ │ ├── index.js │ │ └── timers │ │ │ ├── index.enzyme.test.js │ │ │ ├── index.js │ │ │ ├── index.react-test-utils.test.js │ │ │ └── index.react-testing-library.test.js │ ├── imperative-handle-hook │ │ └── index.js │ ├── index.js │ ├── memo-hook │ │ └── index.js │ ├── reducer-hook │ │ └── index.js │ ├── ref-hook │ │ ├── assign-dynamically │ │ │ ├── index.enzyme.test.js │ │ │ ├── index.js │ │ │ ├── index.react-test-utils.test.js │ │ │ └── index.react-testing-library.test.js │ │ ├── forwarding-ref-hook │ │ │ ├── index.enzyme.test.js │ │ │ ├── index.js │ │ │ ├── index.react-test-utils.test.js │ │ │ └── index.react-testing-library.test.js │ │ ├── index.js │ │ ├── invoke-dom-actions │ │ │ ├── index.enzyme.test.js │ │ │ ├── index.js │ │ │ ├── index.react-test-utils.test.js │ │ │ └── index.react-testing-library.test.js │ │ ├── mutable-state │ │ │ ├── index.enzyme.test.js │ │ │ ├── index.js │ │ │ ├── index.react-test-utils.test.js │ │ │ └── index.react-testing-library.test.js │ │ └── read-dom-attributes │ │ │ ├── index.enzyme.test.js │ │ │ ├── index.js │ │ │ ├── index.react-test-utils.test.js │ │ │ └── index.react-testing-library.test.js │ └── state-hook │ │ ├── index.enzyme.test.js │ │ ├── index.js │ │ ├── index.react-test-utils.test.js │ │ └── index.react-testing-library.test.js └── webpack.config.js ├── react-redux ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── configure-store.js │ ├── counter │ │ ├── component.js │ │ ├── container.js │ │ └── reducer.js │ └── index.js └── webpack.config.js ├── react-testing-library ├── .babelrc ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── App.js │ ├── App.test.js │ └── index.js └── webpack.config.js ├── redux-saga ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── configure-store.js │ ├── counter │ │ ├── component.js │ │ ├── container.js │ │ └── reducer.js │ └── index.js └── webpack.config.js ├── redux-thunk ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── configure-store.js │ ├── counter │ │ ├── component.js │ │ ├── container.js │ │ └── reducer.js │ └── index.js └── webpack.config.js ├── typescript-jest ├── .babelrc ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── App.test.tsx │ ├── App.tsx │ └── index.tsx ├── tsconfig.json └── webpack.config.js └── typescript ├── .babelrc ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src ├── App.tsx └── index.tsx ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | 4 | node_modules/ 5 | dist/ 6 | 7 | **/*.log 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Playbook 2 | A monorepo with many starter projects for various JavaScript libraries for testing and learning purposes. Think of it 3 | like codesandbox. 4 | 5 | I'm routinely adding new packages to repo as I encounter new challenges at work and need a fast easy setup to try out 6 | concepts before I deal with more complex scenarios. It helps me master the fundamentals. 7 | 8 | If you have any suggestions for packages, either create a pull request or mention it as an issue. Feel free to ping 9 | me too. 10 | 11 | ## List of Starters 12 | 13 | | Name | Description | 14 | |:-----|:-----| 15 | | react-basic | minimalistic React setup | 16 | | react-redux | basic setup of Redux with React, commonly used state management library | 17 | | redux-saga | redux middleware for making async side effects that uses generators | 18 | | redux-thunk | redux middleware for making async side effects that uses promises | 19 | | jest-enzyme | most common React testing framework, Jest, alongside a React testing utility library called Enzyme | 20 | | react-testing-library | user-centric testing utility for React | 21 | | typescript | basic setup for TypeScript for React, a type safety system | 22 | 23 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "0.0.0" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "devDependencies": { 5 | "lerna": "^3.22.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/gatsby-starter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-gatsby", "@babel/preset-typescript"] 3 | } -------------------------------------------------------------------------------- /packages/gatsby-starter/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'plugin:react/recommended', 4 | 'standard' 5 | ], 6 | env: { 7 | browser: true, 8 | es2021: true, 9 | jest: true 10 | }, 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true 15 | }, 16 | ecmaVersion: 12, 17 | sourceType: 'module' 18 | }, 19 | settings: { 20 | react: { 21 | version: 'detect' 22 | } 23 | }, 24 | plugins: [ 25 | 'react', 26 | '@typescript-eslint', 27 | 'jest' 28 | ], 29 | ignorePatterns: [ 30 | 'node_modules', '.cache', 'public' 31 | ], 32 | rules: { 33 | 'no-use-before-define': 'off', 34 | '@typescript-eslint/no-use-before-define': ['error'], 35 | 'jest/no-disabled-tests': 'warn', 36 | 'jest/no-focused-tests': 'error', 37 | 'jest/no-identical-title': 'error', 38 | 'jest/prefer-to-have-length': 'warn', 39 | 'jest/valid-expect': 'error' 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/gatsby-starter/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variable files 55 | .env* 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # jetbrains 72 | .idea/ -------------------------------------------------------------------------------- /packages/gatsby-starter/.nvmrc: -------------------------------------------------------------------------------- 1 | v12.18.2 -------------------------------------------------------------------------------- /packages/gatsby-starter/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: yarn 3 | node_js: 4 | - 12 5 | -------------------------------------------------------------------------------- /packages/gatsby-starter/LICENSE: -------------------------------------------------------------------------------- 1 | The BSD Zero Clause License (0BSD) 2 | 3 | Copyright (c) 2020 Gatsby Inc. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /packages/gatsby-starter/README.md: -------------------------------------------------------------------------------- 1 | # Gatsby Starter 2 | 3 | ## Quick Start 4 | ``` 5 | git clone https://github.com/rajjeet/react-playbook 6 | cd react-playbook/packages/gatsby-starter 7 | npm install 8 | npm start 9 | ``` 10 | 11 | Then navigate to `http://localhost:8000` 12 | 13 | Testing using Jest + React Testing Library: `npm test` 14 | Linting using Eslint: `npm run lint` 15 | Safe-Typing using TypeScript: `npm run type-check` 16 | All 3 of these checks are ran during the build and for each pre-commit. 17 | 18 | Other features include: 19 | - `travis.yml`for build pipelines 20 | - pre-commit checks using `husky` and `lint-staged` 21 | 22 | To host your project within minutes, check out [Gatsby Cloud](https://www.gatsbyjs.com/cloud/) -------------------------------------------------------------------------------- /packages/gatsby-starter/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Browser APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.com/docs/browser-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /packages/gatsby-starter/gatsby-config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | siteMetadata: { 5 | title: 'Gatsby Default Starter', 6 | description: 'Kick off your next, great Gatsby project with this default starter. This barebones starter ships with the main Gatsby configuration files you might need.', 7 | author: '@gatsbyjs' 8 | }, 9 | plugins: [ 10 | 'gatsby-plugin-react-helmet', 11 | { 12 | resolve: 'gatsby-source-filesystem', 13 | options: { 14 | name: 'images', 15 | path: path.join(__dirname, '/src/images') 16 | } 17 | }, 18 | 'gatsby-transformer-sharp', 19 | 'gatsby-plugin-sharp', 20 | { 21 | resolve: 'gatsby-plugin-manifest', 22 | options: { 23 | name: 'gatsby-starter-default', 24 | short_name: 'starter', 25 | start_url: '/', 26 | background_color: '#663399', 27 | theme_color: '#663399', 28 | display: 'minimal-ui', 29 | icon: 'src/images/gatsby-icon.png' // This path is relative to the root of the site. 30 | } 31 | } 32 | // this (optional) plugin enables Progressive Web App + Offline functionality 33 | // To learn more, visit: https://gatsby.dev/offline 34 | // `gatsby-plugin-offline`, 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /packages/gatsby-starter/gatsby-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Node APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.com/docs/node-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /packages/gatsby-starter/gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's SSR (Server Side Rendering) APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.com/docs/ssr-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /packages/gatsby-starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-starter-default", 3 | "private": true, 4 | "description": "A simple starter to get up and developing quickly with Gatsby", 5 | "version": "0.1.0", 6 | "author": "Kyle Mathews ", 7 | "dependencies": { 8 | "gatsby": "^2.26.1", 9 | "gatsby-image": "^2.5.0", 10 | "gatsby-plugin-manifest": "^2.6.1", 11 | "gatsby-plugin-offline": "^3.4.0", 12 | "gatsby-plugin-react-helmet": "^3.4.0", 13 | "gatsby-plugin-sharp": "^2.8.0", 14 | "gatsby-source-filesystem": "^2.5.0", 15 | "gatsby-transformer-sharp": "^2.6.0", 16 | "prop-types": "^15.7.2", 17 | "react": "^16.12.0", 18 | "react-dom": "^16.12.0", 19 | "react-helmet": "^6.1.0" 20 | }, 21 | "devDependencies": { 22 | "@babel/preset-typescript": "^7.12.7", 23 | "@testing-library/react": "^11.2.3", 24 | "@types/jest": "^26.0.20", 25 | "@types/react-dom": "^17.0.0", 26 | "@types/react-helmet": "^6.1.0", 27 | "@typescript-eslint/eslint-plugin": "^4.13.0", 28 | "@typescript-eslint/parser": "^4.13.0", 29 | "babel-preset-gatsby": "^0.10.0", 30 | "eslint": "^7.18.0", 31 | "eslint-config-standard": "^16.0.2", 32 | "eslint-plugin-import": "^2.22.1", 33 | "eslint-plugin-jest": "^24.1.3", 34 | "eslint-plugin-node": "^11.1.0", 35 | "eslint-plugin-promise": "^4.2.1", 36 | "eslint-plugin-react": "^7.22.0", 37 | "husky": "^4.3.8", 38 | "jest": "^26.6.3", 39 | "lint-staged": "^10.5.3", 40 | "prettier": "2.2.1", 41 | "typescript": "^4.1.3" 42 | }, 43 | "lint-staged": { 44 | "**/*.{js,jsx,ts,tsx}": [ 45 | "npm run ci" 46 | ] 47 | }, 48 | "husky": { 49 | "hooks": { 50 | "pre-commit": "lint-staged" 51 | } 52 | }, 53 | "keywords": [ 54 | "gatsby" 55 | ], 56 | "license": "0BSD", 57 | "scripts": { 58 | "build": "gatsby build", 59 | "develop": "gatsby develop", 60 | "lint": "eslint . --ext .js,.jsx,.ts.tsx", 61 | "lint:fix": "npm run lint -- --fix", 62 | "start": "npm run develop", 63 | "serve": "gatsby serve", 64 | "clean": "gatsby clean", 65 | "type-check": "tsc -p tsconfig.json", 66 | "test": "jest", 67 | "ci": " npm run type-check && npm run lint && npm run test" 68 | }, 69 | "repository": { 70 | "type": "git", 71 | "url": "https://github.com/gatsbyjs/gatsby-starter-default" 72 | }, 73 | "bugs": { 74 | "url": "https://github.com/gatsbyjs/gatsby/issues" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/gatsby-starter/src/components/header.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react' 2 | import { Header } from './header' 3 | import React from 'react' 4 | 5 | describe('header', function () { 6 | it('should show the site title', function () { 7 | const { queryByText } = render(
) 8 | expect(queryByText('test site')).not.toBeNull() 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /packages/gatsby-starter/src/components/header.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'gatsby' 2 | import React from 'react' 3 | 4 | type HeaderProps = { 5 | siteTitle: string 6 | } 7 | 8 | export const Header = ({ siteTitle }: HeaderProps) => ( 9 |
15 |
22 |

23 | 30 | {siteTitle} 31 | 32 |

33 |
34 |
35 | ) 36 | -------------------------------------------------------------------------------- /packages/gatsby-starter/src/components/image.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useStaticQuery, graphql } from 'gatsby' 3 | import Img from 'gatsby-image' 4 | 5 | /* 6 | * This component is built using `gatsby-image` to automatically serve optimized 7 | * images with lazy loading and reduced file sizes. The image is loaded using a 8 | * `useStaticQuery`, which allows us to load the image from directly within this 9 | * component, rather than having to pass the image data down from pages. 10 | * 11 | * For more information, see the docs: 12 | * - `gatsby-image`: https://gatsby.dev/gatsby-image 13 | * - `useStaticQuery`: https://www.gatsbyjs.com/docs/use-static-query/ 14 | */ 15 | 16 | const Image = () => { 17 | const data = useStaticQuery(graphql` 18 | query { 19 | placeholderImage: file(relativePath: { eq: "gatsby-astronaut.png" }) { 20 | childImageSharp { 21 | fluid(maxWidth: 300) { 22 | ...GatsbyImageSharpFluid 23 | } 24 | } 25 | } 26 | } 27 | `) 28 | 29 | if (!data?.placeholderImage?.childImageSharp?.fluid) { 30 | return
Picture not found
31 | } 32 | 33 | return 34 | } 35 | 36 | export default Image 37 | -------------------------------------------------------------------------------- /packages/gatsby-starter/src/components/layout.css: -------------------------------------------------------------------------------- 1 | html { 2 | -ms-text-size-adjust: 100%; 3 | -webkit-text-size-adjust: 100%; 4 | font: 112.5%/1.45em georgia, serif, sans-serif; 5 | box-sizing: border-box; 6 | overflow-y: scroll; 7 | } 8 | body { 9 | margin: 0; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | color: hsla(0, 0%, 0%, 0.8); 13 | font-family: georgia, serif; 14 | font-weight: normal; 15 | word-wrap: break-word; 16 | font-kerning: normal; 17 | -moz-font-feature-settings: "kern", "liga", "clig", "calt"; 18 | -ms-font-feature-settings: "kern", "liga", "clig", "calt"; 19 | -webkit-font-feature-settings: "kern", "liga", "clig", "calt"; 20 | font-feature-settings: "kern", "liga", "clig", "calt"; 21 | } 22 | article, 23 | aside, 24 | details, 25 | figcaption, 26 | figure, 27 | footer, 28 | header, 29 | main, 30 | menu, 31 | nav, 32 | section, 33 | summary { 34 | display: block; 35 | } 36 | audio, 37 | canvas, 38 | progress, 39 | video { 40 | display: inline-block; 41 | } 42 | audio:not([controls]) { 43 | display: none; 44 | height: 0; 45 | } 46 | progress { 47 | vertical-align: baseline; 48 | } 49 | [hidden], 50 | template { 51 | display: none; 52 | } 53 | a { 54 | background-color: transparent; 55 | -webkit-text-decoration-skip: objects; 56 | } 57 | a:active, 58 | a:hover { 59 | outline-width: 0; 60 | } 61 | abbr[title] { 62 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5); 63 | cursor: help; 64 | text-decoration: none; 65 | } 66 | b, 67 | strong { 68 | font-weight: inherit; 69 | font-weight: bolder; 70 | } 71 | dfn { 72 | font-style: italic; 73 | } 74 | h1 { 75 | margin-left: 0; 76 | margin-right: 0; 77 | margin-top: 0; 78 | padding-bottom: 0; 79 | padding-left: 0; 80 | padding-right: 0; 81 | padding-top: 0; 82 | margin-bottom: 1.45rem; 83 | color: inherit; 84 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 85 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 86 | font-weight: bold; 87 | text-rendering: optimizeLegibility; 88 | font-size: 2.25rem; 89 | line-height: 1.1; 90 | } 91 | mark { 92 | background-color: #ff0; 93 | color: #000; 94 | } 95 | small { 96 | font-size: 80%; 97 | } 98 | sub, 99 | sup { 100 | font-size: 75%; 101 | line-height: 0; 102 | position: relative; 103 | vertical-align: baseline; 104 | } 105 | sub { 106 | bottom: -0.25em; 107 | } 108 | sup { 109 | top: -0.5em; 110 | } 111 | img { 112 | border-style: none; 113 | max-width: 100%; 114 | margin-left: 0; 115 | margin-right: 0; 116 | margin-top: 0; 117 | padding-bottom: 0; 118 | padding-left: 0; 119 | padding-right: 0; 120 | padding-top: 0; 121 | margin-bottom: 1.45rem; 122 | } 123 | svg:not(:root) { 124 | overflow: hidden; 125 | } 126 | code, 127 | kbd, 128 | pre, 129 | samp { 130 | font-family: monospace; 131 | font-size: 1em; 132 | } 133 | figure { 134 | margin-left: 0; 135 | margin-right: 0; 136 | margin-top: 0; 137 | padding-bottom: 0; 138 | padding-left: 0; 139 | padding-right: 0; 140 | padding-top: 0; 141 | margin-bottom: 1.45rem; 142 | } 143 | hr { 144 | box-sizing: content-box; 145 | overflow: visible; 146 | margin-left: 0; 147 | margin-right: 0; 148 | margin-top: 0; 149 | padding-bottom: 0; 150 | padding-left: 0; 151 | padding-right: 0; 152 | padding-top: 0; 153 | margin-bottom: calc(1.45rem - 1px); 154 | background: hsla(0, 0%, 0%, 0.2); 155 | border: none; 156 | height: 1px; 157 | } 158 | button, 159 | input, 160 | optgroup, 161 | select, 162 | textarea { 163 | font: inherit; 164 | margin: 0; 165 | } 166 | optgroup { 167 | font-weight: 700; 168 | } 169 | button, 170 | input { 171 | overflow: visible; 172 | } 173 | button, 174 | select { 175 | text-transform: none; 176 | } 177 | [type="reset"], 178 | [type="submit"], 179 | button, 180 | html [type="button"] { 181 | -webkit-appearance: button; 182 | } 183 | [type="button"]::-moz-focus-inner, 184 | [type="reset"]::-moz-focus-inner, 185 | [type="submit"]::-moz-focus-inner, 186 | button::-moz-focus-inner { 187 | border-style: none; 188 | padding: 0; 189 | } 190 | [type="button"]:-moz-focusring, 191 | [type="reset"]:-moz-focusring, 192 | [type="submit"]:-moz-focusring, 193 | button:-moz-focusring { 194 | outline: 1px dotted ButtonText; 195 | } 196 | fieldset { 197 | border: 1px solid silver; 198 | padding: 0.35em 0.625em 0.75em; 199 | margin-left: 0; 200 | margin-right: 0; 201 | margin-top: 0; 202 | padding-bottom: 0; 203 | padding-left: 0; 204 | padding-right: 0; 205 | padding-top: 0; 206 | margin-bottom: 1.45rem; 207 | } 208 | legend { 209 | box-sizing: border-box; 210 | color: inherit; 211 | display: table; 212 | max-width: 100%; 213 | padding: 0; 214 | white-space: normal; 215 | } 216 | textarea { 217 | overflow: auto; 218 | } 219 | [type="checkbox"], 220 | [type="radio"] { 221 | box-sizing: border-box; 222 | padding: 0; 223 | } 224 | [type="number"]::-webkit-inner-spin-button, 225 | [type="number"]::-webkit-outer-spin-button { 226 | height: auto; 227 | } 228 | [type="search"] { 229 | -webkit-appearance: textfield; 230 | outline-offset: -2px; 231 | } 232 | [type="search"]::-webkit-search-cancel-button, 233 | [type="search"]::-webkit-search-decoration { 234 | -webkit-appearance: none; 235 | } 236 | ::-webkit-input-placeholder { 237 | color: inherit; 238 | opacity: 0.54; 239 | } 240 | ::-webkit-file-upload-button { 241 | -webkit-appearance: button; 242 | font: inherit; 243 | } 244 | * { 245 | box-sizing: inherit; 246 | } 247 | *:before { 248 | box-sizing: inherit; 249 | } 250 | *:after { 251 | box-sizing: inherit; 252 | } 253 | h2 { 254 | margin-left: 0; 255 | margin-right: 0; 256 | margin-top: 0; 257 | padding-bottom: 0; 258 | padding-left: 0; 259 | padding-right: 0; 260 | padding-top: 0; 261 | margin-bottom: 1.45rem; 262 | color: inherit; 263 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 264 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 265 | font-weight: bold; 266 | text-rendering: optimizeLegibility; 267 | font-size: 1.62671rem; 268 | line-height: 1.1; 269 | } 270 | h3 { 271 | margin-left: 0; 272 | margin-right: 0; 273 | margin-top: 0; 274 | padding-bottom: 0; 275 | padding-left: 0; 276 | padding-right: 0; 277 | padding-top: 0; 278 | margin-bottom: 1.45rem; 279 | color: inherit; 280 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 281 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 282 | font-weight: bold; 283 | text-rendering: optimizeLegibility; 284 | font-size: 1.38316rem; 285 | line-height: 1.1; 286 | } 287 | h4 { 288 | margin-left: 0; 289 | margin-right: 0; 290 | margin-top: 0; 291 | padding-bottom: 0; 292 | padding-left: 0; 293 | padding-right: 0; 294 | padding-top: 0; 295 | margin-bottom: 1.45rem; 296 | color: inherit; 297 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 298 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 299 | font-weight: bold; 300 | text-rendering: optimizeLegibility; 301 | font-size: 1rem; 302 | line-height: 1.1; 303 | } 304 | h5 { 305 | margin-left: 0; 306 | margin-right: 0; 307 | margin-top: 0; 308 | padding-bottom: 0; 309 | padding-left: 0; 310 | padding-right: 0; 311 | padding-top: 0; 312 | margin-bottom: 1.45rem; 313 | color: inherit; 314 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 315 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 316 | font-weight: bold; 317 | text-rendering: optimizeLegibility; 318 | font-size: 0.85028rem; 319 | line-height: 1.1; 320 | } 321 | h6 { 322 | margin-left: 0; 323 | margin-right: 0; 324 | margin-top: 0; 325 | padding-bottom: 0; 326 | padding-left: 0; 327 | padding-right: 0; 328 | padding-top: 0; 329 | margin-bottom: 1.45rem; 330 | color: inherit; 331 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 332 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 333 | font-weight: bold; 334 | text-rendering: optimizeLegibility; 335 | font-size: 0.78405rem; 336 | line-height: 1.1; 337 | } 338 | hgroup { 339 | margin-left: 0; 340 | margin-right: 0; 341 | margin-top: 0; 342 | padding-bottom: 0; 343 | padding-left: 0; 344 | padding-right: 0; 345 | padding-top: 0; 346 | margin-bottom: 1.45rem; 347 | } 348 | ul { 349 | margin-left: 1.45rem; 350 | margin-right: 0; 351 | margin-top: 0; 352 | padding-bottom: 0; 353 | padding-left: 0; 354 | padding-right: 0; 355 | padding-top: 0; 356 | margin-bottom: 1.45rem; 357 | list-style-position: outside; 358 | list-style-image: none; 359 | } 360 | ol { 361 | margin-left: 1.45rem; 362 | margin-right: 0; 363 | margin-top: 0; 364 | padding-bottom: 0; 365 | padding-left: 0; 366 | padding-right: 0; 367 | padding-top: 0; 368 | margin-bottom: 1.45rem; 369 | list-style-position: outside; 370 | list-style-image: none; 371 | } 372 | dl { 373 | margin-left: 0; 374 | margin-right: 0; 375 | margin-top: 0; 376 | padding-bottom: 0; 377 | padding-left: 0; 378 | padding-right: 0; 379 | padding-top: 0; 380 | margin-bottom: 1.45rem; 381 | } 382 | dd { 383 | margin-left: 0; 384 | margin-right: 0; 385 | margin-top: 0; 386 | padding-bottom: 0; 387 | padding-left: 0; 388 | padding-right: 0; 389 | padding-top: 0; 390 | margin-bottom: 1.45rem; 391 | } 392 | p { 393 | margin-left: 0; 394 | margin-right: 0; 395 | margin-top: 0; 396 | padding-bottom: 0; 397 | padding-left: 0; 398 | padding-right: 0; 399 | padding-top: 0; 400 | margin-bottom: 1.45rem; 401 | } 402 | pre { 403 | margin-left: 0; 404 | margin-right: 0; 405 | margin-top: 0; 406 | margin-bottom: 1.45rem; 407 | font-size: 0.85rem; 408 | line-height: 1.42; 409 | background: hsla(0, 0%, 0%, 0.04); 410 | border-radius: 3px; 411 | overflow: auto; 412 | word-wrap: normal; 413 | padding: 1.45rem; 414 | } 415 | table { 416 | margin-left: 0; 417 | margin-right: 0; 418 | margin-top: 0; 419 | padding-bottom: 0; 420 | padding-left: 0; 421 | padding-right: 0; 422 | padding-top: 0; 423 | margin-bottom: 1.45rem; 424 | font-size: 1rem; 425 | line-height: 1.45rem; 426 | border-collapse: collapse; 427 | width: 100%; 428 | } 429 | blockquote { 430 | margin-left: 1.45rem; 431 | margin-right: 1.45rem; 432 | margin-top: 0; 433 | padding-bottom: 0; 434 | padding-left: 0; 435 | padding-right: 0; 436 | padding-top: 0; 437 | margin-bottom: 1.45rem; 438 | } 439 | form { 440 | margin-left: 0; 441 | margin-right: 0; 442 | margin-top: 0; 443 | padding-bottom: 0; 444 | padding-left: 0; 445 | padding-right: 0; 446 | padding-top: 0; 447 | margin-bottom: 1.45rem; 448 | } 449 | noscript { 450 | margin-left: 0; 451 | margin-right: 0; 452 | margin-top: 0; 453 | padding-bottom: 0; 454 | padding-left: 0; 455 | padding-right: 0; 456 | padding-top: 0; 457 | margin-bottom: 1.45rem; 458 | } 459 | iframe { 460 | margin-left: 0; 461 | margin-right: 0; 462 | margin-top: 0; 463 | padding-bottom: 0; 464 | padding-left: 0; 465 | padding-right: 0; 466 | padding-top: 0; 467 | margin-bottom: 1.45rem; 468 | } 469 | address { 470 | margin-left: 0; 471 | margin-right: 0; 472 | margin-top: 0; 473 | padding-bottom: 0; 474 | padding-left: 0; 475 | padding-right: 0; 476 | padding-top: 0; 477 | margin-bottom: 1.45rem; 478 | } 479 | b { 480 | font-weight: bold; 481 | } 482 | strong { 483 | font-weight: bold; 484 | } 485 | dt { 486 | font-weight: bold; 487 | } 488 | th { 489 | font-weight: bold; 490 | } 491 | li { 492 | margin-bottom: calc(1.45rem / 2); 493 | } 494 | ol li { 495 | padding-left: 0; 496 | } 497 | ul li { 498 | padding-left: 0; 499 | } 500 | li > ol { 501 | margin-left: 1.45rem; 502 | margin-bottom: calc(1.45rem / 2); 503 | margin-top: calc(1.45rem / 2); 504 | } 505 | li > ul { 506 | margin-left: 1.45rem; 507 | margin-bottom: calc(1.45rem / 2); 508 | margin-top: calc(1.45rem / 2); 509 | } 510 | blockquote *:last-child { 511 | margin-bottom: 0; 512 | } 513 | li *:last-child { 514 | margin-bottom: 0; 515 | } 516 | p *:last-child { 517 | margin-bottom: 0; 518 | } 519 | li > p { 520 | margin-bottom: calc(1.45rem / 2); 521 | } 522 | code { 523 | font-size: 0.85rem; 524 | line-height: 1.45rem; 525 | } 526 | kbd { 527 | font-size: 0.85rem; 528 | line-height: 1.45rem; 529 | } 530 | samp { 531 | font-size: 0.85rem; 532 | line-height: 1.45rem; 533 | } 534 | abbr { 535 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5); 536 | cursor: help; 537 | } 538 | acronym { 539 | border-bottom: 1px dotted hsla(0, 0%, 0%, 0.5); 540 | cursor: help; 541 | } 542 | thead { 543 | text-align: left; 544 | } 545 | td, 546 | th { 547 | text-align: left; 548 | border-bottom: 1px solid hsla(0, 0%, 0%, 0.12); 549 | font-feature-settings: "tnum"; 550 | -moz-font-feature-settings: "tnum"; 551 | -ms-font-feature-settings: "tnum"; 552 | -webkit-font-feature-settings: "tnum"; 553 | padding-left: 0.96667rem; 554 | padding-right: 0.96667rem; 555 | padding-top: 0.725rem; 556 | padding-bottom: calc(0.725rem - 1px); 557 | } 558 | th:first-child, 559 | td:first-child { 560 | padding-left: 0; 561 | } 562 | th:last-child, 563 | td:last-child { 564 | padding-right: 0; 565 | } 566 | tt, 567 | code { 568 | background-color: hsla(0, 0%, 0%, 0.04); 569 | border-radius: 3px; 570 | font-family: "SFMono-Regular", Consolas, "Roboto Mono", "Droid Sans Mono", 571 | "Liberation Mono", Menlo, Courier, monospace; 572 | padding: 0; 573 | padding-top: 0.2em; 574 | padding-bottom: 0.2em; 575 | } 576 | pre code { 577 | background: none; 578 | line-height: 1.42; 579 | } 580 | code:before, 581 | code:after, 582 | tt:before, 583 | tt:after { 584 | letter-spacing: -0.2em; 585 | content: " "; 586 | } 587 | pre code:before, 588 | pre code:after, 589 | pre tt:before, 590 | pre tt:after { 591 | content: ""; 592 | } 593 | @media only screen and (max-width: 480px) { 594 | html { 595 | font-size: 100%; 596 | } 597 | } 598 | -------------------------------------------------------------------------------- /packages/gatsby-starter/src/components/layout.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Layout component that queries for data 3 | * with Gatsby's useStaticQuery component 4 | * 5 | * See: https://www.gatsbyjs.com/docs/use-static-query/ 6 | */ 7 | 8 | import React from 'react' 9 | import PropTypes from 'prop-types' 10 | import { useStaticQuery, graphql } from 'gatsby' 11 | 12 | import { Header } from './header' 13 | import './layout.css' 14 | 15 | type LayoutProps = { 16 | children: React.ReactNode 17 | } 18 | 19 | const Layout = ({ children }: LayoutProps) => { 20 | const data = useStaticQuery(graphql` 21 | query SiteTitleQuery { 22 | site { 23 | siteMetadata { 24 | title 25 | } 26 | } 27 | } 28 | `) 29 | 30 | return ( 31 | <> 32 |
33 |
40 |
{children}
41 |
46 | © {new Date().getFullYear()}, Built with 47 | {' '} 48 | Gatsby 49 |
50 |
51 | 52 | ) 53 | } 54 | 55 | Layout.propTypes = { 56 | children: PropTypes.node.isRequired 57 | } 58 | 59 | export default Layout 60 | -------------------------------------------------------------------------------- /packages/gatsby-starter/src/components/seo.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * SEO component that queries for data with 3 | * Gatsby's useStaticQuery React hook 4 | * 5 | * See: https://www.gatsbyjs.com/docs/use-static-query/ 6 | */ 7 | 8 | import React from 'react' 9 | import PropTypes from 'prop-types' 10 | import { Helmet } from 'react-helmet' 11 | import { useStaticQuery, graphql } from 'gatsby' 12 | 13 | type SEOProps = { 14 | description: string 15 | lang: string 16 | meta: { 17 | name: string 18 | content: string 19 | } 20 | title: string 21 | } 22 | 23 | function SEO ({ description, lang, meta, title }: SEOProps) { 24 | const { site } = useStaticQuery( 25 | graphql` 26 | query { 27 | site { 28 | siteMetadata { 29 | title 30 | description 31 | author 32 | } 33 | } 34 | } 35 | ` 36 | ) 37 | 38 | const metaDescription = description || site.siteMetadata.description 39 | const defaultTitle = site.siteMetadata?.title 40 | 41 | return ( 42 | 83 | ) 84 | } 85 | 86 | SEO.defaultProps = { 87 | lang: 'en', 88 | meta: [], 89 | description: '' 90 | } 91 | 92 | SEO.propTypes = { 93 | description: PropTypes.string, 94 | lang: PropTypes.string, 95 | meta: PropTypes.arrayOf(PropTypes.object), 96 | title: PropTypes.string.isRequired 97 | } 98 | 99 | export default SEO 100 | -------------------------------------------------------------------------------- /packages/gatsby-starter/src/images/gatsby-astronaut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajjeet/react-playbook/066addee765e4c9897cf238dea2591b1b7c135fc/packages/gatsby-starter/src/images/gatsby-astronaut.png -------------------------------------------------------------------------------- /packages/gatsby-starter/src/images/gatsby-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajjeet/react-playbook/066addee765e4c9897cf238dea2591b1b7c135fc/packages/gatsby-starter/src/images/gatsby-icon.png -------------------------------------------------------------------------------- /packages/gatsby-starter/src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Layout from '../components/layout' 4 | import SEO from '../components/seo' 5 | 6 | const NotFoundPage = () => ( 7 | 8 | 9 |

404: Not Found

10 |

You just hit a route that doesn't exist... the sadness.

11 |
12 | ) 13 | 14 | export default NotFoundPage 15 | -------------------------------------------------------------------------------- /packages/gatsby-starter/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Layout from '../components/layout' 4 | import Image from '../components/image' 5 | import SEO from '../components/seo' 6 | 7 | const IndexPage = () => ( 8 | 9 | 10 |

Hi people

11 |

Welcome to your new Gatsby site.

12 |

Now go build something great.

13 |
14 | 15 |
16 |
17 | ) 18 | 19 | export default IndexPage 20 | -------------------------------------------------------------------------------- /packages/gatsby-starter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "removeComments": true, 5 | "preserveConstEnums": true, 6 | "sourceMap": true, 7 | "esModuleInterop": true, 8 | "jsx": "preserve", 9 | "noEmit": true 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules", "**/*.spec.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/jest-enzyme/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [] 4 | } -------------------------------------------------------------------------------- /packages/jest-enzyme/README.md: -------------------------------------------------------------------------------- 1 | # `jest-enzyme` 2 | 3 | A minimalistic implementation of Jest and Enzyme for testing React projects 4 | 5 | ## Usage 6 | 7 | ``` 8 | git clone https://github.com/rajjeet/react-playbook 9 | cd react-playbook/packages/jest-enzyme 10 | npm install 11 | npm test 12 | ``` 13 | 14 | ### Step 1: Add dependencies 15 | Follow the `react-basics` tutorial and run the following command afterwards: 16 | ``` 17 | npm install --dev jest enzyme enzyme-adapter-react-16 @types/jest` 18 | ``` 19 | 20 | Here's for each package is for: 21 | - `jest` is the test runner and framework that executes our tests and providers helper functions for assertions 22 | - `enzyme` is a test helper library that enables us to render React components on a virtual DOM and provides method 23 | to inspect the DOM tree for specific components and their properties. Think of JQuery but for React components for 24 | testing. 25 | - `enzyme-adapter-react-16` is an adapter between React and Enzyme. This will be executed before running the tests. 26 | - `@types/jest` is types library that provides typing and intellisence for global jest keywords such as `describe 27 | ` and `it` in our test file. This makes type safety more robust for type files and provides better IDE support for 28 | test files. 29 | 30 | ### Step 2: Add Enzyme Adapter for React 31 | - add test setup file with the enzyme adapter that executes before each test in `./setupTests.js` 32 | ```javascript 33 | const Enzyme = require('enzyme'); 34 | const Adapter = require('enzyme-adapter-react-16'); 35 | 36 | Enzyme.configure({ adapter: new Adapter() }); 37 | ``` 38 | 39 | ### Step 3: Connect the Setup Test 40 | - point to the setup file using the `jest` property in `package.json`. 41 | ```json 42 | "jest": { 43 | "setupFilesAfterEnv": [ 44 | "/setupTests.js" 45 | ] 46 | } 47 | ``` 48 | 49 | ### Step 4: Add and Run Tests 50 | For the `` component in `./src`: 51 | ```javascript 52 | import React from "react"; 53 | 54 | export const App = () =>

Hello world React!

; 55 | ``` 56 | we can add the following test file: 57 | ``` javascript 58 | import React from 'react'; 59 | import { shallow } from "enzyme"; 60 | import { App } from "./App"; 61 | 62 | describe('Test App Entry point', function () { 63 | it('should have a header tag with Hello world React!', function () { 64 | const wrapper = shallow(); 65 | expect(wrapper.find("h1").text()).toEqual("Hello world React!"); 66 | }); 67 | }); 68 | ``` 69 | The `describe` block groups a set of tests surrounded by `it` blocks as shown above. It's desirable to nest multiple 70 | `describe` blocks to logically separate the scope of each test. The `it` blocks should ideally contain only one 71 | assertion. 72 | 73 | Now, run the test using `npm test`. You should see the test pass as follows: 74 | ``` 75 | $ npm test 76 | 77 | > jest-enzyme@0.0.0 test C:\Users\rajje\WebstormProjects\react-playbook\packages\jest-enzyme 78 | > jest 79 | 80 | PASS src/App.test.js 81 | Test App Entry point 82 | √ should have a header tag with Hello world React! (6 ms) 83 | 84 | Test Suites: 1 passed, 1 total 85 | Tests: 1 passed, 1 total 86 | Snapshots: 0 total 87 | Time: 1.967 s, estimated 2 s 88 | Ran all test suites. 89 | ``` 90 | 91 | ## Checkout the other React Quick Starters 92 | Using these starters, I quickly pick up working knowledge of these libraries and implement them with confidence on 93 | complex projects. [](https://github.com/rajjeet/react-playbook) -------------------------------------------------------------------------------- /packages/jest-enzyme/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello world App 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/jest-enzyme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-enzyme", 3 | "version": "0.0.0", 4 | "description": "A very basic react implementation of Jest and Enzyme", 5 | "keywords": [ 6 | "react", 7 | "jest", 8 | "enzyme", 9 | "starter" 10 | ], 11 | "author": "Rajjeet Phull ", 12 | "homepage": "https://ortmesh.com", 13 | "license": "ISC", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/rajjeet/react-playbook.git" 17 | }, 18 | "scripts": { 19 | "start": "webpack-dev-server --open", 20 | "build": "webpack", 21 | "test": "jest" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/rajjeet/react-playbook/issues" 25 | }, 26 | "dependencies": { 27 | "react": "^16.12.0", 28 | "react-dom": "^16.12.0" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.7.5", 32 | "@babel/preset-env": "^7.12.11", 33 | "@babel/preset-react": "^7.12.10", 34 | "@types/jest": "^26.0.19", 35 | "babel-loader": "^8.0.6", 36 | "enzyme": "^3.11.0", 37 | "enzyme-adapter-react-16": "^1.15.5", 38 | "html-webpack-plugin": "^3.2.0", 39 | "jest": "^26.6.3", 40 | "webpack": "^4.41.3", 41 | "webpack-cli": "^3.3.10", 42 | "webpack-dev-server": "^3.9.0" 43 | }, 44 | "jest": { 45 | "setupFilesAfterEnv": [ 46 | "/setupTests.js" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/jest-enzyme/setupTests.js: -------------------------------------------------------------------------------- 1 | const Enzyme = require('enzyme'); 2 | const Adapter = require('enzyme-adapter-react-16'); 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); -------------------------------------------------------------------------------- /packages/jest-enzyme/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const App = () =>

Hello world React!

; -------------------------------------------------------------------------------- /packages/jest-enzyme/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from "enzyme"; 3 | import { App } from "./App"; 4 | 5 | describe('Test App Entry point', function () { 6 | it('should have a header tag with Hello world React!', function () { 7 | const wrapper = shallow(); 8 | expect(wrapper.find("h1").text()).toEqual("Hello world React!"); 9 | }); 10 | }); -------------------------------------------------------------------------------- /packages/jest-enzyme/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { App } from "./App"; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /packages/jest-enzyme/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: path.resolve(__dirname, 'src', 'index.js'), 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'bundle.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?$/, 15 | exclude: /node_modules/, 16 | loader: 'babel-loader' 17 | } 18 | ] 19 | }, 20 | plugins: [new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'index.html') })] 21 | }; 22 | -------------------------------------------------------------------------------- /packages/react-basic/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [] 4 | } -------------------------------------------------------------------------------- /packages/react-basic/README.md: -------------------------------------------------------------------------------- 1 | # `react-basic` 2 | 3 | A minimalistic implementation of the React framework 4 | 5 | ## Usage 6 | 7 | ``` 8 | git clone https://github.com/rajjeet/react-playbook 9 | cd react-playbook/packages/react-basic 10 | npm install 11 | npm start 12 | ``` 13 | 14 | Use `npm run build` to build a JS bundle -------------------------------------------------------------------------------- /packages/react-basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello world App 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/react-basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-basic", 3 | "version": "0.0.0", 4 | "description": "A very basic react implmentation", 5 | "keywords": [ 6 | "react" 7 | ], 8 | "author": "Rajjeet Phull ", 9 | "homepage": "https://ortmesh.com", 10 | "license": "ISC", 11 | "main": "lib/react-basic.js", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/rajjeet/react-playbook.git" 15 | }, 16 | "scripts": { 17 | "start": "webpack-dev-server --open", 18 | "build": "webpack" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/rajjeet/react-playbook/issues" 22 | }, 23 | "dependencies": { 24 | "react": "^16.12.0", 25 | "react-dom": "^16.12.0" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.7.5", 29 | "@babel/preset-env": "^7.7.6", 30 | "@babel/preset-react": "^7.7.4", 31 | "babel-loader": "^8.0.6", 32 | "html-webpack-plugin": "^3.2.0", 33 | "webpack": "^4.41.3", 34 | "webpack-cli": "^3.3.10", 35 | "webpack-dev-server": "^3.9.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/react-basic/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | ReactDOM.render(

Hello world React!

, document.getElementById('root')); 5 | -------------------------------------------------------------------------------- /packages/react-basic/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: path.resolve(__dirname, 'src', 'index.js'), 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'bundle.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?$/, 15 | exclude: /node_modules/, 16 | loader: 'babel-loader' 17 | } 18 | ] 19 | }, 20 | plugins: [new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'index.html') })] 21 | }; 22 | -------------------------------------------------------------------------------- /packages/react-hooks/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [] 4 | } -------------------------------------------------------------------------------- /packages/react-hooks/README.md: -------------------------------------------------------------------------------- 1 | # `react-hooks` 2 | 3 | This guide explores each React hook, and presents testing techniques for some. 4 | 5 | ## Usage 6 | 7 | ``` 8 | git clone https://github.com/rajjeet/react-playbook 9 | cd react-playbook/packages/react-hooks 10 | npm install 11 | npm test 12 | npm start 13 | ``` 14 | 15 | |React Hook|Use Cases| 16 | |---|---| 17 | |`useState`| Track state within the component | 18 | |`useEffect`| Perform side effects other than rendering such as data fetching and timer-based actions | 19 | |`useContext`| Alternative to prop-drilling; pass down data to component tree without specifying props at every level of the component tree. Uses a provider and consumer relationship | 20 | |`useRef`| Instance variable for component that doesn't cause a re-render when it changes. Also used for referencing DOM elements for imperative control. | 21 | |`useMemo`| Prevent re-renders from re-computing values derived from expensive functions by storing previously computed values in memory. Only re-compute the variable if one of its dependencies changes | 22 | |`useCallback`| Prevent re-renders from re-initializing local functions by saving the function in memory and only re-initializing it if its dependencies change. | 23 | |`useReducer`| Alternative to `useState` for storing component-level state. Useful for tracking complex state objects | 24 | |`useImperativeHandle`| Allows components to provide a specific interface for the forwarded ref | 25 | |`useLayoutEffect` | Identical to `useEffect` but fires synchronously after DOM changes. Use this if it the effect must run before the first render, but it can cause blocking. | 26 | |`useDebugValue` | Used for displaying labels next to custom hooks in React DevTools | 27 | 28 | -------------------------------------------------------------------------------- /packages/react-hooks/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello world App 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/react-hooks/mocks/comments.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": [ 3 | { 4 | "id": 1, 5 | "text": "That was a great video!" 6 | }, 7 | { 8 | "id": 2, 9 | "text": "That could have been done better :(" 10 | }, 11 | { 12 | "id": 3, 13 | "text": "Why are we mocking comments?" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /packages/react-hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-basic", 3 | "version": "0.0.0", 4 | "description": "A very basic react implmentation", 5 | "keywords": [ 6 | "react" 7 | ], 8 | "author": "Rajjeet Phull ", 9 | "homepage": "https://ortmesh.com", 10 | "license": "ISC", 11 | "main": "lib/react-basic.js", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/rajjeet/react-playbook.git" 15 | }, 16 | "scripts": { 17 | "start": "run-p client server", 18 | "build": "webpack", 19 | "test": "jest", 20 | "client": "webpack-dev-server --open", 21 | "server": "json-server --watch mocks/comments.json --port 3001 --delay 1000" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/rajjeet/react-playbook/issues" 25 | }, 26 | "dependencies": { 27 | "json-server": "^0.16.3", 28 | "npm-run-all": "^4.1.5", 29 | "react": "^16.12.0", 30 | "react-dom": "^16.12.0", 31 | "regenerator-runtime": "^0.13.7" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.7.5", 35 | "@babel/preset-env": "^7.7.6", 36 | "@babel/preset-react": "^7.7.4", 37 | "@testing-library/jest-dom": "^5.11.8", 38 | "@testing-library/react": "^11.2.2", 39 | "@types/jest": "^26.0.19", 40 | "@types/react-dom": "^17.0.0", 41 | "@types/testing-library__jest-dom": "^5.9.5", 42 | "babel-loader": "^8.0.6", 43 | "enzyme": "^3.11.0", 44 | "enzyme-adapter-react-16": "^1.15.5", 45 | "html-webpack-plugin": "^3.2.0", 46 | "jest": "^26.6.3", 47 | "webpack": "^4.41.3", 48 | "webpack-cli": "^3.3.10", 49 | "webpack-dev-server": "^3.9.0" 50 | }, 51 | "jest": { 52 | "setupFilesAfterEnv": [ 53 | "/setup-tests.js" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/react-hooks/server.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rajjeet/react-playbook/066addee765e4c9897cf238dea2591b1b7c135fc/packages/react-hooks/server.js -------------------------------------------------------------------------------- /packages/react-hooks/setup-tests.js: -------------------------------------------------------------------------------- 1 | import "regenerator-runtime"; 2 | import "@testing-library/jest-dom/extend-expect"; 3 | 4 | const Enzyme = require('enzyme'); 5 | const Adapter = require('enzyme-adapter-react-16'); 6 | 7 | Enzyme.configure({ adapter: new Adapter() }); -------------------------------------------------------------------------------- /packages/react-hooks/src/callback-hook/index.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from 'react'; 2 | 3 | export const CallbackHook = () => { 4 | const [scrollPosition, setScrollPosition] = useState(0); 5 | // only create this function once for the lifetime of this component 6 | const getMousePosition = useCallback((e) => { 7 | setScrollPosition(`(${e.offsetX},${e.offsetY})`); 8 | }, []); 9 | useEffect(() => { 10 | window.addEventListener("mousemove", getMousePosition); 11 | return () => window.removeEventListener("mousemove", getMousePosition) 12 | }, []) 13 | return ( 14 |
15 |

Callback Hook

16 |
Mouse Position: {scrollPosition}
17 |
18 | ); 19 | }; -------------------------------------------------------------------------------- /packages/react-hooks/src/context-hook/index.enzyme.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow, mount } from "enzyme"; 3 | import { Button, Message, ThemeContext } from "./index"; 4 | 5 | describe('Context Hook > Enzyme', function () { 6 | afterEach(() => { 7 | jest.clearAllMocks(); 8 | }) 9 | describe('Message', function () { 10 | it('should show the color specified by theme context', function () { 11 | const spyInstance = jest.spyOn(React, "useContext") 12 | .mockImplementation(() => ({ 13 | foreground: "#ffffff", 14 | background: "#222222" 15 | })); 16 | const wrapper = shallow(); 17 | expect(spyInstance).toBeCalled(); 18 | const styles = wrapper.find('span').props().style; 19 | expect(styles.color).toBe('#ffffff'); 20 | expect(styles.backgroundColor).toBe('#222222'); 21 | }); 22 | it('should show the color specified by theme context (Alternative)', function () { 23 | const wrapper = mount( 24 | 28 | 29 | 30 | ); 31 | const styles = wrapper.find('span').props().style; 32 | expect(styles.color).toBe('#ffffff'); 33 | expect(styles.backgroundColor).toBe('#222222'); 34 | }); 35 | }); 36 | describe("Button", function () { 37 | it('should show the color specified by theme context', function () { 38 | const wrapper = mount( 39 | 43 | 38 | ) 39 | } 40 | 41 | ); 42 | } 43 | 44 | export const ContextHook = () => { 45 | const [themeType, setThemeType] = useState("dark") 46 | const toggleTheme = () => { 47 | setThemeType(themeType === "dark" ? "light" : "dark"); 48 | } 49 | return ( 50 | <> 51 |

Context Hook

52 | 53 | 54 | 31 | ))} 32 |
{text}
33 | 34 | } -------------------------------------------------------------------------------- /packages/react-hooks/src/effect-hook/timers/index.react-test-utils.test.js: -------------------------------------------------------------------------------- 1 | import { render, unmountComponentAtNode } from "react-dom"; 2 | import { TimerEffectsHook } from "./index"; 3 | import React from "react"; 4 | import { act } from "react-dom/test-utils"; 5 | 6 | describe('Timers > React Test Utils', function () { 7 | let container = null; 8 | beforeEach(() => { 9 | jest.useFakeTimers(); 10 | container = document.createElement("div"); 11 | document.body.appendChild(container); 12 | }) 13 | afterEach(() => { 14 | jest.clearAllTimers(); 15 | unmountComponentAtNode(container); 16 | container.remove(); 17 | container = null; 18 | }) 19 | it('should have disabled buttons for first 2 seconds', function () { 20 | act(() => { 21 | render(, container); 22 | }) 23 | const button = container.querySelector("button[id='1']") 24 | expect(button.hasAttribute("disabled")).toBe(true); 25 | }); 26 | it('should have enabled buttons after 2 seconds', function () { 27 | act(() => { 28 | render(, container); 29 | }) 30 | act(() => { 31 | jest.runAllTimers() 32 | }); 33 | const button = container.querySelector("button[id='1']") 34 | expect(button.hasAttribute("disabled")).toBe(false); 35 | }); 36 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/effect-hook/timers/index.react-testing-library.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen, act } from "@testing-library/react"; 2 | import { TimerEffectsHook } from "./index"; 3 | import React from "react"; 4 | 5 | describe('Timers > React Testing Library', function () { 6 | beforeEach(() => { 7 | jest.useFakeTimers(); 8 | }) 9 | afterEach(() => { 10 | jest.clearAllTimers(); 11 | }) 12 | it('should have disabled buttons for first 2 seconds', function () { 13 | render(); 14 | expect(screen.getByText("1").hasAttribute("disabled")).toBe(true); 15 | }); 16 | it('should have enabled buttons after 2 seconds', function () { 17 | render(); 18 | act(() => { 19 | jest.runAllTimers(); 20 | }); 21 | expect(screen.getByText("1").hasAttribute("disabled")).toBe(false); 22 | }); 23 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/imperative-handle-hook/index.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useImperativeHandle, useRef } from 'react'; 2 | 3 | const Input = forwardRef((props, ref) => { 4 | const inputRef = useRef(); 5 | useImperativeHandle(ref, () => ({ 6 | specialFocus: () => { 7 | inputRef.current.focus(); 8 | } 9 | })) 10 | return 11 | }); 12 | 13 | 14 | export const ImperativeHandleHook = () => { 15 | const inputRef = useRef(); 16 | const onClick = () => { 17 | inputRef.current.specialFocus(); 18 | } 19 | return ( 20 |
21 |

Imperative Hook

22 | 23 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/react-hooks/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { StateHook } from "./state-hook"; 4 | import { EffectHooks } from "./effect-hook"; 5 | import "regenerator-runtime"; 6 | import { ContextHook } from "./context-hook"; 7 | import { RefHooks } from "./ref-hook"; 8 | import { MemoHook } from "./memo-hook"; 9 | import { ReducerHook } from "./reducer-hook"; 10 | import { CallbackHook } from "./callback-hook"; 11 | import { ImperativeHandleHook } from "./imperative-handle-hook"; 12 | 13 | const App = () => ( 14 | <> 15 |

React Hooks

16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | 27 | ReactDOM.render(, document.getElementById('root')); 28 | -------------------------------------------------------------------------------- /packages/react-hooks/src/memo-hook/index.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState } from 'react'; 2 | 3 | function expensiveOperation(loops) { 4 | for(let i = 0; i < loops * 100000000; i++){} 5 | return loops % 7 * 2 + 1; 6 | } 7 | 8 | export const MemoHook = () => { 9 | const [loops, setLoops] = useState(1); 10 | const [check1, setCheck1] = useState(false); 11 | const [check2, setCheck2] = useState(false); 12 | const handleLoopCountChange = ({target: { value }}) => { 13 | setLoops(value); 14 | } 15 | const result = useMemo(() => expensiveOperation(loops), [check1]); 16 | return ( 17 |
18 |

Memo Hook

19 | 20 | 21 |
22 |

Result: {result}

23 |
Expensive operation only occurs when checkbox 1 is clicked! Else the value is memoized
24 | 25 | setCheck1(!check1)} /> 26 | 27 | setCheck2(!check2)} /> 28 |
29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/react-hooks/src/reducer-hook/index.js: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from 'react'; 2 | 3 | export const ReducerHook = () => { 4 | const initialState = { count: 0 }; 5 | 6 | function countReducer(state, action) { 7 | switch (action.type) { 8 | case 'increment': 9 | return { count: state.count + 1 }; 10 | case 'decrement': 11 | return { count: state.count - 1 }; 12 | default: 13 | throw new Error(); 14 | } 15 | } 16 | 17 | const [state, dispatch] = useReducer(countReducer, initialState); 18 | return ( 19 |
20 |

Reducer Hook

21 |
22 | Count: {state.count} 23 |
24 | 25 | 26 |
27 | ); 28 | }; -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/assign-dynamically/index.enzyme.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { mount } from "enzyme"; 3 | import { AssignDynamicallyRefHooks } from "./index"; 4 | 5 | describe('Assign Dynamically Ref Hook > Enzyme', function () { 6 | beforeEach(() => { 7 | const div = document.createElement("div"); 8 | div.setAttribute('id', 'container'); 9 | document.body.appendChild(div); 10 | }) 11 | afterEach(() => { 12 | const div = document.getElementById('container'); 13 | if(div) document.body.removeChild(div); 14 | }) 15 | it('should toggle the text color of the selected task', function () { 16 | const wrapper = mount(, { 17 | attachTo: document.getElementById('container') 18 | }); 19 | wrapper.find('button[id="button-2"]').props().onClick(); 20 | expect(document.getElementById('task-2').style.color).toBe("white"); 21 | wrapper.find('button[id="button-2"]').props().onClick(); 22 | expect(document.getElementById('task-2').style.color).toBe("black"); 23 | wrapper.detach(); 24 | }); 25 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/assign-dynamically/index.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | 3 | const tasks = [ 4 | { name: "Task 1", color: "red" }, 5 | { name: "Task 2", color: "green" }, 6 | { name: "Task 3", color: "yellow" }, 7 | { name: "Task 4", color: "gray" } 8 | ]; 9 | 10 | export const AssignDynamicallyRefHooks = () => { 11 | const refsArray = useRef([]); 12 | const handleButtonClick = (id) => () => { 13 | refsArray.current[id].style.color = refsArray.current[id].style.color === "white" 14 | ? "black" : "white"; 15 | }; 16 | return ( 17 |
18 |

Assign Refs Dynamically

19 | {tasks.map((task, i) => { 20 | return ( 21 | 27 | ); 28 | })} 29 |
30 | {tasks.map((task, i) => ( 31 |
{ 35 | refsArray.current[i] = ref; 36 | }} 37 | style={{ padding: "30px", display: 'inline-block', backgroundColor: task.color }}> 38 | {task.name} 39 |
40 | ))} 41 |
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/assign-dynamically/index.react-test-utils.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, unmountComponentAtNode } from "react-dom"; 3 | import { act } from "react-dom/test-utils"; 4 | import { AssignDynamicallyRefHooks } from "./index"; 5 | 6 | describe('Assign Dynamically Ref Hook > React Test Utils', function () { 7 | let container = null; 8 | beforeEach(() => { 9 | container = document.createElement("div"); 10 | document.body.appendChild(container); 11 | }) 12 | afterEach(() => { 13 | unmountComponentAtNode(container); 14 | container.remove(); 15 | container = null; 16 | }) 17 | it('should focus on the input field after parent component invokes the command', function () { 18 | act(() => { 19 | render(, container); 20 | }); 21 | const button = container.querySelector('#button-2'); 22 | button.dispatchEvent(new Event('click', { bubbles: true })); 23 | const task = container.querySelector('#task-2'); 24 | expect(task.style.color).toBe("white"); 25 | button.dispatchEvent(new Event('click', { bubbles: true })); 26 | expect(task.style.color).toBe("black"); 27 | }); 28 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/assign-dynamically/index.react-testing-library.test.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { render, screen, fireEvent } from "@testing-library/react"; 3 | import { AssignDynamicallyRefHooks } from "./index"; 4 | 5 | describe('Assign Dynamically Ref Hook > React Testing Library', function () { 6 | it('should focus on the input field after parent component invokes the command', function () { 7 | render(); 8 | const button = screen.getByText('Toggle Task 2'); 9 | fireEvent(button, new Event('click', { bubbles: true })); 10 | const task = screen.getByText("Task 2"); 11 | expect(task.style.color).toBe("white"); 12 | fireEvent(button, new Event('click', { bubbles: true })); 13 | expect(task.style.color).toBe("black"); 14 | }); 15 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/forwarding-ref-hook/index.enzyme.test.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { mount } from "enzyme"; 3 | import { ForwardingRefHook } from "./index"; 4 | 5 | describe('Forwarding Ref Hook > Enzyme', function () { 6 | beforeEach(() => { 7 | const div = document.createElement("div"); 8 | div.setAttribute('id', 'container'); 9 | document.body.appendChild(div); 10 | }) 11 | afterEach(() => { 12 | const div = document.getElementById('container'); 13 | if(div) document.body.removeChild(div); 14 | }) 15 | it('should focus on the input field after parent component invokes the command', function () { 16 | const MockParentComponent = () => { 17 | const inputRef = useRef(null); 18 | useEffect(() => { 19 | if (inputRef && inputRef.current) inputRef.current.focus(); 20 | }, []) 21 | return ; 22 | }; 23 | const wrapper = mount(, { 24 | attachTo: document.getElementById('container') 25 | }); 26 | expect(document.activeElement).toEqual(wrapper.find('input').getDOMNode()); 27 | wrapper.detach(); 28 | }); 29 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/forwarding-ref-hook/index.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useRef } from 'react'; 2 | 3 | export const ForwardingRefHook = forwardRef((props, ref) => { 4 | return ( 5 |
6 |

Forwarding Refs

7 |
8 | 9 | 10 |
11 |
12 | ); 13 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/forwarding-ref-hook/index.react-test-utils.test.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { render, unmountComponentAtNode } from "react-dom"; 3 | import { act } from "react-dom/test-utils"; 4 | import { ForwardingRefHook } from "./index"; 5 | 6 | describe('Forwarding Ref Hook > React Test Utils', function () { 7 | let container = null; 8 | beforeEach(() => { 9 | container = document.createElement("div"); 10 | document.body.appendChild(container); 11 | }) 12 | afterEach(() => { 13 | unmountComponentAtNode(container); 14 | container.remove(); 15 | container = null; 16 | }) 17 | it('should focus on the input field after parent component invokes the command', function () { 18 | const MockParentComponent = () => { 19 | const inputRef = useRef(null); 20 | useEffect(() => { 21 | if (inputRef && inputRef.current) inputRef.current.focus(); 22 | }, []) 23 | return ; 24 | }; 25 | act(() => { 26 | render(, container); 27 | }) 28 | const input = container.querySelector('input'); 29 | expect(document.activeElement).toEqual(input); 30 | }); 31 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/forwarding-ref-hook/index.react-testing-library.test.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { render, screen } from "@testing-library/react"; 3 | import { ForwardingRefHook } from "./index"; 4 | 5 | describe('Forwarding Ref Hook > React Testing Library', function () { 6 | it('should focus on the input field after parent component invokes the command', function () { 7 | const MockParentComponent = () => { 8 | const inputRef = useRef(null); 9 | useEffect(() => { 10 | if (inputRef && inputRef.current) inputRef.current.focus(); 11 | }, []) 12 | return ; 13 | }; 14 | render(); 15 | const input = screen.getByLabelText('Name Field:'); 16 | expect(document.activeElement).toEqual(input); 17 | }); 18 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MutableStateRefHook } from "./mutable-state"; 3 | import { ReadDOMAttributesRefHook } from "./read-dom-attributes"; 4 | import { InvokeDOMActionsRefHook } from "./invoke-dom-actions"; 5 | import { ForwardingRefHook } from "./forwarding-ref-hook"; 6 | import { AssignDynamicallyRefHooks } from "./assign-dynamically"; 7 | 8 | export const RefHooks = () => { 9 | return ( 10 |
11 |

Ref Hooks

12 | 13 | 14 | 15 | 16 | 17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/invoke-dom-actions/index.enzyme.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { mount } from "enzyme"; 3 | import { InvokeDOMActionsRefHook } from "./index"; 4 | 5 | describe('Invoke DOM Actions Ref Hook > Enzyme', function () { 6 | beforeEach(() => { 7 | const div = document.createElement("div"); 8 | div.setAttribute('id', 'container'); 9 | document.body.appendChild(div); 10 | }) 11 | afterEach(() => { 12 | const div = document.getElementById('container'); 13 | if(div) document.body.removeChild(div); 14 | }) 15 | it('should focus on the input field after clicking the button', function () { 16 | const wrapper = mount(, { 17 | attachTo: document.getElementById('container') 18 | }) 19 | wrapper.find('button').props().onClick(); 20 | expect(document.activeElement).toEqual(wrapper.find('input').getDOMNode()); 21 | wrapper.detach(); 22 | }); 23 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/invoke-dom-actions/index.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | 3 | export const InvokeDOMActionsRefHook = () => { 4 | const inputRef = useRef(null); 5 | const handleClick = () => { 6 | if (inputRef && inputRef.current){ 7 | inputRef.current.focus(); 8 | } 9 | } 10 | return ( 11 |
12 |

Invoke DOM Actions

13 | 14 |
15 | 16 | 17 |
18 |
19 | ); 20 | }; -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/invoke-dom-actions/index.react-test-utils.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, unmountComponentAtNode } from "react-dom"; 3 | import { act } from "react-dom/test-utils"; 4 | import { InvokeDOMActionsRefHook } from "./index"; 5 | 6 | describe('Invoke DOM Action Ref Hook > React Test Utils', function () { 7 | let container = null; 8 | beforeEach(() => { 9 | container = document.createElement("div"); 10 | document.body.appendChild(container); 11 | }) 12 | afterEach(() => { 13 | unmountComponentAtNode(container); 14 | container.remove(); 15 | container = null; 16 | }) 17 | it('should focus on the input field after clicking the button', function () { 18 | act(() => { 19 | render(, container); 20 | }) 21 | const button = container.querySelector('button'); 22 | const input = container.querySelector('input'); 23 | button.dispatchEvent(new Event('click', { bubbles: true })); 24 | expect(document.activeElement).toEqual(input); 25 | }); 26 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/invoke-dom-actions/index.react-testing-library.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen, fireEvent } from "@testing-library/react"; 3 | import { InvokeDOMActionsRefHook } from "./index"; 4 | 5 | describe('Invoke DOM Action Ref Hook > React Testing Library', function () { 6 | it('should focus on the input field after clicking the button', function () { 7 | render(); 8 | fireEvent(screen.getByText('Click Here to Fill Form'), 9 | new Event('click', { bubbles: true })) 10 | const input = screen.getByLabelText('Name Field:'); 11 | expect(document.activeElement).toEqual(input); 12 | }); 13 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/mutable-state/index.enzyme.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { mount } from "enzyme"; 3 | import { act } from "react-dom/test-utils"; 4 | import { MutableStateRefHook } from "./index"; 5 | 6 | describe('Mutable State Ref Hook > Enzyme', function () { 7 | it('should increment render count for each change action in input field', function () { 8 | const wrapper = mount(); 9 | ["a", "b", "c"].forEach(char => { 10 | act(() => { 11 | wrapper.find('input').props().onChange({ target: { value: char } }); 12 | wrapper.update(); 13 | }); 14 | }) 15 | expect(wrapper.find('#render-count').text()).toBe("Render Count: 3"); 16 | }); 17 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/mutable-state/index.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from 'react'; 2 | 3 | export const MutableStateRefHook = () => { 4 | const renderCount = useRef(0); 5 | const [text, setText] = useState(0); 6 | useEffect(() => { 7 | renderCount.current = renderCount.current + 1; 8 | }, [text]); 9 | const handleInputChange = (e) => setText(e.target.value); 10 | return ( 11 |
12 |

Mutable State

13 |
Text: {text}
14 |
Render Count: {renderCount.current || 0}
15 | 16 | 17 |
18 | ); 19 | }; -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/mutable-state/index.react-test-utils.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, unmountComponentAtNode } from "react-dom"; 3 | import { act, Simulate } from "react-dom/test-utils"; 4 | import { MutableStateRefHook } from "./index"; 5 | 6 | describe('Mutable State Ref Hook > React Test Utils', function () { 7 | let container = null; 8 | beforeEach(() => { 9 | container = document.createElement("div"); 10 | document.body.appendChild(container); 11 | }) 12 | afterEach(() => { 13 | unmountComponentAtNode(container); 14 | container.remove(); 15 | container = null; 16 | }) 17 | it('should increment render count for each change action in input field', function () { 18 | act(() => { 19 | render(, container); 20 | }) 21 | const textInput = container.querySelector('input'); 22 | ["a", "b", "c"].forEach(char => { 23 | act(() => { 24 | Simulate.change(textInput, {target: { value: char}}); 25 | }); 26 | }); 27 | const renderCount = container.querySelector("[id='render-count']") 28 | expect(renderCount.textContent).toBe("Render Count: 3"); 29 | }); 30 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/mutable-state/index.react-testing-library.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen, fireEvent } from "@testing-library/react"; 3 | import { MutableStateRefHook } from "./index"; 4 | 5 | describe('Mutable State Ref Hook > React Testing Library', function () { 6 | it('should increment render count for each change action in input field', function () { 7 | render(); 8 | const input = screen.getByLabelText("Enter text here:"); 9 | ['a', 'b', 'c'].forEach(char => { 10 | fireEvent.change(input, { target: { value: char } }); 11 | }) 12 | expect(screen.queryByText("Render Count: 3")).toBeDefined(); 13 | }); 14 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/read-dom-attributes/index.enzyme.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { mount } from "enzyme"; 3 | import { act } from "react-dom/test-utils"; 4 | import { ReadDOMAttributesRefHook } from "./index"; 5 | 6 | describe('Read DOM Attributes Ref Hook > Enzyme', function () { 7 | beforeEach(() => { 8 | const div = document.createElement("div"); 9 | div.setAttribute('id', 'container'); 10 | document.body.appendChild(div); 11 | }) 12 | afterEach(() => { 13 | const div = document.getElementById('container'); 14 | if(div) document.body.removeChild(div); 15 | }) 16 | it('should display the initial box width in text', function () { 17 | const wrapper = mount(); 18 | expect(wrapper.find('#text-box').text()).toBe("This box's width is 0 px"); 19 | }); 20 | it('should update the box width when window is resized', function () { 21 | const wrapper = mount(, { 22 | attachTo: document.getElementById('container') 23 | }) 24 | const textBox = document.body.querySelector("[id='text-box']"); 25 | jest.spyOn(textBox, "clientWidth", "get").mockImplementation(() => 100); 26 | act(() => { 27 | window.dispatchEvent(new Event('resize')); 28 | wrapper.update(); 29 | }) 30 | expect(wrapper.find('#text-box').text()).toBe("This box's width is 100 px"); 31 | }); 32 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/read-dom-attributes/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | export const ReadDOMAttributesRefHook = () => { 4 | const boxRef = React.useRef(null); 5 | const [boxWidth, setBoxWidth] = useState(0); 6 | useEffect(() => { 7 | const handleResize = () => { 8 | 9 | setBoxWidth(boxRef.current.clientWidth) 10 | }; 11 | if (boxRef && boxRef.current){ 12 | setBoxWidth(boxRef.current.clientWidth) 13 | window.addEventListener("resize", handleResize); 14 | return () => window.removeEventListener("resize", handleResize); 15 | } 16 | }, [boxWidth]) 17 | return ( 18 |
19 |

Read DOM Attributes

20 |
This box's width is {boxWidth} px
21 |
22 | ); 23 | }; -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/read-dom-attributes/index.react-test-utils.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, unmountComponentAtNode } from "react-dom"; 3 | import { act } from "react-dom/test-utils"; 4 | import { ReadDOMAttributesRefHook } from "./index"; 5 | 6 | describe('Read DOM Attributes Ref Hook > React Test Utils', function () { 7 | let container = null; 8 | beforeEach(() => { 9 | container = document.createElement("div"); 10 | document.body.appendChild(container); 11 | }) 12 | afterEach(() => { 13 | unmountComponentAtNode(container); 14 | container.remove(); 15 | container = null; 16 | }) 17 | it('should display the initial box width in text', function () { 18 | act(() => { 19 | render(, container); 20 | }) 21 | const textBox = container.querySelector('[id="text-box"]'); 22 | expect(textBox.textContent).toBe("This box's width is 0 px"); 23 | }); 24 | it('should update the box width when window is resized', function () { 25 | act(() => { 26 | render(, container); 27 | }) 28 | const textBox = container.querySelector('[id="text-box"]'); 29 | jest.spyOn(textBox, "clientWidth", "get").mockImplementation(() => 100); 30 | act(() => { 31 | window.dispatchEvent(new Event('resize')); 32 | }) 33 | expect(textBox.textContent).toBe("This box's width is 100 px"); 34 | }); 35 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/ref-hook/read-dom-attributes/index.react-testing-library.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "@testing-library/react"; 3 | import { act } from "react-dom/test-utils"; 4 | import { ReadDOMAttributesRefHook } from "./index"; 5 | 6 | describe('Read DOM Attributes Ref Hook > React Testing Library', function () { 7 | it('should display the initial box width in text', function () { 8 | render(); 9 | expect(screen.getByText("This box's width is 0 px")); 10 | }); 11 | it('should update the box width when window is resized', function () { 12 | render(); 13 | const textBox = document.body.querySelector('[id="text-box"]'); 14 | jest.spyOn(textBox, "clientWidth", "get").mockImplementation(() => 100); 15 | act(() => { 16 | window.dispatchEvent(new Event('resize')); 17 | }) 18 | expect(screen.getByText("This box's width is 100 px")); 19 | }); 20 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/state-hook/index.enzyme.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import { StateHook } from "./index"; 4 | 5 | describe('State Hook > Enzyme', function () { 6 | it('should initialize count with 0', function () { 7 | const wrapper = shallow(); 8 | expect(wrapper.find('[id="count-display"]').text()).toBe("Count: 0"); 9 | }); 10 | it('should increment count when increment button is clicked', function () { 11 | const wrapper = shallow(); 12 | wrapper.find('[id="increment-btn"]').props().onClick(); 13 | expect(wrapper.find('[id="count-display"]').text()).toBe("Count: 1"); 14 | }); 15 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/state-hook/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | export const StateHook = () => { 4 | const [count, setCount] = useState(0); 5 | return ( 6 | <> 7 |

State Hook

8 |
Count: {count}
9 | 10 | 11 | ); 12 | }; -------------------------------------------------------------------------------- /packages/react-hooks/src/state-hook/index.react-test-utils.test.js: -------------------------------------------------------------------------------- 1 | import { render, unmountComponentAtNode } from "react-dom"; 2 | import { act } from "react-dom/test-utils"; 3 | import { StateHook } from "./index"; 4 | import React from "react"; 5 | 6 | describe('State Hook > React Test Utils', function () { 7 | let container = null; 8 | beforeEach(() => { 9 | container = document.createElement("div"); 10 | document.body.appendChild(container); 11 | }) 12 | afterEach(() => { 13 | unmountComponentAtNode(container); 14 | container.remove(); 15 | container = null; 16 | }) 17 | it('should initialize count with 0', function () { 18 | act(() => { 19 | render(, container); 20 | }); 21 | const countDisplay = document.querySelector("[id=count-display]"); 22 | expect(countDisplay.innerHTML).toBe("Count: 0") 23 | }); 24 | it('should increment count when increment button is clicked', function () { 25 | act(() => { 26 | render(, container); 27 | }); 28 | const incrementButton = document.querySelector("[id=increment-btn]"); 29 | act(() => { 30 | incrementButton.dispatchEvent(new MouseEvent("click", { bubbles: true })); 31 | }); 32 | const countDisplay = document.querySelector("[id=count-display]"); 33 | expect(countDisplay.innerHTML).toBe("Count: 1") 34 | }); 35 | }); -------------------------------------------------------------------------------- /packages/react-hooks/src/state-hook/index.react-testing-library.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen, fireEvent } from '@testing-library/react' 3 | import { StateHook } from "./index"; 4 | 5 | describe('State Hook > React Testing Library', function () { 6 | it('should initialize count with 0', function () { 7 | render(); 8 | expect(screen.getByText("Count: 0")).toBeDefined(); 9 | }); 10 | it('should increment count when increment button is clicked', function () { 11 | render(); 12 | fireEvent.click(screen.getByText('Increment')); 13 | expect(screen.getByText("Count: 1")).toBeDefined(); 14 | }); 15 | }); -------------------------------------------------------------------------------- /packages/react-hooks/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: path.resolve(__dirname, 'src', 'index.js'), 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'bundle.js' 10 | }, 11 | devtool: "source-map", 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.jsx?$/, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader' 18 | } 19 | ] 20 | }, 21 | plugins: [new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'index.html') })] 22 | }; 23 | -------------------------------------------------------------------------------- /packages/react-redux/README.md: -------------------------------------------------------------------------------- 1 | # `react-redux` 2 | 3 | A minimalistic React Redux implementation for learning purposes 4 | 5 | ## Usage 6 | 7 | ``` 8 | git clone https://github.com/rajjeet/react-playbook 9 | cd react-playbook/packages/react-redux 10 | npm install 11 | npm start 12 | ``` 13 | 14 | The starter app just increments and decrements a counter -------------------------------------------------------------------------------- /packages/react-redux/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello world App 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/react-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux", 3 | "version": "0.0.0", 4 | "description": "A minimalistic React Redux project for learning purposes", 5 | "keywords": [ 6 | "react", 7 | "redux" 8 | ], 9 | "author": "Rajjeet Phull ", 10 | "homepage": "https://ortmesh.com", 11 | "license": "ISC", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/rajjeet/react-playbook.git" 15 | }, 16 | "scripts": { 17 | "start": "webpack-dev-server --open", 18 | "build": "webpack" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/rajjeet/react-playbook/issues" 22 | }, 23 | "dependencies": { 24 | "react": "^16.12.0", 25 | "react-dom": "^16.12.0", 26 | "react-redux": "^7.1.3", 27 | "redux": "^4.0.4" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.7.5", 31 | "@babel/preset-env": "^7.7.6", 32 | "@babel/preset-react": "^7.7.4", 33 | "babel-loader": "^8.0.6", 34 | "html-webpack-plugin": "^3.2.0", 35 | "webpack": "^4.41.3", 36 | "webpack-cli": "^3.3.10", 37 | "webpack-dev-server": "^3.9.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/react-redux/src/configure-store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import { countReducer } from './counter/reducer'; 3 | 4 | export const store = createStore(countReducer); 5 | -------------------------------------------------------------------------------- /packages/react-redux/src/counter/component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Component = ({ count, handleIncrementClick, handleDecrementClick }) => ( 4 |
5 |

Helloworld React & Redux! {count}

6 | 7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /packages/react-redux/src/counter/container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { Component } from './component'; 3 | 4 | const mapStateToProps = state => { 5 | return { 6 | count: state 7 | }; 8 | }; 9 | const mapDispatchToProps = dispatch => { 10 | return { 11 | handleIncrementClick: () => dispatch({ type: 'INCREMENT' }), 12 | handleDecrementClick: () => dispatch({ type: 'DECREMENT' }) 13 | } 14 | }; 15 | export const Container = connect(mapStateToProps, mapDispatchToProps)(Component); 16 | -------------------------------------------------------------------------------- /packages/react-redux/src/counter/reducer.js: -------------------------------------------------------------------------------- 1 | export const countReducer = function (state = 0, action) { 2 | switch (action.type) { 3 | case "INCREMENT": 4 | return state + 1; 5 | case "DECREMENT": 6 | return state - 1; 7 | default: 8 | return state; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /packages/react-redux/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { store } from './configure-store'; 5 | import { Container } from './counter/container'; 6 | 7 | const App = () => ( 8 | 9 | 10 | 11 | ); 12 | 13 | ReactDOM.render(, document.getElementById('root')); 14 | -------------------------------------------------------------------------------- /packages/react-redux/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: path.resolve(__dirname, 'src', 'index.js'), 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'bundle.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?$/, 15 | exclude: /node_modules/, 16 | loader: 'babel-loader', 17 | options: { 18 | presets: ["@babel/preset-env", "@babel/preset-react"] 19 | } 20 | } 21 | ] 22 | }, 23 | plugins: [new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'index.html') })] 24 | }; 25 | -------------------------------------------------------------------------------- /packages/react-testing-library/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [] 4 | } -------------------------------------------------------------------------------- /packages/react-testing-library/README.md: -------------------------------------------------------------------------------- 1 | # `react-testing-library` 2 | 3 | A minimalistic implementation of the React Testing Library 4 | 5 | ## Usage 6 | 7 | ``` 8 | git clone https://github.com/rajjeet/react-playbook 9 | cd react-playbook/packages/react-testing-library 10 | npm install 11 | npm test 12 | ``` 13 | 14 | ### Prerequisite 15 | - please complete the tutorial outline in [`react-basics`](https://github.com/rajjeet/react-playbook/tree/master 16 | /packages/react-basic) before beginning this tutorial OR copy and paste the commands listed above to avoid the manual 17 | setup 18 | 19 | ### Step 1: Install 20 | `npm install --save-dev @testing-library/react jest @types/jest prettier` 21 | - `@testing-library/react` - testing utility library 22 | - `jest` - testing framework for javascript and React code 23 | - `@types/jest` - type definitions for Jest for TypeScript and IDE intellisence 24 | - `prettier` - used by Jest to format inline snapshots 25 | 26 | ### Step 2: Add babelrc file 27 | Make sure you have the `.babelrc` in the root directory with the following contents: 28 | ``` 29 | { 30 | "presets": ["@babel/preset-env", "@babel/preset-react"], 31 | "plugins": [] 32 | } 33 | ``` 34 | 35 | ### Step 3: Add Test and Run 36 | For the `` component in the `src` directory, add the following test in `App.test.js`: 37 | 38 | `src/App.js` 39 | ```javascript 40 | import React from "react"; 41 | 42 | export const App = () =>

Hello world React!

; 43 | ``` 44 | `src/App.test.js` 45 | ```javascript 46 | import React from "react"; 47 | import { render } from "@testing-library/react"; 48 | import { App } from "./App"; 49 | 50 | describe("App Component", function () { 51 | it("should have hello world message", function () { 52 | let { getByText } = render(); 53 | expect(getByText("Hello world React!")).toMatchInlineSnapshot(` 54 |

55 | Hello world React! 56 |

57 | `); 58 | }); 59 | }); 60 | ``` 61 | The imported `render()` function from the RTL is used to render the App component. The output of the `render 62 | ()` function's execution gives us query functions that we can use to make assertions about the component. Note: since 63 | we are using the query function straight from the `render()` function, the search binds to the component instead 64 | of the entire document. This is okay as we are writing unit tests, but this syntax is similar for integration 65 | testing. 66 | 67 | The `toMatchInlineSnapshot()` is used so that we are don't have to write out and format the the expected output. 68 | 69 | To run the test, type `npm test` in the directory. You should see this: 70 | ``` 71 | > jest 72 | 73 | PASS src/App.test.js 74 | App Component 75 | √ should have hello world message (20 ms) 76 | 77 | Test Suites: 1 passed, 1 total 78 | Tests: 1 passed, 1 total 79 | Snapshots: 1 passed, 1 total 80 | Time: 1.971 s, estimated 2 s 81 | Ran all test suites. 82 | 83 | ``` 84 | 85 | ## Checkout the other React Quick Starters 86 | Using these starters, I quickly pick up working knowledge of these libraries and implement them with confidence on 87 | complex projects. [Github Repo](https://github.com/rajjeet/react-playbook) -------------------------------------------------------------------------------- /packages/react-testing-library/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello world App 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/react-testing-library/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-basic", 3 | "version": "0.0.0", 4 | "description": "A very basic react implmentation", 5 | "keywords": [ 6 | "react" 7 | ], 8 | "author": "Rajjeet Phull ", 9 | "homepage": "https://ortmesh.com", 10 | "license": "ISC", 11 | "main": "lib/react-basic.js", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/rajjeet/react-playbook.git" 15 | }, 16 | "scripts": { 17 | "start": "webpack-dev-server --open", 18 | "build": "webpack", 19 | "test": "jest" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/rajjeet/react-playbook/issues" 23 | }, 24 | "dependencies": { 25 | "react": "^16.12.0", 26 | "react-dom": "^16.12.0" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.7.5", 30 | "@babel/preset-env": "^7.7.6", 31 | "@babel/preset-react": "^7.7.4", 32 | "@testing-library/react": "^11.2.2", 33 | "@types/jest": "^26.0.19", 34 | "babel-loader": "^8.0.6", 35 | "html-webpack-plugin": "^3.2.0", 36 | "jest": "^26.6.3", 37 | "prettier": "^2.2.1", 38 | "webpack": "^4.41.3", 39 | "webpack-cli": "^3.3.10", 40 | "webpack-dev-server": "^3.9.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/react-testing-library/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const App = () =>

Hello world React!

; -------------------------------------------------------------------------------- /packages/react-testing-library/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "@testing-library/react"; 3 | import { App } from "./App"; 4 | 5 | describe("App Component", function () { 6 | it("should have hello world message", function () { 7 | let { getByText } = render(); 8 | expect(getByText("Hello world React!")).toMatchInlineSnapshot(` 9 |

10 | Hello world React! 11 |

12 | `); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/react-testing-library/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { App } from "./App"; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /packages/react-testing-library/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: path.resolve(__dirname, 'src', 'index.js'), 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'bundle.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?$/, 15 | exclude: /node_modules/, 16 | loader: 'babel-loader' 17 | } 18 | ] 19 | }, 20 | plugins: [new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'index.html') })] 21 | }; 22 | -------------------------------------------------------------------------------- /packages/redux-saga/README.md: -------------------------------------------------------------------------------- 1 | # `redux-saga` 2 | 3 | A minimalistic implementation of Redux saga library for React projects 4 | 5 | ## Usage 6 | 7 | ``` 8 | git clone https://github.com/rajjeet/react-playbook 9 | cd react-playbook/packages/redux-saga 10 | npm install 11 | npm start 12 | ``` 13 | -------------------------------------------------------------------------------- /packages/redux-saga/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello world App 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/redux-saga/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-saga", 3 | "version": "0.0.0", 4 | "description": "A minimalistic implementation of Redux saga library for React projects ", 5 | "keywords": [ 6 | "react", 7 | "redux", 8 | "saga", 9 | "starter" 10 | ], 11 | "author": "Rajjeet Phull ", 12 | "homepage": "https://ortmesh.com", 13 | "license": "ISC", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/rajjeet/react-playbook.git" 17 | }, 18 | "scripts": { 19 | "start": "webpack-dev-server --open", 20 | "build": "webpack" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/rajjeet/react-playbook/issues" 24 | }, 25 | "dependencies": { 26 | "react": "^16.12.0", 27 | "react-dom": "^16.12.0", 28 | "react-redux": "^7.1.3", 29 | "redux": "^4.0.4", 30 | "redux-saga": "^1.1.3" 31 | }, 32 | "devDependencies": { 33 | "@babel/core": "^7.7.5", 34 | "@babel/preset-env": "^7.7.6", 35 | "@babel/preset-react": "^7.7.4", 36 | "babel-loader": "^8.0.6", 37 | "html-webpack-plugin": "^3.2.0", 38 | "webpack": "^4.41.3", 39 | "webpack-cli": "^3.3.10", 40 | "webpack-dev-server": "^3.9.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/redux-saga/src/configure-store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { countReducer } from './counter/reducer'; 3 | import createSagaMiddleware from 'redux-saga'; 4 | import "regenerator-runtime/runtime"; 5 | 6 | function* exampleSaga() { 7 | console.log("Example saga reached"); 8 | } 9 | 10 | const sagaMiddleware = createSagaMiddleware(); 11 | 12 | export const store = createStore(countReducer, applyMiddleware(sagaMiddleware)); 13 | 14 | sagaMiddleware.run(exampleSaga); 15 | -------------------------------------------------------------------------------- /packages/redux-saga/src/counter/component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Component = ({ count, handleIncrementClick, handleDecrementClick }) => ( 4 |
5 |

Hello world Redux Saga! {count}

6 | 7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /packages/redux-saga/src/counter/container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { Component } from './component'; 3 | 4 | const mapStateToProps = state => { 5 | return { 6 | count: state 7 | }; 8 | }; 9 | const mapDispatchToProps = dispatch => { 10 | return { 11 | handleIncrementClick: () => dispatch({ type: 'INCREMENT' }), 12 | handleDecrementClick: () => dispatch({ type: 'DECREMENT' }) 13 | } 14 | }; 15 | export const Container = connect(mapStateToProps, mapDispatchToProps)(Component); 16 | -------------------------------------------------------------------------------- /packages/redux-saga/src/counter/reducer.js: -------------------------------------------------------------------------------- 1 | export const countReducer = function (state = 0, action) { 2 | switch (action.type) { 3 | case "INCREMENT": 4 | return state + 1; 5 | case "DECREMENT": 6 | return state - 1; 7 | default: 8 | return state; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /packages/redux-saga/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { store } from './configure-store'; 5 | import { Container } from './counter/container'; 6 | 7 | const App = () => ( 8 | 9 | 10 | 11 | ); 12 | 13 | ReactDOM.render(, document.getElementById('root')); 14 | -------------------------------------------------------------------------------- /packages/redux-saga/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: path.resolve(__dirname, 'src', 'index.js'), 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'bundle.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?$/, 15 | exclude: /node_modules/, 16 | loader: 'babel-loader', 17 | options: { 18 | presets: ["@babel/preset-env", "@babel/preset-react"] 19 | } 20 | } 21 | ] 22 | }, 23 | plugins: [new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'index.html') })] 24 | }; 25 | -------------------------------------------------------------------------------- /packages/redux-thunk/README.md: -------------------------------------------------------------------------------- 1 | # `redux-thunk` 2 | 3 | A minimalistic implementation of Redux thunk library for React projects 4 | 5 | ## Usage 6 | 7 | ``` 8 | git clone https://github.com/rajjeet/react-playbook 9 | cd react-playbook/packages/redux-thunk 10 | npm install 11 | npm start 12 | ``` 13 | -------------------------------------------------------------------------------- /packages/redux-thunk/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello world Redux Thunk 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/redux-thunk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-thunk", 3 | "version": "0.0.0", 4 | "description": "A minimalistic implmentation of Redux thunk library for React projects", 5 | "keywords": [ 6 | "react", 7 | "redux", 8 | "thunk", 9 | "starter" 10 | ], 11 | "author": "Rajjeet Phull ", 12 | "homepage": "https://ortmesh.com", 13 | "license": "ISC", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/rajjeet/react-playbook.git" 17 | }, 18 | "scripts": { 19 | "start": "webpack-dev-server --open", 20 | "build": "webpack" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/rajjeet/react-playbook/issues" 24 | }, 25 | "dependencies": { 26 | "react": "^16.12.0", 27 | "react-dom": "^16.12.0", 28 | "react-redux": "^7.1.3", 29 | "redux": "^4.0.4", 30 | "redux-thunk": "^2.3.0" 31 | }, 32 | "devDependencies": { 33 | "@babel/core": "^7.7.5", 34 | "@babel/preset-env": "^7.7.6", 35 | "@babel/preset-react": "^7.7.4", 36 | "babel-loader": "^8.0.6", 37 | "html-webpack-plugin": "^3.2.0", 38 | "webpack": "^4.41.3", 39 | "webpack-cli": "^3.3.10", 40 | "webpack-dev-server": "^3.9.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/redux-thunk/src/configure-store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import { countReducer } from './counter/reducer'; 4 | 5 | export const store = createStore(countReducer, applyMiddleware(thunk)); 6 | -------------------------------------------------------------------------------- /packages/redux-thunk/src/counter/component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Component = ({ count, handleIncrementClick, handleDecrementClick }) => ( 4 |
5 |

Hello world Redux Thunk! {count}

6 | 7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /packages/redux-thunk/src/counter/container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { Component } from './component'; 3 | 4 | const mapStateToProps = state => { 5 | return { 6 | count: state 7 | }; 8 | }; 9 | const mapDispatchToProps = dispatch => { 10 | return { 11 | handleIncrementClick: () => dispatch( 12 | innerDispatch => innerDispatch({ type: 'INCREMENT' }) 13 | ), 14 | handleDecrementClick: () => dispatch({ type: 'DECREMENT' }) 15 | } 16 | }; 17 | export const Container = connect(mapStateToProps, mapDispatchToProps)(Component); 18 | -------------------------------------------------------------------------------- /packages/redux-thunk/src/counter/reducer.js: -------------------------------------------------------------------------------- 1 | export const countReducer = function (state = 0, action) { 2 | switch (action.type) { 3 | case "INCREMENT": 4 | return state + 1; 5 | case "DECREMENT": 6 | return state - 1; 7 | default: 8 | return state; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /packages/redux-thunk/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { store } from './configure-store'; 5 | import { Container } from './counter/container'; 6 | 7 | const App = () => ( 8 | 9 | 10 | 11 | ); 12 | 13 | ReactDOM.render(, document.getElementById('root')); 14 | -------------------------------------------------------------------------------- /packages/redux-thunk/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: path.resolve(__dirname, 'src', 'index.js'), 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'bundle.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?$/, 15 | exclude: /node_modules/, 16 | loader: 'babel-loader', 17 | options: { 18 | presets: ["@babel/preset-env", "@babel/preset-react"] 19 | } 20 | } 21 | ] 22 | }, 23 | plugins: [new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'index.html') })] 24 | }; 25 | -------------------------------------------------------------------------------- /packages/typescript-jest/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"], 3 | "plugins": [] 4 | } -------------------------------------------------------------------------------- /packages/typescript-jest/README.md: -------------------------------------------------------------------------------- 1 | # `typescript` 2 | 3 | Integrating Jest into a TypeScript project 4 | 5 | ## Usage 6 | 7 | ``` 8 | git clone https://github.com/rajjeet/react-playbook 9 | cd react-playbook/packages/typescript-jest 10 | npm install 11 | npm run type-check 12 | npm test 13 | ``` 14 | 15 | ### Prerequisite 16 | Please checkout the [typescript](https://github.com/rajjeet/react-playbook/tree/master/packages/typescript) package to 17 | understand the TypeScript setup before proceeding with this guide. 18 | 19 | ### Step 1: Install 20 | `npm install --save-dev jest @types/jest @babel/preset-typescript` 21 | Here's what each package is for: 22 | - `jest` is the test runner and framework that executes our tests and providers helper functions for assertions 23 | - `@types/jest` is types library that provides typing and intellisence for global jest keywords such as `describe 24 | ` and `it` in our test file. These makes type safety more robust for type files and provides better IDE support. 25 | - `@babel/preset-typescript` transpiles tests written in TypeScript to JavaScript, so Jest can understand them. 26 | 27 | ### Step 2: Add Babel Preset 28 | Add `"@babel/preset-typescript"` to `.babelrc` in the root directory. It should look like this: 29 | `.babelrc` 30 | ```json 31 | { 32 | "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"], 33 | "plugins": [] 34 | } 35 | ``` 36 | 37 | ### Step 3: Add Tests 38 | I've added tests, written using `ReactTestUtils`, in the `App.test.tsx` file for `App.tsx`. You can use a less 39 | verbose test utility helper library, such as Enzyme and React-Testing-Library here. 40 | `App.test.tsx` 41 | 42 | ```typescript jsx 43 | import * as React from "react"; 44 | import {act} from 'react-dom/test-utils'; 45 | import * as ReactDOM from "react-dom"; 46 | import {App} from "./App"; 47 | 48 | describe('App', function () { 49 | it('should display pass in number', function () { 50 | let container = document.createElement('div'); 51 | document.body.appendChild(container); 52 | act(() => { 53 | ReactDOM.render(, container); 54 | }) 55 | const header = container.querySelector('h1'); 56 | expect(header.textContent).toBe("Hello world React! Num: 191") 57 | }); 58 | }); 59 | ``` 60 | 61 | ### Step 4: Run Tests 62 | Finally, let's add a npm script in `package.json` as follows: 63 | `package.json` 64 | ``` 65 | ... 66 | "scripts": { 67 | ... 68 | "test": "jest" 69 | ... 70 | }, 71 | ... 72 | ``` 73 | 74 | Now, run our test using `npm test`. That's it! 75 | 76 | ## Checkout the other React Quick Starters 77 | Using these starters, I quickly pick up working knowledge of these libraries and implement them with confidence on 78 | complex projects. [Github Repo](https://github.com/rajjeet/react-playbook) -------------------------------------------------------------------------------- /packages/typescript-jest/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello world App 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/typescript-jest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-basic", 3 | "version": "0.0.0", 4 | "description": "A very basic react implmentation", 5 | "keywords": [ 6 | "react" 7 | ], 8 | "author": "Rajjeet Phull ", 9 | "homepage": "https://ortmesh.com", 10 | "license": "ISC", 11 | "main": "lib/react-basic.js", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/rajjeet/react-playbook.git" 15 | }, 16 | "scripts": { 17 | "start": "webpack-dev-server --open", 18 | "build": "webpack", 19 | "type-check": "tsc --noEmit", 20 | "test": "jest" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/rajjeet/react-playbook/issues" 24 | }, 25 | "dependencies": { 26 | "@types/react": "^17.0.0", 27 | "@types/react-dom": "^17.0.0", 28 | "react": "^16.12.0", 29 | "react-dom": "^16.12.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.7.5", 33 | "@babel/preset-env": "^7.7.6", 34 | "@babel/preset-react": "^7.7.4", 35 | "@babel/preset-typescript": "^7.12.7", 36 | "@types/jest": "^26.0.19", 37 | "babel-loader": "^8.0.6", 38 | "html-webpack-plugin": "^3.2.0", 39 | "jest": "^26.6.3", 40 | "ts-loader": "^8.0.12", 41 | "typescript": "^4.1.3", 42 | "webpack": "^4.41.3", 43 | "webpack-cli": "^3.3.10", 44 | "webpack-dev-server": "^3.9.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/typescript-jest/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {act} from 'react-dom/test-utils'; 3 | import * as ReactDOM from "react-dom"; 4 | import {App} from "./App"; 5 | 6 | describe('App', function () { 7 | it('should display pass in number', function () { 8 | let container = document.createElement('div'); 9 | document.body.appendChild(container); 10 | act(() => { 11 | ReactDOM.render(, container); 12 | }) 13 | const header = container.querySelector('h1'); 14 | expect(header.textContent).toBe("Hello world React! Num: 191") 15 | }); 16 | }); -------------------------------------------------------------------------------- /packages/typescript-jest/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type AppProps = { num: number }; 4 | 5 | export const App = ({num}: AppProps) =>

Hello world React! Num: {num}

; -------------------------------------------------------------------------------- /packages/typescript-jest/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import {App} from "./App"; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /packages/typescript-jest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "es6", 6 | "target": "es5", 7 | "jsx": "react", 8 | "allowJs": true, 9 | "moduleResolution": "Node" 10 | } 11 | } -------------------------------------------------------------------------------- /packages/typescript-jest/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: path.resolve(__dirname, 'src', 'index.tsx'), 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'bundle.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?$/, 15 | exclude: /node_modules/, 16 | loader: 'babel-loader' 17 | }, 18 | { 19 | test: /\.tsx?$/, 20 | exclude: /node_modules/, 21 | loader: 'ts-loader' 22 | } 23 | ] 24 | }, 25 | resolve: { 26 | extensions: [ '.tsx', '.ts', '.js' ], 27 | }, 28 | plugins: [new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'index.html') })] 29 | }; 30 | -------------------------------------------------------------------------------- /packages/typescript/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [] 4 | } -------------------------------------------------------------------------------- /packages/typescript/README.md: -------------------------------------------------------------------------------- 1 | # `typescript` 2 | 3 | A minimalistic setup of TypeScript for React projects 4 | 5 | ## Usage 6 | 7 | ``` 8 | git clone https://github.com/rajjeet/react-playbook 9 | cd react-playbook/packages/typescript 10 | npm install 11 | npm run type-check 12 | ``` 13 | 14 | ### Prerequisite 15 | Please checkout the [react-basics](https://github.com/rajjeet/react-playbook/tree/master/packages/react-basic) package to understand the React setup before completing this tutorial. 16 | 17 | ### Step 1: Install 18 | ```npm install --save-dev typescript ts-loader @types/react @types/react-dom``` 19 | Here's what each package is for: 20 | - `typescript` - the library that converts TypeScript files (file extension with `.ts` or `.tsx`) into JavaScript. 21 | - `ts-loader` - Webpack loader that integrates TypeScript into Webpack. Webpack uses this loader to help convert TS 22 | files into the JS and integrate into the final bundle. 23 | - `@types/react` - provide typing for React API. Also, provides intellisence and documentation. 24 | - `@types/react-dom` - provide typing for React DOM API. Also, provides intellisence and documentation. 25 | 26 | ### Step 2: Add TypeScript Config File 27 | Add `tsconfig.json` to root directory. 28 | 29 | `tsconfig.json` 30 | ``` 31 | { 32 | "compilerOptions": { 33 | "outDir": "./dist/", 34 | "noImplicitAny": true, 35 | "module": "es6", 36 | "target": "es5", 37 | "jsx": "react", 38 | "allowJs": true, 39 | "moduleResolution": "Node" 40 | } 41 | } 42 | ``` 43 | This file lets us add a configuration for the compiling of TypeScript code into JavaScript. 44 | 45 | ### Step 3: Configure Webpack 46 | Webpack needs to be configured to process TypeScript files. Here are the key changes to `webpack.config.js`: 47 | - add `ts-loader` and test for `ts` and `tsx` files 48 | ``` 49 | ... 50 | { 51 | test: /\.tsx?$/, 52 | exclude: /node_modules/, 53 | loader: 'ts-loader' 54 | } 55 | ... 56 | ``` 57 | - add `ts` and `tsx` to the list of extensions to resolve 58 | ``` 59 | ... 60 | resolve: { 61 | extensions: [ '.tsx', '.ts', '.js' ], 62 | } 63 | ... 64 | ``` 65 | - remember to rename the `js` files to `tsx` if they have React code, else `ts` and fix the entry point 66 | ``` 67 | ... 68 | entry: path.resolve(__dirname, 'src', 'index.tsx'), 69 | ... 70 | ``` 71 | 72 | ### Step 4: Change File Extensions of JavaScript/JSX to TypeScript (ts/tsx) 73 | After renaming the files, you can add types to functions such as below: 74 | 75 | `App.tsx` 76 | ``` 77 | import * as React from "react"; 78 | 79 | type AppProps = { num: number }; 80 | 81 | export const App = ({num}: AppProps) =>

Hello world React! Num: {num}

; 82 | ``` 83 | Here's the entry point: 84 | `index.tsx` 85 | ``` 86 | import * as React from 'react'; 87 | import * as ReactDOM from 'react-dom'; 88 | import {App} from "./App"; 89 | 90 | ReactDOM.render(, document.getElementById('root')); 91 | 92 | ``` 93 | 94 | Now run a type-check using `npm run type-check`, which runs the TypeScript compiler check without emitting any 95 | declarations. That's it! You're good to go! 96 | 97 | ## Checkout the other React Quick Starters 98 | Using these starters, I quickly pick up working knowledge of these libraries and implement them with confidence on 99 | complex projects. [Github Repo](https://github.com/rajjeet/react-playbook) -------------------------------------------------------------------------------- /packages/typescript/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello world App 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-basic", 3 | "version": "0.0.0", 4 | "description": "A very basic react implmentation", 5 | "keywords": [ 6 | "react" 7 | ], 8 | "author": "Rajjeet Phull ", 9 | "homepage": "https://ortmesh.com", 10 | "license": "ISC", 11 | "main": "lib/react-basic.js", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/rajjeet/react-playbook.git" 15 | }, 16 | "scripts": { 17 | "start": "webpack-dev-server --open", 18 | "build": "webpack", 19 | "type-check": "tsc --noEmit" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/rajjeet/react-playbook/issues" 23 | }, 24 | "dependencies": { 25 | "@types/react": "^17.0.0", 26 | "@types/react-dom": "^17.0.0", 27 | "react": "^16.12.0", 28 | "react-dom": "^16.12.0" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.7.5", 32 | "@babel/preset-env": "^7.7.6", 33 | "@babel/preset-react": "^7.7.4", 34 | "babel-loader": "^8.0.6", 35 | "html-webpack-plugin": "^3.2.0", 36 | "ts-loader": "^8.0.12", 37 | "typescript": "^4.1.3", 38 | "webpack": "^4.41.3", 39 | "webpack-cli": "^3.3.10", 40 | "webpack-dev-server": "^3.9.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/typescript/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type AppProps = { num: number }; 4 | 5 | export const App = ({num}: AppProps) =>

Hello world React! Num: {num}

; -------------------------------------------------------------------------------- /packages/typescript/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import {App} from "./App"; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /packages/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "es6", 6 | "target": "es5", 7 | "jsx": "react", 8 | "allowJs": true, 9 | "moduleResolution": "Node" 10 | } 11 | } -------------------------------------------------------------------------------- /packages/typescript/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: path.resolve(__dirname, 'src', 'index.tsx'), 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'bundle.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?$/, 15 | exclude: /node_modules/, 16 | loader: 'babel-loader' 17 | }, 18 | { 19 | test: /\.tsx?$/, 20 | exclude: /node_modules/, 21 | loader: 'ts-loader' 22 | } 23 | ] 24 | }, 25 | resolve: { 26 | extensions: [ '.tsx', '.ts', '.js' ], 27 | }, 28 | plugins: [new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'index.html') })] 29 | }; 30 | --------------------------------------------------------------------------------