mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-28 12:25:21 +08:00
fix: improve the warning message for non-serializable values
This commit is contained in:
@@ -17,7 +17,7 @@ import useKeyedChildListeners from './useKeyedChildListeners';
|
||||
import useOptionsGetters from './useOptionsGetters';
|
||||
import useEventEmitter from './useEventEmitter';
|
||||
import useSyncState from './useSyncState';
|
||||
import isSerializable from './isSerializable';
|
||||
import checkSerializable from './checkSerializable';
|
||||
import type {
|
||||
NavigationContainerEventMap,
|
||||
NavigationContainerRef,
|
||||
@@ -29,7 +29,7 @@ type State = NavigationState | PartialState<NavigationState> | undefined;
|
||||
const NOT_INITIALIZED_ERROR =
|
||||
"The 'navigation' object hasn't been initialized yet. This might happen if you don't have a navigator mounted, or if the navigator hasn't finished mounting. See https://reactnavigation.org/docs/navigating-without-navigation-prop#handling-initialization for more details.";
|
||||
|
||||
let hasWarnedForSerialization = false;
|
||||
const serializableWarnings: string[] = [];
|
||||
|
||||
try {
|
||||
/**
|
||||
@@ -267,16 +267,55 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
|
||||
React.useEffect(() => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (
|
||||
state !== undefined &&
|
||||
!isSerializable(state) &&
|
||||
!hasWarnedForSerialization
|
||||
) {
|
||||
hasWarnedForSerialization = true;
|
||||
if (state !== undefined) {
|
||||
const result = checkSerializable(state);
|
||||
|
||||
console.warn(
|
||||
"Non-serializable values were found in the navigation state, which can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use components with callbacks in your options, you can use 'navigation.setOptions' instead. See https://reactnavigation.org/docs/troubleshooting#i-get-the-warning-non-serializable-values-were-found-in-the-navigation-state for more details."
|
||||
);
|
||||
if (!result.serializable) {
|
||||
const { location, reason } = result;
|
||||
|
||||
let path = '';
|
||||
let pointer: Record<any, any> = state;
|
||||
let params = false;
|
||||
|
||||
for (let i = 0; i < location.length; i++) {
|
||||
const curr = location[i];
|
||||
const prev = location[i - 1];
|
||||
|
||||
pointer = pointer[curr];
|
||||
|
||||
if (!params && curr === 'state') {
|
||||
continue;
|
||||
} else if (!params && curr === 'routes') {
|
||||
if (path) {
|
||||
path += ' > ';
|
||||
}
|
||||
} else if (
|
||||
!params &&
|
||||
typeof curr === 'number' &&
|
||||
prev === 'routes'
|
||||
) {
|
||||
path += pointer?.name;
|
||||
} else if (!params) {
|
||||
path += ` > ${curr}`;
|
||||
params = true;
|
||||
} else {
|
||||
if (typeof curr === 'number' || /^[0-9]+$/.test(curr)) {
|
||||
path += `[${curr}]`;
|
||||
} else if (/^[a-z$_]+$/i.test(curr)) {
|
||||
path += `.${curr}`;
|
||||
} else {
|
||||
path += `[${JSON.stringify(curr)}]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const message = `Non-serializable values were found in the navigation state. Check:\n\n${path} (${reason})\n\nThis can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use components with callbacks in your options, you can use 'navigation.setOptions' instead. See https://reactnavigation.org/docs/troubleshooting#i-get-the-warning-non-serializable-values-were-found-in-the-navigation-state for more details.`;
|
||||
|
||||
if (!serializableWarnings.includes(message)) {
|
||||
serializableWarnings.push(message);
|
||||
console.warn(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import isSerializable from '../isSerializable';
|
||||
import checkSerializable from '../checkSerializable';
|
||||
|
||||
it('returns true for serializable object', () => {
|
||||
expect(
|
||||
isSerializable({
|
||||
checkSerializable({
|
||||
index: 0,
|
||||
key: '7',
|
||||
routeNames: ['foo', 'bar'],
|
||||
@@ -22,12 +22,12 @@ it('returns true for serializable object', () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
).toBe(true);
|
||||
).toEqual({ serializable: true });
|
||||
});
|
||||
|
||||
it('returns false for non-serializable object', () => {
|
||||
expect(
|
||||
isSerializable({
|
||||
checkSerializable({
|
||||
index: 0,
|
||||
key: '7',
|
||||
routeNames: ['foo', 'bar'],
|
||||
@@ -47,7 +47,38 @@ it('returns false for non-serializable object', () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
).toBe(false);
|
||||
).toEqual({
|
||||
serializable: false,
|
||||
location: ['routes', 0, 'state', 'routes', 0, 'params'],
|
||||
reason: 'Function',
|
||||
});
|
||||
|
||||
expect(
|
||||
checkSerializable({
|
||||
index: 0,
|
||||
key: '7',
|
||||
routeNames: ['foo', 'bar'],
|
||||
routes: [
|
||||
{
|
||||
key: 'foo',
|
||||
name: 'foo',
|
||||
state: {
|
||||
index: 0,
|
||||
key: '8',
|
||||
routeNames: ['qux', 'lex'],
|
||||
routes: [
|
||||
{ key: 'qux', name: 'qux', params: { foo: Symbol('test') } },
|
||||
{ key: 'lex', name: 'lex' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual({
|
||||
serializable: false,
|
||||
location: ['routes', 0, 'state', 'routes', 0, 'params', 'foo'],
|
||||
reason: 'Symbol(test)',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns false for circular references', () => {
|
||||
@@ -59,7 +90,11 @@ it('returns false for circular references', () => {
|
||||
x.b.b2 = x;
|
||||
x.c = x.b;
|
||||
|
||||
expect(isSerializable(x)).toBe(false);
|
||||
expect(checkSerializable(x)).toEqual({
|
||||
serializable: false,
|
||||
location: ['b', 'b2'],
|
||||
reason: 'Circular reference',
|
||||
});
|
||||
|
||||
const y: any = [
|
||||
{
|
||||
@@ -72,7 +107,11 @@ it('returns false for circular references', () => {
|
||||
y[0].children[0].parent = y[0];
|
||||
y[1].extend.home = y[0].children[0];
|
||||
|
||||
expect(isSerializable(y)).toBe(false);
|
||||
expect(checkSerializable(y)).toEqual({
|
||||
serializable: false,
|
||||
location: [0, 'children', 0, 'parent'],
|
||||
reason: 'Circular reference',
|
||||
});
|
||||
|
||||
const z: any = {
|
||||
name: 'sun',
|
||||
@@ -81,14 +120,18 @@ it('returns false for circular references', () => {
|
||||
|
||||
z.child[0].parent = z;
|
||||
|
||||
expect(isSerializable(z)).toBe(false);
|
||||
expect(checkSerializable(z)).toEqual({
|
||||
serializable: false,
|
||||
location: ['child', 0, 'parent'],
|
||||
reason: 'Circular reference',
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't fail if same object used multiple times", () => {
|
||||
const o = { foo: 'bar' };
|
||||
|
||||
expect(
|
||||
isSerializable({
|
||||
checkSerializable({
|
||||
baz: 'bax',
|
||||
first: o,
|
||||
second: o,
|
||||
@@ -96,5 +139,5 @@ it("doesn't fail if same object used multiple times", () => {
|
||||
b: o,
|
||||
},
|
||||
})
|
||||
).toBe(true);
|
||||
).toEqual({ serializable: true });
|
||||
});
|
||||
74
packages/core/src/checkSerializable.tsx
Normal file
74
packages/core/src/checkSerializable.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
const checkSerializableWithoutCircularReference = (
|
||||
o: { [key: string]: any },
|
||||
seen: Set<any>,
|
||||
location: (string | number)[]
|
||||
):
|
||||
| { serializable: true }
|
||||
| {
|
||||
serializable: false;
|
||||
location: (string | number)[];
|
||||
reason: string;
|
||||
} => {
|
||||
if (
|
||||
o === undefined ||
|
||||
o === null ||
|
||||
typeof o === 'boolean' ||
|
||||
typeof o === 'number' ||
|
||||
typeof o === 'string'
|
||||
) {
|
||||
return { serializable: true };
|
||||
}
|
||||
|
||||
if (
|
||||
Object.prototype.toString.call(o) !== '[object Object]' &&
|
||||
!Array.isArray(o)
|
||||
) {
|
||||
return {
|
||||
serializable: false,
|
||||
location,
|
||||
reason: typeof o === 'function' ? 'Function' : String(o),
|
||||
};
|
||||
}
|
||||
|
||||
if (seen.has(o)) {
|
||||
return {
|
||||
serializable: false,
|
||||
reason: 'Circular reference',
|
||||
location,
|
||||
};
|
||||
}
|
||||
|
||||
seen.add(o);
|
||||
|
||||
if (Array.isArray(o)) {
|
||||
for (let i = 0; i < o.length; i++) {
|
||||
const childResult = checkSerializableWithoutCircularReference(
|
||||
o[i],
|
||||
new Set<any>(seen),
|
||||
[...location, i]
|
||||
);
|
||||
|
||||
if (!childResult.serializable) {
|
||||
return childResult;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const key in o) {
|
||||
const childResult = checkSerializableWithoutCircularReference(
|
||||
o[key],
|
||||
new Set<any>(seen),
|
||||
[...location, key]
|
||||
);
|
||||
|
||||
if (!childResult.serializable) {
|
||||
return childResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { serializable: true };
|
||||
};
|
||||
|
||||
export default function checkSerializable(o: { [key: string]: any }) {
|
||||
return checkSerializableWithoutCircularReference(o, new Set<any>(), []);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
const isSerializableWithoutCircularReference = (
|
||||
o: { [key: string]: any },
|
||||
seen: Set<any>
|
||||
): boolean => {
|
||||
if (
|
||||
o === undefined ||
|
||||
o === null ||
|
||||
typeof o === 'boolean' ||
|
||||
typeof o === 'number' ||
|
||||
typeof o === 'string'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
Object.prototype.toString.call(o) !== '[object Object]' &&
|
||||
!Array.isArray(o)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (seen.has(o)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
seen.add(o);
|
||||
|
||||
if (Array.isArray(o)) {
|
||||
for (const it of o) {
|
||||
if (!isSerializableWithoutCircularReference(it, new Set<any>(seen))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const key in o) {
|
||||
if (!isSerializableWithoutCircularReference(o[key], new Set<any>(seen))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export default function isSerializable(o: { [key: string]: any }) {
|
||||
return isSerializableWithoutCircularReference(o, new Set<any>());
|
||||
}
|
||||
Reference in New Issue
Block a user