I am trying to get my sidebar to close when I click on any of the menu options. I was able to get the sidebar to close/open whenever I click on the burger icon, but not sure if I am supposed to make my sidebar component a class and have its own state. Below are my navigation and sidebar components.
import React from 'react';
import { Link } from 'react-router-dom';
import { ReactComponent as MenuIcon } from '../../assets/menu.svg';
import { ReactComponent as CloseIcon } from '../../assets/x-mark.svg';
import './navigation.styles.scss';
import Sidebar from '../sidebar/sidebar.component';
class Navigation extends React.Component {
constructor(props) {
super(props);
this.state = {
isSidebarHidden: true
};
this.handleSidebar = this.handleSidebar.bind(this);
}
handleSidebar() {
this.setState({ isSidebarHidden: !this.state.isSidebarHidden });
}
render() {
const { isSidebarHidden } = this.state;
return (
<div className='navigation'>
<div className='logo-container'>
<Link className='logo' to='/'>
NAME
</Link>
</div>
<div className='navigation-options'>
<Link className='option' to='/projects'>
PROJECTS
</Link>
<Link className='option' to='contact'>
CONTACT
</Link>
{isSidebarHidden ? (
<MenuIcon className='menu-icon' onClick={this.handleSidebar} />
) : (
<CloseIcon className='menu-icon' onClick={this.handleSidebar} />
)}
</div>
{isSidebarHidden ? null : <Sidebar />}
</div>
);
}
}
export default Navigation;
import React from 'react';
import { Link } from 'react-router-dom';
import './sidebar.styles.scss';
const Sidebar = () => (
<div className='sidebar'>
<Link className='sidebar-option' to='/projects'>
PROJECS
</Link>
<Link className='sidebar-option' to='/contact'>
CONTACT
</Link>
</div>
);
export default Sidebar;
You could create a method to hide the sidebar and pass it to the Sidebar component, so it executes when you click the links.
const Sidebar = ({hideSidebar}) => (
<div className='sidebar'>
<Link onClick={hideSidebar} className='sidebar-option' to='/projects'>
PROJECS
</Link>
<Link onClick={hideSidebar} className='sidebar-option' to='/contact'>
CONTACT
</Link>
</div>
);
Or you could also execute it every time you move to a different path listening to the browser history with react-router.
import { browserHistory } from 'react-router';
browserHistory.listen(handleRouteChange);
I suggest controlling the component with props instead of using if statement inside the parent component.
import React, { useEffect } from 'react';
import { Link } from 'react-router-dom';
import './sidebar.styles.scss';
const Sidebar = ({ visibility, setVisibility }) => {
if (visibility) {
return (
<div className='sidebar'>
<Link className='sidebar-option' to='/projects' onClick={() => setVisibility()}>
PROJECS
</Link>
<Link className='sidebar-option' to='/contact' onClick={() => setVisibility()}>
CONTACT
</Link>
</div>
)
}
return null
};
export default Sidebar;
As you see, I passed setVisibility prop to onClick callback on the sidebar links and checked if visibility is true then return the sidebar contents. So in this step, we just need to pass this.handleSidebar to setVisibility prop and the parent state isSidebarHidden to the visibility prop.
import React from 'react';
import { Link } from 'react-router-dom';
import { ReactComponent as MenuIcon } from '../../assets/menu.svg';
import { ReactComponent as CloseIcon } from '../../assets/x-mark.svg';
import './navigation.styles.scss';
import Sidebar from '../sidebar/sidebar.component';
class Navigation extends React.Component {
constructor(props) {
super(props);
this.state = { isSidebarHidden: true };
this.handleSidebar = this.handleSidebar.bind(this);
}
handleSidebar() {
this.setState({ isSidebarHidden: !this.state.isSidebarHidden });
}
render() {
const { isSidebarHidden } = this.state;
return (
<div className='navigation'>
<div className='logo-container'>
<Link className='logo' to='/'>
NAME
</Link>
</div>
<div className='navigation-options'>
<Link className='option' to='/projects'>
PROJECTS
</Link>
<Link className='option' to='contact'>
CONTACT
</Link>
{isSidebarHidden ? (
<MenuIcon className='menu-icon' onClick={this.handleSidebar} />
) : (
<CloseIcon className='menu-icon' onClick={this.handleSidebar} />
)}
</div>
<Sidebar visibility={isSidebarHidden} setVisibility={this.handleSidebar} />
</div>
);
}
}
export default Navigation;
Then it works.
For the people using bootstrap offcanvas as a sidebar there is a very easy way to do it using only bootstrap and with no JavaScript.
<li data-bs-dismiss="offcanvas">Skills</li>
The above code represent li as one of the item in the sidebar and on upon clicking it takes you to skill section and also closes as it is in dismiss state.
Related
I'm trying to render out the dropdown menu items from DropDown.js whenever i click on Services button from Navbar.js. However, when i click on Services button i got this error "You cannot render a inside another . You should never have more than one in your app.". How can i fix this? Please help me! Thank you so much!
DropDown.js:
import React from "react";
import { serviceDropdown } from "./NavItems";
import { Link, BrowserRouter as Router } from "react-router-dom";
import "./DropDown.css";
function DropDown() {
return (
<div>
<ul className="subnav-items">
{serviceDropdown.map((item) => {
return (
<Router>
<li key={item.id} className={item.cName}>
<Link to={item.path}>{item.title}</Link>
</li>
</Router>
);
})}
</ul>
</div>
);
}
export default DropDown;
Navbar.js:
import React, { useState } from "react";
import { navItems } from "./NavItems";
import { Link, BrowserRouter as Router } from "react-router-dom";
import "./Navbar.css";
import DropDown from "./DropDown";
function Navbar() {
const [dropDown, setDropdown] = useState(false);
return (
<div>
<ul className="nav-items">
{navItems.map((item) => {
if (item.title === "Services") {
return (
<Router>
<li
key={item.id}
className={item.cName}
onMouseEnter={() => setDropdown(true)}
onMouseLeave={() => setDropdown(false)}
>
<Link to={item.path}>{item.title}</Link>
{dropDown && <DropDown />}
</li>
</Router>
);
}
return (
<Router>
<li key={item.id} className={item.cName}>
<Link to={item.path}>{item.title}</Link>
</li>
</Router>
);
})}
</ul>
</div>
);
}
export default Navbar;
It's like the invariant error says, you should only have one router providing a routing context to the entire app, or at a minimum, not have any nested routers. Remove all the extraneous Router components wrapping each individual list item/Link, and ensure you've only one wrapping the app.
DropDown.js:
import React from "react";
import { serviceDropdown } from "./NavItems";
import { Link } from "react-router-dom";
import "./DropDown.css";
function DropDown() {
return (
<div>
<ul className="subnav-items">
{serviceDropdown.map((item) => {
return (
<li key={item.id} className={item.cName}>
<Link to={item.path}>{item.title}</Link>
</li>
);
})}
</ul>
</div>
);
}
export default DropDown;
Navbar.js:
import React, { useState } from "react";
import { navItems } from "./NavItems";
import { Link } from "react-router-dom";
import "./Navbar.css";
import DropDown from "./DropDown";
function Navbar() {
const [dropDown, setDropdown] = useState(false);
return (
<div>
<ul className="nav-items">
{navItems.map((item) => {
if (item.title === "Services") {
return (
<li
key={item.id}
className={item.cName}
onMouseEnter={() => setDropdown(true)}
onMouseLeave={() => setDropdown(false)}
>
<Link to={item.path}>{item.title}</Link>
{dropDown && <DropDown />}
</li>
);
}
return (
<li key={item.id} className={item.cName}>
<Link to={item.path}>{item.title}</Link>
</li>
);
})}
</ul>
</div>
);
}
export default Navbar;
App example:
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
...
return (
<Router>
<DropDown />
<Navbar />
<Routes>
.... routes here ....
</Routes>
</Router>
);
this main App function has a custom hook that will trigger when the button is cliked:
import React, { useEffect } from 'react';
import { BrowserRouter, Route, Link } from 'react-router-dom'
import HomeScreen from './Screens/HomeScreen'
import './App.css';
import { useCurrentLang } from './utils/useCurrentLang'
import {strings as engstrings} from './res/lang/eng/strings'
function App() {
const currentStrings = useCurrentLang(engstrings);
return (
<BrowserRouter>
<div className="grid-container">
<header className="header">
<div className="brand">
<Link to="/" >
</Link>
</div>
<div className="header-side">
{currentStrings.currentlang.subtitle}
</div>
<div className="header-right">
<button {...currentStrings}>
{currentStrings.currentlang.traduction}
</button>
</div>
<div>
</div>
</header>
<main className="main">
<div className="content">
<Route path="/" exact={true} component={HomeScreen} />
</div>
</main>
<footer className="footer">
© 2020
</footer>
</div>
</BrowserRouter>
);
}
export default App;
However this component function that is routed from App also needs to use the same reference of the hook object found in App:
import React from 'react';
import terminalImage from '../res/images/GNOMETerminalIcon.png';
import {useCurrentLang} from '../utils/useCurrentLang'
import {strings as engstrings} from '../res/lang/eng/strings'
const { Link } = require("react-router-dom");
function HomeScreen() {
const currentStrings = useCurrentLang(engstrings);
return <div className="home">
<ul className="menu-list">
<li>
<div className="about-link section">
<Link to="/about">{currentStrings.currentlang.about}</Link>
</div>
</li>
<li>
<div className="projects-link section">
<Link to="/about">{currentStrings.currentlang.about}</Link>
</div>
</li>
<li>
<div className="contacts-link section">
<Link to="/about">{currentStrings.currentlang.about}</Link>
</div>
</li>
<li>
<div className="suggestions-link section">
<Link to="/about">{currentStrings.currentlang.about}</Link>
</div>
</li>
</ul>
<div className="home-main-image">
<img src={terminalImage} />
</div>
</div>
}
export default HomeScreen;
Is it possible for both function to rerender when the hook on App is triggered? if yes, how?
Edit: currentLang hook:
import {useState} from 'react'
import {strings as frstrings} from '../res/lang/fr/strings'
import {strings as engstrings} from '../res/lang/eng/strings'
export const useCurrentLang = initialState => {
if(initialState === 0){
initialState = engstrings
}
const [currentlang, setLang] = useState(initialState);
return {
currentlang: currentlang,
onClick: () => {
if(currentlang === engstrings){
setLang(frstrings)
} else {
setLang(engstrings)
}
}
}
}
Problem
The main problem you're facing is one part of your React Tree has no idea that the language has changed.
Solution
Use a Language Context which provides the updates to all your React tree and wrap it on the top of the application. In layman terms, now your app is on listening mode whenever lang changes. So basically, what React does now is whenever lang changes, it will find wherever lang is used from the context and update the component.
Docs on React context here
import React from 'react'
// import {strings as frstrings} from '../res/lang/fr/strings'
// import {strings as engstrings} from '../res/lang/eng/strings'
const lang = {
// in this way, you could dynamically add lang
// later on which worrying about if-elses in your component
en: {
hello: 'hello'
},
fr: {
hello: 'bonjour',
},
}
const langDict = (key) => lang[key]
const LanguageContext = React.createContext(null);
function LanguageProvider({ initialState = 'en', children }) {
const [lang, setLang] = React.useState(initialState);
return (
<LanguageContext.Provider value={[langDict(lang), setLang]}>
{children}
</LanguageContext.Provider>
)
}
function useLanguage() {
return React.useContext(LanguageContext);
}
export default function AppWrapper() {
return (
<LanguageProvider>
<App />
</LanguageProvider>
)
}
function App() {
const [lang, setLang] = useLanguage();
return (
<div>
<h1>{lang.hello}</h1>
<button onClick={() => setLang('fr')}>French</button>
<button onClick={() => setLang('en')}>English</button>
</div>
)
}
In this function:
import React, { useEffect } from 'react';
import { BrowserRouter, Route, Link } from 'react-router-dom'
import HomeScreen from './Screens/HomeScreen'
import './App.css';
import currentStrings from './utils/currentlang'
function App() {
const [currentlang, setCurrentStrings] = React.useState(currentStrings)
return (
<BrowserRouter>
<div className="grid-container">
<header className="header">
<div className="brand">
<Link to="/" >
</Link>
</div>
<div className="header-side">
{currentlang.getCurrent.subtitle}
</div>
<div className="header-right">
<button onClick={() => setCurrentStrings(currentlang.switchLang())}> {currentlang.getCurrent.traduction} </button>
</div>
<div>
</div>
</header>
<main className="main">
<div className="content">
<Route path="/" exact={true} component={HomeScreen} />
</div>
</main>
<footer className="footer">
© 2020
</footer>
</div>
</BrowserRouter>
);
}
export default App;
I use a button (<button onClick={() => setCurrentStrings(currentlang.switchLang())}> ) to change one of the properties of the object currentStrings (or currentLang used for the useState hook)
However it seems my function does not rerender, i can confirm this since my logs do tell the object does change its properties when the button is clicked, but the screen does not show it.
import {strings as frstrings} from '../res/lang/fr/strings'
import {strings as engstrings} from '../res/lang/eng/strings'
class CurentLang {
constructor(){
this.current = engstrings;
}
switchLang() {
if(this.current === frstrings){
this.current = engstrings;
console.log("engstrings");
} else{
this.current = frstrings;
console.log("frstrings");
}
return new this.CurentLang;
}
get getCurrent(){
return this.current;
}
}
var currentStrings = new CurentLang()
export default currentStrings;
Why does the React tab in my browser hang when visualizing these components? What am I doing wrong? I just want to transfer the text to another component using Provider and Consumer.
Menu component:
import React, {PureComponent, createContext} from 'react'
import { Link } from "react-router-dom";
import './menu.scss';
import ShoppingBasket from '../shoppingBasket/shoppingBasket.js';
const UserContext = React.createContext({})
export const UserProvider = UserContext.Provider
export const UserConsumer = UserContext.Consumer
export default class Menu extends PureComponent {
render() {
return (
<div className="head">
<nav>
<Link to="/">Home</Link>
<Link to="/aboutus">About Us</Link>
</nav>
<UserProvider username={`name`}>
<ShoppingBasket />
</UserProvider>
</div>
)
}
}
ShoppingBasket component:
import React, {PureComponent} from 'react'
import UserProvider from '../menu/menu.js';
import UserConsumer from '../menu/menu.js';
export default class ShoppingBasket extends PureComponent {
render() {
return (
<UserConsumer>
{context => {
return(
<div>
<h2>Profile Page of {context.username}</h2>
</div>
)
}}
</UserConsumer>
)
}
}
As per the React Context Api ,in Context Provider, data should be passed using the "value" prop , what you are doing it passing the username prop to the provider, what you should try is
<UserProvider value={`name`}>
<ShoppingBasket />
</UserProvider>
and while in the consumer
<UserConsumer>
{value => {
return(
<div>
<h2>Profile Page of {value}</h2>
</div>
)
}}
</UserConsumer>
I am adding a props of sidebar Component to my template.
I am passing {...this.props} to Sidebar.
But it still leads to TypeError: Cannot read property 'map' of undefined in my Menu file.
My PostTemplateDetails file that I wish to add the Sidebar component:
import React from 'react'
import Sidebar from '../Sidebar'
import { Link } from 'gatsby'
import moment from 'moment'
import './style.scss'
class PostTemplateDetails extends React.Component {
render() {
const { subtitle, author } = this.props.data.site.siteMetadata
const post = this.props.data.markdownRemark
const tags = post.fields.tagSlugs
const tagsBlock = (
<div className="post-single__tags">
<ul className="post-single__tags-list">
{tags &&
tags.map((tag, i) => (
<li className="post-single__tags-list-item" key={tag}>
<Link to={tag} className="post-single__tags-list-item-link">
{post.frontmatter.tags[i]}
</Link>
</li>
))}
</ul>
</div>
)
return (
<div>
<Sidebar {...this.props} />
<div className="content">
<div className="content__inner">
<div className="post-single">
<div className="post-single__inner">
<h1 className="post-single__title">{post.frontmatter.title}</h1>
<div
className="post-single__body"
/* eslint-disable-next-line react/no-danger */
dangerouslySetInnerHTML={{ __html: post.html }}
/>
<div className="post-single__date">
<em>
Published {moment(post.frontmatter.date).format('D MMM YYYY')}
</em>
</div>
</div>
<div className="post-single__footer">
{tagsBlock}
<hr />
<p className="post-single__footer-text">
{subtitle}
<a
href={`https://twitter.com/${author.twitter}`}
target="_blank"
rel="noopener noreferrer"
>
<br /> <strong>{author.name}</strong> on Twitter
</a>
</p>
</div>
</div>
</div>
</div>
</div>
)
}
}
export default PostTemplateDetails
My Sidebar component file:
import React from 'react'
import get from 'lodash/get'
import { Link } from 'gatsby'
import Menu from '../Menu'
import Links from '../Links'
import profilePic from '../../pages/photo.jpg'
import './style.scss'
class Sidebar extends React.Component {
render() {
const { location } = this.props
const {
author,
subtitle,
copyright,
menu,
} = this.props.data.site.siteMetadata
const isHomePage = get(location, 'pathname', '/') === '/'
/* eslint-disable jsx-a11y/img-redundant-alt */
const authorBlock = (
<div>
<Link to="/">
<img
src={profilePic}
className="sidebar__author-photo"
width="75"
height="75"
alt={author.name}
/>
</Link>
{isHomePage ? (
<h1 className="sidebar__author-title">
<Link className="sidebar__author-title-link" to="/">
{author.name}
</Link>
</h1>
) : (
<h2 className="sidebar__author-title">
<Link className="sidebar__author-title-link" to="/">
{author.name}
</Link>
</h2>
)}
<p className="sidebar__author-subtitle">{subtitle}</p>
</div>
)
/* eslint-enable jsx-a11y/img-redundant-alt */
return (
<div className="sidebar">
<div className="sidebar__inner">
<div className="sidebar__author">{authorBlock}</div>
<div>
<Menu data={menu} />
<Links data={author} />
<p className="sidebar__copyright">{copyright}</p>
</div>
</div>
</div>
)
}
}
export default Sidebar
My Menu component file, which is added in the Sidebar component file - this is where the error seems to be residing.
import React from 'react'
import { Link } from 'gatsby'
import './style.scss'
class Menu extends React.Component {
render() {
const menu = this.props.data
const menuBlock = (
<ul className="menu__list">
{menu.map(item => (
<li className="menu__list-item" key={item.path}>
<Link
to={item.path}
className="menu__list-item-link"
activeClassName="menu__list-item-link menu__list-item-link--active"
>
{item.label}
</Link>
</li>
))}
</ul>
)
return <nav className="menu">{menuBlock}</nav>
}
}
export default Menu
I am not sure why this is not working, since adding in my PAGETemplateDetails file seem to be working fine:
import React from 'react'
import Sidebar from '../Sidebar'
import './style.scss'
class PageTemplateDetails extends React.Component {
render() {
const page = this.props.data.markdownRemark
return (
<div>
<Sidebar {...this.props} />
<div className="content">
<div className="content__inner">
<div className="page">
<h1 className="page__title">{page.frontmatter.title}</h1>
<div
className="page__body"
/* eslint-disable-next-line react/no-danger */
dangerouslySetInnerHTML={{ __html: page.html }}
/>
</div>
</div>
</div>
</div>
)
}
}
export default PageTemplateDetails
SiteMetadata.menu is queried on the Post template File:
import React from 'react'
import Helmet from 'react-helmet'
import { graphql } from 'gatsby'
import Layout from '../components/Layout'
import PostTemplateDetails from '../components/PostTemplateDetails'
class PostTemplate extends React.Component {
render() {
const { title, subtitle } = this.props.data.site.siteMetadata
const post = this.props.data.markdownRemark
const { title: postTitle, description: postDescription } = post.frontmatter
const description = postDescription !== null ? postDescription : subtitle
return (
<Layout>
<div>
<Helmet>
<title>{`${postTitle} - ${title}`}</title>
<meta name="description" content={description} />
</Helmet>
<PostTemplateDetails {...this.props} />
</div>
</Layout>
)
}
}
export default PostTemplate
export const pageQuery = graphql`
query PostBySlug($slug: String!) {
site {
siteMetadata {
title
subtitle
copyright
author {
name
twitter
}
disqusShortname
url
}
}
markdownRemark(fields: { slug: { eq: $slug } }) {
id
html
fields {
tagSlugs
}
frontmatter {
title
tags
date
description
}
}
}
`
Not sure if this is relevant but this is the post file:
import React from 'react'
import { Link } from 'gatsby'
import moment from 'moment'
import './style.scss'
class Post extends React.Component {
render() {
const {
title,
date,
category,
description,
} = this.props.data.node.frontmatter
const { slug, categorySlug } = this.props.data.node.fields
return (
<div className="post">
<div className="post__meta">
<time
className="post__meta-time"
dateTime={moment(date).format('MMMM D, YYYY')}
>
{moment(date).format('MMMM YYYY')}
</time>
<span className="post__meta-divider" />
<span className="post__meta-category" key={categorySlug}>
<Link to={categorySlug} className="post__meta-category-link">
{category}
</Link>
</span>
</div>
<h2 className="post__title">
<Link className="post__title-link" to={slug}>
{title}
</Link>
</h2>
<p className="post__description">{description}</p>
<Link className="post__readmore" to={slug}>
Read
</Link>
</div>
)
}
}
export default Post
You're passing the property data as "menu" <Menu data={menu} />
In the Menu component, you don't have the menu property, you have this.props.data, which is equal to menu value, as defined in the Sidebar component. Probably there's no such property "menu" on this.props.data
So your code should be const data = this.props or const menu = this.props.data if you want to keep the variable name.
Thank you everyone, it was a query problem as ksav suspected. The Menu and Sidebar components were fine. I failed to query the menu information in my sitemetadata, and was able to fix it by going to my Post Template File and querying the menu, like so:
export const pageQuery = graphql`
query PostBySlug($slug: String!) {
site {
siteMetadata {
title
subtitle
copyright
menu {
label
path
}
author {
name
twitter
}
disqusShortname
url
}
}
markdownRemark(fields: { slug: { eq: $slug } }) {
id
html
fields {
tagSlugs
}
frontmatter {
title
tags
date
description
}
}
}
`
Thank you everyone, I'm struggling but I got there.