Home > database >  TypeScript Custom useStateIfMounted React Hook - Not all constituents of type 'T | ((value: Set
TypeScript Custom useStateIfMounted React Hook - Not all constituents of type 'T | ((value: Set

Time:07-28

The full error:

Not all constituents of type 'T | ((value: SetStateAction<T>) => void)' are callable.
Type 'T' has no call signatures.

Summary:

I am trying to create a useStateIfMounted custom hook. It works perfectly fine, but I can't seem to type it properly for TypeScript. The hook is fine, but the caller gets the error when trying to call setState(). How should I type the useCallback() so I can get TypeScript's nice typing system. The args should be exactly the same as a regular setState from React.

My code:

/**
 * Same as useState, but the setState will not run if component is unmounted.
 * 
 * The setState is safe to use in dependency arrays because useCallback() has empty dependencies.
 */
export function useStateIfMounted<T>(initialState: T | (() => T)) {
    const componentUnmountedRef = useRef(false);
    const [state, _setState] = useState<T>(initialState);

    // TEMP: Line below will result in error if we don't type as 'any'
    const setState: any = useCallback((value: SetStateAction<T>) => {
        if (!componentUnmountedRef?.current) {
            _setState(value);
        }
    }, [])

    useEffect(() => {
        return () => {
            componentUnmountedRef.current = true;
        }
    }, [])

    return [state, setState]
}

Then, when a user tries to call setState(), TypeScript displays the error. Only way I can get around it is by making the callback (useCallback) 'any' for now, but that does not give me the nice typing of what's allowed to go in as args. See below for example of how the error happens:

---REDACTED---
const [someState, setSomeState] = useState<CustomType>(null)
---REDACTED---

setSomeState(SomeCustomObject) // <-- This line displays TypeScript errors

What I've tried:

  1. const setState = useCallback((value: SetStateAction<T>) => { ... }
  2. const setState: Dispatch<SetStateAction<T>> = useCallback((value: SetStateAction<T>) => { ... }
  3. const setState = useCallback((value: T | ((prevState: T) => T) => { ... }
  4. const setState = useCallback((value: any) => { ... } <-- This one is curious as I would have expected TypeScript to allow me to pass any argument.

Disclaimer: All of this is my own code, but the name I got from this repo. They did not write their code in TypeScript, so I am asking to to type this properly. My original name was useStateUnmountedAsync, but I liked their naming better. I have found a couple other custom hooks that are essentially the same idea, but none are written in TypeScript.

Edit: Add my tsconfig.json & versions tsconfig.json

{
  "compilerOptions": {
    "baseUrl": "src",
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "downlevelIteration": true,
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "allowUnreachableCode": true
  },
  "include": [
    "src"
  ],
  "exclude": [
    "**/node_modules/**",
    "build/**",
    "public/**"
  ]
}

versions:

"react": "^17.0.1",
"react-scripts": "4.0.3",
"typescript": "^4.2.3",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",

CodePudding user response:

Type the return as a tuple:

return [state, setState] as const;

You need to specify this otherwise both state and setState are of type boolean | ((value: React.SetStateAction<boolean>) => void) for instance, and only one part of that union is callable. The tuple ensures the state is at index 0 and the setter is at index 1.

This is essentially redundant but to get the same types as useState you might also consider explicitly typing your setState as:

const setState: React.Dispatch<SetStateAction<T>>
  • Related