简体   繁体   中英

What is the best practice to close a DropDown on a Link click in React?

I have the following code: Link to Sandbox

import React, { useState } from "react";
import ReactDOM from "react-dom";
import { BrowserRouter, Route, Switch, Link } from "react-router-dom";
import "./styles.css";

function DropDown({ close }) {
  return (
    <ul>
      <li>
        <Link to="/" onClick={close}>
          Page 1
        </Link>
      </li>
      <li>
        <Link to="/page2" onClick={close}>
          Page 2
        </Link>
      </li>
      <li>
        <p>Dont hide dropdown when clicking me!</p>
      </li>
    </ul>
  );
}

function Header() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <div>
      <button onClick={() => setIsOpen(prev => !prev)}>Toggle DropDown</button>
      {isOpen && <DropDown close={() => setIsOpen(false)} />}
    </div>
  );
}

function Page1() {
  return <h1>Page 1</h1>;
}

function Page2() {
  return <h1>Page 2</h1>;
}

function App() {
  return (
    <div className="App">
      <BrowserRouter>
        <Header />
        <Switch>
          <Route exact path="/" component={Page1} />
          <Route path="/page2" component={Page2} />
        </Switch>
      </BrowserRouter>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

How can I implement this without always passing an close callback down to every link in my dropdown?

I am trying to implement this with a hook which listens on mousedown and checks if it is inside the dropdown and a click on type A but the problem is that then it closes the dropdown before the react redirect to the route.

The click outside I already have covered with a useClickAway hook but I would kind of need a useClickInsideOnLink hook as well.

You could create an internal component inside your DropDown component and use the close function by default, also passing the other props via rest parameter . Something like this:

import React, { useState } from "react";
import ReactDOM from "react-dom";
import { BrowserRouter, Route, Switch, Link } from "react-router-dom";
import "./styles.css";

function DropDown({ close }) {

  const MyLink = ({...rest}) => {
    return (
      <Link {...rest} onClick={close}>
        Page 1
      </Link>
    )
  }

  return (
    <ul>
      <li>
        <MyLink to="/">
          Page 1
        </MyLink>
      </li>
      <li>
        <MyLink to="/page2">
          Page 2
        </MyLink>
      </li>
      <li>
        <p>Dont hide dropdown when clicking me!</p>
      </li>
    </ul>
  );
}

function Header() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <div>
      <button onClick={() => setIsOpen(prev => !prev)}>Toggle DropDown</button>
      {isOpen && <DropDown close={() => setIsOpen(false)} />}
    </div>
  );
}

function Page1() {
  return <h1>Page 1</h1>;
}

function Page2() {
  return <h1>Page 2</h1>;
}

function App() {
  return (
    <div className="App">
      <BrowserRouter>
        <Header />
        <Switch>
          <Route exact path="/" component={Page1} />
          <Route path="/page2" component={Page2} />
        </Switch>
      </BrowserRouter>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

I think you have (at least) two options:

  • move Header into the pages (due to new pages are rendered, the header is re-initialized). Forked your CodeSandbox here
  • let the DropDown handle it's state on it's own, and include the toggle button to the dropdown. See CodeSandbox here

Another possible way (I don't recommend it as it adds unnecessary complexity to a simple problem) (added by the comments of the asker):

  • Actually, a component updates, when props are changed or the state changes. To quote react docs: If you want to “reset” some state when a prop changes , consider either making a component fully controlled or fully uncontrolled with a key instead. Fully uncontrolled would be option 2 then.

    To trigger the change of props, you could connect the Header withRouter . This way the Header component get's notified when the location (and other routing-props) change. Based on that, you could apply updates to the isOpen state via the lifecycleMethod componentDidUpdate and compare the previous location.pathname with the current one and set the isOpen back to false if the pathname changed. Actually I do not recommend that. But here you go: CodeSandbox

Probably it's too late but if it can't help someone: You can destrcuture pathname from useLocation() hook, and on every pathname change you can set isOpen to false in useEffect.

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import { BrowserRouter, Route, Switch, Link, useLocation } from "react-router-dom";
import "./styles.css";

function DropDown({ close }) {

  const MyLink = ({...rest}) => {
    return (
      <Link {...rest}>
        Page 1
      </Link>
    )
  }

  return (
    <ul>
      <li>
        <MyLink to="/">
          Page 1
        </MyLink>
      </li>
      <li>
        <MyLink to="/page2">
          Page 2
        </MyLink>
      </li>
      <li>
        <p>Dont hide dropdown when clicking me!</p>
      </li>
    </ul>
  );
}

function Header() {
  const [isOpen, setIsOpen] = useState(false);
  
  const { pathname } = useLocation();
  useEffect(() =>{
    setIsOpen(false);
  }, [pathname])

  return (
    <div>
      <button onClick={() => setIsOpen(prev => !prev)}>Toggle DropDown</button>
      {isOpen && <DropDown close={() => setIsOpen(false)} />}
    </div>
  );
}

function Page1() {
  return <h1>Page 1</h1>;
}

function Page2() {
  return <h1>Page 2</h1>;
}

function App() {
  return (
    <div className="App">
      <BrowserRouter>
        <Header />
        <Switch>
          <Route exact path="/" component={Page1} />
          <Route path="/page2" component={Page2} />
        </Switch>
      </BrowserRouter>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Try to subscribe on current location like this:

const location = useLocation();
useEffect(() => {
  closeHeader();
}, [location]);

This one is good I guess;3

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM