diff --git a/packages/relay-runtime/util/__tests__/recycleNodesInto-test.js b/packages/relay-runtime/util/__tests__/recycleNodesInto-test.js index 93ba8a142defe..9aa7e0ec64d85 100644 --- a/packages/relay-runtime/util/__tests__/recycleNodesInto-test.js +++ b/packages/relay-runtime/util/__tests__/recycleNodesInto-test.js @@ -348,7 +348,7 @@ describe('recycleNodesInto', () => { }); }); - describe('deepFreeze', () => { + describe('freeze', () => { it('does not mutate deeply frozen array in `nextData`', () => { const prevData = [[{x: 1}], 1]; const nextData = [[{x: 1}], 2]; @@ -378,6 +378,25 @@ describe('recycleNodesInto', () => { expect(recycled.a.b).toBe(nextItem); }); + it('does not mutate into frozen object in `nextData`', () => { + const nextItem = { + c: 1, + }; + const nextObject = { + b: nextItem, + }; + const nextData = { + a: nextObject, + }; + const prevData = {a: {b: {c: 1}}, d: 1}; + + Object.freeze(nextData); + const recycled = recycleNodesInto(prevData, nextData); + expect(recycled).toBe(nextData); + expect(recycled.a).toBe(nextObject); + expect(recycled.a.b).toBe(nextItem); + }); + it('reuse prevData and does not mutate deeply frozen array in `nextData`', () => { const nextItem = {x: 1}; const nextArray = [nextItem]; @@ -414,4 +433,28 @@ describe('recycleNodesInto', () => { expect(nextData.a.b).toBe(nextItem); }); }); + + it('reuse prevData and does not mutate frozen object in `nextData`', () => { + const nextItem = { + c: 1, + }; + const nextObject = { + b: nextItem, + }; + const nextData = { + a: nextObject, + }; + const prevData = { + a: { + b: { + c: 1, + }, + }, + }; + Object.freeze(nextData); + const recycled = recycleNodesInto(prevData, nextData); + expect(recycled).toBe(prevData); + expect(nextData.a).toBe(nextObject); + expect(nextData.a.b).toBe(nextItem); + }); }); diff --git a/packages/relay-runtime/util/recycleNodesInto.js b/packages/relay-runtime/util/recycleNodesInto.js index 2d1ebc3dd0591..cc611e530d397 100644 --- a/packages/relay-runtime/util/recycleNodesInto.js +++ b/packages/relay-runtime/util/recycleNodesInto.js @@ -13,8 +13,17 @@ /** * Recycles subtrees from `prevData` by replacing equal subtrees in `nextData`. + * Does not mutate a frozen subtree. */ function recycleNodesInto(prevData: T, nextData: T): T { + return recycleNodesIntoImpl(prevData, nextData, true); +} + +function recycleNodesIntoImpl( + prevData: T, + nextData: T, + canMutate: boolean, +): T { if ( prevData === nextData || typeof prevData !== 'object' || @@ -32,11 +41,16 @@ function recycleNodesInto(prevData: T, nextData: T): T { const prevArray: ?Array = Array.isArray(prevData) ? prevData : null; const nextArray: ?Array = Array.isArray(nextData) ? nextData : null; if (prevArray && nextArray) { + const canMutateNext = canMutate && !Object.isFrozen(nextArray); canRecycle = nextArray.reduce((wasEqual, nextItem, ii) => { const prevValue = prevArray[ii]; - const nextValue = recycleNodesInto(prevValue, nextItem); - if (nextValue !== nextArray[ii] && !Object.isFrozen(nextArray)) { + const nextValue = recycleNodesIntoImpl( + prevValue, + nextItem, + canMutateNext, + ); + if (nextValue !== nextArray[ii] && canMutateNext) { nextArray[ii] = nextValue; } return wasEqual && nextValue === prevArray[ii]; @@ -47,11 +61,16 @@ function recycleNodesInto(prevData: T, nextData: T): T { const nextObject = nextData; const prevKeys = Object.keys(prevObject); const nextKeys = Object.keys(nextObject); + const canMutateNext = canMutate && !Object.isFrozen(nextObject); canRecycle = nextKeys.reduce((wasEqual, key) => { const prevValue = prevObject[key]; - const nextValue = recycleNodesInto(prevValue, nextObject[key]); - if (nextValue !== nextObject[key] && !Object.isFrozen(nextObject)) { + const nextValue = recycleNodesIntoImpl( + prevValue, + nextObject[key], + canMutateNext, + ); + if (nextValue !== nextObject[key] && canMutateNext) { // $FlowFixMe[cannot-write] nextObject[key] = nextValue; }