Skip to content

Commit 7df0d2d

Browse files
committed
[IMPL] useWebWorkerFn hook
1 parent 38ca7eb commit 7df0d2d

14 files changed

Lines changed: 350 additions & 130 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { useCallback, useEffect, useState } from "react";
2+
import { useWebWorkerFn } from "../../../../../../packages/react-tools/src";
3+
4+
/**
5+
The component uses _useWebWorkerFn_ hook to execute an expensive function in a worker. Same function can be executed in main thread. Try to execute it to see the differences.
6+
*/
7+
export const UseWebWorkerFn = () => {
8+
const [ts, setTs] = useState(Date.now());
9+
const [mess, setMess] = useState<string>("");
10+
const heavyTask = useCallback(() => {
11+
const numbers: number[] = Array(5_000_000).fill(true).map(() => Math.random() * 11)
12+
return numbers.slice(0, 5).map(el => Math.floor(el))
13+
}, []);
14+
const execute = useWebWorkerFn(heavyTask);
15+
16+
useEffect(() => {
17+
const id = setInterval(() => setTs(Date.now()), 1);
18+
return () => clearInterval(id);
19+
}, []);
20+
21+
return <div>
22+
<p>Timestamp: {ts}</p>
23+
<p>Result: {mess ? mess : ""}</p>
24+
<button
25+
onClick={() => {
26+
setMess("Pending...");
27+
setMess(heavyTask().join(","))
28+
}}
29+
>
30+
Start in Main Thread
31+
</button>
32+
<button
33+
onClick={() => {
34+
setMess("Pending...");
35+
execute().then(res => setMess(res.join(",")));
36+
}}
37+
>
38+
Start in Web Worker
39+
</button>
40+
</div>
41+
}

apps/react-tools-demo/src/constants/components.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ export const COMPONENTS = [
116116
"usePermission",
117117
"useMediaDevices",
118118
"useDisplayMedia",
119-
"useWebWorker"
119+
"useWebWorker",
120+
"useWebWorkerFn"
120121
]
121122
],
122123
//UTILS

