Skip to content

Instantly share code, notes, and snippets.

@isocroft
Last active April 15, 2026 19:12
Show Gist options
  • Select an option

  • Save isocroft/b488bb3dc7432835a244d374c19e242a to your computer and use it in GitHub Desktop.

Select an option

Save isocroft/b488bb3dc7432835a244d374c19e242a to your computer and use it in GitHub Desktop.
A ReactJS hook that is used to track changes to the characters in the text of an input text box.
import React, { useRef, useEffect } from "react":
import { useIsFirstRender } from "react-busser";
import type { Ref } from "react";
const use = <D extends unknown>(promise: Promise<D>) => {
};
export const useInuptValueUpdate = ({ value, defaultedValue = "" }) => {
const isFirstRender: boolean = useIsFirstRender();
const inputRef: Ref<HTMLInputElement | null> = useRef<HTMLInputElement | null>(null);
const lastCaretStart = useRef<number>(0);
// `defaultedValue` should equal `props.defaultValue`
// `value` should equal `props.value`
useEffect(() => {
if (inputRef.current === null) {
return;
}
if (isFirstRender
&& (typeof defaultedValue !== "string"
|| defaultedValue.length > 0)){
inputRef.current.value = defaultedValue ?? "";
} else {
//ref.current.value = value ?? "";
inputRef.current.selectionStart = ref.current.selectionEnd = lastCaretStart.current ?? ref.current.value.length;
}
}, [defaultedValue, isFirstRender, value]);
const applyInputTransform = (
inputType: string,
inputTextEntry: string
) => {
previousInputValue: string = inputRef.current.value;
caretStartPosition: number = inputRef.current.selectionStart;
carentEndPosition: number = inputRef.current.selectionEnd;
let nextInputValue: string;
let nextCaretStartPosition: number;
if (caretStartPosition !== carentEndPosition) {
nextInputValue = previousInputValue.slice(0, caretStartPosition) + (inputTextEntry || "") + previousInputValue.slice(carentEndPosition);
nextCaretStartPosition = caretStartPosition;
if (inputType.startsWith("insert")) nextCaretStartPosition++;
} else if (inputType.startsWith("insert")) {
nextInputValue = previousInputValue.slice(0, caretStartPosition) + inputTextEntry + previousInputValue.slice(carentEndPosition);
nextCaretStartPosition = caretStartPosition + inputTextEntry.length;
} else if (inputType === "deleteContentBackward") {
nextInputValue = previousInputValue.slice(0, nextCaretStartPosition) + previousInputValue.slice(caretStartPosition);
nextCaretStartPosition = Math.max(0, caretStartPosition - 1);
} else if (inputType === "deleteContentForward") {
nextInputValue = previousInputValue.slice(0, caretStartPosition) + previousInputValue.slice(caretStartPosition + 1);
nextCaretStartPosition = caretStartPosition;
} else {
nextInputValue = previousInputValue;
nextCaretStartPosition = caretStartPosition;
}
lastCaretStart.current = nextCaretStartPosition;
inputRef.current.value = nextInputValue;
return nextInputValue;
};
return {
inputRef,
applyInputTransform
} as const;
};
@isocroft
Copy link
Copy Markdown
Author

isocroft commented Feb 28, 2025

How to use it:

import React from "react";
import { useEffectCallback } from "react-busser";

function Input ({ name, value, ...props }: Omit<React.ComponentPropsWithoutRef<"input">, "defaultValue" | "onInput">) {
    const [inputValue, setInputValue] = useState(() => "");
    const { applyInputTransform, inputRef } = useInuptValueUpdate({ value: inputValue, defaultValue: value });

    const { onChange, onBeforeInput, ...rest } = props;
    
    const stableRefOnChange = useEffectCallback<NonNullable<React.ComponentProps<"input">["onChange"]>>((event) => {
      if (typeof onChange === "function") {
        onChange(e);
      }

      if (e.defaultPrevented) {
        return;
      }

      // @TODO: Do something later...
}, { immutableRef: true });

    return (
        <input key={"sDf57zxre"} name={name} {...rest} defaultValue={inputValue} ref={inputRef} onBeforeInput={(e) => {
            if (typeof onBeforeInput === "function") {
                onBeforeInput(e);
            }

            setInputValue(applyInputTransform(e.inputType, e.data));

            if (typeof onChange === "function") {
                setTimeout((eventTarget) => {
                    stableRefOnChange(
                        Object.assign(
                            new CustomEvent("change"),
                            {
                                target: eventTarget,
                                timestamp: Date.now()
                            }
                        )
                    );
                }, 0, inputRef.current);
            }

            if (!e.defaultPrevented) {
                e.preventDefault();
            }
        }}>
    );
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment