;
9 | componentIndex: number;
10 | componentPath: string;
11 | }
12 |
13 | export interface WorkerRenderComponent
14 | extends ComponentPathMeta
{
15 | id: string;
16 | componentName: string;
17 | setStateState(state: any): void;
18 | getInstanceState(): any;
19 | getInstanceProps(): any;
20 | componentSpec: WorkerRenderComponentSpec;
21 | callMethod(method: string, args: any[]): void;
22 | }
23 | export type ComponentPath = string;
24 | export type ComponentId = string;
25 |
26 | export interface AppComponent
27 | extends ComponentPathMeta
{
28 | postMessage(msg: any): void;
29 | componentNameDefaultPropsMap: Record;
30 | newComponentsPathIdMap: Record;
31 | newComponentsIdStateMap: Record;
32 | addComponent(component: WorkerRenderComponent): void;
33 | setStateState(component: WorkerRenderComponent, state: any): void;
34 | removeComponent(component: WorkerRenderComponent): void;
35 | }
36 |
37 | export interface WorkerRenderComponentSpec
38 | extends React.ComponentLifecycle,
39 | React.StaticLifecycle {
40 | getInitialState?: () => any;
41 | defaultProps?: any;
42 | render: (this: {
43 | nativeComponents: Record;
44 | props: any;
45 | state: any;
46 | getComponent: (name: string) => React.ComponentClass;
47 | getEventHandle: (name: string) => any;
48 | }) => React.ReactNode;
49 | [k: string]: any;
50 | }
51 |
52 | export interface WorkerLike {
53 | postMessage(msg: string): void;
54 | addEventListener: (
55 | type: 'message',
56 | fn: (e: { data: string }) => void,
57 | ) => void;
58 | removeEventListener: (
59 | type: 'message',
60 | fn: (e: { data: string }) => void,
61 | ) => void;
62 | }
63 |
64 | export const MSG_TYPE = 'react-worker-render';
65 | export interface FromWorkerMsg {
66 | type: typeof MSG_TYPE;
67 | newComponentNameDefaultPropsMap: Record;
68 | pendingIdStateMap: Record;
69 | newComponentsPathIdMap: Record;
70 | newComponentsIdStateMap: Record;
71 | }
72 |
73 | export interface FromRenderMsg {
74 | type: typeof MSG_TYPE;
75 | componentId: string;
76 | method: string;
77 | args: any[];
78 | }
79 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-worker-render
2 |
3 | [](http://badge.fury.io/js/react-worker-render)
4 | [](https://npmjs.org/package/react-worker-render)
5 | [](https://app.travis-ci.com/github/yiminghe/react-worker-render)
6 | [](https://dashboard.cypress.io/projects/wog843/runs)
7 |
8 | move react component lifecycle to worker and render to DOM.
9 |
10 | ## example
11 |
12 | https://yiminghe.github.io/react-worker-render
13 |
14 | ## API
15 |
16 | ### types
17 |
18 | ```ts
19 | interface WorkerRenderComponentSpec extends React.ComponentLifecycle, React.StaticLifecycle {
20 | getInitialState?: () => any;
21 | defaultProps?: any;
22 | render: (this: {
23 | nativeComponents: Record;
24 | props: any;
25 | state: any;
26 | getComponent: (name: string) => React.ComponentClass;
27 | getEventHandle: (name: string) => any;
28 | }) => React.ReactNode;
29 | [k: string]: any;
30 | }
31 | interface WorkerLike {
32 | postMessage(msg: string): void;
33 | onmessage: ((e: any) => void) | null;
34 | }
35 | ```
36 |
37 | ### ReactWorker
38 |
39 | ```ts
40 | import { ReactWorker } from 'react-worker-render';
41 | ```
42 |
43 | ```ts
44 | export declare function registerNativeComponent(cls: string, Cls: React.ComponentClass): void;
45 | export declare function registerComponent(name: string, desc: WorkerRenderComponentSpec): void;
46 | export declare function bootstrap(params: {
47 | worker: WorkerLike;
48 | entry: string;
49 | }): void;
50 | ```
51 |
52 | ### ReactRender
53 |
54 | ```ts
55 | import { ReactRender } from 'react-worker-render';
56 | ```
57 |
58 | ```ts
59 | export declare function registerNativeComponent(cls: string, Cls: React.ComponentClass): void;
60 | export declare function registerComponent(name: string, desc: {render:WorkerRenderComponentSpec['render']}): void;
61 | export declare function bootstrap(params: {
62 | worker: WorkerLike;
63 | entry: string;
64 | batchedUpdates: (fn: () => void) => void;
65 | render: (element: React.ReactChild) => void;
66 | }): void;
67 | ```
68 |
69 | ## development
70 |
71 | ```
72 | yarn run bootstrap
73 | yarn start
74 | ```
75 |
76 | open: http://localhost:3000/
77 |
78 | ## supported react versions
79 |
80 | 16-18
81 |
82 | App can override react/react-reconciler version using yarn resolutions.
83 |
--------------------------------------------------------------------------------
/src/render/App.tsx:
--------------------------------------------------------------------------------
1 | import componentPath from '../common/componentPath';
2 | import React from 'react';
3 | import {
4 | AppComponent,
5 | WorkerRenderComponent,
6 | WorkerLike,
7 | FromWorkerMsg,
8 | FromRenderMsg,
9 | MSG_TYPE,
10 | } from '../common/types';
11 | import { getComponentClass } from './getComponentClass';
12 | import { noop, safeJsonParse } from '../common/utils';
13 | import { log } from '../common/log';
14 |
15 | class App
16 | extends React.Component<
17 | {
18 | worker: WorkerLike;
19 | entry: string;
20 | batchedUpdates: (fn: () => void) => void;
21 | },
22 | { inited: boolean }
23 | >
24 | implements AppComponent
25 | {
26 | componentIndex = 0;
27 | componentPath = '1';
28 | componentChildIndex = 0;
29 | componentChildIndexMap = new Map();
30 | newComponentsPathIdMap = {};
31 | componentNameDefaultPropsMap = {};
32 | newComponentsIdStateMap = {};
33 | pendingIdStateMap = {};
34 | components: Map = new Map();
35 | componentSpec = null!;
36 |
37 | constructor(props: any) {
38 | super(props);
39 | this.props.worker.addEventListener('message', this.onmessage);
40 | this.state = {
41 | inited: false,
42 | };
43 | }
44 | onmessage = (e: any) => {
45 | const msg: FromWorkerMsg = safeJsonParse(e.data);
46 | if (msg.type !== MSG_TYPE) {
47 | return;
48 | }
49 | log('from worker', msg);
50 | const {
51 | newComponentsIdStateMap,
52 | newComponentsPathIdMap,
53 | pendingIdStateMap,
54 | newComponentNameDefaultPropsMap,
55 | } = msg;
56 | const { components, componentNameDefaultPropsMap } = this;
57 | Object.assign(
58 | componentNameDefaultPropsMap,
59 | newComponentNameDefaultPropsMap,
60 | );
61 | for (const name of Object.keys(newComponentNameDefaultPropsMap)) {
62 | getComponentClass(name).defaultProps =
63 | newComponentNameDefaultPropsMap[name];
64 | }
65 | this.newComponentsIdStateMap = newComponentsIdStateMap;
66 | this.newComponentsPathIdMap = newComponentsPathIdMap;
67 |
68 | this.props.batchedUpdates(() => {
69 | if (!this.state.inited) {
70 | this.setState({
71 | inited: true,
72 | });
73 | }
74 | for (const id of Object.keys(pendingIdStateMap)) {
75 | const state = pendingIdStateMap[id];
76 | const component = components.get(id)!;
77 | component.setStateState(state);
78 | }
79 | });
80 | };
81 | postMessage(msg: FromRenderMsg) {
82 | log('send to worker', msg);
83 | this.props.worker.postMessage(JSON.stringify(msg));
84 | }
85 |
86 | addComponent(component: WorkerRenderComponent) {
87 | this.components.set(component.id, component);
88 | }
89 | removeComponent(component: WorkerRenderComponent) {
90 | this.components.delete(component.id);
91 | }
92 |
93 | setStateState = noop;
94 |
95 | componentWillUnmount() {
96 | this.props.worker.removeEventListener('message', this.onmessage);
97 | }
98 |
99 | render(): React.ReactNode {
100 | if (this.state.inited) {
101 | const Entry = getComponentClass(this.props.entry);
102 | return componentPath.renderWithComponentContext(this, );
103 | } else {
104 | return null;
105 | }
106 | }
107 | }
108 |
109 | componentPath.classWithContext(App);
110 |
111 | export default App;
112 |
--------------------------------------------------------------------------------
/src/render/getComponentClass.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { getComponentDesc } from '../common/register';
3 | import {
4 | FromRenderMsg,
5 | MSG_TYPE,
6 | WorkerRenderComponent,
7 | } from '../common/types';
8 | import { nativeComponents } from './nativeComponent';
9 | import componentPath from '../common/componentPath';
10 | import ComponentContext, {
11 | ComponentContextValue,
12 | } from '../common/ComponentContext';
13 | import Input from './nativeComponents/Input';
14 | import { noop } from '../common/utils';
15 | import PureRender from 'react-addons-pure-render-mixin';
16 |
17 | const componentClassCache: Record = {};
18 |
19 | export function getComponentClass(name: string): React.ComponentClass {
20 | if (componentClassCache[name]) {
21 | return componentClassCache[name];
22 | }
23 |
24 | const componentSpec = getComponentDesc(name);
25 |
26 | interface State {
27 | __state: any;
28 | __self: Component;
29 | }
30 |
31 | class Component
32 | extends React.Component
33 | implements WorkerRenderComponent
34 | {
35 | componentIndex = 0;
36 | componentPath = '';
37 | componentChildIndex = 0;
38 | componentChildIndexMap = new Map();
39 | eventHandles: Record void> = {};
40 | componentSpec = componentSpec;
41 | componentName = name;
42 | static contextType = ComponentContext;
43 | publicInstance: any = {};
44 | id = '';
45 | constructor(props: any) {
46 | super(props);
47 | this.state = {
48 | __self: this,
49 | __state: {},
50 | };
51 | this.publicInstance = Object.create(componentSpec);
52 | Object.defineProperty(this.publicInstance, 'props', {
53 | get: this.getInstanceProps,
54 | });
55 | Object.defineProperty(this.publicInstance, 'state', {
56 | get: this.getInstanceState,
57 | });
58 | }
59 |
60 | shouldComponentUpdate = PureRender.shouldComponentUpdate;
61 |
62 | static getDerivedStateFromProps(_: any, { __self }: State) {
63 | const instance: Component = __self;
64 | componentPath.updateComponentPath(instance);
65 | let state;
66 | const { app } = instance.context as ComponentContextValue;
67 | if (!instance.id) {
68 | const path = componentPath.getComponentPath(instance);
69 | instance.id = app.newComponentsPathIdMap[path];
70 | if (!instance.id) {
71 | throw new Error(`Can not find id from path: ${path}`);
72 | }
73 | app.addComponent(instance);
74 | state = app.newComponentsIdStateMap[instance.id] || {};
75 | return { __state: state };
76 | }
77 | return {};
78 | }
79 |
80 | setStateState(newState: any) {
81 | this.setState(({ __state }) => {
82 | return {
83 | __state: {
84 | ...__state,
85 | ...newState,
86 | },
87 | };
88 | });
89 | }
90 |
91 | getInstanceProps = () => {
92 | return this.props;
93 | };
94 | getInstanceState() {
95 | return this.state.__state;
96 | }
97 | getContext() {
98 | return this.context as ComponentContextValue;
99 | }
100 |
101 | componentDidMount() {
102 | componentSpec.componentDidMount?.call(this.publicInstance);
103 | }
104 |
105 | callMethod = noop;
106 |
107 | componentDidUpdate(prevProps: any, prevState: State) {
108 | const { publicInstance } = this;
109 | componentSpec.componentDidUpdate?.call(
110 | publicInstance,
111 | prevProps,
112 | prevState.__state,
113 | );
114 | }
115 |
116 | componentWillUnmount() {
117 | componentSpec.componentWillUnmount?.call(this.publicInstance);
118 | this.getContext().app.removeComponent(this);
119 | }
120 |
121 | getEventHandle = (name: string) => {
122 | const { eventHandles } = this;
123 | const { app } = this.context as ComponentContextValue;
124 | if (eventHandles[name]) {
125 | return eventHandles[name];
126 | }
127 | eventHandles[name] = (...args: any) => {
128 | const msg: FromRenderMsg = {
129 | type: MSG_TYPE,
130 | componentId: this.id,
131 | method: name,
132 | args,
133 | };
134 | app.postMessage(msg);
135 | };
136 | (eventHandles as any).handleName = name;
137 | return eventHandles[name];
138 | };
139 |
140 | render(): React.ReactNode {
141 | const element = componentSpec.render.call({
142 | nativeComponents,
143 | props: this.props,
144 | state: this.getInstanceState(),
145 | getEventHandle: this.getEventHandle,
146 | getComponent: getComponentClass,
147 | });
148 | return componentPath.renderWithComponentContext(this, element);
149 | }
150 | }
151 |
152 | const C = Component as any;
153 |
154 | C.displayName = name;
155 | componentClassCache[name] = C;
156 | return C;
157 | }
158 |
159 | Object.assign(nativeComponents, {
160 | Input: (Input as any) || getComponentClass('input'),
161 | });
162 |
--------------------------------------------------------------------------------
/src/worker/App.tsx:
--------------------------------------------------------------------------------
1 | import componentPath from '../common/componentPath';
2 | import React from 'react';
3 | import { getComponentDesc } from '../common/register';
4 | import {
5 | AppComponent,
6 | WorkerRenderComponent,
7 | WorkerLike,
8 | FromWorkerMsg,
9 | FromRenderMsg,
10 | MSG_TYPE,
11 | } from '../common/types';
12 | import { getComponentClass } from './getComponentClass';
13 | import noopRender from './noopRender';
14 | import { cleanFuncJson, safeJsonParse } from '../common/utils';
15 | import { log } from '../common/log';
16 |
17 | class App
18 | extends React.Component<{ worker: WorkerLike; entry: string }>
19 | implements AppComponent
20 | {
21 | componentIndex = 0;
22 | componentPath = '1';
23 | componentChildIndex = 0;
24 | componentChildIndexMap = new Map();
25 | newComponentsPathIdMap: Record = {};
26 | newComponentsIdStateMap: Record = {};
27 | pendingIdStateMap: Record = {};
28 | newComponentIds: Set = new Set();
29 | components: Map = new Map();
30 | scheduled = false;
31 | componentNameDefaultPropsMap: Record = {};
32 |
33 | constructor(props: any) {
34 | super(props);
35 | this.props.worker.addEventListener('message', this.onmessage);
36 | }
37 | onmessage = (e: { data: string }) => {
38 | const msg: FromRenderMsg = safeJsonParse(e.data);
39 | if (msg.type !== MSG_TYPE) {
40 | return;
41 | }
42 | log('from render', msg);
43 | const { componentId, method, args } = msg;
44 | const component = this.components.get(componentId)!;
45 | noopRender.batchedUpdates(() => {
46 | component.callMethod(method, args);
47 | });
48 | };
49 | postMessage(msg: FromWorkerMsg) {
50 | log('send to render', msg);
51 | this.props.worker.postMessage(JSON.stringify(msg));
52 | }
53 |
54 | afterSendToRender() {
55 | this.pendingIdStateMap = {};
56 | this.newComponentIds.clear();
57 | this.newComponentsPathIdMap = {};
58 | this.newComponentsIdStateMap = {};
59 | }
60 |
61 | scheduleSendToRender() {
62 | if (this.scheduled) {
63 | return;
64 | }
65 | this.scheduled = true;
66 | Promise.resolve().then(() => {
67 | this.sendToRender();
68 | this.afterSendToRender();
69 | this.scheduled = false;
70 | });
71 | }
72 |
73 | componentDidMount() {
74 | this.scheduleSendToRender();
75 | }
76 |
77 | componentWillUnmount() {
78 | this.props.worker.removeEventListener('message', this.onmessage);
79 | }
80 |
81 | sendToRender() {
82 | const {
83 | components,
84 | pendingIdStateMap,
85 | newComponentsIdStateMap,
86 | newComponentsPathIdMap,
87 | componentNameDefaultPropsMap,
88 | } = this;
89 |
90 | const newComponentNameDefaultPropsMap: Record = {};
91 |
92 | for (const id of Object.keys(pendingIdStateMap)) {
93 | if (!components.has(id)) {
94 | delete pendingIdStateMap[id];
95 | }
96 | }
97 |
98 | for (const id of Array.from(this.newComponentIds)) {
99 | const component = components.get(id)!;
100 | const { componentName } = component;
101 |
102 | newComponentsIdStateMap[id] = component.getInstanceState();
103 | newComponentsPathIdMap[componentPath.getComponentPath(component)] = id;
104 |
105 | if (!componentNameDefaultPropsMap[componentName]) {
106 | componentNameDefaultPropsMap[componentName] =
107 | getComponentDesc(componentName).defaultProps || {};
108 | newComponentNameDefaultPropsMap[componentName] =
109 | componentNameDefaultPropsMap[componentName];
110 | }
111 | }
112 |
113 | this.postMessage({
114 | type: MSG_TYPE,
115 | newComponentsIdStateMap: cleanFuncJson(newComponentsIdStateMap),
116 | newComponentsPathIdMap,
117 | pendingIdStateMap: cleanFuncJson(pendingIdStateMap),
118 | newComponentNameDefaultPropsMap: cleanFuncJson(
119 | newComponentNameDefaultPropsMap,
120 | ),
121 | });
122 | }
123 | setStateState(component: WorkerRenderComponent, state: any) {
124 | if (this.newComponentIds.has(component.id)) {
125 | return;
126 | }
127 | const { pendingIdStateMap } = this;
128 | const current = pendingIdStateMap[component.id] || {};
129 | Object.assign(current, state);
130 | pendingIdStateMap[component.id] = current;
131 | this.scheduleSendToRender();
132 | }
133 | addComponent(component: WorkerRenderComponent) {
134 | if (!this.components.has(component.id)) {
135 | this.newComponentIds.add(component.id);
136 | }
137 | this.components.set(component.id, component);
138 | }
139 | removeComponent(component: WorkerRenderComponent) {
140 | this.newComponentIds.delete(component.id);
141 | this.components.delete(component.id);
142 | }
143 |
144 | render(): React.ReactNode {
145 | const Entry = getComponentClass(this.props.entry);
146 | return componentPath.renderWithComponentContext(this, );
147 | }
148 | }
149 |
150 | componentPath.classWithContext(App);
151 |
152 | export default App;
153 |
--------------------------------------------------------------------------------
/src/worker/getComponentClass.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { getComponentDesc } from '../common/register';
3 | import { WorkerRenderComponent } from '../common/types';
4 | import { nativeComponents } from './nativeComponent';
5 | import componentPath from '../common/componentPath';
6 | import ComponentContext, {
7 | ComponentContextValue,
8 | } from '../common/ComponentContext';
9 | import { WorkerComponent } from './types';
10 | import NativeInput from './nativeComponents/Input';
11 |
12 | const componentClassCache: Record = {};
13 |
14 | let gid = 1;
15 |
16 | export function getComponentClass(
17 | name: string,
18 | native = false,
19 | ): React.ComponentClass {
20 | if (componentClassCache[name]) {
21 | return componentClassCache[name];
22 | }
23 |
24 | const componentSpec = getComponentDesc(name);
25 |
26 | interface State {
27 | __state: any;
28 | __self: Component;
29 | }
30 |
31 | class Component
32 | extends React.Component
33 | implements WorkerRenderComponent
34 | {
35 | id: string;
36 | componentIndex = 0;
37 | componentPath = '';
38 | componentChildIndex = 0;
39 | componentChildIndexMap = new Map();
40 | eventHandles: Record void>;
41 | componentSpec = componentSpec;
42 | publicInstance: WorkerComponent;
43 | componentName = name;
44 |
45 | static contextType = ComponentContext;
46 |
47 | static defaultProps = componentSpec.defaultProps;
48 |
49 | constructor(props: any) {
50 | super(props);
51 | this.id = '';
52 | this.publicInstance = Object.create(componentSpec);
53 | this.publicInstance.setState = this.setStateState;
54 | Object.defineProperty(this.publicInstance, 'props', {
55 | get: this.getInstanceProps,
56 | });
57 | Object.defineProperty(this.publicInstance, 'state', {
58 | get: this.getInstanceState,
59 | });
60 | this.eventHandles = {};
61 | this.state = {
62 | __self: this,
63 | __state: {},
64 | };
65 | if (componentSpec.getInitialState) {
66 | const state = componentSpec.getInitialState.call(this.publicInstance);
67 | if (state) {
68 | (this.state as State).__state = state;
69 | }
70 | }
71 | }
72 |
73 | shouldComponentUpdate(nextProps: any, nextState: State) {
74 | if (componentSpec.shouldComponentUpdate) {
75 | return componentSpec.shouldComponentUpdate.call(
76 | this.publicInstance,
77 | nextProps,
78 | nextState.__state,
79 | undefined,
80 | );
81 | }
82 | return true;
83 | }
84 |
85 | callMethod(method: string, args: any[]): void {
86 | const publicInstance: any = this.publicInstance;
87 | publicInstance[method](...args);
88 | }
89 |
90 | setStateState = (
91 | newState: any,
92 | callback?: () => void,
93 | sendToRender = true,
94 | ) => {
95 | if (!native) {
96 | sendToRender = true;
97 | }
98 | this.setState(({ __state }) => {
99 | let retState: any = {};
100 | if (typeof newState === 'function') {
101 | retState = newState(__state);
102 | } else {
103 | retState = newState;
104 | }
105 | if (sendToRender) {
106 | this.getContext().app.setStateState(this, retState);
107 | }
108 | return {
109 | __state: {
110 | ...__state,
111 | ...retState,
112 | },
113 | };
114 | }, callback);
115 | };
116 |
117 | static getDerivedStateFromProps(nextProps: any, { __self }: State) {
118 | const instance: Component = __self;
119 | componentPath.updateComponentPath(instance);
120 | const { app } = instance.context as ComponentContextValue;
121 | if (!instance.id) {
122 | instance.id = ++gid + '';
123 | app.addComponent(instance);
124 | }
125 | if (instance.componentSpec.getDerivedStateFromProps) {
126 | const state = instance.getInstanceState();
127 | const newState = instance.componentSpec.getDerivedStateFromProps(
128 | nextProps,
129 | state,
130 | );
131 | return {
132 | __state: {
133 | ...state,
134 | ...newState,
135 | },
136 | };
137 | }
138 | return {};
139 | }
140 |
141 | getContext() {
142 | return this.context as ComponentContextValue;
143 | }
144 |
145 | componentDidMount() {
146 | componentSpec.componentDidMount?.call(this.publicInstance);
147 | }
148 |
149 | componentDidUpdate(prevProps: any, prevState: any) {
150 | const { publicInstance } = this;
151 | componentSpec.componentDidUpdate?.call(
152 | publicInstance,
153 | prevProps,
154 | prevState.__state,
155 | );
156 | }
157 |
158 | componentWillUnmount() {
159 | componentSpec.componentWillUnmount?.call(this.publicInstance);
160 | this.getContext().app.removeComponent(this);
161 | }
162 |
163 | getInstanceProps = () => {
164 | return this.props;
165 | };
166 | getInstanceState = () => {
167 | return this.state.__state;
168 | };
169 |
170 | getEventHandle = (name: string) => {
171 | const { eventHandles } = this;
172 | if (eventHandles[name]) {
173 | return eventHandles[name];
174 | }
175 | const publicInstance = this.publicInstance as any;
176 | const handle: any = (...args: any) => {
177 | publicInstance[name](...args);
178 | };
179 | handle.handleName = name;
180 | eventHandles[name] = handle;
181 | return handle;
182 | };
183 |
184 | render(): React.ReactNode {
185 | const element = componentSpec.render.call({
186 | nativeComponents,
187 | props: this.props,
188 | state: this.getInstanceState(),
189 | getEventHandle: this.getEventHandle,
190 | getComponent: getComponentClass,
191 | });
192 | return componentPath.renderWithComponentContext(this, element);
193 | }
194 | }
195 |
196 | const C = Component as any;
197 |
198 | C.displayName = name;
199 | componentClassCache[name] = C;
200 | return C;
201 | }
202 |
203 | Object.assign(nativeComponents, {
204 | Input: (NativeInput as any) || getComponentClass('input', true),
205 | });
206 |
--------------------------------------------------------------------------------