How to get an component height to trigger Headroom in React? - javascript

This is my first time dealing with Gatsby and React, so I might be using the wrong approach on this matter. Anyway, this is what is going on.
From the gatsby-starter-hello-world, I'm building this site that will be composed of a front page with a Hero on the top, holding the intro information. Right bellow, I'm intending to insert some content (I'm not sure about what yet), with this <Header /> appearing on scroll. For that part, I'm intending on use Headroom.js, which already works in the site.
The thing is I need it to be triggered only after the bottom of the Hero component touches the top of the viewport. And this will happen only in desktop and laptops. On mobile I intend to make it a fixed navbar.
Anyway, right now, this is what I have for a Layout.js
import React from "react"
import PropTypes from "prop-types"
import { useStaticQuery, graphql } from "gatsby"
import Hero from "./index/hero"
import Header from "./header"
import Headroom from "react-headroom"
const Layout = ({ children }) => {
const data = useStaticQuery(graphql`
query SiteTitleQuery {
site {
siteMetadata {
title
}
}
}
`)
function() getHeroSize {
var heroHeight = Hero.clientHeight;
}
return (
<>
<Hero siteTitle={data.site.siteMetadata.title} ref="inner" />
<div className={"container mx-auto"}>
<Headroom pinStart={heroHeight}>
<Header siteTitle={data.site.siteMetadata.title} />
</Headroom>
<div
//style={{
// margin: `0 auto`,
// maxWidth: 960,
// padding: `0px 1.0875rem 1.45rem`,
// paddingTop: 0,
//}}
>
<main>{children}</main>
<footer>
© {new Date().getFullYear()}, Construído com
{` `}
Gatsby
</footer>
</div>
</div>
</>
)
}
Layout.propTypes = {
children: PropTypes.node.isRequired,
}
export default Layout
The function getHeroSize above is more like an intention display of what I'm thinking. It doesn't really work.
I'm using Tailwind CSS and sourcing some content from Trello using gatsby-source-trello. Not yet sure how to make this Layout.js, but this is what I've got from some testing that worked pretty good, so far. I understand that there'll be some work to do within gatsby-node.js, but I believe this header will be there in any other page I create, so.
Any thoughts, suggestions or links to documentation would be really appreaciated. Thanks in advance!

Your Hero component would need to use forwardRef to pass the ref to the nearest React element:
const Hero = React.forwardRef((props, ref) =>
<div ref={ref}>
{props.title}
</div>
Then you'll need to create a ref in Layout and pass that to Hero:
import React, { useRef } from "react"
const Layout = ({ children }) => {
const heroRef = useRef()
// ...
return (
<>
<Hero title={data.site.siteMetadata.title} ref={heroRef} />
{/* ... */}
</>
)
}
Finally, you'll want to use a hook to measure the actual height of the heroRef.current element. Here's one I use:
import { useEffect, useState } from "react"
import debounce from "lodash/debounce"
export default (ref, ttl = 100) => {
const [dimensions, setDimensions] = useState()
useEffect(() => {
if (!ref.current) return
const measure = debounce(() => {
if (ref.current) {
setDimensions(ref.current.getBoundingClientRect())
}
}, ttl)
measure()
window.addEventListener("resize", measure)
return () => {
window.removeEventListener("resize", measure)
}
}, [ref, ttl])
return dimensions
}
And here's how you might add that to Layout:
import useElementDimensions from "hooks/useElementDimensions"
const Layout = ({ children }) => {
const heroRef = useRef()
const heroDims = useElementDimensions(heroRef) || { top: 0, height: 0 }
const heroBottom = heroDims.top + heroDims.height
return (
<Headroom pinStart={heroBottom}>
<Header siteTitle={data.site.siteMetadata.title} />
</Headroom>
)
}

Related

Show a skeleton placeholder until Facebook Comments component loads completely in React (Next.js)

Using Next.js, I want to show a skeleton placeholder until Facebook Comments component loads completely.
Here is the code.
import { useState, useEffect } from "react";
import { initFacebook } from "../utils/initFacebook";
export default function IndexPage() {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const loadFacebook = async () => {
await initFacebook();
setLoaded(true);
};
loadFacebook();
}, []);
const skeletonComponent = (
<div>
<h1>Some skeleton placeholder</h1>
</div>
);
const facebookComponent = (
<div
className="fb-comments"
data-href="https://developers.facebook.com/docs/plugins/comments#configurator"
data-width="580"
data-numposts="10"
/>
);
return (
<div>
{loaded ? facebookComponent : skeletonComponent}
</div>
);
}
I'm using the state to switch between two components.
But the skeleton component does not wait until the Facebook component is fully loaded, and therefore users see the blank screen for about 3-5 seconds.
How should I go about having the skeleton component wait out until the Facebook component is visible?
The full code is available on CodeSandbox.
Any help would be appreciated.

