├── .eslintrc.json ├── .github └── workflows │ ├── cd.yml │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ └── 01_basic_spec.ts ├── examples ├── 01_minimal │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── Hello.worker.js │ │ ├── TextBox.js │ │ └── index.js ├── 02_typescript │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── App.tsx │ │ ├── Counter.tsx │ │ ├── Hello.worker.tsx │ │ ├── TextBox.tsx │ │ └── index.ts ├── 03_fetch │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── App.tsx │ │ ├── Fetcher.worker.tsx │ │ ├── Main.tsx │ │ ├── PostData.tsx │ │ ├── TextBox.tsx │ │ └── index.ts └── 04_multicomps │ ├── package.json │ ├── public │ └── index.html │ └── src │ ├── App.tsx │ ├── Counter.tsx │ ├── Hello.worker.tsx │ ├── TextBox.tsx │ └── index.ts ├── package.json ├── src ├── expose.ts ├── index.ts ├── register.ts ├── serializer.ts └── wrap.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": [ 4 | "@typescript-eslint", 5 | "react-hooks" 6 | ], 7 | "extends": [ 8 | "plugin:@typescript-eslint/recommended", 9 | "airbnb" 10 | ], 11 | "env": { 12 | "browser": true, 13 | "es2021": true 14 | }, 15 | "settings": { 16 | "import/resolver": { 17 | "node": { 18 | "extensions": [".js", ".ts", ".tsx"] 19 | } 20 | } 21 | }, 22 | "rules": { 23 | "react-hooks/rules-of-hooks": "error", 24 | "react-hooks/exhaustive-deps": ["error", { "additionalHooks": "useIsomorphicLayoutEffect" }], 25 | "@typescript-eslint/explicit-function-return-type": "off", 26 | "@typescript-eslint/explicit-module-boundary-types": "off", 27 | "react/jsx-filename-extension": ["error", { "extensions": [".js", ".tsx"] }], 28 | "react/prop-types": "off", 29 | "react/jsx-one-expression-per-line": "off", 30 | "import/extensions": ["error", "never"], 31 | "import/prefer-default-export": "off", 32 | "import/no-unresolved": ["error", { "ignore": ["react-worker-component"] }], 33 | "@typescript-eslint/ban-types": "off", 34 | "no-plusplus": "off", 35 | "symbol-description": "off", 36 | "no-restricted-syntax": "off", 37 | "no-restricted-globals": "off", 38 | "no-use-before-define": "off", 39 | "no-unused-vars": "off", 40 | "no-param-reassign": "off", 41 | "react/function-component-definition": ["error", { "namedComponents": "arrow-function" }] 42 | }, 43 | "overrides": [{ 44 | "files": ["__tests__/**/*"], 45 | "env": { 46 | "jest": true 47 | }, 48 | "rules": { 49 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }] 50 | } 51 | }, { 52 | "files": ["examples/**/*"], 53 | "rules": { 54 | "camelcase": "off" 55 | } 56 | }] 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup Node 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: '12.x' 18 | registry-url: 'https://registry.npmjs.org' 19 | 20 | - name: Get yarn cache 21 | id: yarn-cache 22 | run: echo "::set-output name=dir::$(yarn cache dir)" 23 | 24 | - name: Cache dependencies 25 | uses: actions/cache@v1 26 | with: 27 | path: ${{ steps.yarn-cache.outputs.dir }} 28 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 29 | restore-keys: | 30 | ${{ runner.os }}-yarn- 31 | 32 | - name: Install dependencies 33 | run: yarn install 34 | 35 | - name: Test 36 | run: yarn test 37 | 38 | - name: Compile 39 | run: yarn run compile 40 | 41 | - name: Publish 42 | run: npm publish 43 | env: 44 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Setup Node 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: '12.x' 17 | 18 | - name: Get yarn cache 19 | id: yarn-cache 20 | run: echo "::set-output name=dir::$(yarn cache dir)" 21 | 22 | - name: Cache dependencies 23 | uses: actions/cache@v1 24 | with: 25 | path: ${{ steps.yarn-cache.outputs.dir }} 26 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 27 | restore-keys: | 28 | ${{ runner.os }}-yarn- 29 | 30 | - name: Install dependencies 31 | run: yarn install 32 | 33 | - name: Test 34 | run: yarn test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | node_modules 4 | /dist 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [Unreleased] 4 | 5 | ## [0.2.0] - 2022-01-20 6 | ### Added 7 | - Support multiple components in a single worker 8 | ### Changed 9 | - Fix package.json properly for ESM 10 | 11 | ## [0.1.0] - 2021-01-10 12 | ### Changed 13 | - Fix to avoid memory leak 14 | 15 | ## [0.0.3] - 2021-01-08 16 | ### Changed 17 | - Fix to type check self 18 | 19 | ## [0.0.2] - 2020-12-29 20 | ### Changed 21 | - Fix to render recursively in worker 22 | 23 | ## [0.0.1] - 2020-12-28 24 | ### Added 25 | - Initial experimental release 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2022 Daishi Kato 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-worker-components 2 | 3 | [![CI](https://img.shields.io/github/actions/workflow/status/dai-shi/react-worker-components/ci.yml?branch=main)](https://github.com/dai-shi/react-worker-components/actions?query=workflow%3ACI) 4 | [![npm](https://img.shields.io/npm/v/react-worker-components)](https://www.npmjs.com/package/react-worker-components) 5 | [![size](https://img.shields.io/bundlephobia/minzip/react-worker-components)](https://bundlephobia.com/result?p=react-worker-components) 6 | [![discord](https://img.shields.io/discord/627656437971288081)](https://discord.gg/MrQdmzd) 7 | 8 | React Worker Components simplify using Web Workers 9 | 10 | ## Introduction 11 | 12 | This is an experimental project inspired by 13 | [React Server Component](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html). 14 | 15 | I've been developing several libraries to interact with Web Workers. 16 | 17 | * [react-hooks-worker](https://github.com/dai-shi/react-hooks-worker) 18 | * [redux-in-worker](https://github.com/dai-shi/redux-in-worker) 19 | * [react-suspense-worker](https://github.com/dai-shi/react-suspense-worker) 20 | 21 | While they provide various interfaces with good abstraction, 22 | RSC style would be another approach which is useful for Web Workers. 23 | 24 | RWC is a library to provide RSC-like interface for Web Workers. 25 | It serializes React elements keeping their referential identities 26 | as much as possible. 27 | If a React component is "registered", it will be referenced by string names, 28 | and it can be used at the both ends. 29 | 30 | Project Status: Experimental but basic examples are working. Welcome to try realistic examples. 31 | 32 | ## Install 33 | 34 | ```bash 35 | npm install react-worker-components 36 | ``` 37 | 38 | ## Usage 39 | 40 | ### `TextBox.js` 41 | 42 | This is a component that can be used in the RWC tree. 43 | `register` is important to enable serialization. 44 | 45 | ```js 46 | import React, { useState } from 'react'; 47 | 48 | import { register } from 'react-worker-components'; 49 | 50 | export const TextBox = () => { 51 | const [text, setText] = useState(''); 52 | return ( 53 |
54 | Text: {text} 55 | setText(event.target.value)} /> 56 |
57 | ); 58 | }; 59 | 60 | register(TextBox, 'TextBox'); 61 | ``` 62 | 63 | ### `Hello.worker.js` 64 | 65 | This is a component that runs only on web workers. 66 | `expose` is necessary to communicate with the main thread. 67 | 68 | ```js 69 | import React from 'react'; 70 | 71 | import { expose } from 'react-worker-components'; 72 | 73 | import { TextBox } from './TextBox'; 74 | 75 | const fib = (i) => (i <= 1 ? i : fib(i - 1) + fib(i - 2)); 76 | 77 | const Hello = ({ count, children }) => { 78 | const fibNum = fib(count); 79 | return ( 80 |
81 |
Hello from worker: {fibNum}
82 |

Main TextBox

83 | {children} 84 |

Worker TextBox

85 | 86 |
87 | ); 88 | }; 89 | 90 | expose(Hello); 91 | ``` 92 | 93 | ### `App.js` 94 | 95 | This is the entry point component in the main thread. 96 | `wrap` is to communicate with the worker thread. 97 | 98 | ```js 99 | import React, { Suspense, useState } from 'react'; 100 | 101 | import { wrap } from 'react-worker-components'; 102 | 103 | import { TextBox } from './TextBox'; 104 | 105 | const Hello = wrap(() => new Worker(new URL('./Hello.worker', import.meta.url))); 106 | 107 | export const App = () => { 108 | const [count, setCount] = useState(1); 109 | return ( 110 |
111 | Count: {count} 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 |
120 | ); 121 | }; 122 | ``` 123 | 124 | ## API 125 | 126 | 127 | 128 | ### expose 129 | 130 | Expose a React function component from web workers. 131 | 132 | #### Parameters 133 | 134 | * `Component` **React.FC\** 135 | * `key` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** 136 | 137 | #### Examples 138 | 139 | ```javascript 140 | // foo.worker.js 141 | import { expose } from 'react-worker-components'; 142 | 143 | const Foo = () => { 144 | return

