javascriptreactjstypescript

Property 'current' does not exist on type '[boolean, Dispatch<SetStateAction<boolean>>]'.ts(2339)


I am adding cursor animations to a React/Typescript project and in researching came across a CodePen (Animated Cursor React Component) that works perfectly well.

However, when converting to a Typescript file I come across the error Property 'current' does not exist on type '[boolean, Dispatch<SetStateAction<boolean>>]'.ts(2339) on cursorVisible.current in

const onMouseEnter = () => {
  cursorVisible.current = true;
  toggleCursorVisibility();
};

Property cursorVisible is from const cursorVisible = useState(false);

What does Typescript need me to do so current works in Typescript? Reading the React Hooks docs, I could not see reference to current on useState and interestingly this works as a js file, only not in ts.

In the past I have used current with ref but never across useState hook.

Full file is

import React, { useEffect, useRef, useState } from 'react';

import MobileDetect from './MobileDetect';

interface CursorProps {
    color: string;
    outlineAlpha: number;
    dotSize: number;
    outlineSize: number;
    outlineScale: number;
    dotScale: number;
}

function AnimatedCursor({
    color = '220, 90, 90',
    outlineAlpha = 0.3,
    dotSize = 8,
    outlineSize = 8,
    outlineScale = 5,
    dotScale = 0.7,
}: CursorProps) {
    // Bail if Mobile
    if (typeof navigator !== 'undefined' && MobileDetect!.anyMobile())
        return <></>;

    const cursorOutline = useRef();
    const cursorDot = useRef();
    const requestRef = useRef();
    const previousTimeRef = useRef();
    const [width, setWidth] = useState(window.innerWidth);
    const [height, setHeight] = useState(window.innerHeight);
    const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
    const cursorVisible = useState(false);
    const cursorEnlarged = useState(false);

    const styles = {
        cursors: {
            zIndex: 999,
            pointerEvents: 'none',
            position: 'absolute',
            top: '50%',
            left: '50%',
            borderRadius: '50%',
            opacity: 0,
            transform: 'translate(-50%, -50%)',
            transition: 'opacity 0.15s ease-in-out, transform 0.15s ease-in-out',
        },
        cursorDot: {
            width: dotSize,
            height: dotSize,
            backgroundColor: `rgba(${color}, 1)`,
        },
        cursorOutline: {
            width: outlineSize,
            height: outlineSize,
            backgroundColor: `rgba(${color}, ${outlineAlpha})`,
        },
    };

    // Hide default cursor
    document.body.style.cursor = 'none';

    // Mouse Events
    const onMouseMove = (event: { pageX: number; pageY: number }) => {
        const { pageX: x, pageY: y } = event;
        setMousePosition({ x, y });
        positionDot(event);
    };
    const onMouseEnter = () => {
        cursorVisible.current = true;
        toggleCursorVisibility();
    };
    const onMouseLeave = () => {
        cursorVisible.current = false;
        toggleCursorVisibility();
    };
    const onMouseDown = () => {
        cursorEnlarged.current = true;
        toggleCursorSize();
    };
    const onMouseUp = () => {
        cursorEnlarged.current = false;
        toggleCursorSize();
    };

    // Set window hxw
    const onResize = () => {
        setWidth(window.innerWidth);
        setHeight(window.innerHeight);
    };

    /**
     * Hooks
     */
    useEffect(() => {
        // Bail if mobile
        document.addEventListener('mousemove', onMouseMove);
        document.addEventListener('mouseenter', onMouseEnter);
        document.addEventListener('mouseleave', onMouseLeave);
        document.addEventListener('mousedown', onMouseDown);
        document.addEventListener('mouseup', onMouseUp);
        window.addEventListener('resize', onResize);
        requestRef.current = requestAnimationFrame(animateDotOutline);
        handleLinkEvents();

        return () => {
            document.removeEventListener('mousemove', onMouseMove);
            document.removeEventListener('mouseenter', onMouseEnter);
            document.removeEventListener('mouseleave', onMouseLeave);
            document.removeEventListener('mousedown', onMouseDown);
            document.removeEventListener('mouseup', onMouseUp);
            window.removeEventListener('resize', onResize);
            cancelAnimationFrame(requestRef.current);
        };
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    let { x, y } = mousePosition;
    const winDimensions = { width, height };
    let endX = winDimensions.width / 2;
    let endY = winDimensions.height / 2;

    /**
     * Toggle Cursor Visiblity
     */
    function toggleCursorVisibility() {
        if (cursorVisible.current) {
            cursorDot.current.style.opacity = 1;
            cursorOutline.current.style.opacity = 1;
        } else {
            cursorDot.current.style.opacity = 0;
            cursorOutline.current.style.opacity = 0;
        }
    }

    /**
     * Position Dot (cursor)
     * @param {event}
     */
    function positionDot(e: { pageX: number; pageY: number }) {
        cursorVisible.current = true;
        toggleCursorVisibility();
        // Position the dot
        endX = e.pageX;
        endY = e.pageY;
        cursorDot.current.style.top = `${endY}px`;
        cursorDot.current.style.left = `${endX}px`;
    }

    /**
     * Toggle Cursors Size/Scale
     */
    function toggleCursorSize() {
        if (cursorEnlarged.current) {
            cursorDot.current.style.transform = `translate(-50%, -50%) scale(${dotScale})`;
            cursorOutline.current.style.transform = `translate(-50%, -50%) scale(${outlineScale})`;
        } else {
            cursorDot.current.style.transform = 'translate(-50%, -50%) scale(1)';
            cursorOutline.current.style.transform = 'translate(-50%, -50%) scale(1)';
        }
    }

    /**
     * Handle Links Events
     * Applies mouseover/out hooks on all links
     * to trigger cursor animation
     */
    function handleLinkEvents() {
        document.querySelectorAll('a').forEach((el) => {
            el.addEventListener('mouseover', () => {
                cursorEnlarged.current = true;
                toggleCursorSize();
            });
            el.addEventListener('mouseout', () => {
                cursorEnlarged.current = false;
                toggleCursorSize();
            });
        });
    }

    /**
     * Animate Dot Outline
     * Aniamtes cursor outline with trailing effect.
     * @param {number} time
     */
    const animateDotOutline = (time: undefined) => {
        if (previousTimeRef.current !== undefined) {
            x += (endX - x) / 8;
            y += (endY - y) / 8;
            cursorOutline.current.style.top = `${y}px`;
            cursorOutline.current.style.left = `${x}px`;
        }
        previousTimeRef.current = time;
        requestRef.current = requestAnimationFrame(animateDotOutline);
    };

    return (
        <>
            <div
                ref={cursorOutline}
                id="cursor-outline"
                style={{ ...styles.cursors, ...styles.cursorOutline }}
            />
            <div
                ref={cursorDot}
                id="cursor-inner"
                style={{ ...styles.cursors, ...styles.cursorDot }}
            />
        </>
    );
}

export default AnimatedCursor;

Solution

  • You'll have to ask the author of the code you're using. useState returns an array with the current value and a setter function to change the value. Normally you would use it like this:

    let [cursorVisible, setCursorVisible] = useState(false);
    
    // instead of cursorVisible.current = true
    setCursorVisible(true);
    

    There's no 'current' property on the array, unless maybe it is set by other code which would be bad form I think.