Home > Enterprise >  What Typescript type to set for setState passed to the component
What Typescript type to set for setState passed to the component

Time:12-09

I have some Parent components like:

const ParentOne = () => {
  const [error, setError] = useState<{ one: boolean }>({ one: false });
...omit
  return (
    <>
      <Child setErr={setError} name={"one"} />
    </>
  );
};

const ParentTwo = () => {
  const [error, setError] = useState<{ one: boolean; two: boolean }>({
    one: false,
    two: false,
  });
...omit
  return (
    <>
      <Child setErr={setError} name={"one"} />
      <Child setErr={setError} name={"two"} />
    </>
  );
};

and so on with different numbers of Child components and different

const[...] =  useState<{ one: boolean; two: boolean; ...n: boolean }>({
        one: false,
        two: false,
        ...,
        n: false    
      });

This is my Child component:

const Child: FC<IChild> = ({ setErr, name }) => {
  const handleClick = () => {
    setErr((prev) => ({ ...prev, [name]: true }));
  };
  return <button onClick={handleClick}/>;
};

I dont know which type to set for setErr, because useState has different generics for diffrent Parent components. I tried like this:

interface IChild {
  setErr: React.Dispatch<React.SetStateAction<{ [key: string]: boolean }>>;
  name: string;
}

But in each Parent component, setErr requires an exact match of the type in IChild with the generic in useState

CodePudding user response:

The truthful type of your setErr prop with Child as written is:

type ErrorSetter = React.Dispatch<React.SetStateAction<{[key: string]: boolean}>>;

I see two options:

  1. Use Child as written and use a type assertion on setError (in one of a couple of ways, more below).

  2. Use a custom hook and simplify Child

My preference would be for #2, but let's look at each of them:

Using Child as written with type assertions

So if we use that error setter, we have:

type ErrorSetter = React.Dispatch<React.SetStateAction<{[key: string]: boolean}>>;
interface IChild {
  setErr: ErrorSetter;
  name: string;
}

How to use that without constantly writing as?

Largely, we don't, but you might do it just once per parent component:

const ParentTwo = () => {
  const [error, setError] = useState({
    one: false,
    two: false,
  });
  const setErr = setError as ErrorSetter;
//
  return (
    <>
      <Child setErr={setErr} name="one" />
      <Child setErr={setErr} name="two" />
    </>
  );
};

That's reasonably convenient, and leaves Child unchanged, although as is often (always?) the case with type assertions, it's not entirely typesafe.

Use a custom hook

But I would lean toward using a custom hook and simplifying Child. The hook is:

const useErrorState = <T,>(initial: T): [T, (name: keyof T) => void] => {
  const [error, realSetError] = useState(initial);
  const setError = useCallback(
    (name: keyof T) => {
      realSetError(prev => ({...prev, [name]: true}));
    },
    []
  );
  return [error, setError];
};

Then you'd use it like this if you don't need setErr to be stable:

const ParentTwo = () => {
  const [error, setError] = useErrorState({
    one: false,
    two: false,
  });
//
  return (
    <>
      <Child setErr={() => setError("one")} />
      <Child setErr={() => setError("two")} />
    </>
  );
};

Playground link

...or like this if you do because you want to avoid Child re-rendering (this would assume Child uses React.memo or [in a class component] PureComponent or shouldComponentUpdate):

const ParentTwo = () => {
  const [error, setError] = useErrorState({
    one: false,
    two: false,
  });
  const setErrorOne = useCallback(() => setError("one"), []);
  const setErrorTwo = useCallback(() => setError("two"), []);
//
  return (
    <>
      <Child setErr={setErrorOne} />
      <Child setErr={setErrorTwo} />
    </>
  );
};

Playground link

  • Related