Foo

; 145 | }; 146 | 147 | expose(Foo); 148 | ``` 149 | 150 | ### register 151 | 152 | Register a component with a string name 153 | 154 | This allows serializing components between main and worker threads. 155 | 156 | #### Parameters 157 | 158 | * `component` **AnyComponent** 159 | * `name` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** 160 | 161 | #### Examples 162 | 163 | ```javascript 164 | import { register } from 'react-worker-components'; 165 | 166 | const Counter = () => { 167 | const [count, setCount] = useState(0); 168 | return
{count}
; 169 | }; 170 | 171 | register(Counter, 'Counter'); 172 | ``` 173 | 174 | ### wrap 175 | 176 | Wrap an exposed component in main thread 177 | 178 | This will connect the component in the worker thread. 179 | Requires Suspense. 180 | 181 | It will create a dedicated worker for each createWorker function reference. 182 | 183 | #### Parameters 184 | 185 | * `createWorker` **any** 186 | * `key` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** 187 | 188 | #### Examples 189 | 190 | ```javascript 191 | import { wrap } from 'react-worker-components'; 192 | 193 | const Foo = wrap(() => new Worker(new URL('./Foo.worker', import.meta.url))); 194 | ``` 195 | 196 | ## Examples 197 | 198 | The [examples](examples) folder contains working examples. 199 | You can run one of them with 200 | 201 | ```bash 202 | PORT=8080 npm run examples:01_minimal 203 | ``` 204 | 205 | and open in your web browser. 206 | -------------------------------------------------------------------------------- /__tests__/01_basic_spec.ts: -------------------------------------------------------------------------------- 1 | import { register, expose, wrap } from '../src/index'; 2 | 3 | describe('basic spec', () => { 4 | it('exported function', () => { 5 | expect(register).toBeDefined(); 6 | expect(expose).toBeDefined(); 7 | expect(wrap).toBeDefined(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/01_minimal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-worker-components-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "latest", 7 | "react-dom": "latest", 8 | "react-scripts": "latest", 9 | "react-worker-components": "latest" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "eject": "react-scripts eject" 16 | }, 17 | "browserslist": [ 18 | ">0.2%", 19 | "not dead", 20 | "not ie <= 11", 21 | "not op_mini all" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /examples/01_minimal/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-worker-components example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/01_minimal/src/Hello.worker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { expose } from 'react-worker-components'; 4 | 5 | import { TextBox } from './TextBox'; 6 | 7 | const fib = (i) => (i <= 1 ? i : fib(i - 1) + fib(i - 2)); 8 | 9 | const Hello = ({ count, children }) => { 10 | const fibNum = fib(count); 11 | return ( 12 |
13 |
Hello from worker: {fibNum}
14 |

Main TextBox

15 | {children} 16 |

Worker TextBox

17 | 18 |
19 | ); 20 | }; 21 | 22 | expose(Hello); 23 | -------------------------------------------------------------------------------- /examples/01_minimal/src/TextBox.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { register } from 'react-worker-components'; 4 | 5 | export const TextBox = () => { 6 | const [text, setText] = useState(''); 7 | return ( 8 |
9 | Text: {text} 10 | setText(event.target.value)} /> 11 |
12 | ); 13 | }; 14 | 15 | register(TextBox, 'TextBox'); 16 | -------------------------------------------------------------------------------- /examples/01_minimal/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense, useState } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { wrap } from 'react-worker-components'; 5 | 6 | import { TextBox } from './TextBox'; 7 | 8 | const Hello = wrap(() => new Worker(new URL('./Hello.worker', import.meta.url))); 9 | 10 | const App = () => { 11 | const [count, setCount] = useState(1); 12 | return ( 13 |
14 | Count: {count} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | ); 24 | }; 25 | 26 | ReactDOM.render(, document.getElementById('app')); 27 | -------------------------------------------------------------------------------- /examples/02_typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-worker-components-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/react": "latest", 7 | "@types/react-dom": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "react-scripts": "latest", 11 | "react-worker-components": "latest", 12 | "typescript": "latest" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "browserslist": [ 21 | ">0.2%", 22 | "not dead", 23 | "not ie <= 11", 24 | "not op_mini all" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/02_typescript/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-worker-components example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/02_typescript/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Counter from './Counter'; 4 | 5 | class ErrorBoundary extends Component { 6 | constructor(props: unknown) { 7 | super(props); 8 | this.state = { error: null }; 9 | } 10 | 11 | static getDerivedStateFromError(error: Error) { 12 | return { error }; 13 | } 14 | 15 | render() { 16 | const { error } = this.state; 17 | const { children } = this.props; 18 | if (error) { 19 | return ( 20 |
21 |

Error

22 |

{`${error}`}

23 |
24 | ); 25 | } 26 | return children; 27 | } 28 | } 29 | 30 | const App: React.FC = () => ( 31 | 32 |

Counter

33 | 34 |
35 | 36 |
37 | ); 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /examples/02_typescript/src/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense, useState } from 'react'; 2 | 3 | import { wrap } from 'react-worker-components'; 4 | 5 | import { TextBox } from './TextBox'; 6 | 7 | import { Props as HelloProps } from './Hello.worker'; 8 | 9 | const Hello = wrap(() => new Worker(new URL('./Hello.worker', import.meta.url))); 10 | 11 | const Counter: React.FC = () => { 12 | const [count, setCount] = useState(1); 13 | return ( 14 |
15 | Count: {count} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default Counter; 28 | -------------------------------------------------------------------------------- /examples/02_typescript/src/Hello.worker.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { expose } from 'react-worker-components'; 4 | 5 | import { TextBox } from './TextBox'; 6 | 7 | const fib = (i: number): number => (i <= 1 ? i : fib(i - 1) + fib(i - 2)); 8 | 9 | export type Props = { 10 | count: number; 11 | }; 12 | 13 | const Hello: React.FC = ({ count, children }) => { 14 | const fibNum = fib(count); 15 | return ( 16 |
17 |
Hello from worker: {fibNum}
18 |

Main TextBox

19 | {children} 20 |

Worker TextBox

21 | 22 |
23 | ); 24 | }; 25 | 26 | expose(Hello); 27 | -------------------------------------------------------------------------------- /examples/02_typescript/src/TextBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { register } from 'react-worker-components'; 4 | 5 | export const TextBox = () => { 6 | const [text, setText] = useState(''); 7 | return ( 8 |
9 | Text: {text} 10 | setText(event.target.value)} /> 11 |
12 | ); 13 | }; 14 | 15 | register(TextBox, 'TextBox'); 16 | -------------------------------------------------------------------------------- /examples/02_typescript/src/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | 6 | const ele = document.getElementById('app'); 7 | if (!ele) throw new Error('no app'); 8 | ReactDOM.render(React.createElement(App), ele); 9 | -------------------------------------------------------------------------------- /examples/03_fetch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-worker-components-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/react": "latest", 7 | "@types/react-dom": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "react-scripts": "latest", 11 | "react-suspense-fetch": "latest", 12 | "react-worker-components": "latest", 13 | "typescript": "latest" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /examples/03_fetch/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-worker-components example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/03_fetch/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Main from './Main'; 4 | 5 | class ErrorBoundary extends Component { 6 | constructor(props: unknown) { 7 | super(props); 8 | this.state = { error: null }; 9 | } 10 | 11 | static getDerivedStateFromError(error: Error) { 12 | return { error }; 13 | } 14 | 15 | render() { 16 | const { error } = this.state; 17 | const { children } = this.props; 18 | if (error) { 19 | return ( 20 |
21 |

Error

22 |

{`${error}`}

23 |
24 | ); 25 | } 26 | return children; 27 | } 28 | } 29 | 30 | const App: React.FC = () => ( 31 | 32 |
33 | 34 | ); 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /examples/03_fetch/src/Fetcher.worker.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { createFetchStore } from 'react-suspense-fetch'; 4 | import { expose } from 'react-worker-components'; 5 | 6 | import { PostData } from './PostData'; 7 | import { TextBox } from './TextBox'; 8 | 9 | export type Props = { 10 | uid: number; 11 | }; 12 | 13 | type Result = { 14 | data: { 15 | id: number; 16 | email: string; 17 | first_name: string; 18 | last_name: string; 19 | }; 20 | }; 21 | 22 | const fetchUser = async (uid: number): Promise => { 23 | const response = await fetch(`https://reqres.in/api/users/${uid}?delay=1`); 24 | return response.json(); 25 | }; 26 | 27 | const store = createFetchStore(fetchUser); 28 | 29 | const Fetcher: React.FC = ({ uid, children }) => { 30 | const { data } = store.get(uid); 31 | const postId = 5 + uid; // just to change it for demo 32 | return ( 33 |
34 |
User Name: {data.first_name} {data.last_name}
35 | 36 |

