├── .babelrc ├── .flowconfig ├── .gitignore ├── .prettierrc ├── README.md ├── flow-typed └── npm │ ├── cat-names_v1.x.x.js │ └── express_v4.16.x.js ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.js ├── CommentBody.js ├── CommentForm.js ├── CommentThread.js ├── fetchCommentThreads.js ├── fetchJSON.js ├── getPixelRatio.js ├── index.css ├── index.js ├── server.js └── submitComment.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env"], 4 | ["@babel/flow"] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | ./flow-typed/ 7 | 8 | [lints] 9 | 10 | [options] 11 | 12 | [strict] 13 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "jsxBracketSameLine": true, 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zero-to-flow 2 | 3 | This is a companion repository to the Zero to Flow in 30 Minutes talk. Here, you will find three states of its sample application, in three commits: 4 | 5 | * Working, with Flow types. 6 | * Broken, with Flow types. 7 | * Broken, without Flow types. 8 | 9 | ## Watch the original presentation 10 | 11 | [![Zero to Flow in 30 Minutes – video](https://i.ytimg.com/vi/M1CR0l5xSHg/0.jpg)](https://youtu.be/M1CR0l5xSHg) 12 | 13 | ## Installing 14 | 15 | Install all the dependencies needed to run the example using `yarn install`. 16 | 17 | ## Running Flow 18 | 19 | You can run flow on the command line by typing `yarn flow`. 20 | 21 | ## Integrating Flow with your editor 22 | 23 | To enjoy errors, hover-hints, and autocomplete in your editor, follow the instructions at https://flow.org/en/docs/editors/ 24 | -------------------------------------------------------------------------------- /flow-typed/npm/cat-names_v1.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: d6ec1bda00dd9f86e4bb61fcda4b4331 2 | // flow-typed version: da30fe6876/cat-names_v1.x.x/flow_>=v0.25.x 3 | 4 | declare module "cat-names" { 5 | declare var all: Array; 6 | declare function random(): string; 7 | } 8 | -------------------------------------------------------------------------------- /flow-typed/npm/express_v4.16.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 106bbf49ff0c0b351c95d483d617ffba 2 | // flow-typed version: 7fe23c8e85/express_v4.16.x/flow_>=v0.32.x 3 | 4 | import type { Server } from "http"; 5 | import type { Socket } from "net"; 6 | 7 | declare type express$RouterOptions = { 8 | caseSensitive?: boolean, 9 | mergeParams?: boolean, 10 | strict?: boolean 11 | }; 12 | 13 | declare class express$RequestResponseBase { 14 | app: express$Application; 15 | get(field: string): string | void; 16 | } 17 | 18 | declare type express$RequestParams = { 19 | [param: string]: string 20 | }; 21 | 22 | declare class express$Request extends http$IncomingMessage mixins express$RequestResponseBase { 23 | baseUrl: string; 24 | body: mixed; 25 | cookies: { [cookie: string]: string }; 26 | connection: Socket; 27 | fresh: boolean; 28 | hostname: string; 29 | ip: string; 30 | ips: Array; 31 | method: string; 32 | originalUrl: string; 33 | params: express$RequestParams; 34 | path: string; 35 | protocol: "https" | "http"; 36 | query: { [name: string]: string | Array }; 37 | route: string; 38 | secure: boolean; 39 | signedCookies: { [signedCookie: string]: string }; 40 | stale: boolean; 41 | subdomains: Array; 42 | xhr: boolean; 43 | accepts(types: string): string | false; 44 | accepts(types: Array): string | false; 45 | acceptsCharsets(...charsets: Array): string | false; 46 | acceptsEncodings(...encoding: Array): string | false; 47 | acceptsLanguages(...lang: Array): string | false; 48 | header(field: string): string | void; 49 | is(type: string): boolean; 50 | param(name: string, defaultValue?: string): string | void; 51 | } 52 | 53 | declare type express$CookieOptions = { 54 | domain?: string, 55 | encode?: (value: string) => string, 56 | expires?: Date, 57 | httpOnly?: boolean, 58 | maxAge?: number, 59 | path?: string, 60 | secure?: boolean, 61 | signed?: boolean 62 | }; 63 | 64 | declare type express$Path = string | RegExp; 65 | 66 | declare type express$RenderCallback = ( 67 | err: Error | null, 68 | html?: string 69 | ) => mixed; 70 | 71 | declare type express$SendFileOptions = { 72 | maxAge?: number, 73 | root?: string, 74 | lastModified?: boolean, 75 | headers?: { [name: string]: string }, 76 | dotfiles?: "allow" | "deny" | "ignore" 77 | }; 78 | 79 | declare class express$Response extends http$ServerResponse mixins express$RequestResponseBase { 80 | headersSent: boolean; 81 | locals: { [name: string]: mixed }; 82 | append(field: string, value?: string): this; 83 | attachment(filename?: string): this; 84 | cookie(name: string, value: string, options?: express$CookieOptions): this; 85 | clearCookie(name: string, options?: express$CookieOptions): this; 86 | download( 87 | path: string, 88 | filename?: string, 89 | callback?: (err?: ?Error) => void 90 | ): this; 91 | format(typesObject: { [type: string]: Function }): this; 92 | json(body?: mixed): this; 93 | jsonp(body?: mixed): this; 94 | links(links: { [name: string]: string }): this; 95 | location(path: string): this; 96 | redirect(url: string, ...args: Array): this; 97 | redirect(status: number, url: string, ...args: Array): this; 98 | render( 99 | view: string, 100 | locals?: { [name: string]: mixed }, 101 | callback?: express$RenderCallback 102 | ): this; 103 | send(body?: mixed): this; 104 | sendFile( 105 | path: string, 106 | options?: express$SendFileOptions, 107 | callback?: (err?: ?Error) => mixed 108 | ): this; 109 | sendStatus(statusCode: number): this; 110 | header(field: string, value?: string): this; 111 | header(headers: { [name: string]: string }): this; 112 | set(field: string, value?: string | string[]): this; 113 | set(headers: { [name: string]: string }): this; 114 | status(statusCode: number): this; 115 | type(type: string): this; 116 | vary(field: string): this; 117 | req: express$Request; 118 | } 119 | 120 | declare type express$NextFunction = (err?: ?Error | "route") => mixed; 121 | declare type express$Middleware = 122 | | (( 123 | req: $Subtype, 124 | res: express$Response, 125 | next: express$NextFunction 126 | ) => mixed) 127 | | (( 128 | error: Error, 129 | req: $Subtype, 130 | res: express$Response, 131 | next: express$NextFunction 132 | ) => mixed); 133 | declare interface express$RouteMethodType { 134 | (middleware: express$Middleware): T; 135 | (...middleware: Array): T; 136 | ( 137 | path: express$Path | express$Path[], 138 | ...middleware: Array 139 | ): T; 140 | } 141 | declare class express$Route { 142 | all: express$RouteMethodType; 143 | get: express$RouteMethodType; 144 | post: express$RouteMethodType; 145 | put: express$RouteMethodType; 146 | head: express$RouteMethodType; 147 | delete: express$RouteMethodType; 148 | options: express$RouteMethodType; 149 | trace: express$RouteMethodType; 150 | copy: express$RouteMethodType; 151 | lock: express$RouteMethodType; 152 | mkcol: express$RouteMethodType; 153 | move: express$RouteMethodType; 154 | purge: express$RouteMethodType; 155 | propfind: express$RouteMethodType; 156 | proppatch: express$RouteMethodType; 157 | unlock: express$RouteMethodType; 158 | report: express$RouteMethodType; 159 | mkactivity: express$RouteMethodType; 160 | checkout: express$RouteMethodType; 161 | merge: express$RouteMethodType; 162 | 163 | // @TODO Missing 'm-search' but get flow illegal name error. 164 | 165 | notify: express$RouteMethodType; 166 | subscribe: express$RouteMethodType; 167 | unsubscribe: express$RouteMethodType; 168 | patch: express$RouteMethodType; 169 | search: express$RouteMethodType; 170 | connect: express$RouteMethodType; 171 | } 172 | 173 | declare class express$Router extends express$Route { 174 | constructor(options?: express$RouterOptions): void; 175 | route(path: string): express$Route; 176 | static (options?: express$RouterOptions): express$Router; 177 | use(middleware: express$Middleware): this; 178 | use(...middleware: Array): this; 179 | use( 180 | path: express$Path | express$Path[], 181 | ...middleware: Array 182 | ): this; 183 | use(path: string, router: express$Router): this; 184 | handle( 185 | req: http$IncomingMessage, 186 | res: http$ServerResponse, 187 | next: express$NextFunction 188 | ): void; 189 | param( 190 | param: string, 191 | callback: ( 192 | req: $Subtype, 193 | res: express$Response, 194 | next: express$NextFunction, 195 | id: string 196 | ) => mixed 197 | ): void; 198 | 199 | // Can't use regular callable signature syntax due to https://github.com/facebook/flow/issues/3084 200 | $call: ( 201 | req: http$IncomingMessage, 202 | res: http$ServerResponse, 203 | next?: ?express$NextFunction 204 | ) => void; 205 | } 206 | 207 | /* 208 | With flow-bin ^0.59, express app.listen() is deemed to return any and fails flow type coverage. 209 | Which is ironic because https://github.com/facebook/flow/blob/master/Changelog.md#misc-2 (release notes for 0.59) 210 | says "Improves typings for Node.js HTTP server listen() function." See that? IMPROVES! 211 | To work around this issue, we changed Server to ?Server here, so that our invocations of express.listen() will 212 | not be deemed to lack type coverage. 213 | */ 214 | 215 | declare class express$Application extends express$Router mixins events$EventEmitter { 216 | constructor(): void; 217 | locals: { [name: string]: mixed }; 218 | mountpath: string; 219 | listen( 220 | port: number, 221 | hostname?: string, 222 | backlog?: number, 223 | callback?: (err?: ?Error) => mixed 224 | ): ?Server; 225 | listen( 226 | port: number, 227 | hostname?: string, 228 | callback?: (err?: ?Error) => mixed 229 | ): ?Server; 230 | listen(port: number, callback?: (err?: ?Error) => mixed): ?Server; 231 | listen(path: string, callback?: (err?: ?Error) => mixed): ?Server; 232 | listen(handle: Object, callback?: (err?: ?Error) => mixed): ?Server; 233 | disable(name: string): void; 234 | disabled(name: string): boolean; 235 | enable(name: string): express$Application; 236 | enabled(name: string): boolean; 237 | engine(name: string, callback: Function): void; 238 | /** 239 | * Mixed will not be taken as a value option. Issue around using the GET http method name and the get for settings. 240 | */ 241 | // get(name: string): mixed; 242 | set(name: string, value: mixed): mixed; 243 | render( 244 | name: string, 245 | optionsOrFunction: { [name: string]: mixed }, 246 | callback: express$RenderCallback 247 | ): void; 248 | handle( 249 | req: http$IncomingMessage, 250 | res: http$ServerResponse, 251 | next?: ?express$NextFunction 252 | ): void; 253 | } 254 | 255 | declare type JsonOptions = { 256 | inflate?: boolean, 257 | limit?: string | number, 258 | reviver?: (key: string, value: mixed) => mixed, 259 | strict?: boolean, 260 | type?: string | Array | ((req: express$Request) => boolean), 261 | verify?: ( 262 | req: express$Request, 263 | res: express$Response, 264 | buf: Buffer, 265 | encoding: string 266 | ) => mixed 267 | }; 268 | 269 | declare type express$UrlEncodedOptions = { 270 | extended?: boolean, 271 | inflate?: boolean, 272 | limit?: string | number, 273 | parameterLimit?: number, 274 | type?: string | Array | ((req: express$Request) => boolean), 275 | verify?: ( 276 | req: express$Request, 277 | res: express$Response, 278 | buf: Buffer, 279 | encoding: string 280 | ) => mixed, 281 | } 282 | 283 | declare module "express" { 284 | declare export type RouterOptions = express$RouterOptions; 285 | declare export type CookieOptions = express$CookieOptions; 286 | declare export type Middleware = express$Middleware; 287 | declare export type NextFunction = express$NextFunction; 288 | declare export type RequestParams = express$RequestParams; 289 | declare export type $Response = express$Response; 290 | declare export type $Request = express$Request; 291 | declare export type $Application = express$Application; 292 | 293 | declare module.exports: { 294 | (): express$Application, // If you try to call like a function, it will use this signature 295 | json: (opts: ?JsonOptions) => express$Middleware, 296 | static: (root: string, options?: Object) => express$Middleware, // `static` property on the function 297 | Router: typeof express$Router, // `Router` property on the function 298 | urlencoded: (opts: ?express$UrlEncodedOptions) => express$Middleware, 299 | }; 300 | } 301 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zero-to-flow", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "body-parser": "1.18.2", 7 | "cat-names": "1.0.2", 8 | "cookie-dough": "0.1.0", 9 | "cookie-parser": "1.4.3", 10 | "express": "4.16.3", 11 | "invariant": "2.2.4", 12 | "react": "^16.3.2", 13 | "react-dom": "^16.3.2", 14 | "react-scripts": "1.1.4" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject", 21 | "flow": "flow", 22 | "server": 23 | "npx nodemon src/server.js --exec babel-node --watch src/server.js" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "7.0.0-beta.47", 27 | "@babel/node": "7.0.0-beta.47", 28 | "@babel/preset-env": "7.0.0-beta.47", 29 | "@babel/preset-flow": "7.0.0-beta.47", 30 | "flow-bin": "0.73.0", 31 | "nodemon": "1.17.4", 32 | "prettier": "1.12.1" 33 | }, 34 | "proxy": "http://localhost:5000" 35 | } 36 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steveluscher/zero-to-flow/f84e2d06acd963c9c51602ff6eadb3a9b4ab28d2/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /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.js: -------------------------------------------------------------------------------- 1 | import CommentThread from './CommentThread'; 2 | import CommentForm from './CommentForm'; 3 | import * as React from 'react'; 4 | 5 | import fetchCommentThreads from './fetchCommentThreads'; 6 | 7 | class App extends React.Component { 8 | state = { 9 | comments: [], 10 | }; 11 | componentDidMount() { 12 | const performFetch = async () => { 13 | const comments = fetchCommentThreads(); 14 | this.setState({comments}); 15 | setTimeout(300, performFetch); 16 | }; 17 | performFetch(); 18 | } 19 | render() { 20 | return ( 21 | 22 |

