Skip to content

Commit

Permalink
Kitchen sink to manage rendering budget
Browse files Browse the repository at this point in the history
  • Loading branch information
garronej committed May 29, 2024
1 parent 27beef6 commit cc8e11f
Show file tree
Hide file tree
Showing 7 changed files with 326 additions and 2 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = {
rules: {
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-explicit-any": "off"
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-namespace": "off"
}
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@tanstack/react-router": "^1.33.6",
"axios": "^1.7.1",
"dayjs": "^1.11.11",
"lodash": "^4.17.21",
"oidc-spa": "^4.6.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
4 changes: 3 additions & 1 deletion src/components/TodoApp/Todo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { fr } from "@codegouvfr/react-dsfr";
import { Button } from "@codegouvfr/react-dsfr/Button";
import Checkbox from "@mui/material/Checkbox";
import { useEvent } from "tools/useEvent";
import { deepEqual } from "tools/deepEqual";

export type Todo = {
id: string;
Expand Down Expand Up @@ -80,7 +81,8 @@ export const Todo = memo((props: TodoProps) => {
</div>
</div>
);
});
}, deepEqual);
// NOTE: We use deepEqual above to avoid having the component re-render if the ref of the todo has changed but it's actually the same todo.

const useStyles = tss
.withName({ Todo })
Expand Down
237 changes: 237 additions & 0 deletions src/tools/deepEqual/deepEqual.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/* eslint-disable @typescript-eslint/ban-types */
import { MapLike, SetLike, DateLike, ArrayLike } from "./types";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";

/**
* Function that perform a in depth comparison of two things of arbitrary type T
* to see if they represent the deepEqual date regardless of object references.
*
* Think of it as JSON.stringify(o1) === JSON.stringify(o2)
* but unlike a test performed with JSON.stringify the order in the property
* have been assigned to an object does not matter and circular references are supported.
*
*
* If takeIntoAccountArraysOrdering === false then
* representsSameData(["a", "b"], ["b", "a"]) will return true.
*
* If Date are compared via .getTime()
*
* The objects can includes Map and Set.
* */
export const deepEqual = (() => {
function sameRec<T>(
o1: T,
o2: T,
{ takeIntoAccountArraysOrdering }: { takeIntoAccountArraysOrdering: boolean } = {
takeIntoAccountArraysOrdering: true
},
o1Path: { key: string; obj: any }[],
o2Path: { key: string; obj: any }[],
o1RealRef: T = o1,
o2RealRef: T = o2
): boolean {
if (Object.is(o1, o2)) {
return true;
}

{
const i1 = o1Path.map(({ obj }) => obj).indexOf(o1RealRef);

if (i1 >= 0) {
const i2 = o2Path.map(({ obj }) => obj).indexOf(o2RealRef);

if (i1 !== i2) {
return false;
}

{
const [a, b] = ([o1Path, o2Path] as const).map(oPath =>
oPath.map(({ key }) => key).join("")
);

return a === b;
}
}
}

if (!(o1 instanceof Object && o2 instanceof Object)) {
return false;
}

if (typeof o1 === "function" || typeof o2 === "function") {
return false;
}

if (DateLike.match(o1)) {
if (!DateLike.match(o2)) {
return false;
}

return o1.getTime() === o2!.getTime();
}

if (MapLike.match<any, any>(o1)) {
if (!MapLike.match<any, any>(o2)) {
return false;
}

type Entry = { key: any; value: any };

const newO1 = new Set<Entry>();
const newO2 = new Set<Entry>();

for (const o of [o1, o2]) {
const newO = o === o1 ? newO1 : newO2;

const arr = Array.from(o.keys());

for (let i = 0; i < arr.length; i++) {
const key = arr[i];
const value = o.get(key)!;

newO.add({ key, value });
}
}

return sameRec(
newO1,
newO2,
{ takeIntoAccountArraysOrdering },
o1Path,
o2Path,
o1RealRef as any,
o2RealRef
);
}

let takeIntoAccountArraysOrderingOv: false | undefined = undefined;

if (SetLike.match(o1)) {
if (!SetLike.match(o2)) {
return false;
}

o1 = Array.from(o1.values()) as any;
o2 = Array.from(o2.values()) as any;

takeIntoAccountArraysOrderingOv = false;
}

//NOTE: The two following lines shouldn't be necessary...
assert(is<Object>(o1));
assert(is<Object>(o2));

if (ArrayLike.match<any>(o1)) {
if (!ArrayLike.match<any>(o2)) {
return false;
}

if (o1.length !== o2.length) {
return false;
}

if (
!(takeIntoAccountArraysOrderingOv !== undefined
? takeIntoAccountArraysOrderingOv
: takeIntoAccountArraysOrdering)
) {
const o2Set = new Set(Array.from(o2));

for (let i = 0; i < o1.length; i++) {
if (!(`${i}` in o1)) {
continue;
}

const val1 = o1[i];

if (o2Set.has(val1)) {
o2Set.delete(val1);
continue;
}

let isFound = false;

for (const val2 of o2Set.values()) {
if (
!sameRec(
val1,
val2,
{ takeIntoAccountArraysOrdering },
[...o1Path, { obj: o1RealRef, key: "*" }],
[...o2Path, { obj: o2RealRef, key: "*" }]
)
) {
continue;
}

isFound = true;
o2Set.delete(val2);
break;
}

if (!isFound) {
return false;
}
}

return true;
}

//continue
} else if (
!sameRec(
Object.keys(o1).filter(key => (o1 as any)[key] !== undefined),
Object.keys(o2).filter(key => (o2 as any)[key] !== undefined),
{ takeIntoAccountArraysOrdering: false },
[],
[]
)
) {
return false;
}

for (const key in o1) {
if (
!sameRec(
(o1 as any)[key],
(o2 as any)[key],
{ takeIntoAccountArraysOrdering },
[...o1Path, { obj: o1RealRef, key }],
[...o2Path, { obj: o2RealRef, key }]
)
) {
return false;
}
}

return true;
}

return function deepEqual<T>(
o1: T,
o2: T,
{ takeIntoAccountArraysOrdering }: { takeIntoAccountArraysOrdering: boolean } = {
takeIntoAccountArraysOrdering: true
}
): boolean {
return sameRec(o1, o2, { takeIntoAccountArraysOrdering }, [], []);
};
})();

/**
* Return the "deepEqual" function with "takeIntoAccountArraysOrdering" default value set as desired.
* */
export function deepEqualFactory({
takeIntoAccountArraysOrdering
}: {
takeIntoAccountArraysOrdering: boolean;
}) {
return {
deepEqual: <T>(
o1: T,
o2: T,
prop: { takeIntoAccountArraysOrdering: boolean } = { takeIntoAccountArraysOrdering }
) => deepEqual(o1, o2, prop)
};
}
27 changes: 27 additions & 0 deletions src/tools/deepEqual/getPrototypeChain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* eslint-disable @typescript-eslint/ban-types */

export function getPrototypeChain(obj: Object, callback?: (proto: Object) => boolean): Object[] {
const proto = Object.getPrototypeOf(obj);

if (!proto) {
return [];
}

const doContinue = callback?.(proto);

if (!doContinue) {
return [proto];
}

return [proto, ...getPrototypeChain(proto)];
}
getPrototypeChain.isMatched = (obj: Object, regExp: RegExp): boolean => {
let out = false;

getPrototypeChain(obj, ({ constructor }) => {
out = regExp.test(constructor.name);
return !out;
});

return out;
};
1 change: 1 addition & 0 deletions src/tools/deepEqual/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./deepEqual";
55 changes: 55 additions & 0 deletions src/tools/deepEqual/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* eslint-disable @typescript-eslint/ban-types */
import { typeGuard } from "tsafe/typeGuard";
import { getPrototypeChain } from "./getPrototypeChain";

type SetLike<T> = {
values: () => Iterable<T>;
};

export namespace SetLike {
export function match<T>(set: Object): set is SetLike<T> {
return (
typeGuard<SetLike<T>>(set, true) &&
typeof set.values === "function" &&
getPrototypeChain.isMatched(set, /Set/)
);
}
}

export type MapLike<T, U> = {
keys: () => Iterable<T>;
get(key: T): U | undefined;
};

export namespace MapLike {
export function match<T, U>(map: Object): map is MapLike<T, U> {
return (
typeGuard<MapLike<T, U>>(map, true) &&
typeof map.keys === "function" &&
typeof map.get === "function" &&
getPrototypeChain.isMatched(map, /Map/)
);
}
}

export namespace ArrayLike {
export function match<T>(arr: Object): arr is ArrayLike<T> {
return typeGuard<ArrayLike<T>>(arr, true) && typeof arr.length === "number" && arr.length !== 0
? `${arr.length - 1}` in arr
: getPrototypeChain.isMatched(arr, /Array/);
}
}

export type DateLike = {
getTime: () => number;
};

export namespace DateLike {
export function match(date: Object): date is DateLike {
return (
typeGuard<DateLike>(date, true) &&
typeof date.getTime === "function" &&
getPrototypeChain.isMatched(date, /Date/)
);
}
}

0 comments on commit cc8e11f

Please sign in to comment.