Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

TypeScript Custom useStateIfMounted React Hook – Not all constituents of type 'T | ((value: SetStateAction<T>) => void)' are callable

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.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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",

>Solution :

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>>
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading