3.1 KB110 lines
Blame
1import { useAtom } from 'jotai/react';
2import type { Atom, WritableAtom } from 'jotai/vanilla';
3import { useEffect, useRef } from 'react';
4import {
5 Connection,
6 createReduxConnection,
7} from './redux-extension/createReduxConnection';
8import { getReduxExtension } from './redux-extension/getReduxExtension';
9
10type DevtoolOptions = Parameters<typeof useAtom>[1] & {
11 name?: string;
12 enabled?: boolean;
13};
14
15export function useAtomDevtools<Value, Result>(
16 anAtom: WritableAtom<Value, [Value], Result> | Atom<Value>,
17 options?: DevtoolOptions,
18): void {
19 const { enabled, name } = options || {};
20
21 const extension = getReduxExtension(enabled);
22
23 const [value, setValue] = useAtom(anAtom, options);
24
25 const lastValue = useRef(value);
26 const isTimeTraveling = useRef(false);
27 const devtools = useRef<Connection>();
28
29 const atomName = name || anAtom.debugLabel || anAtom.toString();
30
31 useEffect(() => {
32 if (!extension) {
33 return;
34 }
35 const setValueIfWritable = (value: Value) => {
36 if (typeof setValue === 'function') {
37 (setValue as (value: Value) => void)(value);
38 return;
39 }
40 console.warn(
41 '[Warn] you cannot do write operations (Time-travelling, etc) in read-only atoms\n',
42 anAtom,
43 );
44 };
45
46 devtools.current = createReduxConnection(extension, atomName);
47
48 const unsubscribe = devtools.current?.subscribe((message) => {
49 if (message.type === 'ACTION' && message.payload) {
50 try {
51 setValueIfWritable(JSON.parse(message.payload));
52 } catch (e) {
53 console.error(
54 'please dispatch a serializable value that JSON.parse() support\n',
55 e,
56 );
57 }
58 } else if (message.type === 'DISPATCH' && message.state) {
59 if (
60 message.payload?.type === 'JUMP_TO_ACTION' ||
61 message.payload?.type === 'JUMP_TO_STATE'
62 ) {
63 isTimeTraveling.current = true;
64
65 setValueIfWritable(JSON.parse(message.state));
66 }
67 } else if (
68 message.type === 'DISPATCH' &&
69 message.payload?.type === 'COMMIT'
70 ) {
71 devtools.current?.init(lastValue.current);
72 } else if (
73 message.type === 'DISPATCH' &&
74 message.payload?.type === 'IMPORT_STATE'
75 ) {
76 const computedStates =
77 message.payload.nextLiftedState?.computedStates || [];
78
79 computedStates.forEach(({ state }: { state: Value }, index: number) => {
80 if (index === 0) {
81 devtools.current?.init(state);
82 } else {
83 setValueIfWritable(state);
84 }
85 });
86 }
87 });
88
89 return unsubscribe;
90 }, [anAtom, extension, atomName, setValue]);
91
92 useEffect(() => {
93 if (!devtools.current) {
94 return;
95 }
96 lastValue.current = value;
97 if (devtools.current.shouldInit) {
98 devtools.current.init(value);
99 devtools.current.shouldInit = false;
100 } else if (isTimeTraveling.current) {
101 isTimeTraveling.current = false;
102 } else {
103 devtools.current.send(
104 `${atomName} - ${new Date().toLocaleString()}` as any,
105 value,
106 );
107 }
108 }, [anAtom, extension, atomName, value]);
109}
110