MaterialUI Redux connect() using TextArea and submitting form. Warning about refs in func components [duplicate]

I have the following (using Material UI)....
import React from "react";
import { NavLink } from "react-router-dom";
import Tabs from "#material-ui/core/Tabs";
import Tab from "#material-ui/core/Tab";
function LinkTab(link){
return <Tab component={NavLink}
to={link.link}
label={link.label}
value={link.link}
key={link.link}
/>;
}
In the new versions this causes the following warning...
Warning: Function components cannot be given refs. Attempts to access
this ref will fail. Did you mean to use React.forwardRef()?
Check the render method of ForwardRef.
in NavLink (created by ForwardRef)
I tried changing to...
function LinkTab(link){
// See https://material-ui.com/guides/composition/#caveat-with-refs
const MyLink = React.forwardRef((props, ref) => <NavLink {...props} ref={ref} />);
return <Tab component={MyLink}
to={link.link}
label={link.label}
value={link.link}
key={link.link}
/>;
}
But I still get the warning. How do I resolve this issue?
Just give it as innerRef,
// Client.js
<Input innerRef={inputRef} />
Use it as ref.
// Input.js
const Input = ({ innerRef }) => {
return (
<div>
<input ref={innerRef} />
</div>
)
}
NavLink from react-router is a function component that is a specialized version of Link which exposes a innerRef prop for that purpose.
// required for react-router-dom < 6.0.0
// see https://github.com/ReactTraining/react-router/issues/6056#issuecomment-435524678
const MyLink = React.forwardRef((props, ref) => <NavLink innerRef={ref} {...props} />);
You could've also searched our docs for react-router which leads you to https://mui.com/getting-started/faq/#how-do-i-use-react-router which links to https://mui.com/components/buttons/#third-party-routing-library. The last link provides a working example and also explains how this will likely change in react-router v6
You can use refs instead of ref. This only works as it avoids the special prop name ref.
<InputText
label="Phone Number"
name="phoneNumber"
refs={register({ required: true })}
error={errors.phoneNumber ? true : false}
icon={MailIcon}
/>
In our case, we were was passing an SVG component (Site's Logo) directly to NextJS's Link Component which was a bit customized and we were getting such error.
Header component where SVG was used and was "causing" the issue.
import Logo from '_public/logos/logo.svg'
import Link from '_components/link/Link'
const Header = () => (
<div className={s.headerLogo}>
<Link href={'/'}>
<Logo />
</Link>
</div>
)
Error Message on Console
Function components cannot be given refs. Attempts to access this ref will fail.
Did you mean to use React.forwardRef()?
Customized Link Component
import NextLink from 'next/link'
import { forwardRef } from 'react'
const Link = ({ href, shallow, replace, children, passHref, className }, ref) => {
return href ? (
<NextLink
href={href}
passHref={passHref}
scroll={false}
shallow={shallow}
replace={replace}
prefetch={false}
className={className}
>
{children}
</NextLink>
) : (
<div className={className}>{children}</div>
)
}
export default forwardRef(Link)
Now we made sure we were using forwardRef in the our customized Link Component but we still got that error.
In order to solve it, I changed the wrapper positioning of SVG element to this and :poof:
const Header = () => (
<Link href={'/'}>
<div className={s.headerLogo}>
<Logo />
</div>
</Link>
)
If you find that you cannot add a custom ref prop or forwardRef to a component, I have a trick to still get a ref object for your functional component.
Suppose you want to add ref to a custom functional component like:
const ref = useRef();
//throws error as Button is a functional component without ref prop
return <Button ref={ref}>Hi</Button>;
You can wrap it in a generic html element and set ref on that.
const ref = useRef();
// This ref works. To get button html element inside div, you can do
const buttonRef = ref.current && ref.current.children[0];
return (
<div ref={ref}>
<Button>Hi</Button>
</div>
);
Of course manage state accordingly and where you want to use the buttonRef object.
to fix this warning you should wrap your custom component with the forwardRef function as mentioned in this blog very nicely
const AppTextField =(props) {return(/*your component*/)}
change the above code to
const AppTextField = forwardRef((props,ref) {return(/*your component*/)}
const renderItem = ({ item, index }) => {
return (
<>
<Item
key={item.Id}
item={item}
index={index}
/>
</>
);
};
Use Fragment to solve React.forwardRef()? warning
If you're using functional components, then React.forwardRef is a really nice feature to know how to use for scenarios like this. If whoever ends up reading this is the more hands on type, I threw together a codesandbox for you to play around with. Sometimes it doesn't load the Styled-Components initially, so you may need to refresh the inline browser when the sandbox loads.
https://codesandbox.io/s/react-forwardref-example-15ql9t?file=/src/App.tsx
// MyAwesomeInput.tsx
import React from "react";
import { TextInput, TextInputProps } from "react-native";
import styled from "styled-components/native";
const Wrapper = styled.View`
width: 100%;
padding-bottom: 10px;
`;
const InputStyled = styled.TextInput`
width: 100%;
height: 50px;
border: 1px solid grey;
text-indent: 5px;
`;
// Created an interface to extend the TextInputProps, allowing access to all of its properties
// from the object that is created from Styled-Components.
//
// I also define the type that the forwarded ref will be.
interface AwesomeInputProps extends TextInputProps {
someProp?: boolean;
ref?: React.Ref<TextInput>;
}
// Created the functional component with the prop type created above.
//
// Notice the end of the line, where you wrap everything in the React.forwardRef().
// This makes it take one more parameter, called ref. I showed what it looks like
// if you are a fan of destructuring.
const MyAwesomeInput: React.FC<AwesomeInputProps> = React.forwardRef( // <-- This wraps the entire component, starting here.
({ someProp, ...props }, ref) => {
return (
<Wrapper>
<InputStyled {...props} ref={ref} />
</Wrapper>
);
}); // <-- And ending down here.
export default MyAwesomeInput;
Then on the calling screen, you'll create your ref variable and pass it into the ref field on the component.
// App.tsx
import React from "react";
import { StyleSheet, Text, TextInput, View } from "react-native";
import MyAwesomeInput from "./Components/MyAwesomeInput";
const App: React.FC = () => {
// Set some state fields for the inputs.
const [field1, setField1] = React.useState("");
const [field2, setField2] = React.useState("");
// Created the ref variable that we'll use down below.
const field2Ref = React.useRef<TextInput>(null);
return (
<View style={styles.app}>
<Text>React.forwardRef Example</Text>
<View>
<MyAwesomeInput
value={field1}
onChangeText={setField1}
placeholder="field 1"
// When you're done typing in this field, and you hit enter or click next on a phone,
// this makes it focus the Ref field.
onSubmitEditing={() => {
field2Ref.current.focus();
}}
/>
<MyAwesomeInput
// Pass the ref variable that's created above to the MyAwesomeInput field of choice.
// Everything should work if you have it setup right.
ref={field2Ref}
value={field2}
onChangeText={setField2}
placeholder="field 2"
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
app: {
flex: 1,
justifyContent: "center",
alignItems: "center"
}
});
export default App;
It's that simple! No matter where you place the MyAwesomeInput component, you'll be able to use a ref.
I just paste here skychavda solution, as it provide a ref to a child : so you can call child method or child ref from parent directly, without any warn.
source: https://github.com/reactjs/reactjs.org/issues/2120
/* Child.jsx */
import React from 'react'
class Child extends React.Component {
componentDidMount() {
const { childRef } = this.props;
childRef(this);
}
componentWillUnmount() {
const { childRef } = this.props;
childRef(undefined);
}
alertMessage() {
window.alert('called from parent component');
}
render() {
return <h1>Hello World!</h1>
}
}
export default Child;
/* Parent.jsx */
import React from 'react';
import Child from './Child';
class Parent extends React.Component {
onClick = () => {
this.child.alertMessage(); // do stuff
}
render() {
return (
<div>
<Child childRef={ref => (this.child = ref)} />
<button onClick={this.onClick}>Child.alertMessage()</button>
</div>
);
}
}

How to navigate to another page with a smooth scroll on a specific id with react router and react scroll

i am trying to make navBar that navigate to another page and scroll down to some id on this page
this is the navBar component for my protofolio whih have 3 links the Projects should go to Home and then scroll down to projects id in the Project section
import React, { Component } from "react";
import logo from "../../images/logo.png";
import { Link } from "react-router-dom";
import Scroll from "react-scroll";
import "./NavBar.css";
const ScrollLink = Scroll.Link;
class NavBar extends Component {
render() {
return (
<div className="NavBar">
<Link to="/" className="logo-container">
<img className="logo" src={logo} alt="Mahboub logo"></img>
</Link>
<div className="Nav-links">
<Link to="/">Home</Link>
<ScrollLink
className="navy"
smooth={true}
duration={500}
to="projects"
>
projects
</ScrollLink>
<Link to="/">Contacts</Link>
</div>
</div>
);
}
}
export default NavBar;
here is the prjects component for project section in home page
function Projects() {
return (
<div className="Home-Projects">
<Element id="projects">
<h1>Projects</h1>
</Element>
<div className="Projects-container">
<ProjectElement />
</div>
</div>
);
}
If "duration" parameter isn't of great importance for you and you have no problems with Hooks, I'd like to suggest solution as follows.
You get rid of Scroll component and use plain Link from RR in your NavBar like so
<Link to="/#projects">projects</Link>
Since RR v5.1 Hooks was introduced. There is useLocation hook among them. Insert that hook at the very beginning of your Projects component like so:
function Projects() {
const location = useLocation()
...
When you hit Link from p.1, you get new location object of the form
location = {
...
pathname:"/",
...
hash: "#projects"
}
Call useEffect hook after useLocation like so:
useEffect(()=> {
if (location.hash) {
let elem = document.getElementById(location.hash.slice(1))
if (elem) {
elem.scrollIntoView({behavior: "smooth"})
}
} else {
window.scrollTo({top:0,left:0, behavior: "smooth"})
}
}, [location,])
And thats it. As you can see, if there is no hash in your url, your page would scroll to the top. Use [locaton,] as useEffect dependency prevents form scrolling when your page component rerenders not because of location changes.
I just add this in the index.css
html {
scroll-behavior: smooth;
}
and this
<ScrollLink
className="navy"
smooth={true}
duration={500}
to="projects"
>
projects
</ScrollLink>
became
Projects
this was so fast and simple and worked prefectly
For next.js
//https://yourdomain.com/page?scroll=scroll_here <---
import { useEffect } from 'react';
import { Flex } from '#chakra-ui/react';
import { useRouter } from 'next/router';
export default function DecisionAreaFlex(props) {
const router = useRouter();
useEffect(() => {
console.log('router query', router.query.scroll);
if (router.query.scroll) {
let elem = document.getElementById(router.query.scroll);
if (elem) {
elem.scrollIntoView({ behavior: 'smooth' });
}
} else {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
}
}, [router]);
return (
<Flex
alignSelf="center"
justifyContent="center"
alignItems="center"
mt="15px"
id={'scroll_here'} // <---
>
{props.children}
</Flex>
);
}
I've created a query param called anchor, and with that I can control the scroll based on the useEffect.
The link: /about?anchor=capabilities
The code on the page which should have the smooth scroll:
const router = useRouter();
useEffect(()=> {
if (router?.query?.anchor) {
let elem = document.getElementById(router?.query?.anchor)
if (elem) {
elem.scrollIntoView({behavior: "smooth"});
}
}
}, [router?.query?.anchor])
here is the simplest solution for scrolling to an id from another page.
use this code on the page where you have sections with any xyz id.
now on the other pages use simple NavLink from react-router-dom having "to" attribute like this to="/#xy"
let location = useLocation()
useEffect(()=> {
if (location.hash) {
let elem = document.getElementById(location.hash.slice(1))
if(elem) {
elem.scrollIntoView({behavior: "smooth"})
}
} else {
window.scrollTo({top:0,left:0, behavior: "smooth"})
}
}, [location,])

How to set data from a react hook inside of a function or event?

I'm trying to learn to create hooks so I can re-use data that I have to change in different components.
I'm using Material UI's Tabs and need to use useTab, a custom hook to change the tab id.
import React, { useContext } from 'react';
import { ProductsContext } from './ProductsContext';
import AppBar from '#material-ui/core/AppBar';
import Tabs from '#material-ui/core/Tabs';
import Tab from '#material-ui/core/Tab';
import { useTab } from '../../hooks/tab';
const ProductsNav = () => {
const {products, categories, loading} = useContext(ProductsContext);
const [tabValue] = useTab(0);
const handleTabChange = (e, newTabValue) => {
useTab(newTabValue);
}
return (
<div className="products">
<AppBar position="static">
<Tabs value={tabValue} onChange={ handleTabChange }>
{
Array.from(categories).map(category => (
!category.unlisted && (<Tab label={category.title} key={category.id}/>)
))
}
</Tabs>
</AppBar>
</div>
);
};
export default ProductsNav;
I know it does this with child functions in the docs, but I'm trying to not just copy and paste and do it in my own way.
Here is my custom useTab hook:
import {useState, useEffect} from 'react';
export const useTab = (selectedTab) => {
const [tabValue, setTabValue] = useState(0);
useEffect(() => {
setTabValue(selectedTab);
}, []);
return [tabValue];
}
I'm of course getting an error I can't use a hook inside of a function, but I'm confused how else to do this.
How can I change tabValue from useTabs?
The error is probably here:
const handleTabChange = (e, newTabValue) => {
useTab(newTabValue);
}
You're violating one of the primary Rules of Hooks:
Don’t call Hooks inside loops, conditions, or nested functions.
Instead, always use Hooks at the top level of your React function.
The reason for this rule is a bit complex but it basically boils down to the idea that hooks should only be called at the top level of a React functional component because they must be guaranteed to run every time the component function is run.
Hence why you're getting an error "I can't use a hook inside of a function"...
At any rate, it is unclear why you are using a custom hook with a useEffect() here. That seems completely unnecessary - a regular useEffect() hook inside of your nav component should more than suffice:
const ProductsNav = () => {
const {products, categories, loading} = useContext(ProductsContext);
const [tabValue, setTabValue] = useState(0);
const handleTabChange = (e, newTabValue) => {
setTabValue(newTabValue);
}
return (
<div className="products">
<AppBar position="static">
<Tabs value={tabValue} onChange={ handleTabChange }>
{
Array.from(categories).map(category => (
!category.unlisted && (<Tab label={category.title} key={category.id}/>)
))
}
</Tabs>
</AppBar>
</div>
);
};

ReactJS how to render a component only when scroll down and reach it on the page?

I have a react component Data which includes several charts components; BarChart LineChart ...etc.
When Data component starts rendering, it takes a while till receiving the data required for each chart from APIs, then it starts to respond and render all the charts components.
What I need is, to start rendering each chart only when I scroll down and reach it on the page.
Is there any way could help me achieving this??
You have at least three options how to do that:
Track if component is in viewport (visible to user). And then render it. You can use this HOC https://github.com/roderickhsiao/react-in-viewport
Track ‘y’ scroll position explicitly with https://react-fns.netlify.com/docs/en/api.html#scroll
Write your own HOC using Intersection Observer API https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
To render component you may need another HOC, which will return Chart component or ‘null’ based on props it receives.
I have tried many libraries but couldn't find something that best suited my needs so i wrote a custom hook for that, I hope it helps
import { useState, useEffect } from "react";
const OPTIONS = {
root: null,
rootMargin: "0px 0px 0px 0px",
threshold: 0,
};
const useIsVisible = (elementRef) => {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
if (elementRef.current) {
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.unobserve(elementRef.current);
}
});
}, OPTIONS);
observer.observe(elementRef.current);
}
}, [elementRef]);
return isVisible;
};
export default useIsVisible;
and then you can use the hook as follows :
import React, { useRef } from "react";
import useVisible from "../../hooks/useIsVisible";
function Deals() {
const elemRef = useRef();
const isVisible = useVisible(elemRef);
return (
<div ref={elemRef}>hello {isVisible && console.log("visible")}</div>
)}
I think the easiest way to do this in React is using react-intersection-observer.
Example:
import { useInView } from 'react-intersection-observer';
const Component = () => {
const { ref, inView, entry } = useInView({
/* Optional options */
threshold: 0,
});
useEffect(()=>{
//do something here when inView is true
}, [inView])
return (
<div ref={ref}>
<h2>{`Header inside viewport ${inView}.`}</h2>
</div>
);
};
I also reccommend using triggerOnce: true in the options object so the effect only happens the first time the user scrolls to it.
you can check window scroll position and if the scroll position is near your div - show it.
To do that you can use simple react render conditions.
import React, {Component} from 'react';
import PropTypes from 'prop-types';
class MyComponent extends Component {
constructor(props){
super(props);
this.state = {
elementToScroll1: false,
elementToScroll2: false,
}
this.firstElement = React.createRef();
this.secondElement = React.createRef();
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
handleScroll(e){
//check if scroll position is near to your elements and set state {elementToScroll1: true}
//check if scroll position is under to your elements and set state {elementToScroll1: false}
}
render() {
return (
<div>
<div ref={this.firstElement} className={`elementToScroll1`}>
{this.state.elementToScroll1 && <div>First element</div>}
</div>
<div ref={this.secondElement} className={`elementToScroll2`}>
{this.state.elementToScroll2 && <div>Second element</div>}
</div>
</div>
);
}
}
MyComponent.propTypes = {};
export default MyComponent;
this may help you, it's just a quick solution. It will generate you some rerender actions, so be aware.

Categories