Avoid unnecessary re-render on CHECK_NETWORK_STATUS

This commit is contained in:
Evan Hahn 2020-12-15 19:57:34 -06:00 committed by Josh Perez
parent b70b7a2cee
commit 55091edefa
3 changed files with 138 additions and 3 deletions

View File

@ -3,6 +3,7 @@
import { SocketStatus } from '../../types/SocketStatus';
import { trigger } from '../../shims/events';
import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation';
// State
@ -89,11 +90,12 @@ export function reducer(
if (action.type === CHECK_NETWORK_STATUS) {
const { isOnline, socketStatus } = action.payload;
return {
...state,
// This action is dispatched frequently. We avoid allocating a new object if nothing
// has changed to avoid an unnecessary re-render.
return assignWithNoUnnecessaryAllocation(state, {
isOnline,
socketStatus,
};
});
}
if (action.type === CLOSE_CONNECTING_GRACE_PERIOD) {

View File

@ -0,0 +1,101 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation';
describe('assignWithNoUnnecessaryAllocation', () => {
interface Person {
name?: string;
age?: number;
}
it('returns the same object if there are no modifications', () => {
const empty = {};
assert.strictEqual(assignWithNoUnnecessaryAllocation(empty, {}), empty);
const obj = {
foo: 'bar',
baz: 'qux',
und: undefined,
};
assert.strictEqual(assignWithNoUnnecessaryAllocation(obj, {}), obj);
assert.strictEqual(
assignWithNoUnnecessaryAllocation(obj, { foo: 'bar' }),
obj
);
assert.strictEqual(
assignWithNoUnnecessaryAllocation(obj, { baz: 'qux' }),
obj
);
assert.strictEqual(
assignWithNoUnnecessaryAllocation(obj, { und: undefined }),
obj
);
});
it('returns a new object if there are modifications', () => {
const empty: Person = {};
assert.deepEqual(
assignWithNoUnnecessaryAllocation(empty, { name: 'Bert' }),
{ name: 'Bert' }
);
assert.deepEqual(assignWithNoUnnecessaryAllocation(empty, { age: 8 }), {
age: 8,
});
assert.deepEqual(
assignWithNoUnnecessaryAllocation(empty, { name: undefined }),
{
name: undefined,
}
);
const obj: Person = { name: 'Ernie' };
assert.deepEqual(
assignWithNoUnnecessaryAllocation(obj, { name: 'Big Bird' }),
{
name: 'Big Bird',
}
);
assert.deepEqual(assignWithNoUnnecessaryAllocation(obj, { age: 9 }), {
name: 'Ernie',
age: 9,
});
assert.deepEqual(
assignWithNoUnnecessaryAllocation(obj, { age: undefined }),
{
name: 'Ernie',
age: undefined,
}
);
});
it('only performs a shallow comparison', () => {
const obj = { foo: { bar: 'baz' } };
assert.notStrictEqual(
assignWithNoUnnecessaryAllocation(obj, { foo: { bar: 'baz' } }),
obj
);
});
it("doesn't modify the original object when there are no modifications", () => {
const empty = {};
assignWithNoUnnecessaryAllocation(empty, {});
assert.deepEqual(empty, {});
const obj = { foo: 'bar' };
assignWithNoUnnecessaryAllocation(obj, { foo: 'bar' });
assert.deepEqual(obj, { foo: 'bar' });
});
it("doesn't modify the original object when there are modifications", () => {
const empty: Person = {};
assignWithNoUnnecessaryAllocation(empty, { name: 'Bert' });
assert.deepEqual(empty, {});
const obj = { foo: 'bar' };
assignWithNoUnnecessaryAllocation(obj, { foo: 'baz' });
assert.deepEqual(obj, { foo: 'bar' });
});
});

View File

@ -0,0 +1,32 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { has } from 'lodash';
/**
* This function is like `Object.assign` but won't create a new object if we don't need
* to. This is purely a performance optimization.
*
* This is useful in places where we don't want to create a new object unnecessarily,
* like in reducers where we might cause an unnecessary re-render.
*
* See the tests for the specifics of how this works.
*/
// We want this to work with any object, so we allow `object` here.
// eslint-disable-next-line @typescript-eslint/ban-types
export function assignWithNoUnnecessaryAllocation<T extends object>(
obj: Readonly<T>,
source: Readonly<Partial<T>>
): T {
// We want to bail early so we use `for .. in` instead of `Object.keys` or similar.
// eslint-disable-next-line no-restricted-syntax
for (const key in source) {
if (!has(source, key)) {
continue;
}
if (!(key in obj) || obj[key] !== source[key]) {
return { ...obj, ...source };
}
}
return obj;
}