├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json └── robots.txt ├── src ├── App.tsx ├── components │ ├── Error │ │ ├── AsyncError.tsx │ │ └── MakeError.tsx │ └── Example │ │ ├── FallbackComponentExample.tsx │ │ ├── FallbackExample.tsx │ │ ├── FallbackRenderExample.tsx │ │ ├── ResetKeysExample.tsx │ │ ├── UseErrorHandlerExample.tsx │ │ └── WithErrorBoundaryExample.tsx ├── index.tsx ├── lib │ └── ErrorBoundary.tsx ├── react-app-env.d.ts ├── reportWebVitals.ts ├── setupTests.ts └── utils.tsx ├── tsconfig.json └── yarn.lock /.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 | 25 | .idea 26 | .eslintcache 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Haixiang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 造一个 react-error-boundary 轮子 2 | 3 | > 文章源码: https://github.com/Haixiang6123/my-react-error-bounday 4 | > 5 | > 参考轮子: https://www.npmjs.com/package/react-error-boundary 6 | 7 | ## 发生甚么事了 8 | 9 | ![](https://upload-images.jianshu.io/upload_images/2979799-949612e8dad24e3d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 10 | 11 | 朋友们好啊,我是海怪,刚才老板对我说:海怪,发生甚么事了,怎么页面白屏了?我说:怎么回事?给我发了几张截图。我打开控制台一看: 12 | 13 | ![](https://upload-images.jianshu.io/upload_images/2979799-2edf8953e327e9fb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 14 | 15 | 哦!原来是昨天,有个后端年轻人,说要和我联调接口,我说:可以。然后,我说:小兄弟,你的数据尽量按我需要的格式来: 16 | 17 | ```ts 18 | interface User { 19 | name: string; 20 | age: number; 21 | } 22 | 23 | interface GetUserListResponse { 24 | retcode: number; 25 | data: User[] 26 | } 27 | ``` 28 | 29 | 踏不服气,他说你这个没用,我说我这个有用,这是规范,传统前后端联调返回数据是要讲规范的,对于提高项目质量可以起到四两拨千斤的作用。100多万行代码的系统,只要有了类型规范,都不会轻易崩溃。他说试试,我说行。 30 | 31 | 我请求刚发出去,他的数据,啪!的一下就返回了!很快啊!! 32 | 33 | ```js 34 | { 35 | retcode: 0, 36 | data: [ 37 | {name: '张三', age: 11}, 38 | undefined, 39 | null 40 | ] 41 | } 42 | ``` 43 | 44 | 上来先是一个 `retcode: 0`,然后数组里一个 User 对象,一个 `undefined`,一个 `null`,我全部用判断 falsy 值防过去了啊: 45 | 46 | ```ts 47 | 48 | if (!u) { 49 | return 0; 50 | } 51 | 52 | const trimName = u.name.trim(); 53 | 54 | return getScore(trimName); 55 | ``` 56 | 57 | 防过去之后自然是正常处理业务逻辑和页面展示。虽然没有按照规范来,但是数组里偶尔有个 falsy 值也还好,我把数组类型改成 `Array`,没有和他说,同事之间,点到为止。我笑一下提交测试了,发了正式环境,准备收工。然后,这时候,老板突然说线上白屏爆炸,我一看返回的数据: 58 | 59 | ```js 60 | { 61 | retcode: 0, 62 | data: [ 63 | {name: '张三', age: 11}, 64 | '找不到此用户', 65 | '找不到此用户', 66 | '找不到此用户' 67 | ] 68 | } 69 | ``` 70 | 71 | 我大意了啊!没有做类型判断!虽然这个是后端的异常问题,但是前端也不应该出现白屏。对于这种异常情况,应该使用 React 提供的 **“Error Boundary 错误边界特性”** 来处理。下面来说说怎么打好这一套 Error Boundary。 72 | 73 | ## 第一步:抄 74 | 75 | 直接把官网例子抄下来,将 ErrorBoundary 组件输出: 76 | 77 | ```tsx 78 | class ErrorBoundary extends React.Component { 79 | constructor(props) { 80 | super(props); 81 | this.state = { hasError: false }; 82 | } 83 | 84 | static getDerivedStateFromError(error) { 85 | // 更新 state 使下一次渲染能够显示降级后的 UI 86 | return { hasError: true }; 87 | } 88 | 89 | componentDidCatch(error, errorInfo) { 90 | // 你同样可以将错误日志上报给服务器 91 | logger.error(error, errorInfo); 92 | } 93 | 94 | render() { 95 | if (this.state.hasError) { 96 | // 你可以自定义降级后的 UI 并渲染 97 | return

Something went wrong.

; 98 | } 99 | 100 | return this.props.children; 101 | } 102 | } 103 | ``` 104 | 105 | 然后将业务组件包裹: 106 | 107 | ```html 108 | // 捕获错误 109 | // 使劲报错 110 | 111 | ``` 112 | 113 | 如果 UserList 里报错,ErrorBoundary 就会捕获,然后在 `getDerivedStateFromError` 里更新组件状态,`render` 里就会显示 **Something went wrong**,不会渲染 `this.props.children`。 114 | 115 | ![](https://upload-images.jianshu.io/upload_images/2979799-4431dc3285dd20b2.gif?imageMogr2/auto-orient/strip) 116 | 117 | **总结:** 118 | **1. 将 ErrorBoundary 包裹可能出错的业务组件** 119 | **2. 当业务组件报错时,会调用 componentDidCatch 钩子里的逻辑,将 hasError 设置 true,直接展示

