├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── now.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.css ├── App.tsx ├── Counter.tsx ├── CounterReducer.tsx ├── CounterTitle.tsx ├── GitHubLogo.tsx ├── Title.css ├── Title.tsx ├── index.css ├── index.tsx ├── react-app-env.d.ts └── registerServiceWorker.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !src 3 | !public 4 | !package.json 5 | !yarn.lock -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # From https://zeit.co/docs/examples/create-react-app 2 | 3 | # Use Node.js version 10 4 | FROM mhart/alpine-node:10 5 | 6 | # Set the working directory 7 | WORKDIR /usr/src 8 | 9 | # Copy package manager files to the working directory and run install 10 | COPY package.json yarn.lock ./ 11 | RUN yarn install 12 | 13 | # Copy all files to the working directory 14 | COPY . . 15 | 16 | # Build the app and move the resulting build to the `/public` directory 17 | RUN yarn build 18 | RUN mv ./build /public -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 🎣 React Hooks TypeScript Examples 2 | 3 | [View the live demo here](https://react-hooks-typescript-example-ljpijrbetn.now.sh/) or [edit it live in CodeSandbox](https://codesandbox.io/s/github/skovy/react-hooks-typescript-example). 4 | 5 | ### Getting Started 6 | 7 | Check out [this post to create a React TypeScript app from scratch with support for React Hooks](https://medium.com/@skovy/using-react-hooks-with-typescript-aae6c7b2a3a9), or: 8 | 9 | - Clone this repository `git clone git@github.com:skovy/react-hooks-typescript-example.git` 10 | - Install the dependencies `yarn install` 11 | - Start the app `yarn start` 12 | - Experiment with React Hooks! 13 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "static", 3 | "static": { 4 | "rewrites": [ 5 | { 6 | "source": "**", 7 | "destination": "/index.html" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks-typescript-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "main": "src/index.tsx", 6 | "dependencies": { 7 | "@types/react": "16.8.7", 8 | "@types/react-dom": "^16.8.2", 9 | "react": "16.8.4", 10 | "react-dom": "16.8.4", 11 | "react-scripts": "2.1.8" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test", 17 | "eject": "react-scripts eject" 18 | }, 19 | "devDependencies": { 20 | "@types/jest": "^23.3.9", 21 | "@types/node": "^10.12.5", 22 | "typescript": "^3.1.6" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skovy/react-hooks-typescript-example/e49307f802dff207eb1986ce48a87229f28737f4/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 24 | React TypeScript Hooks 25 | 26 | 27 | 28 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | min-height: 100vh; 6 | } 7 | 8 | .app { 9 | text-align: center; 10 | padding: 32px; 11 | border-radius: 8px; 12 | box-shadow: 0 2px 16px 0 rgba(0, 0, 0, 0.5); 13 | background-color: #444444; 14 | min-width: 600px; 15 | } 16 | 17 | .separator { 18 | margin: 16px 0; 19 | height: 4px; 20 | border-radius: 2px; 21 | width: 100%; 22 | background-color: #f353d0; 23 | } -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./App.css"; 3 | import Counter from "./Counter"; 4 | import CounterReducer from "./CounterReducer"; 5 | import CounterTitle from "./CounterTitle"; 6 | import GitHubLogo from "./GitHubLogo"; 7 | import Title from "./Title"; 8 | 9 | // An object of all possible example components that can be rendered 10 | const EXAMPLES = { 11 | Counter, 12 | CounterTitle, 13 | CounterReducer 14 | }; 15 | 16 | type Examples = keyof typeof EXAMPLES; 17 | 18 | const EXAMPLE_NAMES = Object.keys(EXAMPLES) as Examples[]; 19 | 20 | const App = () => { 21 | // Use state to keep track of the current displayed example component 22 | const [example, setExample] = React.useState("Counter"); 23 | 24 | // The currently selected example component that should be rendered 25 | const ExampleComponent = EXAMPLES[example]; 26 | 27 | // A list of buttons for all examples to render 28 | const exampleButtons = EXAMPLE_NAMES.map(name => ( 29 | 37 | )); 38 | 39 | return ( 40 | <> 41 | 42 | <GitHubLogo /> 43 | <div className="container"> 44 | <div className="app"> 45 | {exampleButtons} 46 | <div className="separator" /> 47 | <ExampleComponent /> 48 | </div> 49 | </div> 50 | </> 51 | ); 52 | }; 53 | 54 | export default App; 55 | -------------------------------------------------------------------------------- /src/Counter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const Counter = () => { 4 | const [count, setCount] = React.useState(0); 5 | 6 | return ( 7 | <div className="counter"> 8 | <p>You clicked {count} times</p> 9 | <button onClick={() => setCount(count + 1)}>Click me</button> 10 | </div> 11 | ); 12 | }; 13 | 14 | export default Counter; 15 | -------------------------------------------------------------------------------- /src/CounterReducer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface State { 4 | count: number; 5 | } 6 | 7 | type Actions = "reset" | "increment" | "decrement"; 8 | 9 | interface Action { 10 | type: Actions; 11 | } 12 | 13 | const initialState: State = { count: 0 }; 14 | 15 | const reducer: React.Reducer<State, Action> = (state, action) => { 16 | switch (action.type) { 17 | case "reset": 18 | return initialState; 19 | case "increment": 20 | return { count: state.count + 1 }; 21 | case "decrement": 22 | return { count: state.count - 1 }; 23 | default: 24 | return state; 25 | } 26 | }; 27 | 28 | const CounterReducer = () => { 29 | const [state, dispatch] = React.useReducer<React.Reducer<State, Action>>( 30 | reducer, 31 | initialState 32 | ); 33 | 34 | return ( 35 | <> 36 | <p>Count: {state.count}</p> 37 | <button onClick={() => dispatch({ type: "reset" })}>Reset</button> 38 | <button onClick={() => dispatch({ type: "increment" })}>+</button> 39 | <button onClick={() => dispatch({ type: "decrement" })}>-</button> 40 | </> 41 | ); 42 | }; 43 | 44 | export default CounterReducer; 45 | -------------------------------------------------------------------------------- /src/CounterTitle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const CounterTitle = () => { 4 | const [count, setCount] = React.useState(0); 5 | 6 | React.useEffect( 7 | () => { 8 | // Update the document title to the current count 9 | document.title = `You clicked ${count} times`; 10 | 11 | // Cleanup the effect so that the document title is properly restored 12 | return () => { 13 | document.title = "React TypeScript Hooks"; 14 | }; 15 | }, 16 | [count] 17 | ); 18 | 19 | return ( 20 | <div className="counter"> 21 | <p>You clicked {count} times</p> 22 | <button onClick={() => setCount(count + 1)}>Click me</button> 23 | </div> 24 | ); 25 | }; 26 | 27 | export default CounterTitle; 28 | -------------------------------------------------------------------------------- /src/GitHubLogo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const GitHubLogo = () => { 4 | return ( 5 | <a 6 | href="https://github.com/skovy/react-hooks-typescript-example" 7 | className="github-corner" 8 | aria-label="View source on GitHub" 9 | > 10 | <svg 11 | width="80" 12 | height="80" 13 | viewBox="0 0 250 250" 14 | style={{ 15 | fill: "#151513", 16 | color: "#9339f9", 17 | position: "absolute", 18 | top: 0, 19 | border: 0, 20 | right: 0 21 | }} 22 | aria-hidden="true" 23 | > 24 | <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z" /> 25 | <path 26 | d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" 27 | fill="currentColor" 28 | style={{ transformOrigin: "130px 106px" }} 29 | className="octo-arm" 30 | /> 31 | <path 32 | d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" 33 | fill="currentColor" 34 | className="octo-body" 35 | /> 36 | </svg> 37 | </a> 38 | ); 39 | }; 40 | 41 | export default GitHubLogo; 42 | -------------------------------------------------------------------------------- /src/Title.css: -------------------------------------------------------------------------------- 1 | .title { 2 | position: absolute; 3 | top: 8px; 4 | left: 16px; 5 | } 6 | 7 | .title h1 { 8 | color: #fff; 9 | margin: 0; 10 | padding: 0; 11 | font-size: 32px; 12 | } 13 | -------------------------------------------------------------------------------- /src/Title.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./Title.css"; 3 | 4 | const Title = () => { 5 | return ( 6 | <div className="title"> 7 | <h1>React Hooks TypeScript</h1> 8 | </div> 9 | ); 10 | }; 11 | 12 | export default Title; 13 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: "Poppins", sans-serif; 5 | font-weight: 500; 6 | background-image: radial-gradient(circle at 62% 44%, #f353d0, #5801bc); 7 | min-height: 100vh; 8 | font-size: 16px; 9 | color: #fff; 10 | } 11 | 12 | #root { 13 | min-height: 100vh; 14 | } 15 | 16 | p { 17 | margin: 0 0 8px; 18 | } 19 | 20 | button { 21 | border-radius: 16px; 22 | background-color: #9339f9; 23 | padding: 8px 16px; 24 | border: 0; 25 | color: #fff; 26 | font-size: 16px; 27 | transition: background-color 200ms ease; 28 | outline: none; 29 | cursor: pointer; 30 | font-weight: 700; 31 | } 32 | 33 | button + button { 34 | margin-left: 8px; 35 | } 36 | 37 | button:hover, 38 | button:focus, 39 | button.active { 40 | background-color: #f353d0; 41 | } 42 | 43 | .github-corner:hover .octo-arm { 44 | animation: octocat-wave 560ms ease-in-out; 45 | } 46 | 47 | @keyframes octocat-wave { 48 | 0%, 49 | 100% { 50 | transform: rotate(0); 51 | } 52 | 20%, 53 | 60% { 54 | transform: rotate(-25deg); 55 | } 56 | 40%, 57 | 80% { 58 | transform: rotate(10deg); 59 | } 60 | } 61 | 62 | @media (max-width: 500px) { 63 | .github-corner:hover .octo-arm { 64 | animation: none; 65 | } 66 | 67 | .github-corner .octo-arm { 68 | animation: octocat-wave 560ms ease-in-out; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import './index.css'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | ReactDOM.render( 8 | <App />, 9 | document.getElementById('root') as HTMLElement 10 | ); 11 | registerServiceWorker(); 12 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="react-scripts" /> 2 | -------------------------------------------------------------------------------- /src/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-console 2 | // In production, we register a service worker to serve assets from local cache. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on the 'N+1' visit to a page, since previously 7 | // cached resources are updated in the background. 8 | 9 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 10 | // This link also includes instructions on opting out of this behavior. 11 | 12 | const isLocalhost = Boolean( 13 | window.location.hostname === 'localhost' || 14 | // [::1] is the IPv6 localhost address. 15 | window.location.hostname === '[::1]' || 16 | // 127.0.0.1/8 is considered localhost for IPv4. 17 | window.location.hostname.match( 18 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 19 | ) 20 | ); 21 | 22 | export default function register() { 23 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 24 | // The URL constructor is available in all browsers that support SW. 25 | const publicUrl = new URL( 26 | process.env.PUBLIC_URL!, 27 | window.location.toString() 28 | ); 29 | if (publicUrl.origin !== window.location.origin) { 30 | // Our service worker won't work if PUBLIC_URL is on a different origin 31 | // from what our page is served on. This might happen if a CDN is used to 32 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 33 | return; 34 | } 35 | 36 | window.addEventListener('load', () => { 37 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 38 | 39 | if (isLocalhost) { 40 | // This is running on localhost. Lets check if a service worker still exists or not. 41 | checkValidServiceWorker(swUrl); 42 | 43 | // Add some additional logging to localhost, pointing developers to the 44 | // service worker/PWA documentation. 45 | navigator.serviceWorker.ready.then(() => { 46 | console.log( 47 | 'This web app is being served cache-first by a service ' + 48 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 49 | ); 50 | }); 51 | } else { 52 | // Is not local host. Just register service worker 53 | registerValidSW(swUrl); 54 | } 55 | }); 56 | } 57 | } 58 | 59 | function registerValidSW(swUrl: string) { 60 | navigator.serviceWorker 61 | .register(swUrl) 62 | .then(registration => { 63 | registration.onupdatefound = () => { 64 | const installingWorker = registration.installing; 65 | if (installingWorker) { 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the old content will have been purged and 70 | // the fresh content will have been added to the cache. 71 | // It's the perfect time to display a 'New content is 72 | // available; please refresh.' message in your web app. 73 | console.log('New content is available; please refresh.'); 74 | } else { 75 | // At this point, everything has been precached. 76 | // It's the perfect time to display a 77 | // 'Content is cached for offline use.' message. 78 | console.log('Content is cached for offline use.'); 79 | } 80 | } 81 | }; 82 | } 83 | }; 84 | }) 85 | .catch(error => { 86 | console.error('Error during service worker registration:', error); 87 | }); 88 | } 89 | 90 | function checkValidServiceWorker(swUrl: string) { 91 | // Check if the service worker can be found. If it can't reload the page. 92 | fetch(swUrl) 93 | .then(response => { 94 | // Ensure service worker exists, and that we really are getting a JS file. 95 | if ( 96 | response.status === 404 || 97 | response.headers.get('content-type')!.indexOf('javascript') === -1 98 | ) { 99 | // No service worker found. Probably a different app. Reload the page. 100 | navigator.serviceWorker.ready.then(registration => { 101 | registration.unregister().then(() => { 102 | window.location.reload(); 103 | }); 104 | }); 105 | } else { 106 | // Service worker found. Proceed as normal. 107 | registerValidSW(swUrl); 108 | } 109 | }) 110 | .catch(() => { 111 | console.log( 112 | 'No internet connection found. App is running in offline mode.' 113 | ); 114 | }); 115 | } 116 | 117 | export function unregister() { 118 | if ('serviceWorker' in navigator) { 119 | navigator.serviceWorker.ready.then(registration => { 120 | registration.unregister(); 121 | }); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "allowJs": true, 5 | "skipLibCheck": false, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | "lib": [ 17 | "dom", 18 | "dom.iterable", 19 | "esnext" 20 | ] 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], 3 | "linterOptions": { 4 | "exclude": [ 5 | "config/**/*.js", 6 | "node_modules/**/*.ts", 7 | "coverage/lcov-report/*.js" 8 | ] 9 | }, 10 | "rules": { 11 | "jsx-no-lambda": false, 12 | "object-literal-sort-keys": false, 13 | "interface-name": [true, "never-prefix"] 14 | } 15 | } 16 | --------------------------------------------------------------------------------