├── .babelrc ├── .gitignore ├── README.md ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── modules.js ├── paths.js ├── pnpTs.js ├── webpack.config.js └── webpackDevServer.config.js ├── index.html ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── scripts ├── build.js ├── start.js └── test.js ├── src ├── course │ ├── 2.closure │ │ ├── app.tsx │ │ ├── index.jsx │ │ └── state.ts │ ├── customHooks │ │ ├── EqualArr │ │ │ ├── equalArr.ts │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ ├── EqualArrPlus │ │ │ ├── index.tsx │ │ │ ├── style.scss │ │ │ └── useEqualArr.ts │ │ ├── MousePos │ │ │ ├── index.tsx │ │ │ └── usePointor.ts │ │ ├── MousePos2 │ │ │ ├── Mouse.tsx │ │ │ └── index.tsx │ │ ├── ZhihuFeed │ │ │ ├── api.ts │ │ │ ├── index.tsx │ │ │ ├── style.scss │ │ │ └── useFeed.ts │ │ ├── ZhihuFeed2 │ │ │ ├── api.ts │ │ │ ├── index.tsx │ │ │ ├── style.scss │ │ │ └── useFeed.ts │ │ ├── app.tsx │ │ ├── style.scss │ │ ├── useCouter.ts │ │ ├── useEqual.ts │ │ ├── useInitial.ts │ │ ├── useLogin.ts │ │ ├── usePageTitle.ts │ │ ├── useSurvivalTime.ts │ │ ├── useUserInfo.ts │ │ └── withMousePos.jsx │ ├── introduce │ │ ├── controlledByClass.tsx │ │ ├── controlledByHooks.tsx │ │ ├── demoByClass.tsx │ │ ├── demoByHooks.tsx │ │ └── index.tsx │ ├── useCallback │ │ ├── index.tsx │ │ └── utils.ts │ ├── useContext │ │ ├── context.tsx │ │ ├── context2.tsx │ │ ├── demo │ │ │ ├── components │ │ │ │ ├── Home │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.scss │ │ │ │ ├── Hot │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.scss │ │ │ │ └── Setting │ │ │ │ │ ├── index.scss │ │ │ │ │ └── index.tsx │ │ │ ├── context.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ └── index.tsx │ ├── useContextWithReducer │ │ ├── context.tsx │ │ └── index.tsx │ ├── useEffect │ │ ├── demo01.tsx │ │ ├── demo02.tsx │ │ ├── demo03.tsx │ │ └── style.scss │ ├── useReducer │ │ ├── Counter │ │ │ ├── combineReducer.ts │ │ │ ├── index.tsx │ │ │ └── reducer.ts │ │ ├── ZhihuFeed2 │ │ │ ├── api.ts │ │ │ ├── index.tsx │ │ │ ├── style.scss │ │ │ └── useFeed.ts │ │ ├── context.tsx │ │ └── index.tsx │ ├── useRef │ │ ├── BaseRefClass.tsx │ │ ├── BaseRefFunction.tsx │ │ ├── components │ │ │ ├── Input │ │ │ │ └── index.tsx │ │ │ └── InputBase │ │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── timer.tsx │ └── useState │ │ ├── Demo.tsx │ │ ├── Rectangle │ │ ├── index.scss │ │ └── index.tsx │ │ └── asyncDemo.tsx ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts └── serviceWorker.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react-app" 4 | ], 5 | "plugins": [ 6 | ["import", { "libraryName": "antd-mobile", "style": "css" }] // `style: true` 会加载 less 文件 7 | ] 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-hooks 2 | React Hooks 3 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. Variable expansion is supported in .env files. 31 | // https://github.com/motdotla/dotenv 32 | // https://github.com/motdotla/dotenv-expand 33 | dotenvFiles.forEach(dotenvFile => { 34 | if (fs.existsSync(dotenvFile)) { 35 | require('dotenv-expand')( 36 | require('dotenv').config({ 37 | path: dotenvFile, 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | // We support resolving modules according to `NODE_PATH`. 44 | // This lets you use absolute paths in imports inside large monorepos: 45 | // https://github.com/facebook/create-react-app/issues/253. 46 | // It works similar to `NODE_PATH` in Node itself: 47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 49 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 50 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 51 | // We also resolve them to make sure all tools using them work consistently. 52 | const appDirectory = fs.realpathSync(process.cwd()); 53 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 54 | .split(path.delimiter) 55 | .filter(folder => folder && !path.isAbsolute(folder)) 56 | .map(folder => path.resolve(appDirectory, folder)) 57 | .join(path.delimiter); 58 | 59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 60 | // injected into the application via DefinePlugin in Webpack configuration. 61 | const REACT_APP = /^REACT_APP_/i; 62 | 63 | function getClientEnvironment(publicUrl) { 64 | const raw = Object.keys(process.env) 65 | .filter(key => REACT_APP.test(key)) 66 | .reduce( 67 | (env, key) => { 68 | env[key] = process.env[key]; 69 | return env; 70 | }, 71 | { 72 | // Useful for determining whether we’re running in production mode. 73 | // Most importantly, it switches React into the correct mode. 74 | NODE_ENV: process.env.NODE_ENV || 'development', 75 | // Useful for resolving the correct path to static assets in `public`. 76 | // For example, . 77 | // This should only be used as an escape hatch. Normally you would put 78 | // images into the `src` and `import` them in code to get their paths. 79 | PUBLIC_URL: publicUrl, 80 | } 81 | ); 82 | // Stringify all values so we can feed into Webpack DefinePlugin 83 | const stringified = { 84 | 'process.env': Object.keys(raw).reduce((env, key) => { 85 | env[key] = JSON.stringify(raw[key]); 86 | return env; 87 | }, {}), 88 | }; 89 | 90 | return { raw, stringified }; 91 | } 92 | 93 | module.exports = getClientEnvironment; 94 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFileName = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFileName}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /config/modules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | 8 | /** 9 | * Get the baseUrl of a compilerOptions object. 10 | * 11 | * @param {Object} options 12 | */ 13 | function getAdditionalModulePaths(options = {}) { 14 | const baseUrl = options.baseUrl; 15 | 16 | // We need to explicitly check for null and undefined (and not a falsy value) because 17 | // TypeScript treats an empty string as `.`. 18 | if (baseUrl == null) { 19 | // If there's no baseUrl set we respect NODE_PATH 20 | // Note that NODE_PATH is deprecated and will be removed 21 | // in the next major release of create-react-app. 22 | 23 | const nodePath = process.env.NODE_PATH || ''; 24 | return nodePath.split(path.delimiter).filter(Boolean); 25 | } 26 | 27 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 28 | 29 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is 30 | // the default behavior. 31 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { 32 | return null; 33 | } 34 | 35 | // Allow the user set the `baseUrl` to `appSrc`. 36 | if (path.relative(paths.appSrc, baseUrlResolved) === '') { 37 | return [paths.appSrc]; 38 | } 39 | 40 | // Otherwise, throw an error. 41 | throw new Error( 42 | chalk.red.bold( 43 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." + 44 | ' Create React App does not support other values at this time.' 45 | ) 46 | ); 47 | } 48 | 49 | function getModules() { 50 | // Check if TypeScript is setup 51 | const hasTsConfig = fs.existsSync(paths.appTsConfig); 52 | const hasJsConfig = fs.existsSync(paths.appJsConfig); 53 | 54 | if (hasTsConfig && hasJsConfig) { 55 | throw new Error( 56 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' 57 | ); 58 | } 59 | 60 | let config; 61 | 62 | // If there's a tsconfig.json we assume it's a 63 | // TypeScript project and set up the config 64 | // based on tsconfig.json 65 | if (hasTsConfig) { 66 | config = require(paths.appTsConfig); 67 | // Otherwise we'll check if there is jsconfig.json 68 | // for non TS projects. 69 | } else if (hasJsConfig) { 70 | config = require(paths.appJsConfig); 71 | } 72 | 73 | config = config || {}; 74 | const options = config.compilerOptions || {}; 75 | 76 | const additionalModulePaths = getAdditionalModulePaths(options); 77 | 78 | return { 79 | additionalModulePaths: additionalModulePaths, 80 | hasTsConfig, 81 | }; 82 | } 83 | 84 | module.exports = getModules(); 85 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(inputPath, needsSlash) { 15 | const hasSlash = inputPath.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return inputPath.substr(0, inputPath.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${inputPath}/`; 20 | } else { 21 | return inputPath; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hooks", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@babel/core": "7.4.3", 7 | "@babel/plugin-transform-react-jsx-self": "^7.2.0", 8 | "@babel/plugin-transform-react-jsx-source": "^7.2.0", 9 | "@svgr/webpack": "4.1.0", 10 | "@types/animejs": "^2.0.2", 11 | "@types/jest": "24.0.13", 12 | "@types/node": "12.0.2", 13 | "@types/react": "16.8.18", 14 | "@types/react-dom": "16.8.4", 15 | "@typescript-eslint/eslint-plugin": "1.6.0", 16 | "@typescript-eslint/parser": "1.6.0", 17 | "animejs": "^3.0.1", 18 | "antd-mobile": "^2.2.13", 19 | "axios": "^0.19.0", 20 | "babel-eslint": "10.0.1", 21 | "babel-jest": "^24.8.0", 22 | "babel-loader": "8.0.5", 23 | "babel-plugin-import": "^1.11.2", 24 | "babel-plugin-named-asset-import": "^0.3.2", 25 | "babel-preset-react-app": "^9.0.0", 26 | "camelcase": "^5.2.0", 27 | "case-sensitive-paths-webpack-plugin": "2.2.0", 28 | "css-loader": "2.1.1", 29 | "dotenv": "6.2.0", 30 | "dotenv-expand": "4.2.0", 31 | "eslint": "^5.16.0", 32 | "eslint-config-react-app": "^4.0.1", 33 | "eslint-loader": "2.1.2", 34 | "eslint-plugin-flowtype": "2.50.1", 35 | "eslint-plugin-import": "2.16.0", 36 | "eslint-plugin-jsx-a11y": "6.2.1", 37 | "eslint-plugin-react": "7.12.4", 38 | "eslint-plugin-react-hooks": "^1.5.0", 39 | "file-loader": "3.0.1", 40 | "fs-extra": "7.0.1", 41 | "html-webpack-plugin": "4.0.0-beta.5", 42 | "identity-obj-proxy": "3.0.0", 43 | "is-wsl": "^1.1.0", 44 | "jest": "24.7.1", 45 | "jest-environment-jsdom-fourteen": "0.1.0", 46 | "jest-resolve": "24.7.1", 47 | "jest-watch-typeahead": "0.3.0", 48 | "mini-css-extract-plugin": "0.5.0", 49 | "mozz": "^0.1.3", 50 | "node-sass": "^4.12.0", 51 | "optimize-css-assets-webpack-plugin": "5.0.1", 52 | "pnp-webpack-plugin": "1.2.1", 53 | "postcss-flexbugs-fixes": "4.1.0", 54 | "postcss-loader": "3.0.0", 55 | "postcss-normalize": "7.0.1", 56 | "postcss-preset-env": "6.6.0", 57 | "postcss-safe-parser": "4.0.1", 58 | "react": "^16.8.6", 59 | "react-app-polyfill": "^1.0.1", 60 | "react-dev-utils": "^9.0.1", 61 | "react-dom": "^16.8.6", 62 | "resolve": "1.10.0", 63 | "sass-loader": "7.1.0", 64 | "semver": "6.0.0", 65 | "style-loader": "0.23.1", 66 | "terser-webpack-plugin": "1.2.3", 67 | "ts-pnp": "1.1.2", 68 | "typescript": "3.4.5", 69 | "url-loader": "1.1.2", 70 | "webpack": "4.29.6", 71 | "webpack-dev-server": "3.2.1", 72 | "webpack-manifest-plugin": "2.0.4", 73 | "workbox-webpack-plugin": "4.2.0" 74 | }, 75 | "scripts": { 76 | "start": "node scripts/start.js", 77 | "build": "node scripts/build.js", 78 | "test": "node scripts/test.js" 79 | }, 80 | "eslintConfig": { 81 | "extends": "react-app" 82 | }, 83 | "browserslist": { 84 | "production": [ 85 | ">0.2%", 86 | "not dead", 87 | "not op_mini all" 88 | ], 89 | "development": [ 90 | "last 1 chrome version", 91 | "last 1 firefox version", 92 | "last 1 safari version" 93 | ] 94 | }, 95 | "jest": { 96 | "collectCoverageFrom": [ 97 | "src/**/*.{js,jsx,ts,tsx}", 98 | "!src/**/*.d.ts" 99 | ], 100 | "setupFiles": [ 101 | "react-app-polyfill/jsdom" 102 | ], 103 | "setupFilesAfterEnv": [], 104 | "testMatch": [ 105 | "/src/**/__tests__/**/*.{js,jsx,ts,tsx}", 106 | "/src/**/*.{spec,test}.{js,jsx,ts,tsx}" 107 | ], 108 | "testEnvironment": "jest-environment-jsdom-fourteen", 109 | "transform": { 110 | "^.+\\.(js|jsx|ts|tsx)$": "/node_modules/babel-jest", 111 | "^.+\\.css$": "/config/jest/cssTransform.js", 112 | "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "/config/jest/fileTransform.js" 113 | }, 114 | "transformIgnorePatterns": [ 115 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$", 116 | "^.+\\.module\\.(css|sass|scss)$" 117 | ], 118 | "modulePaths": [], 119 | "moduleNameMapper": { 120 | "^react-native$": "react-native-web", 121 | "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy" 122 | }, 123 | "moduleFileExtensions": [ 124 | "web.js", 125 | "js", 126 | "web.ts", 127 | "ts", 128 | "web.tsx", 129 | "tsx", 130 | "json", 131 | "web.jsx", 132 | "jsx", 133 | "node" 134 | ], 135 | "watchPlugins": [ 136 | "jest-watch-typeahead/filename", 137 | "jest-watch-typeahead/testname" 138 | ] 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/advance-course/react-hooks/9c5535b6692de70da3c94fce6d1c6d0f0f9641ab/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 35 | 36 | 37 | 38 |
39 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /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": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'production'; 5 | process.env.NODE_ENV = 'production'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | 18 | const path = require('path'); 19 | const chalk = require('react-dev-utils/chalk'); 20 | const fs = require('fs-extra'); 21 | const webpack = require('webpack'); 22 | const configFactory = require('../config/webpack.config'); 23 | const paths = require('../config/paths'); 24 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 25 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 26 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 27 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 28 | const printBuildError = require('react-dev-utils/printBuildError'); 29 | 30 | const measureFileSizesBeforeBuild = 31 | FileSizeReporter.measureFileSizesBeforeBuild; 32 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 33 | const useYarn = fs.existsSync(paths.yarnLockFile); 34 | 35 | // These sizes are pretty large. We'll warn for bundles exceeding them. 36 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 37 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 38 | 39 | const isInteractive = process.stdout.isTTY; 40 | 41 | // Warn and crash if required files are missing 42 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 43 | process.exit(1); 44 | } 45 | 46 | // Generate configuration 47 | const config = configFactory('production'); 48 | 49 | // We require that you explicitly set browsers and do not fall back to 50 | // browserslist defaults. 51 | const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 52 | checkBrowsers(paths.appPath, isInteractive) 53 | .then(() => { 54 | // First, read the current file sizes in build directory. 55 | // This lets us display how much they changed later. 56 | return measureFileSizesBeforeBuild(paths.appBuild); 57 | }) 58 | .then(previousFileSizes => { 59 | // Remove all content but keep the directory so that 60 | // if you're in it, you don't end up in Trash 61 | fs.emptyDirSync(paths.appBuild); 62 | // Merge with the public folder 63 | copyPublicFolder(); 64 | // Start the webpack build 65 | return build(previousFileSizes); 66 | }) 67 | .then( 68 | ({ stats, previousFileSizes, warnings }) => { 69 | if (warnings.length) { 70 | console.log(chalk.yellow('Compiled with warnings.\n')); 71 | console.log(warnings.join('\n\n')); 72 | console.log( 73 | '\nSearch for the ' + 74 | chalk.underline(chalk.yellow('keywords')) + 75 | ' to learn more about each warning.' 76 | ); 77 | console.log( 78 | 'To ignore, add ' + 79 | chalk.cyan('// eslint-disable-next-line') + 80 | ' to the line before.\n' 81 | ); 82 | } else { 83 | console.log(chalk.green('Compiled successfully.\n')); 84 | } 85 | 86 | console.log('File sizes after gzip:\n'); 87 | printFileSizesAfterBuild( 88 | stats, 89 | previousFileSizes, 90 | paths.appBuild, 91 | WARN_AFTER_BUNDLE_GZIP_SIZE, 92 | WARN_AFTER_CHUNK_GZIP_SIZE 93 | ); 94 | console.log(); 95 | 96 | const appPackage = require(paths.appPackageJson); 97 | const publicUrl = paths.publicUrl; 98 | const publicPath = config.output.publicPath; 99 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 100 | printHostingInstructions( 101 | appPackage, 102 | publicUrl, 103 | publicPath, 104 | buildFolder, 105 | useYarn 106 | ); 107 | }, 108 | err => { 109 | console.log(chalk.red('Failed to compile.\n')); 110 | printBuildError(err); 111 | process.exit(1); 112 | } 113 | ) 114 | .catch(err => { 115 | if (err && err.message) { 116 | console.log(err.message); 117 | } 118 | process.exit(1); 119 | }); 120 | 121 | // Create the production build and print the deployment instructions. 122 | function build(previousFileSizes) { 123 | // We used to support resolving modules according to `NODE_PATH`. 124 | // This now has been deprecated in favor of jsconfig/tsconfig.json 125 | // This lets you use absolute paths in imports inside large monorepos: 126 | if (process.env.NODE_PATH) { 127 | console.log( 128 | chalk.yellow( 129 | 'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.' 130 | ) 131 | ); 132 | console.log(); 133 | } 134 | 135 | console.log('Creating an optimized production build...'); 136 | 137 | const compiler = webpack(config); 138 | return new Promise((resolve, reject) => { 139 | compiler.run((err, stats) => { 140 | let messages; 141 | if (err) { 142 | if (!err.message) { 143 | return reject(err); 144 | } 145 | messages = formatWebpackMessages({ 146 | errors: [err.message], 147 | warnings: [], 148 | }); 149 | } else { 150 | messages = formatWebpackMessages( 151 | stats.toJson({ all: false, warnings: true, errors: true }) 152 | ); 153 | } 154 | if (messages.errors.length) { 155 | // Only keep the first error. Others are often indicative 156 | // of the same problem, but confuse the reader with noise. 157 | if (messages.errors.length > 1) { 158 | messages.errors.length = 1; 159 | } 160 | return reject(new Error(messages.errors.join('\n\n'))); 161 | } 162 | if ( 163 | process.env.CI && 164 | (typeof process.env.CI !== 'string' || 165 | process.env.CI.toLowerCase() !== 'false') && 166 | messages.warnings.length 167 | ) { 168 | console.log( 169 | chalk.yellow( 170 | '\nTreating warnings as errors because process.env.CI = true.\n' + 171 | 'Most CI servers set it automatically.\n' 172 | ) 173 | ); 174 | return reject(new Error(messages.warnings.join('\n\n'))); 175 | } 176 | 177 | return resolve({ 178 | stats, 179 | previousFileSizes, 180 | warnings: messages.warnings, 181 | }); 182 | }); 183 | }); 184 | } 185 | 186 | function copyPublicFolder() { 187 | fs.copySync(paths.appPublic, paths.appBuild, { 188 | dereference: true, 189 | filter: file => file !== paths.appHtml, 190 | }); 191 | } 192 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | 18 | const fs = require('fs'); 19 | const chalk = require('react-dev-utils/chalk'); 20 | const webpack = require('webpack'); 21 | const WebpackDevServer = require('webpack-dev-server'); 22 | const clearConsole = require('react-dev-utils/clearConsole'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const { 25 | choosePort, 26 | createCompiler, 27 | prepareProxy, 28 | prepareUrls, 29 | } = require('react-dev-utils/WebpackDevServerUtils'); 30 | const openBrowser = require('react-dev-utils/openBrowser'); 31 | const paths = require('../config/paths'); 32 | const configFactory = require('../config/webpack.config'); 33 | const createDevServerConfig = require('../config/webpackDevServer.config'); 34 | 35 | const useYarn = fs.existsSync(paths.yarnLockFile); 36 | const isInteractive = process.stdout.isTTY; 37 | 38 | // Warn and crash if required files are missing 39 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 40 | process.exit(1); 41 | } 42 | 43 | // Tools like Cloud9 rely on this. 44 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 45 | const HOST = process.env.HOST || '0.0.0.0'; 46 | 47 | if (process.env.HOST) { 48 | console.log( 49 | chalk.cyan( 50 | `Attempting to bind to HOST environment variable: ${chalk.yellow( 51 | chalk.bold(process.env.HOST) 52 | )}` 53 | ) 54 | ); 55 | console.log( 56 | `If this was unintentional, check that you haven't mistakenly set it in your shell.` 57 | ); 58 | console.log( 59 | `Learn more here: ${chalk.yellow('https://bit.ly/CRA-advanced-config')}` 60 | ); 61 | console.log(); 62 | } 63 | 64 | // We require that you explicitly set browsers and do not fall back to 65 | // browserslist defaults. 66 | const { checkBrowsers } = require('react-dev-utils/browsersHelper'); 67 | checkBrowsers(paths.appPath, isInteractive) 68 | .then(() => { 69 | // We attempt to use the default port but if it is busy, we offer the user to 70 | // run on a different port. `choosePort()` Promise resolves to the next free port. 71 | return choosePort(HOST, DEFAULT_PORT); 72 | }) 73 | .then(port => { 74 | if (port == null) { 75 | // We have not found a port. 76 | return; 77 | } 78 | const config = configFactory('development'); 79 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 80 | const appName = require(paths.appPackageJson).name; 81 | const useTypeScript = fs.existsSync(paths.appTsConfig); 82 | const urls = prepareUrls(protocol, HOST, port); 83 | const devSocket = { 84 | warnings: warnings => 85 | devServer.sockWrite(devServer.sockets, 'warnings', warnings), 86 | errors: errors => 87 | devServer.sockWrite(devServer.sockets, 'errors', errors), 88 | }; 89 | // Create a webpack compiler that is configured with custom messages. 90 | const compiler = createCompiler({ 91 | appName, 92 | config, 93 | devSocket, 94 | urls, 95 | useYarn, 96 | useTypeScript, 97 | webpack, 98 | }); 99 | // Load proxy config 100 | const proxySetting = require(paths.appPackageJson).proxy; 101 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 102 | // Serve webpack assets generated by the compiler over a web server. 103 | const serverConfig = createDevServerConfig( 104 | proxyConfig, 105 | urls.lanUrlForConfig 106 | ); 107 | const devServer = new WebpackDevServer(compiler, serverConfig); 108 | // Launch WebpackDevServer. 109 | devServer.listen(port, HOST, err => { 110 | if (err) { 111 | return console.log(err); 112 | } 113 | if (isInteractive) { 114 | clearConsole(); 115 | } 116 | 117 | // We used to support resolving modules according to `NODE_PATH`. 118 | // This now has been deprecated in favor of jsconfig/tsconfig.json 119 | // This lets you use absolute paths in imports inside large monorepos: 120 | if (process.env.NODE_PATH) { 121 | console.log( 122 | chalk.yellow( 123 | 'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.' 124 | ) 125 | ); 126 | console.log(); 127 | } 128 | 129 | console.log(chalk.cyan('Starting the development server...\n')); 130 | openBrowser(urls.localUrlForBrowser); 131 | }); 132 | 133 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 134 | process.on(sig, function() { 135 | devServer.close(); 136 | process.exit(); 137 | }); 138 | }); 139 | }) 140 | .catch(err => { 141 | if (err && err.message) { 142 | console.log(err.message); 143 | } 144 | process.exit(1); 145 | }); 146 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--watchAll') === -1 45 | ) { 46 | // https://github.com/facebook/create-react-app/issues/5210 47 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 48 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 49 | } 50 | 51 | 52 | jest.run(argv); 53 | -------------------------------------------------------------------------------- /src/course/2.closure/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useState} from './state'; 3 | 4 | function Demo() { 5 | const [counter, setCounter] = useState(0); 6 | 7 | return ( 8 |
setCounter(counter + 1)}>hello world, {counter}
9 | ) 10 | } 11 | 12 | export default Demo(); -------------------------------------------------------------------------------- /src/course/2.closure/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function() { 4 | return
hello worlld
5 | } -------------------------------------------------------------------------------- /src/course/2.closure/state.ts: -------------------------------------------------------------------------------- 1 | let state: any = null; 2 | 3 | export const useState = (value: number) => { 4 | state = state || value; 5 | 6 | function dispatch(newValue: any) { 7 | state = newValue; 8 | // 假设此方法能触发页面渲染 9 | // render(); 10 | } 11 | 12 | return [state, dispatch]; 13 | } 14 | -------------------------------------------------------------------------------- /src/course/customHooks/EqualArr/equalArr.ts: -------------------------------------------------------------------------------- 1 | export default function equalArr(a: number[], b: number[]) { 2 | if (a.length !== b.length) { 3 | return false; 4 | } 5 | if (a.length === 0 && b.length === 0) { 6 | return true; 7 | } 8 | return a.every((item, i) => item === b[i]); 9 | } 10 | -------------------------------------------------------------------------------- /src/course/customHooks/EqualArr/index.tsx: -------------------------------------------------------------------------------- 1 | /** 对比两个数组是否相等 */ 2 | import React, { useState } from 'react'; 3 | import {Button, Flex, Card} from 'antd-mobile'; 4 | import equalArr from './equalArr'; 5 | import './style.scss'; 6 | 7 | export default function EqualArr() { 8 | const [arrA, setArrA] = useState([]); 9 | const [arrB, setArrB] = useState([]); 10 | const isEqual = equalArr(arrA, arrB); 11 | 12 | return ( 13 | 14 | 15 | 16 | {arrA.map((item, i) => ( 17 |
{item}
18 | ))} 19 | 20 |
21 | 22 | {isEqual.toString()} 23 | 24 | 25 | 26 | {arrB.map((item, i) => ( 27 |
{item}
28 | ))} 29 | 30 |
31 |
32 | ) 33 | } -------------------------------------------------------------------------------- /src/course/customHooks/EqualArr/style.scss: -------------------------------------------------------------------------------- 1 | .queal_arr_container { 2 | margin: 20px auto; 3 | width: 320px; 4 | 5 | .inner { 6 | width: 130px; 7 | } 8 | 9 | .middle { 10 | flex: 1; 11 | height: 100px; 12 | font-size: 20px; 13 | color: red; 14 | } 15 | 16 | .btn { 17 | font-size: 12px; 18 | } 19 | .item { 20 | height: 40px; 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | } 25 | } -------------------------------------------------------------------------------- /src/course/customHooks/EqualArrPlus/index.tsx: -------------------------------------------------------------------------------- 1 | /** 对比两个数组是否相等 */ 2 | import React from 'react'; 3 | import {Button, Flex, Card} from 'antd-mobile'; 4 | import useEqualArr from './useEqualArr'; 5 | import './style.scss'; 6 | 7 | export default function EqualArr() { 8 | const {arrA, arrB, setArrA, setArrB, isEqual} = useEqualArr(); 9 | 10 | return ( 11 | 12 | 13 | 14 | {arrA.map((item, i) => ( 15 |
{item}
16 | ))} 17 | 18 |
19 | 20 | {isEqual.toString()} 21 | 22 | 23 | 24 | {arrB.map((item, i) => ( 25 |
{item}
26 | ))} 27 | 28 |
29 |
30 | ) 31 | } -------------------------------------------------------------------------------- /src/course/customHooks/EqualArrPlus/style.scss: -------------------------------------------------------------------------------- 1 | .queal_arr_container { 2 | margin: 20px auto; 3 | width: 320px; 4 | 5 | .inner { 6 | width: 130px; 7 | } 8 | 9 | .middle { 10 | flex: 1; 11 | height: 100px; 12 | font-size: 20px; 13 | color: red; 14 | } 15 | 16 | .btn { 17 | font-size: 12px; 18 | } 19 | .item { 20 | height: 40px; 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | } 25 | } -------------------------------------------------------------------------------- /src/course/customHooks/EqualArrPlus/useEqualArr.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | function equalArr(a: number[], b: number[]) { 4 | if (a.length !== b.length) { 5 | return false; 6 | } 7 | if (a.length === 0 && b.length === 0) { 8 | return true; 9 | } 10 | return a.every((item, i) => item === b[i]); 11 | } 12 | 13 | 14 | export default function useEqualArr() { 15 | const [arrA, setArrA] = useState([]); 16 | const [arrB, setArrB] = useState([]); 17 | const isEqual = equalArr(arrA, arrB); 18 | 19 | return { 20 | arrA, 21 | setArrA, 22 | arrB, 23 | setArrB, 24 | isEqual 25 | } 26 | } -------------------------------------------------------------------------------- /src/course/customHooks/MousePos/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import usePointor from './usePointor'; 3 | 4 | export default function MousePos() { 5 | const {position, handleMouseMove} = usePointor(); 6 | return ( 7 |
8 |
x: {position.x}, y: {position.y}
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/course/customHooks/MousePos/usePointor.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export default function usePointor() { 4 | const [position, setPosition] = useState({x: 0, y: 0}); 5 | 6 | function handleMouseMove(event: React.MouseEvent) { 7 | setPosition({x: event.clientX, y: event.clientY}); 8 | } 9 | return {position, handleMouseMove} 10 | } -------------------------------------------------------------------------------- /src/course/customHooks/MousePos2/Mouse.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props { 4 | render: (props: {x: number, y: number}) => any 5 | } 6 | 7 | export default class Mouse extends React.Component { 8 | state = { x: 0, y: 0 } 9 | 10 | handleMouseMove = (event: any) => { 11 | this.setState({ 12 | x: event.clientX, 13 | y: event.clientY 14 | }) 15 | } 16 | 17 | render() { 18 | return ( 19 |
20 | {this.props.render(this.state)} 21 |
22 | ) 23 | } 24 | } -------------------------------------------------------------------------------- /src/course/customHooks/MousePos2/index.tsx: -------------------------------------------------------------------------------- 1 | import Mouse from './Mouse'; 2 | 3 | import React from 'react'; 4 | 5 | export default function MousePos() { 6 | return ( 7 |
8 | ( 10 |
x: {x}, y: {y}
11 | )} 12 | /> 13 |
14 | ) 15 | } -------------------------------------------------------------------------------- /src/course/customHooks/ZhihuFeed/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | interface Story { 4 | id?: number, 5 | ga_prefix?: string, 6 | hint?: string, 7 | image_hue?: string, 8 | title?: string, 9 | type?: number, 10 | url?: string, 11 | images?: string[] 12 | image?: string 13 | } 14 | 15 | export interface Feed { 16 | date: string, 17 | stories: Story[], 18 | top_stories: Story[] 19 | } 20 | 21 | export function zhLastFeedApi(): Promise { 22 | return axios.get('https://news-at.zhihu.com/api/4/news/latest').then(res => { 23 | return res.data; 24 | }); 25 | } -------------------------------------------------------------------------------- /src/course/customHooks/ZhihuFeed/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useFeed from './useFeed'; 3 | import { ActivityIndicator, Carousel } from 'antd-mobile'; 4 | import './style.scss'; 5 | 6 | // 执行如下指令,禁用chrome跨域限制 7 | // open -a "Google Chrome" --args --disable-web-security --user-data-dir 8 | 9 | export default function ZhihuFeed() { 10 | const feed = useFeed(); 11 | 12 | if (!feed) { 13 | return
14 | } 15 | 16 | const {stories, top_stories} = feed; 17 | 18 | return ( 19 |
20 | 21 | {top_stories.map((item, i) => ( 22 | 23 | 24 |
{item.title}
25 |
26 | ))} 27 |
28 | 29 |
30 | {stories.map((item, i) => ( 31 | 32 | 33 |
34 |
{item.title}
35 |
{item.hint}
36 |
37 |
38 | ))} 39 |
40 |
41 | ) 42 | } -------------------------------------------------------------------------------- /src/course/customHooks/ZhihuFeed/style.scss: -------------------------------------------------------------------------------- 1 | .feed_container { 2 | width: 375px; 3 | margin: 0 auto; 4 | background: #FFF; 5 | 6 | &.loading { 7 | display: flex; 8 | justify-content: center; 9 | } 10 | 11 | .top_feed_item { 12 | display: block; 13 | height: 200px; 14 | position: relative; 15 | 16 | img { 17 | display: block; 18 | width: 100%; 19 | height: 100%; 20 | } 21 | .title { 22 | position: absolute; 23 | width: 100%; 24 | height: 30px; 25 | bottom: 0; 26 | background-color: rgba($color: #000000, $alpha: 0.5); 27 | color: #FFF; 28 | line-height: 30px; 29 | text-indent: 10px; 30 | } 31 | } 32 | 33 | .feed_item { 34 | display: flex; 35 | height: 50px; 36 | padding: 10px; 37 | align-items: center; 38 | 39 | img { 40 | display: block; 41 | width: 50px; 42 | height: 50px; 43 | margin-right: 10px; 44 | } 45 | 46 | .info { 47 | flex: 1 48 | } 49 | .title { 50 | font-size: 16px; 51 | color: #333; 52 | } 53 | .tip { 54 | font-size: 13px; 55 | color: #666; 56 | margin-top: 10px; 57 | } 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/course/customHooks/ZhihuFeed/useFeed.ts: -------------------------------------------------------------------------------- 1 | import {useState, useEffect} from 'react'; 2 | import {zhLastFeedApi, Feed} from './api'; 3 | 4 | export default function useFeed() { 5 | 6 | const [feed, setFeed] = useState(); 7 | 8 | useEffect(() => { 9 | zhLastFeedApi().then(res => { 10 | setFeed(res); 11 | }) 12 | }, []); 13 | 14 | return feed; 15 | } -------------------------------------------------------------------------------- /src/course/customHooks/ZhihuFeed2/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | interface Story { 4 | id?: number, 5 | ga_prefix?: string, 6 | hint?: string, 7 | image_hue?: string, 8 | title?: string, 9 | type?: number, 10 | url?: string, 11 | images?: string[] 12 | image?: string 13 | } 14 | 15 | export interface Feed { 16 | date: string, 17 | stories: Story[], 18 | top_stories: Story[] 19 | } 20 | 21 | export function zhLastFeedApi(): Promise { 22 | return axios.get('https://news-at.zhihu.com/api/4/news/latest').then(res => { 23 | return res.data; 24 | }); 25 | } -------------------------------------------------------------------------------- /src/course/customHooks/ZhihuFeed2/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useFeed from './useFeed'; 3 | import { ActivityIndicator, Carousel, Button } from 'antd-mobile'; 4 | import './style.scss'; 5 | 6 | // 执行如下指令,禁用chrome跨域限制 7 | // open -a "Google Chrome" --args --disable-web-security --user-data-dir 8 | 9 | export default function ZhihuFeed() { 10 | const {feed, setLoading, loading} = useFeed(); 11 | 12 | if (loading) { 13 | return
14 | } 15 | 16 | if (!feed) { return null }; 17 | const {stories, top_stories} = feed; 18 | 19 | return ( 20 |
21 | 22 | {top_stories.map((item, i) => ( 23 | 24 | 25 |
{item.title}
26 |
27 | ))} 28 |
29 | 30 |
31 | {stories.map((item, i) => ( 32 | 33 | 34 |
35 |
{item.title}
36 |
{item.hint}
37 |
38 |
39 | ))} 40 |
41 | 42 |
43 | ) 44 | } -------------------------------------------------------------------------------- /src/course/customHooks/ZhihuFeed2/style.scss: -------------------------------------------------------------------------------- 1 | .feed_container { 2 | width: 375px; 3 | margin: 0 auto; 4 | background: #FFF; 5 | 6 | &.loading { 7 | display: flex; 8 | justify-content: center; 9 | } 10 | 11 | .top_feed_item { 12 | display: block; 13 | height: 200px; 14 | position: relative; 15 | 16 | img { 17 | display: block; 18 | width: 100%; 19 | height: 100%; 20 | } 21 | .title { 22 | position: absolute; 23 | width: 100%; 24 | height: 30px; 25 | bottom: 0; 26 | background-color: rgba($color: #000000, $alpha: 0.5); 27 | color: #FFF; 28 | line-height: 30px; 29 | text-indent: 10px; 30 | } 31 | } 32 | 33 | .feed_item { 34 | display: flex; 35 | height: 50px; 36 | padding: 10px; 37 | align-items: center; 38 | 39 | img { 40 | display: block; 41 | width: 50px; 42 | height: 50px; 43 | margin-right: 10px; 44 | } 45 | 46 | .info { 47 | flex: 1 48 | } 49 | .title { 50 | font-size: 16px; 51 | color: #333; 52 | } 53 | .tip { 54 | font-size: 13px; 55 | color: #666; 56 | margin-top: 10px; 57 | } 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/course/customHooks/ZhihuFeed2/useFeed.ts: -------------------------------------------------------------------------------- 1 | import {useState, useEffect} from 'react'; 2 | import {zhLastFeedApi, Feed} from './api'; 3 | 4 | export default function useFeed() { 5 | const [feed, setFeed] = useState(); 6 | const [loading, setLoading] = useState(true); 7 | 8 | useEffect(() => { 9 | // 做一个优化判断 10 | if (!loading) { 11 | return; 12 | } 13 | zhLastFeedApi().then(res => { 14 | setLoading(false); 15 | setFeed(res); 16 | }) 17 | }, [loading]); 18 | 19 | return {feed, setLoading, loading}; 20 | } 21 | 22 | /** 判断两个数组是否相等 */ 23 | export function equal(a: number[], b: number[]) { 24 | if (a.length !== b.length) { 25 | return false; 26 | } 27 | if (a.length === 0 && b.length === 0) { 28 | return true; 29 | } 30 | 31 | return a.every((item, i) => item === b[i]); 32 | } -------------------------------------------------------------------------------- /src/course/customHooks/app.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import usePageTitle from './usePageTitle'; 3 | import useCounter from './useCouter'; 4 | import './style.scss'; 5 | 6 | interface Postion { 7 | x: number, 8 | y: number 9 | } 10 | 11 | export default function AnimateDemo() { 12 | const def = { x: 0, y: 0 }; 13 | const [startPointPos, setStartPointPos] = useState(def); 14 | const [curPos, setCurPos] = useState(def); 15 | const [startElePos, setStartElePos] = useState(def); 16 | 17 | usePageTitle(`x位置: ${curPos.x}`); 18 | const coutner = useCounter(curPos.x); 19 | 20 | function start(e: any) { 21 | setStartPointPos({ x: e.clientX, y: e.clientY }); 22 | } 23 | 24 | function move(e: any) { 25 | if (!e.clientX && !e.clientX) { 26 | return; 27 | } 28 | 29 | const dis = { 30 | x: e.clientX - startPointPos.x, 31 | y: e.clientY - startPointPos.y 32 | } 33 | 34 | setCurPos({ 35 | x: startElePos.x + dis.x, 36 | y: startElePos.y + dis.y 37 | }) 38 | } 39 | 40 | function end(e: any) { 41 | setStartElePos(curPos); 42 | } 43 | 44 | return ( 45 |
46 |
白色方块位置变动次数:{coutner}
47 |
55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/course/customHooks/style.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 600px; 3 | height: 400px; 4 | margin: 0 auto; 5 | background-color: #FF4B4B; 6 | padding-left: 30px; 7 | display: flex; 8 | align-items: center; 9 | 10 | .el { 11 | background: #FFF; 12 | width: 50px; 13 | height: 50px; 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | font-weight: bold; 18 | color: #FF4B4B; 19 | position: relative; 20 | } 21 | } -------------------------------------------------------------------------------- /src/course/customHooks/useCouter.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | // 用于记录某个值的变化次数 4 | export default function useCounter(param: any) { 5 | const [counter, setCounter] = useState(0); 6 | 7 | useEffect(() => { 8 | setCounter(counter + 1); 9 | }, [param]); 10 | 11 | return counter; 12 | } -------------------------------------------------------------------------------- /src/course/customHooks/useEqual.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export default function useEqual(a: number[], b: number[], q: boolean) { 4 | const [isEqual, setEqual] = useState(false); 5 | 6 | useEffect(() => { 7 | if (q) { 8 | if (a.length != b.length) { 9 | return setEqual(false); 10 | } 11 | 12 | a.forEach((item, index) => { 13 | if (item != b[index]) { 14 | setEqual(false); 15 | } 16 | }) 17 | } 18 | }, [q]); 19 | 20 | return isEqual; 21 | } 22 | -------------------------------------------------------------------------------- /src/course/customHooks/useInitial.ts: -------------------------------------------------------------------------------- 1 | import {useState, useEffect} from 'react'; 2 | 3 | export default function useInitial( 4 | api: (params: P) => Promise, 5 | params: P, 6 | defaultData: T 7 | ) { 8 | const [loading, setLoading] = useState(true); 9 | const [response, setResponse] = useState(defaultData); 10 | const [errMsg, setErrmsg] = useState(''); 11 | 12 | useEffect(() => { 13 | if (!loading) { return }; 14 | getData(); 15 | }, [loading]); 16 | 17 | function getData() { 18 | api(params).then(res => { 19 | setResponse(res); 20 | }).catch(e => { 21 | setErrmsg(errMsg); 22 | }).finally(() => { 23 | setLoading(false); 24 | }) 25 | } 26 | 27 | return { 28 | loading, 29 | setLoading, 30 | response, 31 | errMsg 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/course/customHooks/useLogin.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export default function useLogin() { 4 | const [loginInfo, setLoginInfo] = useState(); 5 | const _loginInfo = localStorage.getItem('loginInfo'); 6 | 7 | /** 8 | * 从可能缓存登陆信息的地方取登陆信息 9 | * + 本地缓存 10 | * + cookie 11 | * + 内存中等 12 | * */ 13 | if (_loginInfo) { 14 | setLoginInfo(JSON.parse(_loginInfo)); 15 | } 16 | 17 | /** 如果没有拿到登陆信息,则跳转至登陆页面,或者利用缓存的账号密码信息自动重新登录,获得loginInfo */ 18 | window.location.href = '/user/login'; 19 | 20 | return loginInfo; 21 | } 22 | 23 | // 如果是小程序,需要处理的前置逻辑就非常多。 -------------------------------------------------------------------------------- /src/course/customHooks/usePageTitle.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | /** 设置标题 */ 4 | export default function useDrag(title: string) { 5 | useEffect(() => { 6 | document.title = title; 7 | }, [title]) 8 | } -------------------------------------------------------------------------------- /src/course/customHooks/useSurvivalTime.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | /** 4 | * 记录组件的存活时间 5 | * @param {destory} Boolean 默认为false,当组件确认要被销毁时,传入true 6 | */ 7 | export default function useSurvivalTime() { 8 | const [startTime] = useState(new Date().getTime()); 9 | 10 | useEffect(() => { 11 | return () => { 12 | const curTime = new Date().getTime(); 13 | 14 | // 计算得到时差,通常的处理是调用埋点接口,将时差数据保存在服务端 15 | const disTime = curTime - startTime; 16 | // api(disTime); 调用埋点接口 17 | } 18 | }, []); 19 | } -------------------------------------------------------------------------------- /src/course/customHooks/useUserInfo.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import axios from 'axios'; 3 | 4 | export default function useUserInfo() { 5 | const [userInfo, setUserInfo] = useState({}); 6 | 7 | useEffect(() => { 8 | axios.get('/userinfo').then(res => { 9 | setUserInfo(res.data); 10 | }) 11 | }, []); 12 | 13 | return userInfo; 14 | } 15 | -------------------------------------------------------------------------------- /src/course/customHooks/withMousePos.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default function withMousePos(MouseComponent) { 4 | return class extends Component { 5 | state = { 6 | x: 0, 7 | y: 0 8 | } 9 | 10 | mouseMoveHandler = (event) => { 11 | this.setState({ 12 | x: event.clientX, 13 | y: event.clientX 14 | }) 15 | } 16 | 17 | render() { 18 | return ( 19 |
20 | 21 |
22 | ) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/course/introduce/controlledByClass.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | interface Props { 4 | value: number, 5 | onChange: (num: number) => any 6 | } 7 | 8 | interface State { 9 | count: number 10 | } 11 | 12 | export default class Counter extends Component { 13 | static getDerivedStateFromProps(nextProps: Props) { 14 | return { 15 | count: nextProps.value 16 | } 17 | } 18 | 19 | public state = { 20 | count: 0 21 | } 22 | 23 | public onClickHandler = (count: number) => { 24 | const { onChange } = this.props; 25 | this.setState({ 26 | count 27 | }) 28 | onChange && onChange(count); 29 | } 30 | 31 | render() { 32 | const { count } = this.state; 33 | return [ 34 |
{count}
, 35 | 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/course/introduce/controlledByHooks.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | interface Props { 4 | value: number, 5 | onChange: (num: number) => any 6 | } 7 | 8 | export default function Counter({ value, onChange }: Props) { 9 | const [count, setCount] = useState(0); 10 | 11 | useEffect(() => { 12 | value && setCount(value); 13 | }, [value]); 14 | 15 | return [ 16 |
{count}
, 17 | 20 | ] 21 | } -------------------------------------------------------------------------------- /src/course/introduce/demoByClass.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class Counter extends Component { 4 | state = { 5 | count: 0 6 | } 7 | 8 | render() { 9 | const { count } = this.state; 10 | return ( 11 | <> 12 |
{count}
13 | 16 | 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/course/introduce/demoByHooks.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | export default function Counter() { 4 | const [counter, setCounter] = useState(0); 5 | 6 | return [ 7 |
{counter}
, 8 | 11 | ] 12 | } -------------------------------------------------------------------------------- /src/course/introduce/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Counter from './controlledByHooks'; 3 | 4 | export default function Introduce() { 5 | const [count, setCount] = useState(0); 6 | 7 | return ( 8 | // @ts-ignore 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/course/useCallback/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useCallback } from 'react'; 2 | import { Button } from 'antd-mobile'; 3 | 4 | export default function App() { 5 | const [target, setTarget] = useState(0); 6 | const [other, setOther] = useState(0) 7 | 8 | const sum = useMemo(() => { 9 | console.log('重新计算一次'); 10 | let _sum = 0; 11 | for (let i = 1; i <= target; i++) { 12 | _sum += i; 13 | } 14 | return _sum; 15 | }, [target]); 16 | 17 | const inputChange = useCallback((e) => { 18 | console.log(e.target.value); 19 | }, []); 20 | 21 | return ( 22 |
23 | 24 |
{target} {sum}
25 | 26 | 27 | 28 |
干扰项 {other}
29 | 30 | 31 |
32 | ) 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/course/useCallback/utils.ts: -------------------------------------------------------------------------------- 1 | export function summation(target: number) { 2 | let sum = 0; 3 | for(let i = 1; i <= target; i++) { 4 | sum += i; 5 | } 6 | return sum; 7 | } 8 | 9 | // 初始化一个非正常数字,用于缓存上一次的计算结果 10 | let preTarget = -1; 11 | let memoSum = 0; 12 | 13 | export function memoSummation(target: number) { 14 | // 如果传入的参数与上一次一样,直接换回缓存结果 15 | if (preTarget > 0 && preTarget === target) { 16 | return memoSum; 17 | } 18 | 19 | console.log('我出现,就表示重新计算了一次'); 20 | // 缓存本次传入的参数 21 | preTarget = target; 22 | let sum = 0; 23 | for (let i = 1; i <= target; i++) { 24 | sum += i; 25 | } 26 | // 缓存本次的计算结果 27 | memoSum = sum; 28 | return sum; 29 | } 30 | -------------------------------------------------------------------------------- /src/course/useContext/context.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ReactNode, useState, Dispatch } from 'react'; 2 | 3 | interface Injected { 4 | counter: number, 5 | setCounter: Dispatch, 6 | increment: () => any, 7 | decrement: () => any 8 | } 9 | 10 | export const context = createContext({} as Injected); 11 | 12 | interface Props { 13 | children?: ReactNode 14 | } 15 | 16 | export function CounterProvider({ children }: Props) { 17 | const [counter, setCounter] = useState(0); 18 | 19 | const value = { 20 | counter, 21 | setCounter, 22 | increment: () => setCounter(counter + 1), 23 | decrement: () => setCounter(counter - 1) 24 | } 25 | 26 | return ( 27 | {children} 28 | ) 29 | } -------------------------------------------------------------------------------- /src/course/useContext/context2.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ReactNode, useState, Dispatch } from 'react'; 2 | 3 | interface Injected { 4 | counter: number, 5 | setCounter: Dispatch, 6 | increment: () => any, 7 | decrement: () => any 8 | } 9 | 10 | export const context = createContext({} as Injected); 11 | 12 | interface Props { 13 | children?: ReactNode 14 | } 15 | 16 | export function CounterProvider({ children }: Props) { 17 | const [counter, setCounter] = useState(0); 18 | 19 | const value = { 20 | counter, 21 | setCounter, 22 | increment: () => setCounter(counter + 1), 23 | decrement: () => setCounter(counter - 1) 24 | } 25 | 26 | return ( 27 | {children} 28 | ) 29 | } -------------------------------------------------------------------------------- /src/course/useContext/demo/components/Home/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | interface Story { 4 | id?: number, 5 | ga_prefix?: string, 6 | hint?: string, 7 | image_hue?: string, 8 | title?: string, 9 | type?: number, 10 | url?: string, 11 | images?: string[] 12 | image?: string 13 | } 14 | 15 | export interface Feed { 16 | date: string, 17 | stories: Story[], 18 | top_stories: Story[] 19 | } 20 | 21 | export function zhLastFeedApi(): Promise { 22 | return axios.get('https://news-at.zhihu.com/api/4/news/latest').then(res => { 23 | return res.data; 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/course/useContext/demo/components/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from 'react'; 2 | import {zhLastFeedApi, Feed} from './api'; 3 | import { ActivityIndicator, Carousel } from 'antd-mobile'; 4 | import {ctx} from '../../context'; 5 | import './style.scss'; 6 | 7 | // 执行如下指令,禁用chrome跨域限制 8 | // open -a "Google Chrome" --args --disable-web-security --user-data-dir 9 | 10 | export default function ZhihuFeed() { 11 | const [feed, setFeed] = useState(); 12 | const {setUnreadIndex} = useContext(ctx); 13 | 14 | useEffect(() => { 15 | setUnreadIndex(0); 16 | zhLastFeedApi().then(res => { 17 | setFeed(res); 18 | }); 19 | }, []); 20 | 21 | if (!feed) { 22 | return
23 | } 24 | 25 | const {stories, top_stories} = feed; 26 | 27 | return ( 28 |
29 | 30 | {top_stories.map((item, i) => ( 31 | 32 |
{item.title}
33 |
34 | ))} 35 |
36 | 37 |
38 | {stories.map((item, i) => ( 39 | 40 | 41 |
42 |
{item.title}
43 |
{item.hint}
44 |
45 |
46 | ))} 47 |
48 |
49 | ) 50 | } -------------------------------------------------------------------------------- /src/course/useContext/demo/components/Home/style.scss: -------------------------------------------------------------------------------- 1 | .feed_container { 2 | width: 375px; 3 | height: 400px; 4 | margin: 0 auto; 5 | 6 | &.loading { 7 | display: flex; 8 | justify-content: center; 9 | } 10 | 11 | .top_feed_item { 12 | display: block; 13 | height: 200px; 14 | position: relative; 15 | background-size: cover; 16 | background-repeat: no-repeat; 17 | 18 | img { 19 | display: block; 20 | width: 100%; 21 | height: 100%; 22 | } 23 | .title { 24 | position: absolute; 25 | width: 100%; 26 | height: 30px; 27 | bottom: 0; 28 | background-color: rgba($color: #000000, $alpha: 0.5); 29 | color: #FFF; 30 | line-height: 30px; 31 | text-indent: 10px; 32 | } 33 | } 34 | 35 | .feed_item { 36 | display: flex; 37 | height: 50px; 38 | padding: 15px; 39 | align-items: center; 40 | background-color: #FFF; 41 | margin-bottom: 1px; 42 | 43 | img { 44 | display: block; 45 | width: 50px; 46 | height: 50px; 47 | margin-right: 10px; 48 | } 49 | 50 | .info { 51 | flex: 1 52 | } 53 | .title { 54 | font-size: 16px; 55 | color: #333; 56 | } 57 | .tip { 58 | font-size: 13px; 59 | color: #666; 60 | margin-top: 10px; 61 | } 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /src/course/useContext/demo/components/Hot/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | interface Story { 4 | id?: number, 5 | ga_prefix?: string, 6 | hint?: string, 7 | image_hue?: string, 8 | title?: string, 9 | type?: number, 10 | url?: string, 11 | images?: string[] 12 | image?: string 13 | } 14 | 15 | export interface Feed { 16 | date: string, 17 | stories: Story[], 18 | top_stories: Story[] 19 | } 20 | 21 | export function topViewApi(): Promise { 22 | return axios.get('http://wcf.open.cnblogs.com/blog/48HoursTopViewPosts/10').then(res => { 23 | return res.data; 24 | }); 25 | } -------------------------------------------------------------------------------- /src/course/useContext/demo/components/Hot/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from 'react'; 2 | import {topViewApi} from './api'; 3 | import { ActivityIndicator } from 'antd-mobile'; 4 | import { ctx } from '../../context'; 5 | import './style.scss'; 6 | 7 | // 执行如下指令,禁用chrome跨域限制 8 | // open -a "Google Chrome" --args --disable-web-security --user-data-dir 9 | 10 | export default function ZhihuFeed() { 11 | const [feed, setFeed] = useState(); 12 | const {setUnreadHot} = useContext(ctx); 13 | 14 | useEffect(() => { 15 | setUnreadHot(0); 16 | topViewApi().then(res => { 17 | setFeed(res); 18 | }) 19 | }, []); 20 | 21 | if (!feed) { 22 | return
23 | } 24 | 25 | return ( 26 |
27 |
28 | ) 29 | } -------------------------------------------------------------------------------- /src/course/useContext/demo/components/Hot/style.scss: -------------------------------------------------------------------------------- 1 | .feed_container { 2 | width: 375px; 3 | height: 400px; 4 | margin: 0 auto; 5 | 6 | &.loading { 7 | display: flex; 8 | justify-content: center; 9 | } 10 | } 11 | 12 | .blog_container { 13 | padding: 0 15px; 14 | 15 | title { 16 | display: block; 17 | height: 50px; 18 | line-height: 50px; 19 | font-size: 18px; 20 | overflow: hidden; 21 | white-space: nowrap; 22 | text-overflow: ellipsis 23 | } 24 | 25 | id { 26 | display: block; 27 | } 28 | 29 | updated { 30 | display: block; 31 | } 32 | 33 | entry { 34 | display: block; 35 | margin-top: 10px; 36 | } 37 | } -------------------------------------------------------------------------------- /src/course/useContext/demo/components/Setting/index.scss: -------------------------------------------------------------------------------- 1 | .setting_container { 2 | .title { 3 | padding: 0 15px; 4 | height: 50px; 5 | line-height: 50px; 6 | } 7 | 8 | .tip { 9 | font-size: 12px; 10 | color: #666; 11 | padding: 15px; 12 | line-height: 20px; 13 | } 14 | } -------------------------------------------------------------------------------- /src/course/useContext/demo/components/Setting/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { List, Stepper } from 'antd-mobile'; 3 | import {ctx} from '../../context'; 4 | import './index.scss'; 5 | 6 | export default function Setting() { 7 | const {unreadIndex, unreadHot, setUnreadIndex, setUnreadHot} = useContext(ctx); 8 | 9 | return ( 10 |
11 |
基本设置
12 | 13 | } 20 | > 21 | 首页未读 22 | 23 | } 30 | > 31 | 热门未读 32 | 33 | 34 | 35 |
该设置项仅仅用于展示context功能,实践中几乎不会有这样的需求,不过需要使用相同处理方式的需求很多
36 |
37 | ) 38 | } -------------------------------------------------------------------------------- /src/course/useContext/demo/context.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState, Dispatch, ReactNode } from 'react'; 2 | 3 | interface Injected { 4 | unreadIndex: number, 5 | setUnreadIndex: Dispatch, 6 | unreadHot: number, 7 | setUnreadHot: Dispatch, 8 | } 9 | 10 | export const ctx = createContext({} as Injected); 11 | 12 | interface Props { 13 | children?: ReactNode 14 | } 15 | 16 | export function Provider({children}: Props) { 17 | const [unreadIndex, setUnreadIndex] = useState(0); 18 | const [unreadHot, setUnreadHot] = useState(0); 19 | 20 | const value = { 21 | unreadIndex, 22 | setUnreadIndex, 23 | unreadHot, 24 | setUnreadHot, 25 | } 26 | 27 | return ( 28 | {children} 29 | ) 30 | } -------------------------------------------------------------------------------- /src/course/useContext/demo/index.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | 5 | #root { 6 | height: 100%; 7 | } 8 | 9 | .use_context_container { 10 | height: 100%; 11 | max-width: 475px; 12 | width: 100%; 13 | margin: 0 auto; 14 | display: flex; 15 | flex-direction: column; 16 | 17 | .tab_wrapper { 18 | height: 50px; 19 | display: flex; 20 | justify-content: space-around; 21 | align-items: center; 22 | border-bottom: 1px solid orange; 23 | } 24 | 25 | .content_wrapper { 26 | flex: 1; 27 | overflow: auto; 28 | } 29 | } -------------------------------------------------------------------------------- /src/course/useContext/demo/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext, useState} from 'react'; 2 | import {ctx, Provider} from './context'; 3 | import {Badge} from 'antd-mobile'; 4 | import Home from './components/Home'; 5 | import Hot from './components/Hot'; 6 | import Setting from './components/Setting'; 7 | import './index.scss'; 8 | 9 | function App() { 10 | const {unreadIndex, unreadHot} = useContext(ctx); 11 | const [tabIndex, setTabindex] = useState(0); 12 | 13 | return ( 14 |
15 |
16 | 17 |
setTabindex(0)}>首页
18 |
19 | 20 | 21 |
setTabindex(1)}>热门
22 |
23 | 24 |
setTabindex(2)}>设置
25 |
26 | 27 |
28 | {tabIndex === 0 && ( 29 | 30 | )} 31 | 32 | {tabIndex === 1 && ( 33 | 34 | )} 35 | 36 | {tabIndex === 2 && ( 37 | 38 | )} 39 |
40 |
41 | ) 42 | } 43 | 44 | export default () => ( 45 | 46 | 47 | 48 | ) -------------------------------------------------------------------------------- /src/course/useContext/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Button } from 'antd-mobile'; 3 | import { context, CounterProvider } from './context'; 4 | import { context as context2, CounterProvider as CounterProvider2 } from './context2'; 5 | 6 | function Counter() { 7 | const { counter = 0, increment, decrement } = useContext(context); 8 | const { counter: coutner2, increment: increment2, decrement: decrement2 } = useContext(context2); 9 | 10 | return ( 11 |
12 |
{counter}
13 | 14 | 15 | 16 |
{coutner2}
17 | 18 | 19 |
20 | ); 21 | } 22 | 23 | export default () => ( 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /src/course/useContextWithReducer/context.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ReactNode, useReducer } from 'react'; 2 | 3 | const initialState: number = 0; 4 | const reduer = (state: number, action: string) => { 5 | switch (action) { 6 | case 'increment': return state + 1; 7 | case 'decrement': return state - 1; 8 | case 'reset': return 0; 9 | } 10 | } 11 | 12 | interface Injected { 13 | counter: number, 14 | increment: () => any, 15 | decrement: () => any, 16 | reset: () => any 17 | } 18 | 19 | export const contenxt = createContext({} as Injected); 20 | 21 | interface Props { 22 | children?: ReactNode 23 | } 24 | 25 | export function CounterProvider({ children }: Props) { 26 | // @ts-ignore 27 | const [counter, dispatch] = useReducer(reduer, initialState); 28 | 29 | const value = { 30 | counter, 31 | increment: () => dispatch('increment'), 32 | decrement: () => dispatch('decrement'), 33 | reset: () => dispatch('reset') 34 | } 35 | 36 | return ( 37 | {children} 38 | ) 39 | } -------------------------------------------------------------------------------- /src/course/useContextWithReducer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Button } from 'antd-mobile'; 3 | import { contenxt, CounterProvider } from './context'; 4 | 5 | function Counter() { 6 | const { counter = 0, increment, decrement, reset } = useContext(contenxt); 7 | 8 | return ( 9 |
10 |
{counter}
11 | 12 | 13 | 14 |
15 | ); 16 | } 17 | 18 | export default () => ; -------------------------------------------------------------------------------- /src/course/useEffect/demo01.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import anime from 'animejs'; 3 | import './style.scss'; 4 | 5 | export default function AnimateDemo() { 6 | const [anime01, setAnime01] = useState(false); 7 | const [anime02, setAnime02] = useState(false); 8 | const element = useRef(); 9 | 10 | useEffect(() => { 11 | anime01 && !anime02 && animate01(); 12 | anime02 && !anime01 && animate02(); 13 | 14 | return () => { 15 | console.log('xxx'); 16 | } 17 | }, [anime01, anime02]); 18 | 19 | function animate01() { 20 | if (element) { 21 | anime({ 22 | targets: element.current, 23 | translateX: 400, 24 | backgroundColor: '#FF8F42', 25 | borderRadius: ['0%', '50%'], 26 | complete: () => { 27 | setAnime01(false); 28 | } 29 | }) 30 | } 31 | } 32 | 33 | function animate02() { 34 | if (element) { 35 | anime({ 36 | targets: element.current, 37 | translateX: 0, 38 | backgroundColor: '#FFF', 39 | borderRadius: ['50%', '0%'], 40 | easing: 'easeInOutQuad', 41 | complete: () => { 42 | setAnime02(false); 43 | } 44 | }) 45 | } 46 | } 47 | 48 | function clickHandler() { 49 | setAnime01(true); 50 | setTimeout(setAnime02.bind(null, true), 500); 51 | } 52 | 53 | return ( 54 |
55 |
56 |
57 | ) 58 | } -------------------------------------------------------------------------------- /src/course/useEffect/demo02.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import './style.scss'; 3 | 4 | export default function AnimateDemo() { 5 | const [counter, setCounter] = useState(0); 6 | 7 | // DOM渲染完成之后副作用执行 8 | useEffect(() => { 9 | const timer = setTimeout(() => { 10 | setCounter(counter + 1); 11 | }, 300); 12 | console.log('effect:', timer); 13 | 14 | return () => { 15 | console.log('clear:', timer); 16 | clearTimeout(timer); 17 | } 18 | }); 19 | 20 | console.log('before render'); 21 | 22 | return ( 23 |
24 |
{counter}
25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/course/useEffect/demo03.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect, useCallback } from 'react'; 2 | import anime from 'animejs'; 3 | import './style.scss'; 4 | 5 | export default function AnimateDemo() { 6 | const [anime01, setAnime01] = useState(false); 7 | const [anime02, setAnime02] = useState(false); 8 | const element = useRef(); 9 | 10 | const animate01 = useCallback(() => { 11 | if (element) { 12 | anime({ 13 | targets: element.current, 14 | translateX: 400, 15 | backgroundColor: '#FF8F42', 16 | borderRadius: ['0%', '50%'], 17 | complete: () => { 18 | setAnime01(false); 19 | } 20 | }) 21 | } 22 | }, []); 23 | 24 | const animate02 = useCallback(() => { 25 | if (element) { 26 | anime({ 27 | targets: element.current, 28 | translateX: 0, 29 | backgroundColor: '#FFF', 30 | borderRadius: ['50%', '0%'], 31 | easing: 'easeInOutQuad', 32 | complete: () => { 33 | setAnime02(false); 34 | } 35 | }) 36 | } 37 | }, []); 38 | 39 | useEffect(() => { 40 | anime01 && animate01(); 41 | }, [anime01]); 42 | 43 | useEffect(() => { 44 | anime02 && animate02(); 45 | }, [anime02]); 46 | 47 | const clickHandler = useCallback(() => { 48 | setAnime01(true); 49 | setTimeout(setAnime02.bind(null, true), 500); 50 | }, []); 51 | 52 | return ( 53 |
54 |
55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/course/useEffect/style.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 600px; 3 | height: 400px; 4 | margin: 0 auto; 5 | background-color: #FF4B4B; 6 | padding-left: 30px; 7 | display: flex; 8 | align-items: center; 9 | 10 | .el { 11 | background: #FFF; 12 | width: 50px; 13 | height: 50px; 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | font-weight: bold; 18 | color: #FF4B4B; 19 | } 20 | } -------------------------------------------------------------------------------- /src/course/useReducer/Counter/combineReducer.ts: -------------------------------------------------------------------------------- 1 | export interface IterationOB { 2 | [key: string]: any 3 | } 4 | 5 | // 参数为一个对象,里面有所有的reducer 6 | const combineReducer = (reducers: IterationOB) => { 7 | // 取得所有 key 8 | const reducerKeys = Object.keys(reducers); 9 | // 合并之后的State放在这里 10 | let objInitState: IterationOB = {}; 11 | 12 | // 检查每一项是否有默认值 13 | reducerKeys.forEach((key) => { 14 | // 传入空的type,获取默认值,这样写了之后,action的类型就只能是 { type: 'xxx', } 这种格式了 15 | const initState = reducers[key](undefined, { type: '' }) 16 | if (initState == 'undefined'){ 17 | throw new Error(`${key} does not return state.`) 18 | } 19 | objInitState[key] = initState; 20 | }) 21 | 22 | // 返回的函数里执行每一项reducer,最终返回合并之后的state 23 | return (state?: any, action?: any) => { 24 | if(action){ 25 | reducerKeys.forEach((key) => { 26 | const previousState = objInitState[key]; 27 | objInitState[key] = reducers[key](previousState, action); 28 | }) 29 | } 30 | 31 | return { ...objInitState }; 32 | } 33 | } 34 | 35 | export default combineReducer; -------------------------------------------------------------------------------- /src/course/useReducer/Counter/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from 'react'; 2 | import { Button } from 'antd-mobile'; 3 | import reducer from './reducer'; 4 | 5 | export default function Counter() { 6 | const [counter, dispatch] = useReducer(reducer, reducer()); 7 | console.log(counter); 8 | 9 | return ( 10 |
11 |
{counter.reducerA}
12 | 13 | 14 |
{counter.reducerB}
15 | 16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/course/useReducer/Counter/reducer.ts: -------------------------------------------------------------------------------- 1 | import combineReducer from './combineReducer'; 2 | 3 | interface Action { 4 | type: string, 5 | payload: number 6 | } 7 | 8 | const stateA: number = 0 9 | 10 | function reducerA(state = stateA, action: Action) { 11 | switch (action.type) { 12 | case 'incrementA': 13 | return state + action.payload 14 | case 'decrementA': 15 | return state - action.payload 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | const stateB: number = 0 22 | 23 | function reducerB(state = stateB, action: Action) { 24 | switch (action.type) { 25 | case 'incrementB': 26 | return state + action.payload 27 | case 'decrementB': 28 | return state - action.payload 29 | default: 30 | return state; 31 | } 32 | } 33 | 34 | export default combineReducer({reducerA, reducerB}); -------------------------------------------------------------------------------- /src/course/useReducer/ZhihuFeed2/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | interface Story { 4 | id?: number, 5 | ga_prefix?: string, 6 | hint?: string, 7 | image_hue?: string, 8 | title?: string, 9 | type?: number, 10 | url?: string, 11 | images?: string[] 12 | image?: string 13 | } 14 | 15 | export interface Feed { 16 | date: string, 17 | stories: Story[], 18 | top_stories: Story[] 19 | } 20 | 21 | export function zhLastFeedApi(): Promise { 22 | return axios.get('https://news-at.zhihu.com/api/4/news/latest').then(res => { 23 | return res.data; 24 | }); 25 | } -------------------------------------------------------------------------------- /src/course/useReducer/ZhihuFeed2/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useFeed from './useFeed'; 3 | import { ActivityIndicator, Carousel, Button } from 'antd-mobile'; 4 | import './style.scss'; 5 | 6 | // 执行如下指令,禁用chrome跨域限制 7 | // open -a "Google Chrome" --args --disable-web-security --user-data-dir 8 | 9 | export default function ZhihuFeed() { 10 | const {feed, setLoading, loading} = useFeed(); 11 | 12 | if (loading) { 13 | return
14 | } 15 | 16 | if (!feed) { return null }; 17 | const {stories, top_stories} = feed; 18 | 19 | return ( 20 |
21 | 22 | {top_stories.map((item, i) => ( 23 | 24 | 25 |
{item.title}
26 |
27 | ))} 28 |
29 | 30 |
31 | {stories.map((item, i) => ( 32 | 33 | 34 |
35 |
{item.title}
36 |
{item.hint}
37 |
38 |
39 | ))} 40 |
41 | 42 |
43 | ) 44 | } -------------------------------------------------------------------------------- /src/course/useReducer/ZhihuFeed2/style.scss: -------------------------------------------------------------------------------- 1 | .feed_container { 2 | width: 375px; 3 | margin: 0 auto; 4 | background: #FFF; 5 | 6 | &.loading { 7 | display: flex; 8 | justify-content: center; 9 | } 10 | 11 | .top_feed_item { 12 | display: block; 13 | height: 200px; 14 | position: relative; 15 | 16 | img { 17 | display: block; 18 | width: 100%; 19 | height: 100%; 20 | } 21 | .title { 22 | position: absolute; 23 | width: 100%; 24 | height: 30px; 25 | bottom: 0; 26 | background-color: rgba($color: #000000, $alpha: 0.5); 27 | color: #FFF; 28 | line-height: 30px; 29 | text-indent: 10px; 30 | } 31 | } 32 | 33 | .feed_item { 34 | display: flex; 35 | height: 50px; 36 | padding: 10px; 37 | align-items: center; 38 | 39 | img { 40 | display: block; 41 | width: 50px; 42 | height: 50px; 43 | margin-right: 10px; 44 | } 45 | 46 | .info { 47 | flex: 1 48 | } 49 | .title { 50 | font-size: 16px; 51 | color: #333; 52 | } 53 | .tip { 54 | font-size: 13px; 55 | color: #666; 56 | margin-top: 10px; 57 | } 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/course/useReducer/ZhihuFeed2/useFeed.ts: -------------------------------------------------------------------------------- 1 | import {useState, useEffect} from 'react'; 2 | import {zhLastFeedApi, Feed} from './api'; 3 | 4 | export default function useFeed() { 5 | const [feed, setFeed] = useState(); 6 | const [loading, setLoading] = useState(true); 7 | 8 | useEffect(() => { 9 | // 做一个优化判断 10 | if (!loading) { 11 | return; 12 | } 13 | zhLastFeedApi().then(res => { 14 | setLoading(false); 15 | setFeed(res); 16 | }) 17 | }, [loading]); 18 | 19 | return {feed, setLoading, loading}; 20 | } 21 | 22 | /** 判断两个数组是否相等 */ 23 | export function equal(a: number[], b: number[]) { 24 | if (a.length !== b.length) { 25 | return false; 26 | } 27 | if (a.length === 0 && b.length === 0) { 28 | return true; 29 | } 30 | 31 | return a.every((item, i) => item === b[i]); 32 | } -------------------------------------------------------------------------------- /src/course/useReducer/context.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ReactNode, useState, Dispatch } from 'react'; 2 | 3 | interface Injected { 4 | counter: number, 5 | setCounter: Dispatch, 6 | increment: () => any, 7 | decrement: () => any 8 | } 9 | 10 | export const contenxt = createContext({} as Injected); 11 | 12 | interface Props { 13 | children?: ReactNode 14 | } 15 | 16 | export function CounterProvider({ children }: Props) { 17 | const [counter, setCounter] = useState(0); 18 | 19 | const value = { 20 | counter, 21 | setCounter, 22 | increment: () => setCounter(counter + 1), 23 | decrement: () => setCounter(counter - 1) 24 | } 25 | 26 | return ( 27 | {children} 28 | ) 29 | } -------------------------------------------------------------------------------- /src/course/useReducer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useReducer, useState } from 'react'; 2 | import { Button } from 'antd-mobile'; 3 | 4 | const initialState: number = 0; 5 | const reduer = (state: number, action: string) => { 6 | switch(action) { 7 | case 'increment': return state + 1; 8 | case 'decrement': return state - 1; 9 | case 'reset': return 0; 10 | default: 11 | return state; 12 | } 13 | } 14 | 15 | export default function Counter() { 16 | const [counter, dispatch] = useReducer(reduer, initialState); 17 | 18 | return ( 19 |
20 |
{counter}
21 | 22 | 23 | 24 |
25 | ); 26 | } 27 | 28 | export function Counter1() { 29 | const [counter, setCounter] = useState(10); 30 | return ( 31 |
32 |
{counter}
33 | 34 | 35 | 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/course/useRef/BaseRefClass.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from "react"; 2 | 3 | export default class Demo extends Component { 4 | textInput = createRef(); 5 | 6 | focusTextInput = () => { 7 | if (this.textInput.current) { 8 | this.textInput.current.focus(); 9 | } 10 | } 11 | 12 | render() { 13 | return ( 14 | <> 15 | 16 | 17 | 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/course/useRef/BaseRefFunction.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef} from "react"; 2 | 3 | export default function Demo() { 4 | const inputRef = useRef(null); 5 | 6 | const focusTextInput = () => { 7 | if (inputRef.current) { 8 | inputRef.current.focus(); 9 | } 10 | } 11 | 12 | return ( 13 | <> 14 | 15 | 16 | 17 | ); 18 | } -------------------------------------------------------------------------------- /src/course/useRef/components/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef, useImperativeHandle, forwardRef, Ref, useState, ChangeEvent} from 'react'; 2 | 3 | export interface InputProps { 4 | value?: string, 5 | onChange?: (value: string) => any 6 | } 7 | 8 | export interface XInput { 9 | focus: () => void; 10 | blur: () => void; 11 | sayHi: () => void 12 | } 13 | 14 | function Input({value, onChange}: InputProps, ref: Ref) { 15 | const inputRef = useRef(null); 16 | const [_value, setValue] = useState(value || ''); 17 | 18 | useImperativeHandle(ref, () => ({ 19 | focus: () => { 20 | inputRef.current && inputRef.current.focus() 21 | }, 22 | blur: () => { 23 | inputRef.current && inputRef.current.blur() 24 | }, 25 | sayHi: () => { 26 | console.log('hello, world!'); 27 | } 28 | })); 29 | 30 | const _onChange = (e: ChangeEvent) => { 31 | const value = e.target.value; 32 | console.log(value); 33 | setValue(value); 34 | onChange && onChange(value); 35 | } 36 | 37 | return ( 38 |
39 | 自定义Input组件 40 | 41 |
42 | ); 43 | } 44 | 45 | export default forwardRef(Input); -------------------------------------------------------------------------------- /src/course/useRef/components/InputBase/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {forwardRef, useState, ChangeEvent} from 'react'; 2 | 3 | export interface InputProps { 4 | value?: string, 5 | onChange?: (value: string) => any 6 | } 7 | 8 | function Input({value, onChange}: InputProps, ref: any) { 9 | const [_value, setValue] = useState(value || ''); 10 | 11 | const _onChange = (e: ChangeEvent) => { 12 | const value = e.target.value; 13 | setValue(value); 14 | onChange && onChange(value); 15 | } 16 | 17 | return ( 18 |
19 | 自定义Input组件 20 | 21 |
22 | ); 23 | } 24 | 25 | export default forwardRef(Input); -------------------------------------------------------------------------------- /src/course/useRef/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from "react"; 2 | import Input from './components/Input'; 3 | import { Button } from "antd-mobile"; 4 | 5 | const Demo = () => { 6 | const textInput = useRef(null); 7 | const [text, setText] = useState('') 8 | 9 | const focusTextInput = () => { 10 | if (textInput.current) { 11 | textInput.current.focus(); 12 | textInput.current.sayHi(); 13 | } 14 | } 15 | 16 | return ( 17 | <> 18 | 19 | 20 |
{text}
21 | 22 | ); 23 | } 24 | 25 | export default Demo; -------------------------------------------------------------------------------- /src/course/useRef/timer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from 'react'; 2 | 3 | export default function Timer() { 4 | const timerRef = useRef(); 5 | 6 | useEffect(() => { 7 | timerRef.current = setInterval(() => { 8 | console.log('do something'); 9 | }, 1000); 10 | 11 | // 组件卸载时,清除定时器 12 | return () => { 13 | timerRef.current && clearInterval(timerRef.current); 14 | } 15 | }, []); 16 | 17 | return ( 18 |
19 | // ... 20 |
21 | ) 22 | } -------------------------------------------------------------------------------- /src/course/useState/Demo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | function Demo() { 4 | const [counter, setCounter] = useState(0); 5 | const [pen, setPen] = useState(true); 6 | 7 | useEffect(() => { 8 | if (pen) { 9 | setCounter(counter + 1); 10 | setCounter(counter + 1); 11 | setCounter(counter + 1); 12 | setPen(false); 13 | } 14 | }, [pen]); 15 | 16 | console.log(counter, pen); // 点击之后仅执行一次,合成事件中,,即使是多个不同的State,也会进行合并 17 | 18 | return ( 19 |
setPen(true)}>click me!
20 | ) 21 | } 22 | 23 | export default Demo; 24 | -------------------------------------------------------------------------------- /src/course/useState/Rectangle/index.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 375px; 3 | margin: 20px auto; 4 | 5 | .reatangle { 6 | background-color: orange; 7 | margin-top: 50px 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/course/useState/Rectangle/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Slider } from 'antd-mobile'; 3 | import './index.scss'; 4 | 5 | interface Color { 6 | r: number, 7 | g: number, 8 | b: number 9 | } 10 | 11 | export default function Rectangle() { 12 | const [height, setHeight] = useState(10); 13 | const [width, setWidth] = useState(10); 14 | const [color, setColor] = useState({ r: 0, g: 0, b: 0 }); 15 | const [radius, setRadius] = useState(0); 16 | 17 | const style = { 18 | height: `${height}px`, 19 | width: `${width}px`, 20 | backgroundColor: `rgb(${color.r}, ${color.g}, ${color.b})`, 21 | borderRadius: `${radius}px` 22 | } 23 | 24 | return ( 25 |
26 |

height:

27 | setHeight(n || 0)} 31 | /> 32 |

width:

33 | setWidth(n || 0)} 37 | /> 38 | 39 |

color: R:

40 | setColor({ ...color, r: n })} 44 | /> 45 | 46 |

color: G:

47 | setColor({ ...color, g: n })} 51 | /> 52 | 53 |

color: B:

54 | setColor({ ...color, b: n })} 58 | /> 59 |