Join the conversation

23 |
    24 | {this.state.comments.map(comment => ( 25 | 26 | ))} 27 |
  • 28 | 29 |
  • 30 |
31 |
32 | ); 33 | } 34 | } 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /src/CommentBody.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import getPixelRatio from './getPixelRatio'; 4 | 5 | const AVATAR_SIZE = 32; 6 | 7 | class CommentBody extends React.Component { 8 | render() { 9 | return ( 10 | 11 | {`A 17 | {this.props.author.name} {this.props.text} 18 | 19 | ); 20 | } 21 | } 22 | 23 | export default CommentBody; 24 | -------------------------------------------------------------------------------- /src/CommentForm.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import submitComment from './submitComment'; 4 | 5 | class CommentForm extends React.Component { 6 | state = { 7 | commentText: '', 8 | }; 9 | handleChange = e => { 10 | this.setState({ 11 | commentText: e.target.value, 12 | }); 13 | }; 14 | handleSubmit = async e => { 15 | e.stop(); 16 | await submitComment(this.state.commentText, this.props.parentCommentID); 17 | this.setState({ 18 | commentText: null, 19 | }); 20 | }; 21 | render() { 22 | return ( 23 |
24 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | export default CommentForm; 36 | -------------------------------------------------------------------------------- /src/CommentThread.js: -------------------------------------------------------------------------------- 1 | import CommentBody from './CommentBody'; 2 | import CommentForm from './CommentForm'; 3 | import * as React from 'react'; 4 | 5 | class CommentThread extends React.Component { 6 | render() { 7 | return ( 8 |
  • 9 | 13 |
      14 | {this.props.comment.replies.map(reply => ( 15 | 16 | ))} 17 | {this.props.depth === 0 ? ( 18 |
    • 19 | 23 |
    • 24 | ) : null} 25 |
    26 |
  • 27 | ); 28 | } 29 | } 30 | 31 | export default CommentThread; 32 | -------------------------------------------------------------------------------- /src/fetchCommentThreads.js: -------------------------------------------------------------------------------- 1 | import fetchJSON from './fetchJSON'; 2 | 3 | async function fetchCommentThreads() { 4 | return await fetchJSON('/comments'); 5 | } 6 | 7 | export default fetchCommentThreads; 8 | -------------------------------------------------------------------------------- /src/fetchJSON.js: -------------------------------------------------------------------------------- 1 | async function fetchJSON(endpointPath, params) { 2 | return await fetch(endpointPath, { 3 | body: params, 4 | credentials: 'include', 5 | headers: { 6 | 'content-type': 'application/json', 7 | }, 8 | method: params == null ? 'GET' : 'POST', 9 | }).then(res => res.json()); 10 | } 11 | 12 | export default fetchJSON; 13 | -------------------------------------------------------------------------------- /src/getPixelRatio.js: -------------------------------------------------------------------------------- 1 | function getPixelRatio() { 2 | return window.devicePixelRatio; 3 | } 4 | 5 | export default getPixelRatio; 6 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | margin: 2em; 4 | padding: 0; 5 | } 6 | 7 | li { 8 | margin-bottom: 5px; 9 | } 10 | li:last-child { 11 | margin-bottom: 0; 12 | } 13 | 14 | img { 15 | border-radius: 50%; 16 | height: 32px; 17 | margin-right: 5px; 18 | width: 32px; 19 | vertical-align: middle; 20 | } 21 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import App from './App'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | import './index.css'; 6 | 7 | const mountPoint = document.getElementById('root'); 8 | ReactDOM.render(, mountPoint); 9 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import CatNames from 'cat-names'; 2 | 3 | import bodyParser from 'body-parser'; 4 | import cookieParser from 'cookie-parser'; 5 | import express from 'express'; 6 | 7 | const app = express(); 8 | 9 | /** 10 | * Assign a user account; save it in a session cookie. 11 | */ 12 | const authors = {}; 13 | let nextAuthorID = 0; 14 | app.use(cookieParser(`sekrit-${Date.now()}`)); 15 | app.use((req, res, next) => { 16 | const authorID = req.signedCookies['authorID']; 17 | if (isNaN(authorID)) { 18 | const authorID = nextAuthorID++; 19 | res.setCookie('authorID', JSON.stringify(authorID), {signed: true}); 20 | authors[authorID] = { 21 | id: authorID, 22 | name: CatNames.random(), 23 | }; 24 | } 25 | req.author = authors[authorID]; 26 | next(); 27 | }); 28 | 29 | /** 30 | * Vend the comment thread. 31 | */ 32 | const comments = []; 33 | app.get('/comments', (req, res) => { 34 | res.json(comments); 35 | }); 36 | 37 | /** 38 | * Add a comment to the thread. 39 | */ 40 | const commentsIndex = {}; 41 | let nextCommentID = 0; 42 | app.use(bodyParser.json()); 43 | app.post('/comments/add', (req, res) => { 44 | const parentCommentID = req.body.parentCommentID; 45 | const text = req.body.text; 46 | let threadToAppendCommentTo; 47 | if (parentCommentID == null) { 48 | threadToAppendCommentTo = comments; 49 | } else { 50 | const parentComment = commentsIndex[parentCommentID]; 51 | threadToAppendCommentTo = parentComment.replies; 52 | } 53 | const newComment = { 54 | author: req.author, 55 | id: nextCommentID++, 56 | text, 57 | }; 58 | threadToAppendCommentTo.push(newComment); 59 | commentsIndex[newComment.id] = newComment; 60 | res.json(comments); 61 | }); 62 | 63 | app.listen(5000); 64 | -------------------------------------------------------------------------------- /src/submitComment.js: -------------------------------------------------------------------------------- 1 | import fetchJSON from './fetchJSON'; 2 | 3 | async function submitComment(text, parentCommentID) { 4 | await fetchJSON('/comments/add', JSON.stringify({parentCommentID, text})); 5 | } 6 | 7 | export default submitComment; 8 | --------------------------------------------------------------------------------