Main TextBox

37 | {children} 38 |

Worker TextBox

39 | 40 |
41 | ); 42 | }; 43 | 44 | expose(Fetcher); 45 | -------------------------------------------------------------------------------- /examples/03_fetch/src/Main.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense, useState } from 'react'; 2 | 3 | import { wrap } from 'react-worker-components'; 4 | 5 | import { TextBox } from './TextBox'; 6 | 7 | import { Props as FetcherProps } from './Fetcher.worker'; 8 | 9 | const Fetcher = wrap(() => new Worker(new URL('./Fetcher.worker', import.meta.url))); 10 | 11 | const Main: React.FC = () => { 12 | const [uid, setUid] = useState(1); 13 | return ( 14 |
15 | User ID: {uid} 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | ); 24 | }; 25 | 26 | export default Main; 27 | -------------------------------------------------------------------------------- /examples/03_fetch/src/PostData.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { createFetchStore } from 'react-suspense-fetch'; 4 | 5 | type Props = { 6 | name: string; 7 | id: number; 8 | }; 9 | 10 | type Result = { 11 | data: { 12 | id: number; 13 | name: string; 14 | color: string; 15 | }; 16 | }; 17 | 18 | const fetchPost = async (path: string): Promise => { 19 | const response = await fetch(`https://reqres.in/api/${path}?delay=1`); 20 | return response.json(); 21 | }; 22 | 23 | const store = createFetchStore(fetchPost); 24 | 25 | export const PostData: React.FC = ({ name, id }) => { 26 | const { data } = store.get(`${name}/${id}`); 27 | return ( 28 |
Post Data: {data.name}
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /examples/03_fetch/src/TextBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { register } from 'react-worker-components'; 4 | 5 | export const TextBox = () => { 6 | const [text, setText] = useState(''); 7 | return ( 8 |
9 | Text: {text} 10 | setText(event.target.value)} /> 11 |
12 | ); 13 | }; 14 | 15 | register(TextBox, 'TextBox'); 16 | -------------------------------------------------------------------------------- /examples/03_fetch/src/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | 6 | const ele = document.getElementById('app'); 7 | if (!ele) throw new Error('no app'); 8 | ReactDOM.render(React.createElement(App), ele); 9 | -------------------------------------------------------------------------------- /examples/04_multicomps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-worker-components-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@types/react": "latest", 7 | "@types/react-dom": "latest", 8 | "react": "latest", 9 | "react-dom": "latest", 10 | "react-scripts": "latest", 11 | "react-worker-components": "latest", 12 | "typescript": "latest" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "browserslist": [ 21 | ">0.2%", 22 | "not dead", 23 | "not ie <= 11", 24 | "not op_mini all" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/04_multicomps/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-worker-components example 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/04_multicomps/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Suspense } from 'react'; 2 | 3 | import { wrap } from 'react-worker-components'; 4 | 5 | import './TextBox'; 6 | import './Counter'; 7 | import { Props as HelloProps } from './Hello.worker'; 8 | 9 | const createWorker = () => new Worker(new URL('./Hello.worker', import.meta.url)); 10 | 11 | const Hello = wrap(createWorker, 'Hello'); 12 | 13 | const WorkerCounter = wrap(createWorker, 'WorkerCounter'); 14 | 15 | class ErrorBoundary extends Component { 16 | constructor(props: unknown) { 17 | super(props); 18 | this.state = { error: null }; 19 | } 20 | 21 | static getDerivedStateFromError(error: Error) { 22 | return { error }; 23 | } 24 | 25 | render() { 26 | const { error } = this.state; 27 | const { children } = this.props; 28 | if (error) { 29 | return ( 30 |
31 |

Error

32 |

{`${error}`}

33 |
34 | ); 35 | } 36 | return children; 37 | } 38 | } 39 | 40 | const App: React.FC = () => ( 41 | 42 | 43 |

Hello

44 | 45 | 46 |
47 |
48 | ); 49 | 50 | export default App; 51 | -------------------------------------------------------------------------------- /examples/04_multicomps/src/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { register } from 'react-worker-components'; 4 | 5 | export const Counter: React.FC = () => { 6 | const [count, setCount] = useState(1); 7 | return ( 8 |
9 | Count: {count} 10 | 11 | 12 |
13 | ); 14 | }; 15 | 16 | register(Counter, 'Counter'); 17 | -------------------------------------------------------------------------------- /examples/04_multicomps/src/Hello.worker.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { expose } from 'react-worker-components'; 4 | 5 | import { TextBox } from './TextBox'; 6 | import { Counter } from './Counter'; 7 | 8 | const fib = (i: number): number => (i <= 1 ? i : fib(i - 1) + fib(i - 2)); 9 | 10 | export type Props = { 11 | count: number; 12 | }; 13 | 14 | const Hello: React.FC = ({ count }) => { 15 | const fibNum = fib(count); 16 | return ( 17 |
18 |
Hello from worker: {fibNum}
19 |

Worker TextBox

20 | 21 |
22 | ); 23 | }; 24 | 25 | expose(Hello, 'Hello'); 26 | 27 | const WorkerCounter: React.FC = () => ( 28 |
29 |

Worker Counter

30 | 31 |
32 | ); 33 | 34 | expose(WorkerCounter, 'WorkerCounter'); 35 | -------------------------------------------------------------------------------- /examples/04_multicomps/src/TextBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { register } from 'react-worker-components'; 4 | 5 | export const TextBox = () => { 6 | const [text, setText] = useState(''); 7 | return ( 8 |
9 | Text: {text} 10 | setText(event.target.value)} /> 11 |
12 | ); 13 | }; 14 | 15 | register(TextBox, 'TextBox'); 16 | -------------------------------------------------------------------------------- /examples/04_multicomps/src/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | 6 | const ele = document.getElementById('app'); 7 | if (!ele) throw new Error('no app'); 8 | ReactDOM.render(React.createElement(App), ele); 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-worker-components", 3 | "description": "React Worker Components simplify using Web Workers", 4 | "version": "0.2.0", 5 | "author": "Daishi Kato", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/dai-shi/react-worker-components.git" 9 | }, 10 | "source": "./src/index.ts", 11 | "main": "./dist/index.umd.js", 12 | "module": "./dist/index.modern.js", 13 | "types": "./dist/src/index.d.ts", 14 | "exports": { 15 | "./package.json": "./package.json", 16 | ".": { 17 | "types": "./dist/src/index.d.ts", 18 | "module": "./dist/index.modern.js", 19 | "import": "./dist/index.modern.mjs", 20 | "default": "./dist/index.umd.js" 21 | } 22 | }, 23 | "sideEffects": false, 24 | "files": [ 25 | "src", 26 | "dist" 27 | ], 28 | "scripts": { 29 | "compile": "microbundle build -f modern,umd --jsx React.createElement", 30 | "postcompile": "cp dist/index.modern.mjs dist/index.modern.js && cp dist/index.modern.mjs.map dist/index.modern.js.map", 31 | "test": "run-s eslint tsc-test jest", 32 | "eslint": "eslint --ext .js,.ts,.tsx --ignore-pattern dist .", 33 | "jest": "jest --preset ts-jest/presets/js-with-ts", 34 | "tsc-test": "tsc --project . --noEmit", 35 | "apidoc": "documentation readme --section API --markdown-toc false --parse-extension ts --require-extension ts src/expose.ts src/wrap.ts", 36 | "examples:01_minimal": "DIR=01_minimal EXT=js webpack-dev-server", 37 | "examples:02_typescript": "DIR=02_typescript webpack-dev-server", 38 | "examples:03_fetch": "DIR=03_fetch webpack-dev-server", 39 | "examples:04_multicomps": "DIR=04_multicomps webpack-dev-server" 40 | }, 41 | "keywords": [ 42 | "react", 43 | "worker", 44 | "components", 45 | "suspense", 46 | "webworker" 47 | ], 48 | "license": "MIT", 49 | "dependencies": {}, 50 | "devDependencies": { 51 | "@testing-library/react": "^12.1.2", 52 | "@types/jest": "^27.4.0", 53 | "@types/react": "^17.0.38", 54 | "@types/react-dom": "^17.0.11", 55 | "@typescript-eslint/eslint-plugin": "^5.9.0", 56 | "@typescript-eslint/parser": "^5.9.0", 57 | "documentation": "^13.2.5", 58 | "eslint": "^8.6.0", 59 | "eslint-config-airbnb": "^19.0.4", 60 | "eslint-plugin-import": "^2.25.4", 61 | "eslint-plugin-jsx-a11y": "^6.5.1", 62 | "eslint-plugin-react": "^7.28.0", 63 | "eslint-plugin-react-hooks": "^4.3.0", 64 | "html-webpack-plugin": "^5.5.0", 65 | "jest": "^27.4.7", 66 | "microbundle": "^0.14.2", 67 | "npm-run-all": "^4.1.5", 68 | "react": "^17.0.2", 69 | "react-dom": "^17.0.2", 70 | "react-suspense-fetch": "^0.4.0", 71 | "ts-jest": "^27.1.2", 72 | "ts-loader": "^9.2.6", 73 | "typescript": "^4.5.4", 74 | "webpack": "^5.65.0", 75 | "webpack-cli": "^4.9.1", 76 | "webpack-dev-server": "^4.7.2" 77 | }, 78 | "peerDependencies": { 79 | "react": ">=16.8.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/expose.ts: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react'; 2 | 3 | import { isComponentRegistered } from './register'; 4 | import { serialize, deserialize } from './serializer'; 5 | 6 | // TODO cache render result (only for React.memo?) 7 | 8 | const render = (Component: React.FC, props: Props) => { 9 | const ele = Component(props); 10 | return walk(ele); 11 | }; 12 | 13 | const eleTypeof = '$$typeof'; 14 | const eleSymbol = Symbol.for('react.element'); 15 | 16 | const walk = (x: T): T => { 17 | if (typeof x !== 'object' || x === null) return x; 18 | const obj = x as unknown as Record; 19 | if (obj[eleTypeof] === eleSymbol) { 20 | const { type } = obj; 21 | if (typeof type !== 'string' && !isComponentRegistered(type)) { 22 | return render( 23 | type as React.FC, 24 | obj.props, 25 | ) as unknown as T; 26 | } 27 | } 28 | if (typeof obj.props !== 'object' || obj.props === null) return x; 29 | const { children } = obj.props as Record; 30 | if (Array.isArray(children)) { 31 | const newChildren = children.map(walk); 32 | if (newChildren.every((child, index) => child === children[index])) { 33 | return x; 34 | } 35 | return { ...x, props: { ...obj.props, children: newChildren } }; 36 | } 37 | const newChildren = walk(children); 38 | if (newChildren === children) { 39 | return x; 40 | } 41 | return { ...x, props: { ...obj.props, children: newChildren } }; 42 | }; 43 | 44 | /** 45 | * Expose a React function component from web workers. 46 | * 47 | * @example 48 | * // foo.worker.js 49 | * import { expose } from 'react-worker-components'; 50 | * 51 | * const Foo = () => { 52 | * return

Foo

; 53 | * }; 54 | * 55 | * expose(Foo); 56 | */ 57 | export const expose = (Component: React.FC, key?: string) => { 58 | self.addEventListener('message', (e) => { 59 | const { key: dataKey, id, props } = e.data; 60 | if (dataKey !== key) { 61 | return; 62 | } 63 | if (!id || !props) { 64 | throw new Error('no id or props found'); 65 | } 66 | const deserialized = deserialize(props) as PropsWithChildren; // unsafe type assertion 67 | const thunk = () => { 68 | try { 69 | const ele = render(Component, deserialized); 70 | (self as unknown as Worker).postMessage({ id, ele: serialize(ele) }); 71 | } catch (err) { 72 | if (err instanceof Promise) { 73 | err.then(thunk); 74 | } else { 75 | (self as unknown as Worker).postMessage({ id, err }); 76 | } 77 | } 78 | }; 79 | thunk(); 80 | }); 81 | }; 82 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { register } from './register'; 2 | export { expose } from './expose'; 3 | export { wrap } from './wrap'; 4 | -------------------------------------------------------------------------------- /src/register.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType } from 'react'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | type AnyComponent = ComponentType; 5 | 6 | const component2name = new Map(); 7 | const name2component = new Map(); 8 | 9 | /** 10 | * Register a component with a string name 11 | * 12 | * This allows serializing components between main and worker threads. 13 | * 14 | * @example 15 | * import { register } from 'react-worker-components'; 16 | * 17 | * const Counter = () => { 18 | * const [count, setCount] = useState(0); 19 | * return
{count}
; 20 | * }; 21 | * 22 | * register(Counter, 'Counter'); 23 | */ 24 | export const register = ( 25 | component: AnyComponent, 26 | name: string, 27 | ) => { 28 | if (component2name.has(component)) { 29 | throw new Error(`component ${component} is already registered`); 30 | } 31 | if (name2component.has(name)) { 32 | throw new Error(`component name ${name} is already registered`); 33 | } 34 | component2name.set(component, name); 35 | name2component.set(name, component); 36 | }; 37 | 38 | export const isComponentRegistered = ( 39 | component: unknown, 40 | ) => component2name.has(component as AnyComponent); 41 | 42 | export const getName = ( 43 | component: AnyComponent, 44 | ) => { 45 | const name = component2name.get(component); 46 | if (!name) throw new Error(`component ${component} is not registered`); 47 | return name; 48 | }; 49 | 50 | export const getComponent = ( 51 | name: string, 52 | ) => { 53 | const component = name2component.get(name); 54 | if (!component) throw new Error(`component name ${name} is not registered`); 55 | return component; 56 | }; 57 | -------------------------------------------------------------------------------- /src/serializer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ComponentType, 3 | ReactNode, 4 | createElement as createElementOrig, 5 | } from 'react'; 6 | 7 | import { getName, getComponent } from './register'; 8 | 9 | const createElement = ( 10 | type: Parameters[0], 11 | { children, ...props }: Record, 12 | ) => { 13 | if (Array.isArray(children)) { 14 | return createElementOrig(type, props, ...children as ReactNode[]); 15 | } 16 | if (children) { 17 | return createElementOrig(type, props, children as ReactNode); 18 | } 19 | return createElementOrig(type, props); 20 | }; 21 | 22 | const eleTypeof = '$$typeof'; 23 | const eleSymbol = Symbol.for('react.element'); 24 | 25 | type Serialized = 26 | | { v: unknown } 27 | | { i: number } & ( 28 | | { e: { props: Serialized; type: string | { c: string } } } 29 | | { a: Serialized[] } 30 | | { o: Record } 31 | | { u: object } 32 | ); 33 | 34 | const isSerialized = (x: unknown): x is Serialized => { 35 | if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { 36 | if (typeof x !== 'object' || x === null) return false; 37 | if ('v' in x) return true; 38 | if (typeof (x as { i: unknown }).i !== 'number') return false; 39 | if ('e' in x) { 40 | const { e } = x as { e: unknown }; 41 | if (typeof e !== 'object' || e === null) return false; 42 | if (!isSerialized((e as { props: unknown }).props)) return false; 43 | const { type } = e as { type: unknown }; 44 | if (typeof type === 'string') return true; 45 | if (typeof type !== 'object' || type === null) return false; 46 | return typeof (type as { c: unknown }).c === 'string'; 47 | } 48 | if ('a' in x) { 49 | const { a } = x as { a: unknown }; 50 | return Array.isArray(a) && a.every(isSerialized); 51 | } 52 | if ('o' in x) { 53 | const { o } = x as { o: unknown }; 54 | if (typeof o !== 'object' || o === null) return false; 55 | return Object.values(o).every(isSerialized); 56 | } 57 | if (typeof (x as { u: unknown }).u === 'object') { 58 | return true; 59 | } 60 | return false; 61 | } 62 | return true; 63 | }; 64 | 65 | const idx2obj = new Map>(); 66 | const obj2idx = new WeakMap(); 67 | 68 | const isWorker = typeof self !== 'undefined' && !self.document; 69 | let index = 0; 70 | const nextIndex = isWorker ? (() => ++index) : (() => --index); 71 | 72 | let lastGcSize = 0; 73 | const gc = () => { 74 | if (idx2obj.size < lastGcSize + 10) return; 75 | for (const [idx, ref] of idx2obj) { 76 | if (!ref.deref()) { 77 | idx2obj.delete(idx); 78 | } 79 | } 80 | lastGcSize = idx2obj.size; 81 | }; 82 | 83 | export const serialize = (x: unknown): Serialized => { 84 | setTimeout(gc, 1); 85 | if (typeof x !== 'object' || x === null) { 86 | return { v: x }; 87 | } 88 | let i: number; 89 | if (obj2idx.has(x)) { 90 | i = obj2idx.get(x) as number; 91 | } else { 92 | i = nextIndex(); 93 | obj2idx.set(x, i); 94 | idx2obj.set(i, new WeakRef(x)); 95 | } 96 | if ((x as { [eleTypeof]: unknown })[eleTypeof] === eleSymbol) { 97 | const e = { 98 | props: serialize((x as { props: unknown }).props), 99 | type: typeof (x as { type: unknown }).type === 'string' 100 | ? (x as { type: string }).type 101 | : { c: getName((x as { type: ComponentType }).type) }, 102 | }; 103 | return { i, e }; 104 | } 105 | if (Array.isArray(x)) { 106 | const a = x.map(serialize); 107 | return { i, a }; 108 | } 109 | if (Object.getPrototypeOf(x) === Object.prototype) { 110 | const o: Record = {}; 111 | Object.entries(x).forEach(([key, val]) => { 112 | if (typeof key === 'symbol') throw new Error('symbol keys are not supported'); 113 | o[key] = serialize(val); 114 | }); 115 | return { i, o }; 116 | } 117 | return { i, u: x }; 118 | }; 119 | 120 | export const deserialize = (x: unknown): unknown => { 121 | if (!isSerialized(x)) throw new Error('not serialized type'); 122 | if ('v' in x) return x.v; 123 | if (idx2obj.has(x.i)) { 124 | const obj = idx2obj.get(x.i)?.deref(); 125 | if (obj) { 126 | return obj; 127 | } 128 | } 129 | if ('e' in x) { 130 | const type = typeof x.e.type === 'string' 131 | ? x.e.type 132 | : getComponent(x.e.type.c); 133 | const ele: object = createElement(type, deserialize(x.e.props) as Record); 134 | idx2obj.set(x.i, new WeakRef(ele)); 135 | return ele; 136 | } 137 | if ('a' in x) { 138 | const arr = x.a.map(deserialize); 139 | idx2obj.set(x.i, new WeakRef(arr)); 140 | return arr; 141 | } 142 | if ('o' in x) { 143 | const obj: Record = {}; 144 | Object.entries(x.o).forEach(([key, val]) => { 145 | obj[key] = deserialize(val); 146 | }); 147 | idx2obj.set(x.i, new WeakRef(obj)); 148 | return obj; 149 | } 150 | if ('u' in x) { 151 | idx2obj.set(x.i, new WeakRef(x.u)); 152 | return x.u; 153 | } 154 | throw new Error('should not reach here'); 155 | }; 156 | -------------------------------------------------------------------------------- /src/wrap.ts: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useEffect } from 'react'; 2 | 3 | import { serialize, deserialize } from './serializer'; 4 | 5 | type AnyProps = Record; 6 | 7 | type Entry = { 8 | err?: unknown; 9 | result?: unknown; 10 | resolve?: () => void; 11 | promise?: Promise; 12 | }; 13 | 14 | type EmptyObject = Record; 15 | 16 | type WorkerState = { 17 | worker: Worker; 18 | used: Set; 19 | propsMap: WeakMap; 20 | idMap: Map; 21 | i: number; 22 | }; 23 | 24 | /** 25 | * Wrap an exposed component in main thread 26 | * 27 | * This will connect the component in the worker thread. 28 | * Requires Suspense. 29 | * 30 | * It will create a dedicated worker for each createWorker function reference. 31 | * 32 | * @example 33 | * import { wrap } from 'react-worker-components'; 34 | * 35 | * const Foo = wrap(() => new Worker(new URL('./Foo.worker', import.meta.url))); 36 | */ 37 | export const wrap = ( 38 | createWorker: (() => Worker) & { state?: WorkerState }, 39 | key?: string, 40 | ) => { 41 | const getWorkerState = () => { 42 | let { state } = createWorker; 43 | if (!state) { 44 | const worker = createWorker(); 45 | const propsMap = new WeakMap(); 46 | const idMap = new Map(); 47 | worker.addEventListener('message', (e) => { 48 | const { id, err, ele } = e.data; 49 | const entry = propsMap.get(idMap.get(id) as AnyProps); 50 | idMap.delete(id); 51 | if (entry) { 52 | if (err) { 53 | entry.err = err; 54 | } else { 55 | entry.result = deserialize(ele); 56 | } 57 | entry.resolve?.(); 58 | } 59 | }); 60 | state = { 61 | worker, 62 | used: new Set(), 63 | propsMap, 64 | idMap, 65 | i: 0, 66 | }; 67 | createWorker.state = state; 68 | } 69 | return state; 70 | }; 71 | const Component: React.FC = (props) => { 72 | const state = getWorkerState(); 73 | useEffect(() => { 74 | const id = Symbol(); 75 | state.used.add(id); 76 | return () => { 77 | state.used.delete(id); 78 | if (!state.used.size && state.worker) { 79 | state.worker.terminate(); 80 | delete createWorker.state; 81 | } 82 | }; 83 | }, [state]); 84 | if (!state.propsMap.has(props)) { 85 | const entry: Entry = {}; 86 | const promise = new Promise((resolve) => { 87 | const id = `id${++state.i}`; 88 | entry.resolve = resolve; 89 | state.propsMap.set(props, entry); 90 | state.idMap.set(id, props); 91 | state.worker.postMessage({ key, id, props: serialize(props) }); 92 | }); 93 | entry.promise = promise; 94 | throw promise; 95 | } 96 | const entry = state.propsMap.get(props) as Entry; 97 | if (entry.err) throw entry.err; 98 | if (entry.result === undefined) throw entry.promise; 99 | return entry.result as ReactElement; // unsafe type assertion 100 | }; 101 | return Component; 102 | }; 103 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "es5", 5 | "lib": ["esnext", "dom"], 6 | "downlevelIteration": true, 7 | "esModuleInterop": true, 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "jsx": "react", 11 | "allowJs": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "sourceMap": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "react-worker-components": ["./src"] 18 | }, 19 | "outDir": "./dist" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | const { DIR, EXT = 'ts' } = process.env; 5 | 6 | module.exports = { 7 | mode: 'development', 8 | devtool: 'cheap-module-source-map', 9 | entry: `./examples/${DIR}/src/index.${EXT}`, 10 | output: { 11 | publicPath: '/', 12 | }, 13 | plugins: [ 14 | new HtmlWebpackPlugin({ 15 | template: `./examples/${DIR}/public/index.html`, 16 | }), 17 | ], 18 | module: { 19 | rules: [{ 20 | test: /\.[jt]sx?$/, 21 | exclude: /node_modules/, 22 | loader: 'ts-loader', 23 | options: { 24 | transpileOnly: true, 25 | }, 26 | }], 27 | }, 28 | resolve: { 29 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 30 | alias: { 31 | 'react-worker-components': `${__dirname}/src`, 32 | }, 33 | }, 34 | devServer: { 35 | port: process.env.PORT || '8080', 36 | static: { 37 | directory: `./examples/${DIR}/public`, 38 | }, 39 | historyApiFallback: true, 40 | }, 41 | }; 42 | --------------------------------------------------------------------------------