├── .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 | [](https://github.com/dai-shi/react-worker-components/actions?query=workflow%3ACI)
4 | [](https://www.npmjs.com/package/react-worker-components)
5 | [](https://bundlephobia.com/result?p=react-worker-components)
6 | [](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