** 120 | 121 | ## 第二步:造个灵活的轮子 122 | 123 | 上面只是解决了燃眉之急,如果真要造一个好用的轮子,不应直接写死 `return

Something went wrong

`,应该添加 props 来传入报错显示内容(以下统称为 fallback): 124 | 125 | ```tsx 126 | // 出错后显示的元素类型 127 | type FallbackElement = React.ReactElement | null; 128 | 129 | // 出错显示组件的 props 130 | export interface FallbackProps { 131 | error: Error; 132 | } 133 | 134 | // 本组件 ErrorBoundary 的 props 135 | interface ErrorBoundaryProps { 136 | fallback?: FallbackElement; 137 | onError?: (error: Error, info: string) => void; 138 | } 139 | 140 | // 本组件 ErrorBoundary 的 props 141 | interface ErrorBoundaryState { 142 | error: Error | null; // 将 hasError 的 boolean 改为 Error 类型,提供更丰富的报错信息 143 | } 144 | 145 | // 初始状态 146 | const initialState: ErrorBoundaryState = { 147 | error: null, 148 | } 149 | 150 | class ErrorBoundary extends React.Component, ErrorBoundaryState> { 151 | state = initialState; 152 | 153 | static getDerivedStateFromError(error: Error) { 154 | return {error}; 155 | } 156 | 157 | componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { 158 | if (this.props.onError) { 159 | this.props.onError(error, errorInfo.componentStack); 160 | } 161 | } 162 | 163 | render() { 164 | const {fallback} = this.props; 165 | const {error} = this.state; 166 | 167 | if (error !== null) { 168 | if (React.isValidElement(fallback)) { 169 | return fallback; 170 | } 171 | 172 | throw new Error('ErrorBoundary 组件需要传入 fallback'); 173 | } 174 | 175 | return this.props.children; 176 | } 177 | } 178 | 179 | export default ErrorBoundary 180 | ``` 181 | 182 | 上面提供 onError 和 falback 两个 props,前者为出错的回调,可以做错误信息上报或者用户提示,后者则传入错误提示内容,像下面这样: 183 | 184 | ```tsx 185 | const App = () => { 186 | return ( 187 | 出错啦} onError={() => logger.error('出错啦')}> 188 | 189 | 190 | ) 191 | } 192 | ``` 193 | 194 | 这已经让 ErrorBoundary 变得稍微灵活一点了。但是有人就喜欢把 fallback 渲染函数、Fallback 组件作为 props 传入 ErrorBoundary,而不传一段 ReactElement,所以为了照顾更多人,将 fallback 进行扩展: 195 | 196 | ```tsx 197 | export declare function FallbackRender (props: FallbackProps): FallbackElement; 198 | 199 | // 本组件 ErrorBoundary 的 props 200 | interface ErrorBoundaryProps { 201 | fallback?: FallbackElement; // 一段 ReactElement 202 | FallbackComponent?: React.ComponentType; // Fallback 组件 203 | fallbackRender?: typeof FallbackRender; // 渲染 fallback 元素的函数 204 | onError?: (error: Error, info: string) => void; 205 | } 206 | 207 | class ErrorBoundary extends React.Component, ErrorBoundaryState> { 208 | ... 209 | 210 | render() { 211 | const {fallback, FallbackComponent, fallbackRender} = this.props; 212 | const {error} = this.state; 213 | 214 | // 多种 fallback 的判断 215 | if (error !== null) { 216 | const fallbackProps: FallbackProps = { 217 | error, 218 | } 219 | // 判断 fallback 是否为合法的 Element 220 | if (React.isValidElement(fallback)) { 221 | return fallback; 222 | } 223 | // 判断 render 是否为函数 224 | if (typeof fallbackRender === 'function') { 225 | return (fallbackRender as typeof FallbackRender)(fallbackProps); 226 | } 227 | // 判断是否存在 FallbackComponent 228 | if (FallbackComponent) { 229 | return 230 | } 231 | 232 | throw new Error('ErrorBoundary 组件需要传入 fallback, fallbackRender, FallbackComponent 其中一个'); 233 | } 234 | 235 | return this.props.children; 236 | } 237 | } 238 | ``` 239 | 240 | 上面提供 3 种方式来传入出错提示组件: fallback(元素)、FallbackComponent(组件),fallbackRender(render 函数)。现在使用轮子就更灵活了: 241 | 242 | ```tsx 243 | const App = () => { 244 | const onError = () => logger.error('出错啦') 245 | 246 | return ( 247 |
248 | 出错啦
} onError={onError}> 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | } 258 | onError={onError} 259 | > 260 | 261 | 262 | 263 | ) 264 | } 265 | ``` 266 | 267 | **总结一下这里的改动:** 268 | **1. 将原来的 hasError 转为 error,从 boolean 转为 Error 类型,有利于获得更多的错误信息,上报错误时很有用** 269 | **2. 添加 fallback, FallbackComponent, fallbackRender 3个 props,提供多种方法来传入展示 fallback** 270 | 271 | ## 第三步:添加重置回调 272 | 273 | 有时候会遇到这种情况:服务器突然抽风了,503、502了,前端获取不到响应,这时候某个组件报错了,但是过一会又正常了。比较好的方法是允许用户点一下 fallback 里的一个按钮来重新加载出错组件,不需要重刷页面,这样的操作下面称为**“重置”**。 274 | 275 | 同时,有些开发者也需要在重置里添加自己逻辑,比如弹提示、日志上报等。 276 | 277 | 图解: 278 | 279 | ![](https://upload-images.jianshu.io/upload_images/2979799-7118fa47d5decea6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 280 | 281 | 下面给出上面两个需求的实现: 282 | 283 | ```tsx 284 | // 出错后显示的元素类型 285 | type FallbackElement = React.ReactElement | null; 286 | 287 | // 出错显示组件的 props 288 | export interface FallbackProps { 289 | error: Error; 290 | resetErrorBoundary: () => void; // fallback 组件里将该函数绑定到“重置”按钮 291 | } 292 | 293 | // 本组件 ErrorBoundary 的 props 294 | interface ErrorBoundaryProps { 295 | ... 296 | onReset?: () => void; // 开发者自定义重置逻辑,如日志上报、 toast 提示 297 | } 298 | 299 | class ErrorBoundary extends React.Component, ErrorBoundaryState> { 300 | ... 301 | // 重置该组件状态,将 error 设置 null 302 | reset = () => { 303 | this.setState(initialState); 304 | } 305 | 306 | // 执行自定义重置逻辑,并重置组件状态 307 | resetErrorBoundary = () => { 308 | if (this.props.onReset) { 309 | this.props.onReset(); 310 | } 311 | this.reset(); 312 | } 313 | 314 | render() { 315 | const {fallback, FallbackComponent, fallbackRender} = this.props; 316 | const {error} = this.state; 317 | 318 | if (error !== null) { 319 | const fallbackProps: FallbackProps = { 320 | error, 321 | resetErrorBoundary: this.resetErrorBoundary, // 将 resetErrorBoundary 传入 fallback 322 | } 323 | 324 | if (React.isValidElement(fallback)) { 325 | return fallback; 326 | } 327 | if (typeof fallbackRender === 'function') { 328 | return (fallbackRender as typeof FallbackRender)(fallbackProps); 329 | } 330 | if (FallbackComponent) { 331 | return 332 | } 333 | 334 | throw new Error('ErrorBoundary 组件需要传入 fallback, fallbackRender, FallbackComponent 其中一个'); 335 | } 336 | 337 | return this.props.children; 338 | } 339 | } 340 | ``` 341 | 342 | 改写之后,在业务代码中添加重置逻辑: 343 | 344 | ```tsx 345 | const App = () => { 346 | const onError = () => logger.error('出错啦') 347 | const onReset = () => { 348 | console.log('已重置') 349 | message.info('刚刚出错了,不好意思,现在已经重置好了,请找老板锤这个开发') 350 | } 351 | // fallback 组件的渲染函数 352 | const renderFallback = (props: FallbackProps) => { 353 | return ( 354 |
355 | 出错啦,你可以 356 |
357 | ) 358 | } 359 | 360 | return ( 361 |
362 | 367 | 368 | 369 |
370 | ) 371 | } 372 | ``` 373 | 374 | 上面例子中,在 onReset 里自定义想要重试的逻辑,然后在 renderFallback 里将 props.resetErrorBoudnary 绑定到重置即可,当点击“重置”时,就会调用 onReset ,同时将 ErrorBoundary 组件状态清空(将 error 设为 null)。 375 | 376 | **总结:** 377 | **1. 添加 onReset 来实现重置的逻辑** 378 | **2. 在 fallback 组件里找个按钮绑定 `props.resetErrorBoundary` 来触发重置逻辑** 379 | 380 | ## 第四步:监听渲染以重置 381 | 382 | 上面的重置逻辑简单也很实用,但是有时也会有局限性:触发重置的动作只能在 fallback 里面。假如我的重置按钮不在 fallback 里呢?或者 onReset 函数根本不在这个 App 组件下那怎么办呢?难道要将 onReset 像传家宝一路传到这个 App 再传入 ErrorBoundary 里? 383 | 384 | 这时,我们就会想:能不能监听状态的更新,只要状态更新就重置,反正就重新加载组件也没什么损失,这里的状态完全用全局状态管理,放到 Redux 中。 385 | 386 | 上面的思路听起来不就和 useEffect 里的依赖项 deps 数组一样嘛,不妨在 props 提供一个 `resetKeys` 数组,如果这个数组里的东西变了,ErrorBoundary 就重置,这样一控制是否要重置就更灵活了。马上动手实现一下: 387 | 388 | ```tsx 389 | // 本组件 ErrorBoundary 的 props 390 | interface ErrorBoundaryProps { 391 | ... 392 | resetKeys?: Array; 393 | } 394 | 395 | // 检查 resetKeys 是否有变化 396 | const changedArray = (a: Array = [], b: Array = []) => { 397 | return a.length !== b.length || a.some((item, index) => !Object.is(item, b[index])); 398 | } 399 | 400 | class ErrorBoundary extends React.Component, ErrorBoundaryState> { 401 | ... 402 | 403 | componentDidUpdate(prevProps: Readonly>) { 404 | const {error} = this.state; 405 | const {resetKeys, onResetKeysChange} = this.props; 406 | 407 | // 只要 resetKeys 有变化,直接 reset 408 | if (changedArray(prevProps.resetKeys, resetKeys)) { 409 | // 重置 ErrorBoundary 状态,并调用 onReset 回调 410 | this.reset(); 411 | } 412 | } 413 | 414 | render() { 415 | ... 416 | } 417 | } 418 | ``` 419 | 420 | 首先,在 `componentDidupdate` 里去做 resetKeys 的监听,只要组件有 render 就看看 `resetKeys` 里面的元素是否改过了,改过了就会重置。 421 | 可以把 `onResetKeysChange` 提供给外面去做 resetKeys 监听(原版的 `react-error-boundary` 提供的 API 是为了一些 Edge Case 场景)。 422 | 423 | ```tsx 424 | // 本组件 ErrorBoundary 的 props 425 | interface ErrorBoundaryProps { 426 | ... 427 | resetKeys?: Array; 428 | onResetKeysChange?: ( 429 | prevResetKey: Array | undefined, 430 | resetKeys: Array | undefined, 431 | ) => void; 432 | } 433 | 434 | class ErrorBoundary extends React.Component, ErrorBoundaryState> { 435 | ... 436 | 437 | componentDidUpdate(prevProps: Readonly>) { 438 | const {resetKeys, onResetKeysChange} = this.props; 439 | 440 | if (changedArray(prevProps.resetKeys, resetKeys)) { 441 | if (onResetKeysChange) { 442 | onResetKeysChange(prevProps.resetKeys, resetKeys); 443 | } 444 | 445 | // 重置 ErrorBoundary 状态,并调用 onReset 回调 446 | this.reset(); 447 | } 448 | } 449 | 450 | render() { 451 | ... 452 | } 453 | } 454 | ``` 455 | 456 | 还有没有问题呢?嗯,还有问题。这里注意这里的 `componentDidUpdate` 钩子逻辑,假如某个 key 是触发 error 的元凶,那么就有可能触发二次 error 的情况: 457 | 458 | 1. `xxxKey` 触发了 error,组件报错 459 | 2. 组件报错导致 `resetKeys` 里的一些东西改了 460 | 3. `componentDidUpdate` 发现 `resetKeys` 里有东西更新了,不废话,马上重置 461 | 4. 重置完了,显示报错的组件,因为 error 还存在(或者还未解决),报错的组件又再次触发了 error 462 | 5. ... 463 | 464 | 所以要区分出来这一次到底是因为 error 才 render 还是普通组件的 render,而且还需要确保当前有错误才重置,都没错误还重置个毛。具体实现思路如图所示: 465 | 466 | ![](https://upload-images.jianshu.io/upload_images/2979799-7054b70b38d787e0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 467 | 468 | 实现如下 469 | 470 | ```tsx 471 | class ErrorBoundary extends React.Component, ErrorBoundaryState> { 472 | state = initialState; 473 | // 是否已经由于 error 而引发的 render/update 474 | updatedWithError = false; 475 | 476 | static getDerivedStateFromError(error: Error) { 477 | return {error}; 478 | } 479 | 480 | componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { 481 | if (this.props.onError) { 482 | this.props.onError(error, errorInfo.componentStack); 483 | } 484 | } 485 | 486 | componentDidUpdate(prevProps: Readonly>) { 487 | const {error} = this.state; 488 | const {resetKeys, onResetKeysChange} = this.props; 489 | 490 | // 已经存在错误,并且是第一次由于 error 而引发的 render/update,那么设置 flag=true,不会重置 491 | if (error !== null && !this.updatedWithError) { 492 | this.updatedWithError = true; 493 | return; 494 | } 495 | 496 | // 已经存在错误,并且是普通的组件 render,则检查 resetKeys 是否有改动,改了就重置 497 | if (error !== null && changedArray(prevProps.resetKeys, resetKeys)) { 498 | if (onResetKeysChange) { 499 | onResetKeysChange(prevProps.resetKeys, resetKeys); 500 | } 501 | 502 | this.reset(); 503 | } 504 | } 505 | 506 | reset = () => { 507 | this.updatedWithError = false; 508 | this.setState(initialState); 509 | } 510 | 511 | resetErrorBoundary = () => { 512 | if (this.props.onReset) { 513 | this.props.onReset(); 514 | } 515 | this.reset(); 516 | } 517 | 518 | render() { 519 | ... 520 | } 521 | } 522 | ``` 523 | 524 | 上面的改动有: 525 | 1. 用 `updatedWithError` 作为 flag 判断是否已经由于 error 出现而引发的 render/update 526 | 2. 如果当前没有错误,无论如何都不会重置 527 | 3. 每次更新:当前存在错误,且第一次由于 error 出现而引发的 render/update,则设置 `updatedWithError = true`,不会重置状态 528 | 4. 每次更新:当前存在错误,且如果 `updatedWithError` 为 `true` 说明已经由于 error 而更新过了,以后的更新只要 `resetKeys` 里的东西改了,都会被重置 529 | 530 | 至此,我们拥有了两种可以实现重置的方式了: 531 | 532 | |方法|触发范围|使用场景|思想负担| 533 | |--|--|--|--| 534 | |手动调用 resetErrorBoundary|一般在 fallback 组件里|用户可以在 fallback 里手动点击“重置”实现重置|最直接,思想负担较轻| 535 | |更新 resetKeys|哪里都行,范围更广|用户可以在报错组件外部重置、`resetKeys` 里有报错组件依赖的数据、渲染时自动重置|间接触发,要思考哪些值放到 `resetKeys` 里,思想负担较重| 536 | 537 | **总结这一鞭的改动:** 538 | **1. 添加 `resetKeys` 和 `onResetKeysChange` 两个 props,为开发者提供监听值变化而自动重置的功能** 539 | **2. 在 componentDidUpdate 里,只要不是由于 error 引发的组件渲染或更新,而且 `resetKeys` 有变化了,那么直接重置组件状态来达到自动重置** 540 | 541 | 这里自动重置还有一个好处:假如是由于网络波动引发的异常,那页面当然会显示 fallback 了,如果用上面直接调用 props.resetErrorBoundary 方法来重置,只要用户不点“重置”按钮,那块地方永远不会被重置。又由于是因为网络波动引发的异常,有可能就那0.001 秒有问题,别的时间又好了,所以如果我们将一些变化频繁的值放到 `resetKeys` 里就很容易自动触发重置。例如,报错后,其它地方的值变了从而更改了 `resetKeys` 的元素值就会触发自动重置。对于用户来说,最多只会看到一闪而过的 fallback,然后那块地方又正常了。这样一来,用户也不需要亲自触发重置了。 542 | 543 | ## 第五步:输出轮子 544 | 545 | 上面四步里,到最后都是 `export default ErrorBoundary` 将组件输出,如果代理里很多个地方都要 catch error,就有这样很啰嗦的代码: 546 | 547 | ```html 548 |
549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 |
562 | ``` 563 | 564 | 要处理这样啰嗦的包裹,可以借鉴 React Router 的 `withRouter` 函数,我们也可以输出一个高阶函数 `withErrorBoundary` : 565 | 566 | ```tsx 567 | /** 568 | * with 写法 569 | * @param Component 业务组件 570 | * @param errorBoundaryProps error boundary 的 props 571 | */ 572 | function withErrorBoundary