Radius:

60 | setRadius(n)} 64 | /> 65 |
66 |
67 | ) 68 | } -------------------------------------------------------------------------------- /src/course/useState/asyncDemo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const listApi = function() {} 4 | 5 | interface ListItem { 6 | name: string, 7 | id: number, 8 | thumb: string 9 | } 10 | 11 | interface Param { 12 | current?: number, 13 | pageSize?: number, 14 | name?: string, 15 | id?: number, 16 | time?: Date 17 | } 18 | 19 | // 定义为同步变量 20 | let param: Param = {} 21 | 22 | export default function AsyncDemo() { 23 | const [listData, setListData] = useState([]); 24 | 25 | function fetchListData() { 26 | // @ts-ignore 27 | listApi(param).then(res => { 28 | setListData(res.data); 29 | }) 30 | } 31 | 32 | function searchByName(name: string) { 33 | param = { ...param, name }; 34 | fetchListData(); 35 | } 36 | 37 | return [ 38 |
data list
, 39 | 40 | ] 41 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | 5 | // import App from './course/useState/Demo'; 6 | // import App from './course/introduce/demoByClass'; 简单对比demo 7 | // import App from './course/introduce/demoByHooks'; 简单对比demo 8 | // import App from './course/introduce'; 对比受控组件 9 | // import App from './course/useState/Rectangle'; useState demo 10 | // import App from './course/useEffect/demo01'; 11 | // import App from './course/useEffect/demo02'; 12 | // import App from './course/useEffect/demo03'; 13 | // import App from './course/customHooks/app'; 14 | // import App from './course/useContext'; 15 | // import App from './course/useContext/demo'; 16 | // import App from './course/useReducer'; 17 | // import App from './course/useReducer/Counter'; 18 | // import App from './course/useContextWithReducer'; 19 | 20 | // 比较两个数组是否相等 21 | // import App from './course/customHooks/EqualArrPlus'; 22 | 23 | // 获取知乎日报最新列表 24 | // import App from './course/customHooks/ZhihuFeed2'; 25 | 26 | // 获取鼠标位置 27 | // import App from './course/customHooks/MousePos'; 28 | 29 | // import App from './course/useRef'; 30 | 31 | import App from './course/useCallback'; 32 | 33 | import * as serviceWorker from './serviceWorker'; 34 | 35 | ReactDOM.render(, document.getElementById('root')); 36 | 37 | // If you want your app to work offline and load faster, you can change 38 | // unregister() to register() below. Note this comes with some pitfalls. 39 | // Learn more about service workers: https://bit.ly/CRA-PWA 40 | serviceWorker.unregister(); 41 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | declare namespace NodeJS { 6 | interface ProcessEnv { 7 | readonly NODE_ENV: 'development' | 'production' | 'test'; 8 | readonly PUBLIC_URL: string; 9 | } 10 | } 11 | 12 | declare module '*.bmp' { 13 | const src: string; 14 | export default src; 15 | } 16 | 17 | declare module '*.gif' { 18 | const src: string; 19 | export default src; 20 | } 21 | 22 | declare module '*.jpg' { 23 | const src: string; 24 | export default src; 25 | } 26 | 27 | declare module '*.jpeg' { 28 | const src: string; 29 | export default src; 30 | } 31 | 32 | declare module '*.png' { 33 | const src: string; 34 | export default src; 35 | } 36 | 37 | declare module '*.webp' { 38 | const src: string; 39 | export default src; 40 | } 41 | 42 | declare module '*.svg' { 43 | import * as React from 'react'; 44 | 45 | export const ReactComponent: React.FunctionComponent>; 46 | 47 | const src: string; 48 | export default src; 49 | } 50 | 51 | declare module '*.module.css' { 52 | const classes: { [key: string]: string }; 53 | export default classes; 54 | } 55 | 56 | declare module '*.module.scss' { 57 | const classes: { [key: string]: string }; 58 | export default classes; 59 | } 60 | 61 | declare module '*.module.sass' { 62 | const classes: { [key: string]: string }; 63 | export default classes; 64 | } 65 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 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 subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2016", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "commonjs", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "preserve" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------