Someone with React-Router experience can probably answer this. I have an application with a landing page, a dropdown menu on that landing page and a "sign up" button that <Link>'s to a simple page with a login form.
The dropdown menu is using the react ref system to put a ref on the dropdown menu. There is a click event listener that references this ref so that the state of the dropdown is not toggled when anything inside that ref is clicked. And there is a cleanup function for that click event listener so that when the menu is closed, the event listener is no longer present.
What is happening is that if the dropdown menu is open, and you click on the <Link> to the login page, it is creating a null ref on the dropdown menu from the click event listener, even though I'm trying to completely unmount these components and mount another one with react-router.
It's not clear to me whether I should be converting this LandingMenu component to class component so that I can use componentDidUnmount to trigger the cleanup function, or whether I am using react router incorrectly, or if there is another "correct" method to solve this problem that i'm not aware of. Here is the code.
App.js
import React, { Component } from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import "../styles/index.css";
import Landing from "./landing/Landing";
import Login from "./login/Login";
class App extends Component {
render() {
return (
<div>
<BrowserRouter>
<Switch>
<Route path="/" exact component={Landing} />
<Route path="/login" exact component={Login} />
</Switch>
</BrowserRouter>
</div>
);
}
}
export default App;
Login Button
import React from "react";
import { Link } from "react-router-dom";
import Button from "../common/Button";
import ChevronRight from "../icons/ChevronRight";
const LandingAuthentication = () => {
return (
<div className="login-buttons-wrapper">
<Link to="/login">
<Button Text="Sign in" Style="button-primary" Icon={<ChevronRight />} />
</Link>
</div>
);
};
export default LandingAuthentication;
Landing Menu
import React, { useState, useEffect, useRef } from "react";
import "../../styles/landing-menu.css";
const LandingMenu = ({ Title, Icon, children }) => {
// visibility toggle, default closed
const [open, setOpen] = useState(false);
// set menu ref to prevent double click event
const menuRef = useRef();
// close menu if clicked anywhere outside of menu
// on initial render, add click event listener
useEffect(() => {
const onBodyClick = (event) => {
// check if element clicked is inside of menu
// if so no action is required from this event listener so exit
if (menuRef.current.contains(event.target)) {
return;
}
// else close the menu
setOpen(false);
};
// add event listener and cleanup only if menu is open
if (open === true) {
document.body.addEventListener("click", onBodyClick);
// CLEANUP
// remove event listener
return () => {
document.body.removeEventListener("click", onBodyClick);
};
}
}, [open]);
// on click toggle state
return (
<nav className="landing-menu" ref={menuRef}>
<div
className="landing-menu-title-wrapper"
onClick={() => {
setOpen(!open);
}}
>
<h3 className="landing-menu-title">{Title}</h3>
<div className="landing-menu-title-icon">{Icon}</div>
</div>
<ul className={`dropdown ${open ? "visible" : "hidden"}`}>{children}</ul>
</nav>
);
};
export default LandingMenu;
The error ended up being caused by an update to the way React handles references in React 17. The solution is to update the if statement from:
if (menuRef.current.contains(event.target)) {
to
if (menuRef.current && menuRef.current.contains(event.target)) {
Related
I have a simple page transition that gets called when clicking a link. The new page animates from the bottom of the screen upwards to the top. It works fine, but it's also working when you click the back button in the browser. I don't want the transition to fire if someone clicks the back button on the browser, I just want to go back to the page at whatever scrolled position I left it in, in exactly the same way you would expect to happen by clicking the browser back button.
If I take the CSS transitions off then the back button works fine, so I need to somehow differentiate between the back button being clicked and a link being clicked and then play the transition accordingly.
I don't know if I need to setState or something else? I did try that, but it seemed to set the state variable after the transition had happened.
import React, { useEffect, useState } from 'react';
import {Route, NavLink, Switch, useHistory } from "react-router-dom";
import './App.css';
import Home from './Home';
import About from './About';
import { CSSTransition, TransitionGroup,} from 'react-transition-group';
const usePageMeta = (title, description) =>{
const defaultTitle = "app-name";
const defaultDesc = "meta description";
useEffect(() => {
document.title = title || defaultTitle;
document.querySelector("meta[name='description']").setAttribute("content", description || defaultDesc);
}, [defaultTitl
e, title, defaultDesc, description]);
};
const App = () =>{
return (
<div className="App">
<div className="nav fixed top-0 left-0 z-10 w-full">
<NavLink exact to="/" activeClassName="active">Home</NavLink>
<NavLink to="/about" activeClassName="active">About</NavLink>
</div>
<Route render={({location}) => (
<TransitionGroup>
<CSSTransition
key={location.key}
timeout={1000}
classNames="fade"
>
<Switch location={location}>
<Route exact path="/">
<Home usePageMeta={usePageMeta}/>
</Route>
<Route path="/about">
<About usePageMeta={usePageMeta}/>
</Route>
</Switch>
</CSSTransition>
</TransitionGroup>
)} />
</div>
);
}
export default App;
Can anyone point me in the right direction please?
After some testing, I can see that this is bound to happen as the path is being loaded into the history of the browser.
tl;dr
This is not a quirk of the library, its just the way the browsers work. The current session always pushes state on the history object.
To prevent this you can do two things
Overwrite the history state whenever the user navigates to a new path.
Prevent the default action of the back button and redirect to the entry site.
Solution 1:
import createBrowserHistory from 'history' package and make a mutable history outside the app component.
make a immutable string of the start state when entering the app with history.createHref(history.location)
define an onClick handler which replaces the histories's top state to current location.
register a click handler on both NavLinks with the above defined function.
add a listener to the history (history.listen) and call history.goBack() if the action is pop.
call unlisten on unmount.
import { createBrowserHistory } from 'history';
let history = createBrowserHistory();
const ContainerComponent = () => {
// removed uselocation and usehistory hooks
const location = history.createHref(history.location);
const replaceHistory = () => {
history.replace(location);
}
const unlisten = history.listen((location, action) => {
if (action == "POP")
history.goBack();
})
useEffect(() => {
return () => unlisten();
})
return (
<div>
<Navlink to='/' onClick={replaceHistory} />
<Navlink to='/about' onClick={replaceHistory} />
</div>
);
}
Solution 2:
import createBrowserHistory from the 'history' module and create a mutable history object outside the app component.
add a listener to the history (history.listen) which goes back '-1' number of pages when it recives the "POP" action.
useEffect to 'unlisten' this event.
Note: this is assuming the user has to be navigated to the site which came from.
import { createBrowserHistory } from 'history';
// Creates a mutable history state.
let history = createBrowserHistory();
const ContainerComponent = () => {
//removed the eventlistener on window
const unlisten() = history.listen((location, action) => {
//remove the logging once satisfied with the result.
console.log(`${location}, ${action}`);
if ( action == "POP" ) {
history.goBack();
}
})
// removed 'popstate' evenhandler
// adding useEffect to call unlisten on unmount
useEffect (() => {
return () => unlisten();
}, [])
const count = () => {
setPageChanges(pageChanges + 1);
}
return (
<div>
<NavLink to='/' onClick={count} />
<NavLink to='/about' onClick={count} />
</div>
{/*... rest of the switch code with the transition ...*/}
);
}
Edit: Solution 2 is refactored to use the 'history' package utilities.
This is seen more appropriate as the browser states are not touched with react code.
Also in the docs, it was mentioned that the history object is mutable which does result in some problems with mounting.
Caveat: this solution goes through all the states in between where the user is and where they started from, so expect some flashes of the earlier components.
Solution 1 also uses the same package, with the caveat that it will go to the first page and then goBack() 1 page more. This will be less of a flash then solution 2.
I'm trying to setup a url that changes depending on the useState. I'm doing this because I want to be able to access both states with the url. So this is what my router file looks like:
import React from 'react';
import {BrowserRouter as Router, Switch, Route} from "react-router-dom";
function router(){
return (
<Router>
<Switch>
<Route path="/restaurant/:toggleParameter" children={<Restaurant/>}/>
</Switch>
</Router>
);
}
export default router;
An the component Restaurant looks like this:
import React, {useEffect, useState} from 'react';
import {useParams,Redirect} from "react-router-dom";
function RestaurantLandingPage(){
const {toggleParameter} = useParams();
console.log("Parameter");
console.log(toggleParameter);
const [profileToggle,setProfileToggle] = useState(toggleParameter);
const restaurantID =localStorage.getItem("restaurantId");
console.log(restaurantID);
const changeParameterToProfile =()=>{
setProfileToggle("profile");
};
const changeParameterToMenu=()=>{
setProfileToggle("menu");
}
return (
<div id="wrapper">
<div className="restaurantHover">
<button
className="switchButton"
onClick={()=>{changeParameterToProfile()}}
style={profileToggle==="profile"? {textDecoration:'underline',textDecorationThickness:'3px',textDecorationColor:'#6C5AF2'}:{}}
>
Profile
</button>
<button
className="switchButton"
onClick={changeParameterToMenu}
style={profileToggle==="menu"?{textDecoration:'underline',textDecorationThickness:'3px',textDecorationColor:'#6C5AF2'}:{}}
>
Menu
</button>
<div id="switchBottom"/>
{(profileToggle==="profile")&&(
<Contact profileToggle={profileToggle} changeParameterToMenu={changeParameterToMenu}/>
)}
{(profileToggle==="menu")&&(
<RestauarntMenuOverview/>
)}
</div>
</div>
)}
}
export default RestaurantLandingPage;
The url-param "toggleParameter" is "profile" or "menu". I'll access it with useParams(). Now if I press the button Profile the url-param "toggleParameter" should switch to Profile and if I press the button Menu the url-param "toggleParameter" should switch to Menu. I thought I could use Redirect like this:
<button
className="switchButton"
onClick={()=>{changeParameterToProfile();
<Redirect to={/restaurant/{profileToggle}/>}}
style={profileToggle==="profile"? {textDecoration:'underline',textDecorationThickness:'3px',textDecorationColor:'#6C5AF2'}:{}}>
Profile
</button>
But this doesn't work. I'm a little bit confused with all the react-router possibilities because I haven't found the right one yet.
React doesn't allow updating the state when the component is unmounted & it will cause some serious memory leak hidden before your eyes. Also setState function is an Asynchrounus function & it can get called after the history.push method. Changing the route with history.push will unmount the component & in some cases setState might get called afterwards causing state update on unmounted component. Also the value of profileToggle will only get changed after setProfileToggle is called keeping the value of profileToggle same & history.push will also use the previous value & you or the user have to click the button twice to go to /restaurant/profile or /restaurant/menu
Code:
import {useParams,useHistory } from "react-router-dom";
function RestaurantLandingPage(){
const history = useHistory()
const {toggleParameter} = useParams();
const changeParameterToProfile =()=>{
history.push(`/restaurant/profile`)
};
const changeParameterToMenu=()=>{
history.push(`/restaurant/menu`)
}
return(
......
<button
className="switchButton"
onClick={()=>changeParameterToProfile()}
style={toggleParameter==="profile"?
{ textDecoration:'underline',
textDecorationThickness:'3px',
textDecorationColor:'#6C5AF2'
}:{}
}
>
Profile
</button>
......
)
}
Try this
import {useParams,useHistory } from "react-router-dom";
function RestaurantLandingPage(){
const history = useHistory()
const {toggleParameter} = useParams();
const changeParameterToProfile =()=>{
setProfileToggle("profile");
history.push(`/restaurant/${profileToggle} `)
};
const changeParameterToMenu=()=>{
setProfileToggle("menu")
history.push(`/restaurant/${profileToggle} `)
}
return(
......
<button
className="switchButton"
onClick={()=>{changeParameterToProfile();}
style={profileToggle==="profile"? {textDecoration:'underline',textDecorationThickness:'3px',textDecorationColor:'#6
C5AF2'}:{}}>
Profile
</button>
<button
className="switchButton"
onClick={()=>{changeParameterToMenu();}
style={profileToggle==="profile"?
{textDecoration:'underline',textDecorationThickness:'3px',textDecorationColor:'#6
C5AF2'}:{}}>
Menu
</button>
......
)
}
Let me know if it works
Thanks in advance for any help you can provide. I am trying to create a Modal is react and call a get request to load details of a task.
I have most of it working (I think), but essentially what I have done is createa custom Modal Hook that toggles two modals.
The second of the two modals is meant to open a task and render the task details in a form for editing but I am unable to get it working.
Here is the useModal hook:
import { useState } from "react";
const useModal = () => {
const [isShowing, setIsShowing] = useState(false);
const [secondModalIsShowing, secondModalSetIsShowing] = useState(false);
function toggle() {
setIsShowing(!isShowing);
}
function secondToggle() {
secondModalSetIsShowing(!secondModalIsShowing);
console.log("clicked");
}
return {
isShowing,
toggle,
secondModalIsShowing,
secondToggle,
};
};
export default useModal;
I then call the function for the secondToggle which fires the code below to render the modal. Now as you may see I have to comment out the section where it calls getTask() with match.params.id, as well as the component that is then meant to be rendered in the modal.
If I don't do that I get an error message with the following " Line 23:5: Expected an assignment or function call and instead saw an expression no-unused-expressions"
import React, { Fragment, useEffect, useState } from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import TaskItem from "../tasks/task-item/TaskItem";
import { getTask } from "../../actions/task";
import ReactDOM from "react-dom";
import "./Modal.styles.scss";
import "../dashboard/Dashboard.styles.scss";
import Task from "../task/Task";
import TaskEdit from "../task/TaskEdit";
const TaskModal = ({
getTask,
task: { task, loading },
match,
secondModalIsShowing,
hide,
}) => {
const [displayEdit, toggleDisplayEdit] = useState(false);
useEffect(() => {
getTask();
// match.params.id;
}, [getTask]);
return secondModalIsShowing
? ReactDOM.createPortal(
<React.Fragment>
<button
type="submit"
value="toggle"
onClick={() => toggleDisplayEdit(!displayEdit)}
>
Show/Edit
</button>
{(displayEdit && <TaskItem task={task} />) || (
<div>{/* <TaskEdit /> */}</div>
)}
<div className="modal-overlay" />
<div
className="modal-wrapper"
aria-modal
aria-hidden
tabIndex={-1}
role="dialog"
>
<div className="modal">
<div className="modal-header">
Add New Task
<button
type="button"
className="modal-header__button"
data-dismiss="modal"
aria-label="Close"
onClick={hide}
>
<span aria-hidden="true">×</span>
</button>
</div>
</div>
</div>
</React.Fragment>,
document.body
)
: null;
};
Now if I render this EditTask component outside the modal as a normal component it works correctly. I can also get the modal to render when it's not trying to display the EditTask component.
As a result, I think it's related to the Route path and passing the response to the TaskModal component? When I click the modal to open I cant get it to render the URL with the task ID and therefore I cant render the details of the task in the modal.
<Route path="/taskedit/:id" component={TaskModal} />
For context, I think this guide comes close to solving my issue (https://blog.logrocket.com/building-a-modal-module-for-react-with-react-router/) but I am not familiar with working with class-based components and when I try and convert to functional-based components I'm running into even more issues.
Thanks in advance for any insight you can provide as I keep trying to work through this.
Paul
The first issue I am seeing is you have to pass the task id to TaskModal component
<Route path="/taskedit/:id"
render={(props) => <TaskModal {...props} />}>
</Route>
This will make the task id available as property in TaskModal.
Then in the TaskModal, fetch like below
let taskid = prop.match.params.id;
I am recreating a simple React app that I have already created in Angular. The React app has two components: one (menus.js) for a side menu and a second (content.js) that will display the content from each item in the menu when each link is clicked (just like an iframe of sorts). In the App.js I am making a REST API call to populate the state for the menus.js component. Note that both components are in the App.js as follows:
App.js
import React,{Component} from 'react';
import Menus from './components/menus';
import Content from './components/content';
class App extends Component {
state = {
menus: []
}
componentDidMount(){
fetch('api address')
.then(res => res.json())
.then((data)=> {
this.setState({menus: data})
})
.catch(console.log)
}
render(){
return (
<div>
<div><Menus menus={this.state.menus} /></div>
<div><Content /></div>
</div>
);
}
}
export default App;
here is the menu.js component; it takes a prop (menus) from App.js and builds the menu links with items from it:
import React from 'react';
import { BrowserRouter as Router, Link,} from "react-router-dom";
const Menus = ({ menus }) => {
return (
<Router>
<div>
<center><h1>Lessons</h1></center>
{menus.map(menu => (
<li key={menu.lesson}>
<Link to={`/lesson/${menu.lesson}`}>{menu.lessonName}</Link>
</li>
))}
</div>
</Router>
);
};
export default Menus;
Here is what I need - how do I pass items from the same prop (from App.js) to the content component? FYI - I need this to happen each time a link in the menu in menu.js is clicked (which is why a key is used in the list The simple idea is content will update in the content component each time a menu link in the menu component is clicked.
**content.js**
import React from 'react'
const Content = () => {
return (
<div>{menu.content}</div>
)
};
export default Content
Based on your description of the problem and what I can see of what you've written, it seems to me like you are trying to build an application where the menu persists, but the content changes based on menu clicks. For a simple application, this is how I would structure it differently.
<ParentmostComponent>
<MenuComponent someProp={this.state.some_data} />
<Switch>
<Route path={"/path"} render={(props) => <Dashboard {...props} someProp={this.state.some_other_data_from_parents} />
</Switch>
</ParentMostComponent>
This would allow the menu to always stay there no matter what the content is doing, and you also won't have to pass the menu prop to two components.
In your menu.js, attach the menu object to the Link
...
{menus.map(menu => (
<li key={menu.lesson}>
<Link to={{
pathname: `/lesson/${menu.lesson}`,
state: menu
}}> {menu.lessonName} </Link>
</li>
))}
...
In your content.js receive the menu like this:
import React from 'react'
const Content = () => {
console.log(props.location.state.menu.content);
return (
<div>{props.location.state && props.location.state.menu.content }</div>
)
};
export default Content
Read more here
Your example uses React Router, so this answer uses it as well.
First of all, move the Router up the hierarchy from Menus to App to make the router props available to all components. Then wrap your Content inside a Route to render it conditionally (i.e. if the path matches "/lesson/:lesson"):
class App extends Component {
state = {
menus: [
{
lesson: '61355373',
lessonName: 'Passing props from parent to sibling in React',
content: 'I am recreating a simple React app…'
},
{
lesson: '27991366',
lessonName: 'What is the difference between state and props in React?',
content: 'I was watching a Pluralsight course on React…'
}
]
}
render() {
const { menus } = this.state
return (
<Router>
<div>
<div><Menus menus={menus}/></div>
<Route path="/lesson/:lesson" render={({ match }) => (
<div><Content menu={menus.find(menu => menu.lesson === match.params.lesson)}/></div>
)}>
</Route>
</div>
</Router>
)
}
}
With the help of the render prop, you can access the router props (in this case match.params.lesson) before rendering your child component. We use them to pass the selected menu to Content. Done!
Note: The basic technique (without React Router, Redux etc.) to pass props between siblings is to lift the state up.
In our react app (we use reactstrap), we've multiple pages from where a confirmation modal can be shown. We do not want to include the modal code in every page. Is there a way to do this programmatically by invoking a method?
We can use plain bootstrap modals directly in the public index.html and from the util method use dom selector and invoke the modal but want to avoid this. Any pointers on how to go about this?
If what you want is only one modal which can be used across multiple pages(instead of putting one modal in every page), you can put it in the root component usually names as App.
import Modal from "somewhere";
function App() {
const [modal, setModal] = useState(false);
return <>
<Model isOpen={modal} />
{/* If you need to toggle modal when clicking something in PageA, you can pass the prop down like this */}
<PageA onToggleModel={()=>{setModal(!modal)}} />
<PageB />
</>;
}
Just in case, doing import Modal from "somewhere" in every page wouldn't result in duplicate code in your final bundle. It's totally fine to do that.
Here's what we did.
Alert Component:
import React, { useState } from "react";
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap";
const Alert = props => {
const [modal, setModal] = useState(props.open ? props.open : true);
const toggle = () => {
setModal(!modal);
if (props.cb) {
props.cb();
}
if (props.reloadPage) {
window.location.reload();
}
};
return (
<div>
<Modal isOpen={modal} toggle={toggle}>
<ModalHeader toggle={toggle}>{props.title}</ModalHeader>
<ModalBody>{props.text}</ModalBody>
<ModalFooter>
<Button color="primary" onClick={toggle}>
Ok
</Button>
</ModalFooter>
</Modal>
</div>
);
};
export default Alert;
Util.js:
import React from "react";
import ReactDOM from "react-dom";
import Alert from "./Alert";
const Util = {
alert: (message, okCb, reload) => {
ReactDOM.render(
<Alert
title="Done"
text={message}
cb={() => {
ReactDOM.unmountComponentAtNode(
document.getElementById("modalHolder")
);
if (okCb) {
okCb();
}
}}
reloadPage={reload}
/>,
document.getElementById("modalHolder")
);
}
};
export default Util;
In index.html we created a dom element:
<div id="modalHolder"></div>
So to invoke the modal imperatively, call:
Util.alert("Data has been saved")