Skip to content

Instantly share code, notes, and snippets.

@MoritzKn
Last active June 2, 2023 14:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MoritzKn/afc1c8a87dfb1414b1d3ad8624c1fa35 to your computer and use it in GitHub Desktop.
Save MoritzKn/afc1c8a87dfb1414b1d3ad8624c1fa35 to your computer and use it in GitHub Desktop.
Debug React.memo and changes in props

useDebugChanges

Debug when something changes in react. Relevant for React.memo, useMemo or useEffect.

Example usage:

import { memo, useState, useCallback } from "react";

interface ExampleProps {
  name: string;
  id: number;
}

function Example(props: ExampleProps) {
  const {name, id} = props;
  const [value, setValue] = useState("");
  const onChange = useCallback((event) => { setValue(event.target.value) }, []);
  
  console.log("=== Render <Example> ===");
  useDebugChanges("props", props);
  useDebugChanges("state", { value, onChnage });
  
  return (
    <div>
      Name: {name}<br/>
      Id: {name}<br/>
      <inpuz value={value} onChange={onChange}/>
    </div>
  );
}

export default memo(Example);
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useState } from "react";
interface Change {
key: string;
hasChanged: boolean;
hasJsonChanges: boolean;
deepChanges: Change[];
appeared: boolean;
disappeared: boolean;
}
function getChanges(current: any, last: any): Change[] {
return Object.entries(current).map(([key, value]) => {
const hasChanged = value !== last[key];
const hasJsonChanges = JSON.stringify(value) !== JSON.stringify(last[key]);
let deepChanges: Change[] = [];
if (
value &&
last[key] &&
typeof value === "object" &&
typeof last[key] === "object"
) {
deepChanges = getChanges(value, last[key]).filter((c) => c.hasChanged);
}
let appeared = false;
let disappeared = false;
if (
(typeof value !== "boolean" || typeof last[key] !== "boolean") &&
!!value !== !!last[key]
) {
if (value) {
appeared = true;
}
if (last[key]) {
disappeared = true;
}
}
return {
key,
hasChanged,
hasJsonChanges,
deepChanges,
appeared,
disappeared,
};
});
}
function formatChangeShort(change: Change): string[] {
const deepChanges = change.deepChanges.flatMap(formatChangeShort);
if (deepChanges.length > 0) {
if (deepChanges.length < 10) {
return deepChanges.map((key) => `${change.key}.${key}`);
} else {
return [`${change.key}.*`];
}
}
return [change.key];
}
function formatChangeList(change: Change): string {
let infos = change.hasChanged ? "CHANGED" : "unchanged";
if (change.hasChanged && !change.hasJsonChanges) {
infos += " (same json)";
}
if (change.deepChanges.length) {
infos += ` (deep changes: ${change.deepChanges
.flatMap(formatChangeShort)
.join(", ")})`;
}
if (change.appeared) {
infos += ` (appeared)`;
}
if (change.disappeared) {
infos += ` (disappeared)`;
}
return `- '${change.key}' ${infos}`;
}
const lastState: Record<string, any> = {};
/**
* Log changes in an object, e.g. the props
*
* Can be used to debug re-rendering and React.memo
*/
export default function useDebugChanges(what: string, current: any) {
const [debugId] = useState(what + "_" + Math.random().toString());
const last = lastState[debugId] || {};
const changes = getChanges(current, last);
console.log(`${what}:\n` + changes.map(formatChangeList).join("\n"));
lastState[debugId] = current;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment