Thursday, June 26, 2025

React: Memoized unchanged properties

This demonstrates that it is expensive to re-render/re-process a variable when its value is not changing
import { memo, useMemo, useState } from 'react';
import './App.css';

type Person = {
  firstName: string;
  lastName: string;
};

type WithInitial = Person & {
  initials: string;
};

const initialPeople: Person[] = [
  { firstName: 'John', lastName: 'Lennon' },
  { firstName: 'Paul', lastName: 'McCartney' },
  { firstName: 'George', lastName: 'Harrison' },
  { firstName: 'Ringo', lastName: 'Starr' },
];

function App() {
  const [theme, setTheme] = useState('light');
  const [people, setPeople] = useState(initialPeople);

  const [counter, setCounter] = useState(0);

  return (
    <div>
      <button onClick={() => setCounter((prev) => prev + 1)}>
        Increase {counter}
      </button>
      <button onClick={toggleTheme}>Toggle {theme}</button>
      <button onClick={addRandomPerson}>Add Random Person</button>
      <ListComponentNoMemo {...{ theme, people }} />
      <ListComponentWithMemo {...{ theme, people }} />
      <MemoListComponentWithMemo {...{ theme, people }} />
    </div>
  );

  function toggleTheme() {
    setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
  }

  function addRandomPerson() {
    setPeople((prev) => [
      ...prev,
      {
        firstName: Math.ceil(Math.random() * 100).toString(),
        lastName: Math.ceil(Math.random() * 100).toString(),
      },
    ]);
  }
}

function ListComponentNoMemo({
  theme,
  people,
}: {
  theme: string;
  people: Person[];
}) {
  console.log('ListComponentNoMemo');
  const withInitials = processInitials('noMemo', people);

  return (
    <>
      <p>Theme is {theme}</p>
      <ul>
        {withInitials.map((e, index) => (
          <li key={index}>
            {e.firstName} {e.lastName} {e.initials}
          </li>
        ))}
      </ul>
    </>
  );
}

function ListComponentWithMemo({
  theme,
  people,
}: {
  theme: string;
  people: Person[];
}) {
  console.log('ListComponenWithMemo');
  const withInitials = useMemo(
    () => processInitials('withMemo', people),
    [people]
  );

  return (
    <>
      <p>Theme is {theme}</p>
      <ul>
        {withInitials.map((e, index) => (
          <li key={index}>
            {e.firstName} {e.lastName} {e.initials}
          </li>
        ))}
      </ul>
    </>
  );
}

const MemoListComponentWithMemo = memo(function ({
  theme,
  people,
}: {
  theme: string;
  people: Person[];
}) {
  console.log('MemoListComponenWithMemo');
  const withInitials = useMemo(
    () => processInitials('memoWithMemo', people),
    [people]
  );

  return (
    <>
      <p>Theme is {theme}</p>
      <ul>
        {withInitials.map((e, index) => (
          <li key={index}>
            {e.firstName} {e.lastName} {e.initials}
          </li>
        ))}
      </ul>
    </>
  );
});

function processInitials(calledFrom: string, people: Person[]): WithInitial[] {
  console.log({ calledFrom });
  return people.map((e) => ({
    firstName: e.firstName,
    lastName: e.lastName,
    initials: toInitials(e),
  }));

  function toInitials(p: Person) {
    console.log('emphasized expensiveness');
    return p.firstName[0] + p.lastName[0];
  }
}

export default App;
Live: https://stackblitz.com/edit/vitejs-vite-drznsqwc?file=src%2FApp.tsx

Strongly-typed MUI Form Helper

form.ts
import React from 'react';

type UnifiedChangeEvent =
  | React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  | React.ChangeEvent<{ name?: string; value: unknown }>
  | (Event & { target: { name: string; value: unknown } });

export type ErrorObject<T> = Partial<Record<keyof T, string | boolean>>;

function handleInputChange<T>(
  values: T,
  setValues: (o: T) => void,
  fieldName: keyof T
) {
  return {
    onChange(event: UnifiedChangeEvent) {
      const { name, value } = event.target;
      setValues({ ...values, [name!]: value });
    },
    name: fieldName,
    value: values[fieldName],
  };
}

export function setupInputChange<T>(values: T, setValues: (o: T) => void) {
  return (fieldName: keyof T) =>
    handleInputChange(values, setValues, fieldName);
}

function handleErrorChange<T>(errors: T, fieldName: keyof T) {
  return errors[fieldName] && { error: true, helperText: errors[fieldName] };
}