(Component: React.ComponentType

, errorBoundaryProps: ErrorBoundaryProps): React.ComponentType

{ 573 | const Wrapped: React.ComponentType

= props => { 574 | return ( 575 | 576 | 577 | 578 | ) 579 | } 580 | 581 | // DevTools 显示的组件名 582 | const name = Component.displayName ||Component.name || 'Unknown'; 583 | Wrapped.displayName = `withErrorBoundary(${name})`; 584 | 585 | return Wrapped; 586 | } 587 | ``` 588 | 589 | 使用的时候就更简洁了一些了: 590 | 591 | ```tsx 592 | // 业务子组件 593 | const User = () => { 594 | return

User
595 | } 596 | 597 | // 在业务组件加一层 ErrorBoundary 598 | const UserWithErrorBoundary = withErrorBoundary(User, { 599 | onError: () => logger.error('出错啦'), 600 | onReset: () => console.log('已重置') 601 | }) 602 | 603 | // 业务父组件 604 | const App = () => { 605 | return ( 606 |
607 | 608 |
609 | ) 610 | } 611 | ``` 612 | 613 | *其实 `withXXX` 这种写法还可以写成装饰器,将 `@withXXX` 放到 class component 上也很方便,但是对于 functional component 就放不了了,有点受限,这里不展开了。* 614 | 615 | 还有没有更好的设计呢?我们观察到只有一些比较“严重的异常”浏览器才会报错,比如开头提到的 `TypeError: xxx is not a function`。JS 是个动态类型语言,在浏览器里你可以:`NaN + 1`,可以 `NaN.toString()`,可以 `'1' + 1` 都不报任何错误。其实官网也说了,对于一些错误 `componenDidCatch` 是不能自动捕获的: 616 | 617 | ![](https://upload-images.jianshu.io/upload_images/2979799-1279a3d1702313ba.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 618 | 619 | 不过,这些错误在代码里开发者其实是知道的呀。既然开发者们有办法拿到这些错误,那把错误直接抛出就可以让 ErrorBoundary catch 到了: 620 | 621 | 1. 有错误的时候,开发者自己调用 `handleError(error)` 将错误传入函数中 622 | 2. `handleError` 将错误 `throw new Error(error)` 623 | 3. ErrorBoundary 发现有上面抛出的 Error,调用 `componentDidCatch` 处理错误 624 | 4. ... 625 | 626 | 我来提供一种使用 React Hook 的实现方式: 627 | 628 | ```tsx 629 | /** 630 | * 自定义错误的 handler 631 | * @param givenError 632 | */ 633 | function useErrorHandler( 634 | givenError?: P | null | undefined, 635 | ): React.Dispatch> { 636 | const [error, setError] = React.useState

(null); 637 | if (givenError) throw givenError; // 初始有错误时,直接抛出 638 | if (error) throw error; // 后来再有错误,也直接抛出 639 | return setError; // 返回开发者可手动设置错误的钩子 640 | } 641 | ``` 642 | 643 | 使用上面的 hook,对于一些需要自己处理的错误,可以有两种处理方法: 644 | 645 | 1. `const handleError = useErrorHandler()`,然后 `handleError(yourError)` 646 | 2. `useErrorHandler(otherHookError)`,如果别的 hooks 里有 export error,完全可以直接将这个 error 传入 `useErrorHandler`,直接处理 647 | 648 | 比如: 649 | 650 | ```tsx 651 | function Greeting() { 652 | const [greeting, setGreeting] = React.useState(null) 653 | const handleError = useErrorHandler() 654 | 655 | function handleSubmit(event) { 656 | event.preventDefault() 657 | const name = event.target.elements.name.value 658 | fetchGreeting(name).then( 659 | newGreeting => setGreeting(newGreeting), 660 | handleError, // 开发者自己处理错误,将错误抛出 661 | ) 662 | } 663 | 664 | return greeting ? ( 665 |

{greeting}
666 | ) : ( 667 |
668 | 669 | 670 | 671 |
672 | ) 673 | } 674 | // 用 ErrorBoundary 包裹,处理手动抛出的错误 675 | export default withErrorBoundary(Greeting) 676 | ``` 677 | 678 | 或者: 679 | 680 | ```tsx 681 | function Greeting() { 682 | const [name, setName] = React.useState('') 683 | const {greeting, error} = useGreeting(name) 684 | 685 | // 开发者自己处理错误,将错误抛出 686 | useErrorHandler(error) 687 | 688 | function handleSubmit(event) { 689 | event.preventDefault() 690 | const name = event.target.elements.name.value 691 | setName(name) 692 | } 693 | 694 | return greeting ? ( 695 |
{greeting}
696 | ) : ( 697 |
698 | 699 | 700 | 701 |
702 | ) 703 | } 704 | 705 | // 用 ErrorBoundary 包裹,处理手动抛出的错误 706 | export default withErrorBoundary(Greeting) 707 | ``` 708 | 709 | **总结:** 710 | **1. 提供 `withErrorBoundary` 方法来包裹业务组件实现异常捕获** 711 | **2. 提供 `useErrorHandler` hook 让开发者自己处理/抛出错误** 712 | 713 | ## 总结 714 | 715 | 再次总结一下上面的要点: 716 | 717 | 1. 造一个 ErrorBoundary 轮子 718 | 2. `componentDidCatch` 捕获页面报错,`getDerivedStateFromError` 更新 ErrorBoundary 的 state,并获取具体 error 719 | 3. 提供多种展示错误内容入口:`fallback`, `FallbackComponent`, `fallbackRender` 720 | 4. 重置钩子:提供 `onReset`, `resetErrorBoundary` 的传值和调用,以实现重置 721 | 5. 重置监听数组:监听 `resetKeys` 的变化来重置。 722 | 6. 提供 ErrorBoundary 的2种使用方法:嵌套业务组件,将业务组件传入`withErrorBoundary` 高阶函数。提供 `useErrorBoundary` 钩子给开发者自己抛出 ErrorBoundary 不能自动捕获的错误 723 | 724 | ## 耗子尾汁,好好反思 725 | 726 | 打完了这一套“五连鞭”,再次发布上线,一切OK。 727 | 728 | 然后我找到这位后端,跟他说了线上事故。当时他就流眼泪了,捂着脸,两分多钟以后,就好了。 729 | 730 | 我说:小伙子,你不讲码德你不懂。他说:对不起,我不懂规矩。后来他说他写了好几年动态语言,啊,看来是有 bear 来。这个年轻人不讲码德。来!骗!来!偷袭我一个24岁小前端,这好吗?这不好,我劝,这位后端,耗子尾汁,好好反思,以后不要搞这样的聪明,小聪明。程序猿要以和为贵,要讲码德,不要搞窝里斗。 731 | 732 | 谢谢朋友们。 733 | 734 | (故事纯属虚构,如有雷同,请自我检讨或者一键三连) 735 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-error-bounday", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "@types/jest": "^26.0.15", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^16.9.53", 12 | "@types/react-dom": "^16.9.8", 13 | "react": "^17.0.1", 14 | "react-dom": "^17.0.1", 15 | "react-error-boundary": "^3.1.0", 16 | "react-scripts": "4.0.1", 17 | "typescript": "^4.0.3", 18 | "web-vitals": "^0.2.4" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app", 29 | "react-app/jest" 30 | ] 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haixiangyan/my-react-error-bounday/cbe183969dbe5de9c6ab8ce8bb119c8eadf4e5b9/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FallbackComponentExample from './components/Example/FallbackComponentExample'; 3 | import FallbackRenderExample from './components/Example/FallbackRenderExample'; 4 | import FallbackExample from './components/Example/FallbackExample'; 5 | import WithErrorBoundaryExample from './components/Example/WithErrorBoundaryExample'; 6 | import UseErrorHandlerExample from './components/Example/UseErrorHandlerExample'; 7 | import ResetKeysExample from './components/Example/ResetKeysExample'; 8 | 9 | const App = () => { 10 | return ( 11 |
12 |

