/use-controlled-state

A utility for managing controlled local states

Primary LanguageTypeScriptMIT LicenseMIT

@susisu/use-controlled-state

CI

npm i @susisu/use-controlled-state
# or
yarn add @susisu/use-controlled-state

Motivation

Suppose we have the following React component (CodeSandbox).

const NumberInput: React.VFC<{
  value: number;
  onChange: (value: number) => void;
}> = ({ value, onChange }) => {
  return (
    <input
      type="text"
      value={value.toString()}
      onChange={(event) => onChange(parseFloat(event.target.value))}
    />
  );
};

const MyForm: React.VFC = () => {
  const [number, setNumber] = useState(0);
  return <NumberInput value={number} onChange={setNumber} />;
};

Actually this component almost does not work. For example,

  • "" is parsed as NaN and then printed as "NaN", so you cannot clear the input
  • both "0" and "0." represents the same number 0, so you cannot input like "0.123"

There are roughly two ways for solving this problem:

  • change the parent (MyForm) so that it manages the state as a string
  • change the child (NumberInput) so that it manages an additional string state and synchronize with value

Usually the latter is preferable, because it is completely local to the child input, and the parent does not need to know how the child behaves.

@susisu/use-controlled-state provides a utility for managing such local states. With this utility, the above component can be fixed like this (CodeSandbox):

import { createUseControlledState } from "@susisu/use-controlled-state";

// create a hook
const useControlledNumberString = createUseControlledState(
  (value: number) => value.toString(),
  (state: string) => parseFloat(state)
);

const NumberInput: React.VFC<{
  value: number;
  onChange: (value: number) => void;
}> = ({ value, onChange }) => {
  // use like useState
  const [string, setString] = useControlledNumberString(value, onChange);
  return (
    <input
      type="text"
      value={string}
      onChange={(event) => setString(event.target.value)}
    />
  );
};

const MyForm: React.VFC = () => {
  const [number, setNumber] = useState(0);
  return <NumberInput value={number} onChange={setNumber} />;
};

Usage

createUseControlledState(convert, invert, options?)

  • convert: The function that converts a parent value into a state.
  • invert The function that converts a state into a value for the parent.
  • options.equalValue? The equality function for values. Default is Object.is.
  • options.equalState? The equality function for states. Default is Object.is.

Examples

number and string

const useControlledNumberString = createUseControlledState(
  (value: number) => value.toString(),
  (state: string) => parseFloat(state)
);

Date and string

function stringify(date: Date): string {
  try {
    return date.toISOString();
  } catch {
    return "";
  }
}

function parse(text: string): Date {
  if (text === "") {
    return new Date(NaN);
  } else {
    return new Date(text);
  }
}

function equal(a: Date, b: Date): boolean {
  return Object.is(a.getTime(), b.getTime());
}

const useControlledDateString = createUseControlledState(
  stringify,
  parse,
  { equalValue: equal }
);

License

MIT License

Author

Susisu (GitHub, Twitter)