How to initialise React Materialize Drop down after componentDidMount - reactjs

My Navbar component doesn't have the Materialize Dropdown markup until a user signs in at which point I use conditional rendering and add the new markup. However I don't seem to be able to use React.createRef() to reference the new markup in order to initialise the Dropdown.
Please find a link to my project where you can create an account and see the issue for yourself.
What I think the problem is, is that I cannot recreate the ref after compononentDidMount.
I have tried creating the ref(React.createRef()) in componentWillReceiveProps and componentWillUpdate but I get a null value when I try to create the ref there.
class Navbar extends Component {
userRef = React.createRef();
render() {
const { isAuthenticated, user } = this.props.auth;
// show these when logged
const authLinks = (
<ul id="nav-mobile" className="right hide-on-med-and-down">
<li className="avatar">
<a href="#" ref={this.userRef} data-target="dropdown1" className="dropdown-trigger">
<div className="avatar">
</div>
{user.username}
</a>
<ul id="dropdown1" className="dropdown-content">
<li><Link to="/dashboard">Dashboard</Link></li>
</ul>
</li>
</ul>
);
// show these when not logged in
const guestLinks = (
<ul id="nav-mobile" className="right hide-on-med-and-down">
<li><Link to="/login">Login</Link></li>
<li><Link to="/register">Register</Link></li>
</ul>
);
return (
<nav>
{isAuthenticated ? authLinks : guestLinks}
</nav>
)
}
}
I need to reference the Dropdown DOM element when my Navbar receives new props(when the users successfully has signed in) so I can initialise it(window.M.Dropdown.init(this.dropdownRef.current)) when it has the right markup.

Related

How to avoid using a fall back value for to prop in Link

I have a header component that gets data using the context API in react. - It works.
However I don't like the fact that I need to provide a fall back value for the to prop in my link. Just so the page dosn't crash.
My Understanding is that this happens becuase when the page first loads "headerData" is not defind yet. When "headerData" gets the data then the page is rerendered and I get the correct value.
How can I refactor my code so that I don't need to provide a fall back value to my link component?
function Header() {
const headerData = useContext(HeaderContext);
console.log("heder data");
console.log(headerData.Menu?.PageUrl);
return (
<nav>
<Link to="/">Logo here</Link>
<ul>
<li><Link to={headerData?.Menu?.PageUrl || ""}>Menu</Link></li>
<li><Link to="/contact">Contact</Link></li>
</ul>
</nav>
)
}
From the output in the console I can see that the code runs twice.
I want to be able to change:
<li><Link to={headerData?.Menu?.PageUrl || ""}>Menu</Link></li>
to this:
<li><Link to={headerData.Menu.PageUrl}>Menu</Link></li>
You can conditionally render something else when that data is not available
function Header() {
const headerData = useContext(HeaderContext);
console.log("heder data");
console.log(headerData.Menu?.PageUrl);
return (
<nav>
<Link to="/">Logo here</Link>
<ul>
{!!headerData?.Menu?.PageUrl
? <li><Link to={headerData?.Menu?.PageUrl!!}>Menu</Link></li>
: <></> // Or whatever you want to have when there is no header data
// e.g. <li>Menu</li>
}
<li><Link to="/contact">Contact</Link></li>
</ul>
</nav>
)
}
To wait for the data being arrived, you can wait for it using a url state, and set the state when when the data has arrived. You don't render the link until url is being set:
function Header() {
const headerData = useContext(HeaderContext);
const [url, setUrl] = useState();
console.log("heder data");
console.log(headerData.Menu?.PageUrl);
headerData?.Menu?.PageUrl && !url && setUrl(headerData.Menu.PageUrl);
return (
<nav>
<Link to="/">Logo here</Link>
<ul>
{url?<li><Link to={url}>Menu</Link></li>:null}
<li><Link to="/contact">Contact</Link></li>
</ul>
</nav>
)
}
You could go even further and return nothing as long as url is undefined:
return (<>
{url?<nav>
<Link to="/">Logo here</Link>
<ul>
<li><Link to={url}>Menu</Link></li>
<li><Link to="/contact">Contact</Link></li>
</ul>
</nav>:null}
</>
)
}
Or use your context directly without a state:
return (<>
{headerData?.Menu?.PageUrl?<nav>
<Link to="/">Logo here</Link>
<ul>
<li><Link to={headerData.Menu.PageUrl}>Menu</Link></li>
<li><Link to="/contact">Contact</Link></li>
</ul>
</nav>:null}
</>
)
}

Target specific child inside React loop with refs and state onClick

I've got a mobile nav menu with a couple dropdown menus:
/* nav structure */
/about
/portfolio
/services
/service-1
/service-2
contact
etc..
I'm creating this menu dynamically with a loop in my render function.
I've set up a state variable to assign a class to the active dropdown menu. I'm using an onClick event/attribute on the trigger element (a small arrow image) to apply an active class to the respective dropdown menu. Kinda...
const myNavMenu = () => {
const [isSubMenuOpen, toggleSubMenu] = useState(false)
return (
<nav id="mainNav">
<ul>
{navItems.items.map((item, i) => (
<li key={i} className={
(item.child_items) !== null ? 'nav-item has-child-items' : 'nav-item'}>
<Link to={item.slug}>{item.title}</Link>
{(item.child_items === null) ? null :
<>
<img className="arrow down" src={arrowDownImg} onClick={() => toggleSubMenu(!isSubMenuOpen)} />
{printSubMenus(item)}
</>
}
</li>
))}
</ul>
</nav>
)
}
/**
* Prints nav item children two levels deep
* #param {Obj} item a navigation item that has sub items
*/
function printSubMenus(item) {
return (
<ul className={(isSubMenuOpen.current) ? "sub-menu sub-menu-open" : "sub-menu"}>
{item.child_items.map( (childItem, i) => (
<li className="nav-item" key={i}>
<Link to={childItem.slug}>{childItem.title}</Link>
{(childItem.child_items === null) ? null :
<>
<ul className="sub-sub-menu">
{childItem.child_items.map( (subItem, i) => (
<li className="sub-nav-item" key={i}>
<img className="arrow right" src={arrowRight} alt={''} />
<Link to={subItem.slug}>{subItem.title}</Link>
</li>
))}
</ul>
</>
}
</li>
))}
</ul>
)
}
}
*<Link> is a Gatsby helper component that replaces <a> tags.
The issue is that when I click my trigger, the active class is being applied to both (all) sub-menus.
I need to insert some sort of index (or Ref) on each trigger and connect it to their respective dropdowns but I'm not quite sure how to implement this.
I was reading up on useRef() for use inside of function components. I believe that's the tool I need but I'm not sure how to implement it in a loop scenario. I haven't been able to find a good example online yet.
Thanks,
p.s. my functions and loops are pretty convoluted. Very open to refactoring suggestions!
Move sub-menu to a new react component and put all the related logic in there (applying class included).

Add css class onClick to idividual elements generated by mapping an array in React

<div className="sub-nav">
<ul>
{props.data.map((item, index) => {
return (
<li key={index} className="sub-nav-item" >{item.name} </li>
);
})}
</ul>
</div>
Hi! I want to add an active class to an individual li when clicked, but I cannot seem to get it to work on mapped elements without adding it to all of them. This is part of a menu component in a React project. Thanks!
The following is hooks implementation.Incase you're using class I think you can derive from the same.
const [selectedItemIndex,setSelectedItemIndex] = useState(null);
<div className="sub-nav">
<ul>
{props.data.map((item, index) => {
return (
<li key={index} onClick={()=>setSelectedItemIndex(index)} className={`sub-nav-item ${selectedItemIndex===index?'active':''}`} >{item.name} </li>
);
})}
</ul>
</div>

TypeError: Cannot read property 'choice' of null in Gatsby Link State

Im Trying to make some ClassName added when i click on submenu from the dropdown navbar
this is what the navbar look like
https://imgur.com/9iCDj7k
the Code is worked before but after restarting gatsby development, it show error TypeError: Cannot read property 'choice' of null
this navbar is a Component, so its always loaded on every page
import React from "react"
import { Link } from "gatsby"
const Navbar = () => {
return (
<div
className="collapse navbar-collapse" id="navbarSupportedContent">
<ul className="navbar-nav mr-auto justify-content-around w-100">
<li className="nav-item"><Link className="nav-link pb-md-3" activeClassName="nav-link-active" to="/">Home</Link></li>
<li className="nav-item">
<div className={window.history.state.choice === 'Convention' ? 'dropdown nav-link pb-md-3 active' : 'dropdown nav-link pb-md-3'}>
<div className="dropdown-toggle" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Convention</div>
<div className="dropdown-menu" aria-labelledby="dropdownMenuButton"><Link className="dropdown-item" activeClassName="nav-sub-active" to="/convention/general-information" state={{ choice: 'Convention' }}>General Information</Link><Link className="dropdown-item" href="/coming-soon">Plenary & Special Sessions</Link><Link className="dropdown-item" href="/coming-soon">Technology Session</Link>
<div
className="dropdown-toggle dropdown-item nested-dropitem" id="dropdownMenu1Button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Technical Program</div>
<div className="dropright">
<div className="dropdown-menu" aria-labelledby="dropdownMenuButton1"><Link className="dropdown-item" href="/coming-soon">Oral Presentation</Link><Link className="dropdown-item" href="/coming-soon">Poster Presentation</Link></div>
</div><Link className="dropdown-item" href="/coming-soon">Download Full Paper</Link><Link className="dropdown-item" href="/coming-soon">Registration</Link></div>
</div>
</li>
<li className="nav-item">
<div className={window.history.state.choice === 'Exhibition' ? 'dropdown nav-link pb-md-3 active' : 'dropdown nav-link pb-md-3'}>
<div className="dropdown-toggle" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Exhibition</div>
<div className="dropdown-menu" aria-labelledby="dropdownMenuButton"><Link className="dropdown-item" to="/exhibition/why-exhibit" state={{ choice: 'Exhibition' }}>Why Exhibit?</Link><Link className="dropdown-item" to="/exhibition/book-your-space" state={{ choice: 'Exhibition' }}>Book Your Space</Link><Link className="dropdown-item" href="/coming-soon">Exhibitor Services</Link>
<Link
className="dropdown-item" to="/exhibition/about-the-venue" state={{ choice: 'Exhibition' }}>About the Venue</Link>
</div>
</div>
</li>
As you guys can see, on the Subnav link i put the state name of the parent nav, so when i click the sub nav, there are ternary operator that added active style
im not using ActiveClassName or PartiallyActive because what i want is when i click the sub menu from Convention, the Convention style get changed.
According to the docs:
gatsby-link, you should be able to retrieve state using location, but unfortunately, that did not work for me either, and was receiving a similar error like you.
I was able to retrieve state from gatsby Link using window.history.state
I found the answer here: gatsby-issue#9091
I see you set the state here:
<Link
className="dropdown-item"
to="/exhibition/about-the-venue"
state={{ choice: 'Exhibition' }}>
About the Venue
</Link>
But window.history.state is only populated when user has already clicked on the link. So on first load, it won't be there. Also, you might run into problem during gatsby build, since the window object is not available there.
You can either add a null check:
const getHistoryState = (prop) => {
if (typeof window === 'undefined') return
if (!window.history.state) return
return window.history.state[prop]
}
<div className={`dropdown nav-link pb-md-3 ${getHistoryState(choice) === 'Exhibition' && 'active'}`}>
Or set that property globally:
// gatsby-browser.js
export const onClientEntry = () => {
window.history.pushState({ choice: '...' }, '')
}
You'd still need to check for window before using window.history... in your component though.
To avoid window check completely, use the location object passed down by reach-router to page components, for example:
// src/index.js
export default const IndexPage = ({ location }) => (
<NavBar location={location} />
)
// NavBar.js
const NavBar = ({ location }) => (
<div className={`class name here ${location.state && location.state.choice && 'active'}`} />
)

Redux component, can I write click events without using class

I am writing the following redux component (dumb)
const TopMenuComponent = () => (
<div className={styles.topMenuIndex}>
<header role="banner">
<nav role='navigation'>
<ul>
<li className={styles.expando}>☰</li>
<li>Home</li>
<li>About</li>
</ul>
</nav>
</header>
</div>
);
Now on the click event of the ☰ link I want to write some code to perform some function. I have two questions around this
Is it possible to do this without deriving this from the React.Component class using the above code. My concern is that this is a dumb component and if we derive this from React.Component then we are adding more intelligence into this.
Should this expansion click trigger a stage change on the redux store? This is just a simple expansion of a bunch of links thats all
If you want to make it a dumb component, then you can pass in a property, e.g. isOpen, which defines if the menu is expanded or not, and a function, e.g. handleClick, which should be called on a button click. This way, the state can be managed by a parent component.
Now whether you manage this state in the internal state of parent component or with your redux store is entirely up to you. You should choose redux if you will need to open and close the menu remotely.
There are two ways to achieve what you want to without affecting the redux store
First, Passing function as a prop to the component from the parent component and then calling this onClick
const TopMenuComponent = (props) => {
return (
<div className={styles.topMenuIndex}>
<header role="banner">
<nav role='navigation'>
<ul>
<li className={styles.expando} onClick={props.myhandler}>☰</li>
<li>Home</li>
<li>About</li>
</ul>
</nav>
</header>
</div>
)
};
Second way is to define a onClick handler in the component itsef like
const TopMenuComponent = () => {
const myHandler = (e) => {console.log('clicked'); // some other code}
return (
<div className={styles.topMenuIndex}>
<header role="banner">
<nav role='navigation'>
<ul>
<li className={styles.expando} onClick={myhandler}>☰</li>
<li>Home</li>
<li>About</li>
</ul>
</nav>
</header>
</div>
)
};

Resources