fallback 例子

13 | 14 | 15 |

FallbackComponent 例子

16 | 17 | 18 |

fallbackRender 例子

19 | 20 | 21 |

withErrorBoundary 例子

22 | 23 | 24 |

useErrorHandler 例子

25 | 26 | 27 |

ResetKeys 例子

28 | 29 |
30 | ); 31 | } 32 | 33 | export default App; 34 | -------------------------------------------------------------------------------- /src/components/Error/AsyncError.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react'; 2 | import {useErrorHandler} from '../../lib/ErrorBoundary'; 3 | 4 | const AsyncError = () => { 5 | const handleError = useErrorHandler(); 6 | 7 | const [number, setNumber] = useState(0); 8 | 9 | const randomlyFetchData = async () => { 10 | return Math.random(); 11 | } 12 | 13 | useEffect(() => { 14 | randomlyFetchData().then(number => { 15 | if (number > 0.5) { 16 | throw new Error('async 大于 0.5'); 17 | } else { 18 | setNumber(number); 19 | } 20 | }).catch(handleError); 21 | }, []); 22 | 23 | return
{number}
24 | } 25 | 26 | export default AsyncError; 27 | -------------------------------------------------------------------------------- /src/components/Error/MakeError.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect } from 'react'; 2 | 3 | const MakeError = () => { 4 | useEffect(() => { 5 | const number = Math.random(); 6 | if (number > 0.5) { 7 | throw new Error('大于0.5'); 8 | } 9 | }, []); 10 | 11 | return
12 | } 13 | 14 | export default MakeError; 15 | -------------------------------------------------------------------------------- /src/components/Example/FallbackComponentExample.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import MakeError from '../Error/MakeError'; 3 | import {ErrorFallback } from '../../utils'; 4 | import {ErrorBoundary} from '../../lib/ErrorBoundary'; 5 | 6 | const FallbackComponentExample = () => { 7 | const [hasError, setHasError] = useState(false); 8 | 9 | const onError = (error: Error) => { 10 | // 日志上報 11 | console.log(error); 12 | setHasError(true); 13 | } 14 | 15 | const onReset = () => { 16 | console.log('尝试恢复错误'); 17 | setHasError(false); 18 | } 19 | 20 | return ( 21 | 26 | { !hasError ? : null } 27 | 28 | ) 29 | }; 30 | 31 | export default FallbackComponentExample; 32 | -------------------------------------------------------------------------------- /src/components/Example/FallbackExample.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import MakeError from '../Error/MakeError'; 3 | import {ErrorBoundary} from '../../lib/ErrorBoundary'; 4 | 5 | const FallbackExample = () => { 6 | const [hasError, setHasError] = useState(false); 7 | 8 | const onError = (error: Error) => { 9 | // 日志上報 10 | console.log(error); 11 | setHasError(true); 12 | } 13 | 14 | const onReset = () => { 15 | console.log('尝试恢复错误'); 16 | setHasError(false); 17 | } 18 | 19 | return ( 20 | 出错啦
} 22 | onError={onError} 23 | onReset={onReset} 24 | > 25 | { !hasError ? : null } 26 | 27 | ) 28 | }; 29 | 30 | export default FallbackExample; 31 | -------------------------------------------------------------------------------- /src/components/Example/FallbackRenderExample.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import MakeError from '../Error/MakeError'; 3 | import {ErrorFallback } from '../../utils'; 4 | import {ErrorBoundary} from '../../lib/ErrorBoundary'; 5 | 6 | const FallbackRenderExample = () => { 7 | const [hasError, setHasError] = useState(false); 8 | 9 | const onError = (error: Error) => { 10 | // 日志上報 11 | console.log(error); 12 | setHasError(true); 13 | } 14 | 15 | const onReset = () => { 16 | console.log('尝试恢复错误'); 17 | setHasError(false); 18 | } 19 | 20 | return ( 21 | } 23 | onError={onError} 24 | onReset={onReset} 25 | > 26 | { !hasError ? : null } 27 | 28 | ) 29 | }; 30 | 31 | export default FallbackRenderExample; 32 | -------------------------------------------------------------------------------- /src/components/Example/ResetKeysExample.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import MakeError from '../Error/MakeError'; 3 | import {ErrorFallback} from '../../utils'; 4 | import {ErrorBoundary} from '../../lib/ErrorBoundary'; 5 | 6 | const FallbackExample = () => { 7 | const [retry, setRetry] = useState(0); 8 | 9 | return ( 10 |
11 | 12 | 13 | 17 | 18 | 19 |
20 | ) 21 | }; 22 | 23 | export default FallbackExample; 24 | -------------------------------------------------------------------------------- /src/components/Example/UseErrorHandlerExample.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {ErrorFallback } from '../../utils'; 3 | import {ErrorBoundary} from '../../lib/ErrorBoundary'; 4 | import AsyncError from '../Error/AsyncError'; 5 | 6 | const UseErrorHandlerExample = () => { 7 | const [hasError, setHasError] = useState(false); 8 | 9 | const onError = (error: Error) => { 10 | // 日志上報 11 | console.log(error); 12 | setHasError(true); 13 | } 14 | 15 | const onReset = () => { 16 | console.log('尝试恢复错误'); 17 | setHasError(false); 18 | } 19 | 20 | return ( 21 | 26 | { !hasError ? : null } 27 | 28 | ) 29 | }; 30 | 31 | export default UseErrorHandlerExample; 32 | -------------------------------------------------------------------------------- /src/components/Example/WithErrorBoundaryExample.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MakeError from '../Error/MakeError'; 3 | import {ErrorFallback} from '../../utils'; 4 | import {withErrorBoundary} from '../../lib/ErrorBoundary'; 5 | 6 | const WithErrorBoundaryExample = withErrorBoundary(MakeError, { 7 | FallbackComponent: ErrorFallback, 8 | onError: (error, info) => console.log(error, info), 9 | }); 10 | 11 | export default WithErrorBoundaryExample; 12 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import reportWebVitals from './reportWebVitals'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | // If you want to start measuring performance in your app, pass a function 14 | // to log results (for example: reportWebVitals(console.log)) 15 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 16 | reportWebVitals(); 17 | -------------------------------------------------------------------------------- /src/lib/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | // 出错后显示的元素类型 4 | type FallbackElement = React.ReactElement | null; 5 | 6 | // 出错显示组件的 props 7 | export interface FallbackProps { 8 | error: Error; 9 | resetErrorBoundary: () => void; 10 | } 11 | 12 | // 出错显示组件的 renderer 13 | export declare function FallbackRender (props: FallbackProps): FallbackElement; 14 | 15 | // 本组件 ErrorBoundary 的 props 16 | interface ErrorBoundaryProps { 17 | fallback?: FallbackElement; 18 | FallbackComponent?: React.ComponentType; 19 | fallbackRender?: typeof FallbackRender; 20 | onError?: (error: Error, info: string) => void; 21 | onReset?: () => void; 22 | resetKeys?: Array; 23 | onResetKeysChange?: ( 24 | prevResetKey: Array | undefined, 25 | resetKeys: Array | undefined, 26 | ) => void; 27 | } 28 | 29 | // 本组件 ErrorBoundary 的 props 30 | interface ErrorBoundaryState { 31 | error: Error | null; 32 | } 33 | 34 | const changedArray = (a: Array = [], b: Array = []) => { 35 | return a.length !== b.length || a.some((item, index) => !Object.is(item, b[index])); 36 | } 37 | 38 | // 初始状态 39 | const initialState: ErrorBoundaryState = { 40 | error: null, 41 | } 42 | 43 | class ErrorBoundary extends React.Component, ErrorBoundaryState> { 44 | state = initialState; 45 | updatedWithError = false; 46 | 47 | static getDerivedStateFromError(error: Error) { 48 | return {error}; 49 | } 50 | 51 | componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { 52 | if (this.props.onError) { 53 | this.props.onError(error, errorInfo.componentStack); 54 | } 55 | } 56 | 57 | componentDidUpdate(prevProps: Readonly>) { 58 | const {error} = this.state; 59 | const {resetKeys, onResetKeysChange} = this.props; 60 | 61 | if (error !== null && !this.updatedWithError) { 62 | this.updatedWithError = true; 63 | return; 64 | } 65 | 66 | if (error !== null && changedArray(prevProps.resetKeys, resetKeys)) { 67 | if (onResetKeysChange) { 68 | onResetKeysChange(prevProps.resetKeys, resetKeys); 69 | } 70 | 71 | this.reset(); 72 | } 73 | } 74 | 75 | reset = () => { 76 | this.updatedWithError = false; 77 | this.setState(initialState); 78 | } 79 | 80 | resetErrorBoundary = () => { 81 | if (this.props.onReset) { 82 | this.props.onReset(); 83 | } 84 | this.reset(); 85 | } 86 | 87 | render() { 88 | const {fallback, FallbackComponent, fallbackRender} = this.props; 89 | const {error} = this.state; 90 | 91 | if (error !== null) { 92 | const fallbackProps: FallbackProps = { 93 | error, 94 | resetErrorBoundary: this.resetErrorBoundary, 95 | } 96 | 97 | if (React.isValidElement(fallback)) { 98 | return fallback; 99 | } 100 | if (typeof fallbackRender === 'function') { 101 | return (fallbackRender as typeof FallbackRender)(fallbackProps); 102 | } 103 | if (FallbackComponent) { 104 | return 105 | } 106 | 107 | throw new Error('ErrorBoundary 组件需要传入 fallback, fallbackRender, FallbackComponent 其中一个'); 108 | } 109 | 110 | return this.props.children; 111 | } 112 | } 113 | 114 | /** 115 | * with 写法 116 | * @param Component 业务组件 117 | * @param errorBoundaryProps error boundary 的 props 118 | */ 119 | function withErrorBoundary

( 120 | Component: React.ComponentType

, 121 | errorBoundaryProps: ErrorBoundaryProps 122 | ): React.ComponentType

{ 123 | const Wrapped: React.ComponentType

= props => { 124 | return ( 125 | 126 | 127 | 128 | ) 129 | } 130 | 131 | // DevTools 显示的组件名 132 | const name = Component.displayName ||Component.name || 'Unknown'; 133 | Wrapped.displayName = `withErrorBoundary(${name})`; 134 | 135 | return Wrapped; 136 | } 137 | 138 | /** 139 | * 自定义错误的 handler 140 | * @param givenError 141 | */ 142 | function useErrorHandler( 143 | givenError?: P | null | undefined, 144 | ): React.Dispatch> { 145 | const [error, setError] = React.useState

(null); 146 | if (givenError) throw givenError; 147 | if (error) throw error; 148 | return setError; 149 | } 150 | 151 | export { 152 | ErrorBoundary, 153 | withErrorBoundary, 154 | useErrorHandler, 155 | } 156 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/utils.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {FallbackProps} from './lib/ErrorBoundary'; 3 | 4 | /** 5 | * 出错后现时的组件 6 | * @param error 7 | * @param resetErrorBoundary 8 | * @constructor 9 | */ 10 | export const ErrorFallback = ({error, resetErrorBoundary}: FallbackProps) => { 11 | return ( 12 |

13 |

出错啦

14 |
{error.message}
15 | 16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 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 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------