I am working on a navbar for my react app (using gatsbyjs to be precise). In the navbar I have a marquee that I initialize in the navbar component in componentDidMount.
It works as intended, but upon every route change componentDidMount will run again which results in the marquee speeding up for every page change, making it go faster and faster.
Is this expected behaviour? And if so, how do I make sure that the script is only run once?
navbar.js
import React from 'react';
import { Link } from 'gatsby';
import styles from '../styles/navbar.module.css';
import NewsMarquee from './newsMarquee';
import Marquee3k from 'marquee3000';
const topLevelNav = [
{
href: '/news',
label: <NewsMarquee/>,
extraClass: styles.navLinkNews,
mediaQueryClass: styles.navLinkHiddenSmall,
},
];
export default class Navbar extends React.Component {
componentDidMount() {
Marquee3k.init();
}
render() {
return (
<div>
<header className={styles.navbar} role="banner">
<nav className={styles.nav}>
{topLevelNav.map(({ href, label, extraClass = '', mediaQueryClass = '' }) => (
<Link
key={label}
to={href}
className={`${styles.navLink} ${extraClass} ${mediaQueryClass} ${menuItemsHidden}`}
activeClassName={styles.navLinkActive}
>
{label}
</Link>
))}
</nav>
</header>
</div>
)
}
}
newsMarquee.js
import React from 'react';
import { StaticQuery, graphql } from "gatsby";
import styles from '../styles/newsMarquee.module.css';
export default () => (
<StaticQuery
query={graphql`
query {
allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC } limit: 10) {
totalCount
edges {
node {
id
frontmatter {
title
date(formatString: "YYYY.MM.DD")
}
fields {
slug
}
}
}
}
}
`}
render={data => (
<div className={`marquee3k ${styles.marquee}`}>
<div>
{data.allMarkdownRemark.edges.map(({ node }) => (
<span className={styles.marqueeItem} key={node.id}>
{node.frontmatter.date} {node.frontmatter.title}
</span>
))}
</div>
</div>
)}
/>
)
Since I'm using GatsbyJS I went with this plugin from V1, which makes my layout component persist across pages.
gatsby-plugin-layout
This plugin enables adding components which live above the page components and persist across page changes.
This can be helpful for:
Persisting layout between page changes for e.g. animating navigation
Storing state when navigating pages
Custom error handling using componentDidCatch
Inject additional data into pages using React Context.
This plugin reimplements the behavior of layout components in gatsby#1, which was removed in version 2.
Related
Small premise: I'm not a great Typescript expert
Hi everyone, I'm working on my personal site, I decided to develop it in Typescript to learn the language.
My component tree is composed, as usual, of App.tsx which render the sub-components, in this case Navbar.jsx and Home.jsx.
Below is the App.jsx code:
import './App.css';
import { BrowserRouter as Router, useRoutes } from 'react-router-dom';
import Home from './components/Home';
import Navbar from './components/Navbar';
import { useState } from 'react';
function App(){
const [navbarScroll,setNavbarScrool]=useState(Object)
const handleLocationChange = (navbarScroll : boolean) => {
setNavbarScrool(navbarScroll)
return navbarScroll
}
const AppRoutes = () => {
let routes = useRoutes([
{ path: "/", element: <Home handleLocationChange={handleLocationChange}/> },
{ path: "component2", element: <></> },
]);
return routes;
};
return (
<Router>
<Navbar navbarScroll={navbarScroll}/>
<AppRoutes/>
</Router>
);
}
export default App;
Here, instead, the Home.jsx code:
import { useInView } from 'react-intersection-observer';
import HomeCSS from "../styles/home.module.css"
import mePhoto from "../assets/me.png"
import { useEffect, useState } from 'react';
interface AppProps {
handleLocationChange: (values: any) => boolean;
}
export default function Home(props: AppProps){
const { ref: containerChange , inView: containerChangeIsVisible, entry} = useInView();
useEffect(()=>{
props.handleLocationChange(containerChangeIsVisible)
//returns false at first render as expected
console.log("Home "+containerChangeIsVisible)
},[])
return(
<>
<div className={`${ HomeCSS.container} ${containerChangeIsVisible? HomeCSS.container_variation: ''}`}>
<div className={HomeCSS.container__children}>
{/* when i scroll on the div the css change (this works)*/}
<h1 className={`${ HomeCSS.container__h1} ${containerChangeIsVisible? HomeCSS.container__h1_variation: ''}`}>My<br/> Name</h1>
<p>Computer Science student.</p>
</div>
<img src={mePhoto} className={HomeCSS.image_style}/>
</div>
<div ref={containerChange} style={{height:800,background:"orange"}}>
<p style={{marginTop:20}}>HIII</p>
</div>
</>
)
}
And Navbar.jsx:
import NavbarCSS from "../styles/navbar.module.css"
import acPhoto from "../assets/ac.png"
import { Link } from "react-router-dom";
import { useEffect, useState } from "react";
interface NavbarScroolProp{
navbarScroll:boolean
}
export default function Navbar(props:NavbarScroolProp){
const [scrollState,setScrollState]=useState(false)
const [pVisible,setpVisible] = useState('')
useEffect(()=>{
setTimeout(() => {
setpVisible("")
}, 3000)
setpVisible("100%")
},[])
//returns false also when should be true
console.log(props.navbarScroll)
return (
<>
{/*the props is undefined so the css doesn't change, i need to do this*/}
<nav className={`${props.navbarScroll?NavbarCSS.nav__variation:NavbarCSS.nav}`}>
<div className={NavbarCSS.nav_row}>
<div className={NavbarCSS.nav_row_container}>
<img src={acPhoto} className={NavbarCSS.image_style}/>
<p className={NavbarCSS.p_style} style={{maxWidth: pVisible}}>My name</p>
</div>
<div className={NavbarCSS.nav_row_tagcontainer}>
<Link className={NavbarCSS.nav_row_tag} to="/"> Home</Link>
<Link className={NavbarCSS.nav_row_tag} to="/"> About</Link>
<Link className={NavbarCSS.nav_row_tag} to="/"> Contact</Link>
</div>
</div>
</nav>
</>
);
}
In my application I want to change the background color whenever the div referring to the InsertionObserver ( I use "useInView" hook , from :https://github.com/thebuilder/react-intersection-observer) is displayed. The problem is that the div in question is in the Home.jsx component and I need to change the color of the divs in the navbar as well when the div in Home is triggered(or other components in case I need to in the future).
The question is: How can I dynamically trigger DOM elements of other components (to then perform certain operations) using the InsertionObserver ?
As you can see from the code I tried to create Props, but everything returns undefined and doesn't involve any changes.
I've tried without useEffect, without using the useInView hook, passing the object instead of the boolean value, but I can't find any solutions to this problem.
You would be of great help to me.
PS: I would like to leave the Navbar.jsx component where it is now, so that it is visible in all components.
Any advice or constructive criticism is welcome.
I'm trying to understand how suspense / await work with nested routes.
The scenario I have:
I have a collection page that shows a list of items.
I have a detail page that shows the details of a single item (this page is a nested route)
What I want to achieve:
When the collection page is loaded, the page shows a waiting indicator while the data is being fetched
(I have added a random delay to simulate this).
Once the data is loaded, if I click in one of the items it will navigate to the nested detail page
(this page is also wrapped in a suspense / await), so we get on the left side the list and the right side
the detail page for the selected character, ... here I'm showing a loading indicator.
The expected behavior:
Collection loading indicator is shown when the collection page is loaded.
Detail loading indicator is shown when the detail page is loaded.
The actual behavior:
Collection loading indicator is shown when the collection page is loaded.
Detail loading indicator is shown when the detail page is loaded, plus list page is reloaded.
How I have defined the routes:
routes.tsx
import React from "react";
import { defer, createBrowserRouter } from "react-router-dom";
import { ListPage, DetailPage } from "./pages";
import { getCharacter, getCharacterCollection } from "./api";
export const applicationRoutes = createBrowserRouter([
{
path: "/",
loader: () => defer({ characterCollection: getCharacterCollection("") }),
element: <ListPage />,
children: [
{
path: ":id",
loader: ({ params }) =>
defer({ character: getCharacter(params.id as string) }),
element: <DetailPage />
}
]
}
]);
How I have implemented each page:
list.tsx
import React from "react";
import { Await, Link, Outlet, useLoaderData } from "react-router-dom";
import { Character } from "../api/model";
export const ListPage = () => {
const characters = useLoaderData() as { characterCollection: Character[] };
return (
<div>
<h2>Character Collection</h2>
<React.Suspense
fallback={<h4 style={{ color: "green" }}>👬 Loading characters...</h4>}
>
<Await resolve={characters.characterCollection}>
{(characters) => (
<div style={{ display: "flex", flexDirection: "row" }}>
<div>
<ul>
{characters.map((character) => (
<li key={character.id}>
<Link to={`./${character.id}`}>{character.name}</Link>
</li>
))}
</ul>
</div>
<Outlet />
</div>
)}
</Await>
</React.Suspense>
</div>
);
};
detail.tsx
import React from "react";
import { Await, useLoaderData } from "react-router-dom";
import { Character } from "../api/model";
export const DetailPage: React.FunctionComponent = () => {
const data = useLoaderData() as { character: Promise<Character> };
return (
<div>
<h1>Detail</h1>
<React.Suspense
fallback={<h4 style={{ color: "blue" }}> 👩🏻 Loading character...</h4>}
></React.Suspense>
<Await resolve={data.character}>
{(character) => (
<div>
<img src={character.image} alt={character.name} />
<h2>{character.name}</h2>
<p>{character.status}</p>
</div>
)}
</Await>
</div>
);
};
Codesandbox example: https://codesandbox.io/s/vigilant-agnesi-z7tvk1
It seems like the way that I'm using to navigate just forces a full reload:
<Link to={`./${character.id}`}>{character.name}</Link>
Instead of updating only loading the nested page, is this behavior by default? or am I doing something wrong?
i am very new to React/NEXT.JS and i guess i am facing a issue where i want to fetch data for my dynamic navigation bar, which is available on all the pages. I am using context API to set the value and fetch 1 time at the initial render of the whole document. Then it should show navigation items directly.
Here is how i am trying to do it.
import { createContext, useState, useEffect } from "react";
import Layout from "../components/layout";
import "../styles/globals.css";
import BasicProviders from "../helpers/BasicProviders";
export const NavContext = createContext();
function MyApp({ Component, pageProps, navigationItems }) {
const [navitems, setNavitems] = useState(navigationItems);
return (
<NavContext.Provider value={{ navitems }}>
<Layout>
<Component {...pageProps} />
</Layout>
</NavContext.Provider>
);
}
MyApp.getInitialProps = async (ctx) => {
const res = await fetch("http://www.myserver.com/navigation");
console.log(res);
return { navigationItems: res };
};
export default MyApp;
Now i have folder structure like this:
- components (in root directory of next.js)
- layout.js
- /Header
- DesktopNav.js <<<<<--------------- here i want to send my value of **navitems**
- Header.js
This is how my DesktopNav.js is looking :
import Link from "next/link";
import React, { useContext } from "react";
import NavContext from "../../stores/NavContext";
import DesktopSubMenuLoop from "./DesktopSubMenuLoop";
export default function DesktopNav() {
const { navitems } = useContext(NavContext);
return (
<nav>
<DesktopSubMenuLoop navitems={navitems}></DesktopSubMenuLoop>
</nav>
);
}
Somehow it is sending the data to the component. But everytime i click on the Navigation Link it reloads the complete page. Not sure how i can achieve this. For the time being i am using useEffect to do achieve the same. But that is not SEO compatible.
Problems need to be solved here:
API should be called once for whole page. As the navigation will stay same for all the pages.
Page routing should be as smooth as it is with static tags. Currently it reloads the page like a static HTML web page.
Possible solutions i though of:
is there any way i can fetch the navitems on _document.js and send to _app.js?
Calling the API on each page with getServerSideProps but that will make all of my pages non-static for internal pages as well at the build time.
Any solution i can get on this will be really helpful and thankful.
#juliomalves for you:
MainNavigation Loop currently i am using useEffect to display the navigation, but as per my understanding its not good for SEO.
You can check my loop here DesktopSubMenuLoop.js
import Link from "next/link";
import React from "react";
export default function DesktopSubMenuLoop({ navitems, isSubmenu = false }) {
return (
<ul className={isSubmenu ? "sub-menu-style" : ""}>
{navitems.map((item, index) => (
<li key={item.id}>
<Link href={item.slug !== "/" ? `/${item.slug}` : item.slug}>
<a>
{item.title}{" "}
{item.children && item.children.length > 0 && (
<i className="icon-arrow-down"></i>
)}
</a>
</Link>
{item.children && item.children.length > 0 && (
<DesktopSubMenuLoop
key={item.id}
navitems={item.children}
isSubmenu={true}
></DesktopSubMenuLoop>
)}
</li>
))}
</ul>
);
}
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.
Context: In the app, I am using react-slick to allows users to navigate through components like a carousel. (NOTE: as users navigate through the carousel, the URL for the application does not change/update; always https: //myApplicationURL.com)
What I am attempting to accomplish: Each component within the carousel uses react-ga to initialize and track pageview analytics on a component level.
What I expect: Google Analytics dashboard will return the correct component name a user is currently viewing.
What is actually happening: Google Analytics dashboard displays an incorrect component name. (Ex: application is on contact component - should display '/contact' but GA dashboard displays another component name)
**CAROUSEL COMPONENT**
import React, { Component } from "react";
import Slider from "react-slick";
import ReactGA from 'react-ga';
import About from '../../components/about';
import {default as Project1} from '../../components/projectTemplate';
import {default as Project2} from '../../components/projectTemplate';
import {default as Project3} from '../../components/projectTemplate';
import {default as Project4} from '../../components/projectTemplate';
import Contact from '../../components/contact';
export default class Carousel extends Component {
constructor(props) {
super(props);
this.state = {
nav1: null,
nav2: null,
pageNumber: 0
};
}
componentDidMount() {
this.setState({
nav1: this.slider1,
nav2: this.slider2
});
}
afterChangeHandler = currentSlide => {
this.setState({
pageNumber: currentSlide++
})
};
render() {
const carousel1 = {
asNavFor: this.state.nav2,
ref: slider => (this.slider1 = slider),
afterChange: this.afterChangeHandler
}
const carousel2 = {
asNavFor: this.state.nav1,
ref: slider => (this.slider2 = slider),
}
return (
<div id="carousel-container">
<Slider {...carousel1}>
<div>
<About props={this.props.props} />
</div>
<div>
<Project1 project={this.props.props.project[0]} />
</div>
<div>
<Project2 project={this.props.props.project[1]} />
</div>
<div>
<Project3 project={this.props.props.project[2]} />
</div>
<div>
<Project4 project={this.props.props.project[3]} />
</div>
<div>
<Contact {/*props*/} />
</div>
</Slider>
<Slider {...carousel2}>
{/*slider2 content*/}
</Slider>
</div>
);
}
}
**ABOUT COMPONENT**
import React from 'react';
import ReactGA from 'react-ga';
const About = props => {
//Google Analytics
ReactGA.initialize('[User ID removed]');
ReactGA.ga('set', 'page', '/about');
ReactGA.ga('send', 'pageview');
return(
<div id="aboutContainer">
{/*Component Content*/}
</div>
);
};
export default About;
**PROJECT COMPONENT**
import React from 'react';
import ReactGA from 'react-ga';
const ProjectTemp = props => {
const name = props.project.name
// Google Analytics
ReactGA.initialize('[User ID removed]');
ReactGA.ga('set', 'page', `/project/${name}`);
ReactGA.ga('send', 'pageview');
return(
<div id="projectTempContainer">
{/*Project Content*/}
</div>
);
};
export default ProjectTemp;
**CONTACT COMPONENT**
import React from 'react';
import ReactGA from 'react-ga';
const Contact = props => {
//Google Analytics
ReactGA.initialize('[User ID removed]');
ReactGA.ga('set', 'page', '/contact');
ReactGA.ga('send', 'pageview');
return(
<div id="contactContainer">
{/*Contact Content*/}
</div>
);
};
export default Contact;
I suggest using the Segment analytics library and following our React quickstart guide to track page calls. If you are rendering individual components inside the carousel, you can use componentDidMount to invoke page calls. You’ll be able to manually set the page name via the parameter, which will help you avoid the issue you’re having with /contact. The example below shows one way you could do this:
export default class CarouselContact extends Component {
componentDidMount() {
window.analytics.page('Contact');
}
render() {
return (
<h1>
Contact page.
</h1>
);
}
}
I’m the maintainer of https://github.com/segmentio/analytics-react. With Segment, you’ll be able to switch different destinations on-and-off by the flip of a switch if you are interested in trying multiple analytics tools (we support over 250+ destinations) without having to write any additional code. 🙂