apps/react-tools-demo/src/markdown/useWebWorker.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@ Hook to use [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Wo
66
```tsx
77
export const UseWebWorker = () => {
88
const [ts, setTs] = useState(Date.now());
9-
const [res, setRes] = useState<number[]>([]);
109
const [mess, setMess] = useState<string>("");
1110

1211
const { send } = useWebWorker({
1312
url: new URL('./worker.ts', import.meta.url),
1413
onMessage: useCallback((e: MessageEvent) => {
15-
setMess("");
16-
setRes(e.data)
14+
setMess(e.data.res.join(","))
1715
}, [])
1816
});
1917

@@ -24,7 +22,7 @@ export const UseWebWorker = () => {
2422

2523
return <div>
2624
<p>Timestamp: {ts}</p>
27-
<p>Result: {mess ? mess : res ? res.toString() : ""}</p>
25+
<p>Result: {mess ? mess : ""}</p>
2826
<button
2927
onClick={() => {
3028
setMess("Pending...");
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# useWebWorkerFn
2+
Hook to run expensive functions using a Web Worker without blocking the UI handling execution as Promise.
3+
4+
## Usage
5+
6+
```tsx
7+
export const UseWebWorkerFn = () => {
8+
const [ts, setTs] = useState(Date.now());
9+
const [mess, setMess] = useState<string>("");
10+
const heavyTask = useCallback(() => {
11+
const numbers: number[] = Array(55_000_000).fill(true).map(() => Math.random() * 11)
12+
return numbers.slice(0, 5).map(el => Math.floor(el))
13+
}, []);
14+
const execute = useWebWorkerFn(heavyTask);
15+
16+
useEffect(() => {
17+
const id = setInterval(() => setTs(Date.now()), 1);
18+
return () => clearInterval(id);
19+
}, []);
20+
21+
return <div>
22+
<p>Timestamp: {ts}</p>
23+
<p>Result: {mess ? mess : ""}</p>
24+
<button
25+
onClick={() => {
26+
setMess("Pending...");
27+
setMess(heavyTask().join(","))
28+
}}
29+
>
30+
Start in Main Thread
31+
</button>
32+
<button
33+
onClick={() => {
34+
setMess("Pending...");
35+
execute().then(res => setMess(res.join(",")));
36+
}}
37+
>
38+
Start in Web Worker
39+
</button>
40+
</div>
41+
}
42+
```
43+
44+
> The component uses _useWebWorkerFn_ hook to execute an expensive function in a worker. Same function can be executed in main thread. Try to execute it to see the differences.
45+
46+
47+
## API
48+
49+
```tsx
50+
useWebWorkerFn<T extends (...args: unknown[]) => unknown>(fn: UseWebWorkerFnProps<T>["fn"], deps?: UseWebWorkerFnProps<T>["deps"]): UseWebWorkerFnResult<T>
51+
```
52+
53+
> ### Params
54+
>
55+
> - __fn__: _UseWebWorkerFnProps["fn"]_
56+
Expensive function to be executed in worker.
57+
> - __deps?__: _UseWebWorkerFnProps["deps"]_
58+
An array that contains the external dependencies needed to run the worker.
59+
>
60+
61+
> ### Returns
62+
>
63+
> __execute__: function to execute expansive function: return a promise.
64+
> - _UseWebWorkerFnResult_
65+
>

packages/react-tools/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@
120120
- [x] useMediaDevices
121121
- [x] useDisplayMedia
122122
- [x] useWebWorker
123-
- [ ] useWebWorkerFn (https://vueuse.org/core/useWebWorkerFn/)
123+
- [x] useWebWorkerFn
124124
- [ ] useIndexedDB
125125
- [ ] useFetch (with suspense ???)
126126
- [ ] useLock - (https://developer.mozilla.org/en-US/docs/Web/API/LockManager/request)

packages/react-tools/src/hooks/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,5 @@ export { useMediaDevices } from './useMediaDevices';
9797
export { usePermission } from './usePermission';
9898
export { useDisplayMedia } from './useDisplayMedia';
9999
export { useSwipe } from './useSwipe';
100-
export { useWebWorker } from './useWebWorker';
100+
export { useWebWorker } from './useWebWorker';
101+
export { useWebWorkerFn } from './useWebWorkerFn';

packages/react-tools/src/hooks/useEventSource.ts

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useCallback, useMemo, useRef } from "react";
22
import { UseEventSourceProps, UseEventSourceResult } from "../models";
3-
import { useSyncExternalStore } from ".";
3+
import { useEffectOnce, useSyncExternalStore } from ".";
44

55
/**
66
* **`useEventSource`**: Hook to handle an [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) or [Server-Sent-Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) connection to an HTTP server, which sends events in text/event-stream format.
@@ -20,7 +20,6 @@ import { useSyncExternalStore } from ".";
2020
* - __close__: function that closes connection.
2121
*/
2222
export const useEventSource = <T>({ url, opts, events, immediateConnection, onOpen, onError, onMessage }: UseEventSourceProps): UseEventSourceResult<T> => {
23-
const alreadyOpened = useRef(false);
2423
const notifyRef = useRef<() => void>();
2524
const sourceRef = useRef<EventSource>();
2625
const eventsHandler = useRef((events || []).map(evt => {
@@ -37,44 +36,6 @@ export const useEventSource = <T>({ url, opts, events, immediateConnection, onOp
3736
status: immediateConnection && url ? "CONNECTING" : "READY",
3837
data: null
3938
});
40-
if (url && immediateConnection && !alreadyOpened.current) {
41-
alreadyOpened.current = true;
42-
sourceRef.current = new EventSource(url, opts);
43-
}
44-
45-
if (sourceRef.current) {
46-
sourceRef.current.onopen = (evt: Event) => {
47-
!!onOpen && onOpen(evt);
48-
cachedState.current.status = "OPENED";
49-
notifyRef.current && notifyRef.current();
50-
};
51-
sourceRef.current.onerror = (evt: Event) => {
52-
!!onError && onError(evt);
53-
cachedState.current.status = "CLOSED";
54-
notifyRef.current && notifyRef.current();
55-
};
56-
sourceRef.current.onmessage = (evt: MessageEvent<T>) => {
57-
!!onMessage && onMessage(evt);
58-
cachedState.current.data = evt.data;
59-
notifyRef.current && notifyRef.current();
60-
};
61-
eventsHandler.current.forEach(evt => {
62-
sourceRef.current?.removeEventListener(evt.name, evt.hanlder);
63-
});
64-
eventsHandler.current = (events || []).map(evt => {
65-
return {
66-
name: evt.name,
67-
hanlder: (e: MessageEvent) => {
68-
!!evt.handler && evt.handler(e);
69-
cachedState.current.data = e.data;
70-
notifyRef.current && notifyRef.current();
71-
}
72-
}
73-
});
74-
eventsHandler.current.forEach(evt => {
75-
sourceRef.current?.addEventListener(evt.name, evt.hanlder);
76-
});
77-
}
7839

7940
const open = useCallback((urlParam?: UseEventSourceProps["url"]) => {
8041
if (url || urlParam) {
@@ -117,6 +78,46 @@ export const useEventSource = <T>({ url, opts, events, immediateConnection, onOp
11778

11879
const data = useMemo(() => (state.data), [state.data]);
11980

81+
useEffectOnce(() => {
82+
if (url && immediateConnection) {
83+
sourceRef.current = new EventSource(url, opts);
84+
}
85+
})
86+
87+
if (sourceRef.current) {
88+
sourceRef.current.onopen = (evt: Event) => {
89+
!!onOpen && onOpen(evt);
90+
cachedState.current.status = "OPENED";
91+
notifyRef.current && notifyRef.current();
92+
};
93+
sourceRef.current.onerror = (evt: Event) => {
94+
!!onError && onError(evt);
95+
cachedState.current.status = "CLOSED";
96+
notifyRef.current && notifyRef.current();
97+
};
98+
sourceRef.current.onmessage = (evt: MessageEvent<T>) => {
99+
!!onMessage && onMessage(evt);
100+
cachedState.current.data = evt.data;
101+
notifyRef.current && notifyRef.current();
102+
};
103+
eventsHandler.current.forEach(evt => {
104+
sourceRef.current?.removeEventListener(evt.name, evt.hanlder);
105+
});
106+
eventsHandler.current = (events || []).map(evt => {
107+
return {
108+
name: evt.name,
109+
hanlder: (e: MessageEvent) => {
110+
!!evt.handler && evt.handler(e);
111+
cachedState.current.data = e.data;
112+
notifyRef.current && notifyRef.current();
113+
}
114+
}
115+
});
116+
eventsHandler.current.forEach(evt => {
117+
sourceRef.current?.addEventListener(evt.name, evt.hanlder);
118+
});
119+
}
120+
120121
return {
121122
status: state.status,
122123
data,

packages/react-tools/src/hooks/useSpeechRecognition.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useCallback, useMemo, useRef } from "react";
22
import { SpeechRecognition, SpeechRecognitionConfig, SpeechRecognitionEvent, SpeechRecognitionState, UseSpeechRecognitionProps } from "../models";
3-
import { useSyncExternalStore } from ".";
3+
import { useEffectOnce, useSyncExternalStore } from ".";
44

55
/**
66
* **`useSpeechRecognition`**: Hook to use _SpeechRecognition API_. Refer to [Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition).
@@ -40,7 +40,6 @@ export const useSpeechRecognition = ({ alreadyStarted, defaultConfig, onAudioSta
4040
const recognition = useRef<SpeechRecognition>();
4141
const notifRef = useRef<() => void>();
4242
const isListening = useRef(false);
43-
const firstExecution = useRef(true);
4443
const result = useRef<{ results: SpeechRecognitionEvent["results"]|null, resultIndex: SpeechRecognitionEvent["resultIndex"]|null }>({resultIndex: null, results: null});
4544

4645
const start = useCallback((config?: typeof defaultConfig) => {
@@ -145,11 +144,12 @@ export const useSpeechRecognition = ({ alreadyStarted, defaultConfig, onAudioSta
145144
}, [])
146145
);
147146

148-
if (firstExecution.current && isSupported.current && alreadyStarted) {
149-
isListening.current = true;
150-
firstExecution.current = false;
151-
start();
152-
}
147+
useEffectOnce(() => {
148+
if (isSupported.current && alreadyStarted) {
149+
isListening.current = true;
150+
start();
151+
}
152+
})
153153

154154
return [state, start, stop.current, reset.current];
155155
}

0 commit comments

Comments
 (0)