Pomax / react-onclickoutside

An onClickOutside wrapper for React components
MIT License
1.83k stars 187 forks source link

Hooks with Typescript: ts(2339) Property 'handleClickOutside' does not exist. #310

Open aliankarali opened 5 years ago

aliankarali commented 5 years ago

Update: Another thing I noticed with this pattern is that when you have multiple instances of the same Component, only the last one triggers handleClickOutside.

Here is my code, grossly simplified (sorry if it is more of a TS question rather than this library, but in any way it would be nice to see an example).

error: Property 'handleClickOutside' does not exist on type '({ options, }: Props) => Element'. ts(2339)

The code works fine but in the editor Dropdown.handleClickOutside lines highlights this error. Other than using // @ts-ignore, how can I solve this issue? how can I specify the property?

import * as React from 'react';
import onClickOutside from 'react-onclickoutside';

interface Props {
    options: string[];
}

export const Dropdown = ({
    options,
}: Props) => {
    const [isOpen, setIsOpen] = React.useState(false);

    Dropdown.handleClickOutside = () => {
        setIsOpen(false);
    };

    return (
        <div>
            <ul isOpen={isOpen}>
                {options.map((option, i) => {
                    return (
                        <li
                            key={i}
                        >
                            {option}
                        </li>
                    );
                })}
            </ul>
        </div>
    );
};

export default onClickOutside(
    Dropdown,
    { handleClickOutside: () => Dropdown.handleClickOutside }
);
pasih commented 5 years ago

try this in the same file:

namespace Dropdown {
  export let handleClickOutside: () => void;
}
aliankarali commented 5 years ago

thanks for the tip 👍

nerdmax commented 5 years ago

I tried this solution but typescript complains this:

Cannot redeclare block-scoped variable 'Dropdown'.

n10v commented 5 years ago

I also tried it and I get Duplicate identifier 'Dropdown'. react-onclickoutside v6.8.0, typescript v.3.3.4000

hlolli commented 5 years ago

Third on the train, the create-react-app default linter doesn't like it

Failed to compile.

./src/components/MenuBar/MenuBar.tsx
  Line 45:  ES2015 module syntax is preferred over custom TypeScript modules and namespaces  @typescript-eslint/no-namespace

Search for the keywords to learn more about each error.

not an error, but if someone could suggest an alternative solution, would be great.

n10v commented 5 years ago

As a solution you can get rid of this library and use this small hook from https://usehooks.com/useOnClickOutside/. It works for me really well:

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

// Usage
function App() {
  // Create a ref that we add to the element for which we want to detect outside clicks
  const ref = useRef();
  // State for our modal
  const [isModalOpen, setModalOpen] = useState(false);
  // Call hook passing in the ref and a function to call on outside click
  useOnClickOutside(ref, () => setModalOpen(false));

  return (
    <div>
      {isModalOpen ? (
        <div ref={ref}>
          👋 Hey, I'm a modal. Click anywhere outside of me to close.
        </div>
      ) : (
        <button onClick={() => setModalOpen(true)}>Open Modal</button>
      )}
    </div>
  );
}

// Hook
function useOnClickOutside(ref, handler) {
  useEffect(
    () => {
      const listener = event => {
        // Do nothing if clicking ref's element or descendent elements
        if (!ref.current || ref.current.contains(event.target)) {
          return;
        }

        handler(event);
      };

      document.addEventListener('mousedown', listener);
      document.addEventListener('touchstart', listener);

      return () => {
        document.removeEventListener('mousedown', listener);
        document.removeEventListener('touchstart', listener);
      };
    },
    // Add ref and handler to effect dependencies
    // It's worth noting that because passed in handler is a new ...
    // ... function on every render that will cause this effect ...
    // ... callback/cleanup to run every render. It's not a big deal ...
    // ... but to optimize you can wrap handler in useCallback before ...
    // ... passing it into this hook.
    [ref, handler]
  );
}
aliankarali commented 5 years ago

Thanks for the hooks @bogem ;) For those looking for an external solution I would recommend https://github.com/airbnb/react-outside-click-handler

softatac commented 3 years ago

Defining the handler on the function after its definition worked for me. I think it's ok, as its value will be set on rendering the component anyways.

import {useState} from 'react'
import onClickOutside from 'react-onclickoutside'

const UserInfo = () => {
  const [menuVisible, setMenuVisible] = useState(false)
  UserInfo.clickOutside = () => setMenuVisible(false)

  return (
    <div className="cursor-pointer"
      onClick={() => setMenuVisible(!menuVisible)}      
    >
      <p>Click me</p>
      <div className={!menuVisible && 'hidden'}>
        <p>I am zee menu</p>
        <p>I am zee menu</p>
      </div>
    </div>
  )
}
UserInfo.clickOutside = null /// !! this

export default onClickOutside(UserInfo, {
  handleClickOutside: () => UserInfo.clickOutside,
})
Pomax commented 3 years ago

Just a note: if you've already written the code to make hooks work, PRs that add a hook solution are most certainly welcome, and I'll be more than happy to push out a new version.