export function setupErrorChange<T>(errors: T) {
  return (fieldName: keyof T) => handleErrorChange(errors, fieldName);
}

export function isValid<T>(errors: ErrorObject<T>): boolean {
  for (const v of Object.values(errors)) {
    if (v) {
      return false;
    }
  }
  return true;
}

export function hasErrors<T>(errors: ErrorObject<T>) {
  return !isValid(errors);
}

export function setupInputProps<T, U>(
  values: T,
  setValues: (o: T) => void,
  errors: ErrorObject<U>,
  setErrors: (o: ErrorObject<U>) => void
) {
  return {
    handleInputProps: (fieldName: keyof T & keyof U) => ({
      ...handleInputChange(values, setValues, fieldName),
      ...handleErrorChange(errors, fieldName),
    }),
    checkValidators: (errors: ErrorObject<U>) => {
      setErrors(errors);
      return isValid(errors);
    },
  };
}
Example use:
import { FormEvent, useState } from 'react';

import {
  Button,
  TextField,
} from '@mui/material';

import './App.css';

import { setupInputProps, type ErrorObject } from './utils/form';

class DonationCandidateDto {
  donationCandidateId = 0;
  fullname = '';
  mobile = '';
  email = '';
  age = 0;
  bloodGroup = '';
  address = '';
  active = false;
}

const initialFieldValues: DonationCandidateDto = {
  donationCandidateId: 0,
  fullname: '',
  mobile: '',
  email: '',
  age: 0,
  bloodGroup: '',
  address: '',
  active: false,
};

type FormType = typeof initialFieldValues;

// out-of-band validations
type OtherStates = {
  areZombiesInLab: boolean;
  day?: number;
};

function App() {
  const [values, setValues] = useState(structuredClone(initialFieldValues));
  const [errors, setErrors] = useState<ErrorObject<FormType & OtherStates>>({});

  const { handleInputProps, checkValidators } = setupInputProps(
    values,
    setValues,
    errors,
    setErrors
  );

  return (
    <div>
      <h1>Hello world</h1>
      <form autoComplete="off" noValidate onSubmit={handleSubmit}>
        <div>
          <TextField
            label="Full name"
            required={true}
            {...handleInputProps('fullname')}
          />
        </div>
        <br />
        <div>
          <TextField label="Age" {...handleInputProps('age')} />
        </div>

        <div>
          {errors.areZombiesInLab && (
            <div style={{ color: 'red' }}>{errors.areZombiesInLab}</div>
          )}
        </div>

        <br />
        <div>
          <Button color="primary" type="submit" variant="contained">
            Submit
          </Button>
          &nbsp;&nbsp;&nbsp;
          <Button type="submit" color="inherit" variant="contained">
            Reset
          </Button>
        </div>
      </form>
      <hr />
      {/* {JSON.stringify(values)} */}
    </div>
  );

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();

    const isValid = checkValidators({
      fullname: !values.fullname && 'Must have full name',
      age: !(values.age >= 18) && 'Must be an adult',
      // areZombiesInLab: true && 'Zombies in lab',
    });

    if (!isValid) {
      return;
    }

    // POST/PUT here
  }
}

export default App;
If the name is not existing from the DTO, it will not compile:













Monday, June 23, 2025

macOS vs Windows keys

 

Function 

macOS 

Windows 

Beginning of Page 

Command+Up (Home if non-typing like browser page) 

Ctrl+Home 

Ending of Page 

Command+Down (End if non-typing like browser page) 

Ctrl+End 

Beginning of line on screen 

Command+Left (or fn+Left) 

Home 

Ending of line on screen 

Command+Right (or fn+Right) 

End 

Beginning of line 

Control+A 

Not supported 

Ending of line 

Control+E 

Not supported 

Page Up 

Fn+Up (Page Up if available) (Alt+Up if non-typing like browser page) 

Page Up 

Page Down 

Fn+Down (Page Down if available) (Alt+Down if non-typing like browser page) 

Page Down 

  

  

  

macOS when on typing (e.g., IDE, word processor) 

  

Keys 

Function 

Home 

Beginning of line on screen 

End 

Ending of line on screen 

Alt+Up 

VSCode: Move one line up. Word: Go to previous paragraph 

Alt+Down 

VSCode: Move one line down. Word: Go to next paragraph 

JetBrains: 
Command+Home 
 
if no Home key: 
Command+fn+Left 

Beginning of Page 

JetBrains: 
Command+End 
 
if no End key: 
Command+fn+Right 

Ending of Page