@react-navigation/devtools
Version:
Developer tools for React Navigation
144 lines (142 loc) • 4.49 kB
JavaScript
;
import deepEqual from 'fast-deep-equal';
import * as React from 'react';
import { parseErrorStack } from "./parseErrorStack.js";
export function useDevToolsBase(ref, callback) {
const lastStateRef = React.useRef(undefined);
const lastActionRef = React.useRef(undefined);
const callbackRef = React.useRef(callback);
const lastResetRef = React.useRef(undefined);
React.useEffect(() => {
callbackRef.current = callback;
});
const symbolicate = async stack => {
if (stack == null) {
return undefined;
}
const frames = parseErrorStack(stack).slice(2).filter(frame => frame.file !== '[native code]');
const urlMatch = frames[0]?.file?.match(/^https?:\/\/.+(:\d+)?\//);
if (!urlMatch) {
return stack;
}
try {
const result = await fetch(`${urlMatch[0]}symbolicate`, {
method: 'POST',
body: JSON.stringify({
stack: frames
})
}).then(res => res.json());
return result.stack.filter(it => !it.collapse).map(({
methodName,
file,
lineNumber,
column
}) => `${methodName}@${file}:${lineNumber}:${column}`).join('\n');
} catch (err) {
return stack;
}
};
const pendingPromiseRef = React.useRef(Promise.resolve());
const send = React.useCallback(data => {
// We need to make sure that our callbacks executed in the same order
// So we add check if the last promise is settled before sending the next one
pendingPromiseRef.current = pendingPromiseRef.current.catch(() => {
// Ignore any errors from the last promise
}).then(async () => {
// eslint-disable-next-line promise/always-return
if (data.stack) {
let stack;
try {
stack = await symbolicate(data.stack);
} catch (err) {
// Ignore errors from symbolicate
}
callbackRef.current({
...data,
stack
});
} else {
callbackRef.current(data);
}
});
}, []);
React.useEffect(() => {
let timer;
let unsubscribeAction;
let unsubscribeState;
const initialize = async () => {
if (!ref.current) {
// If the navigation object isn't ready yet, wait for it
await new Promise(resolve => {
timer = setInterval(() => {
if (ref.current) {
resolve();
clearTimeout(timer);
const state = ref.current.getRootState();
lastStateRef.current = state;
callbackRef.current({
type: 'init',
state
});
}
}, 100);
});
}
const navigation = ref.current;
unsubscribeAction = navigation.addListener('__unsafe_action__', e => {
const action = e.data.action;
if (e.data.noop) {
// Even if the state didn't change, it's useful to show the action
send({
type: 'action',
action,
state: lastStateRef.current,
stack: e.data.stack
});
} else {
lastActionRef.current = e.data;
}
});
unsubscribeState = navigation.addListener('state', e => {
// Don't show the action in dev tools if the state is what we sent to reset earlier
if (lastResetRef.current && deepEqual(lastResetRef.current, e.data.state)) {
lastStateRef.current = undefined;
return;
}
const state = navigation.getRootState();
const lastState = lastStateRef.current;
const lastChange = lastActionRef.current;
lastActionRef.current = undefined;
lastStateRef.current = state;
// If we don't have an action and the state didn't change, then it's probably extraneous
if (lastChange === undefined && deepEqual(state, lastState)) {
return;
}
send({
type: 'action',
action: lastChange ? lastChange.action : {
type: '@@UNKNOWN'
},
state,
stack: lastChange?.stack
});
});
};
initialize();
return () => {
unsubscribeAction?.();
unsubscribeState?.();
clearTimeout(timer);
};
}, [ref, send]);
const resetRoot = React.useCallback(state => {
if (ref.current) {
lastResetRef.current = state;
ref.current.resetRoot(state);
}
}, [ref]);
return {
resetRoot
};
}
//# sourceMappingURL=useDevToolsBase.js.map