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:
Use
Child
as written and use a type assertion onsetError
(in one of a couple of ways, more below).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")} />
</>
);
};
...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} />
</>
);
};