I am trying to implement a loading screen when changing routes in my Next.js app, for example /home -> /about.
My current implementation is as follows. I am setting the initial loaded state to false and then changing it on componentDidMount. I am also calling the Router.events.on function inside componentDidMount to change the loading state when the route change starts.
_app.js in pages folder
class MyApp extends App {
constructor(props) {
super(props);
this.state = {
loaded: false,
};
}
componentDidMount() {
this.setState({ loaded: true });
Router.events.on('routeChangeStart', () => this.setState({ loaded: false }));
Router.events.on('routeChangeComplete', () => this.setState({ loaded: true }));
}
render() {
const { Component, pageProps } = this.props;
const { loaded } = this.state;
const visibleStyle = {
display: '',
transition: 'display 3s',
};
const inVisibleStyle = {
display: 'none',
transition: 'display 3s',
};
return (
<Container>
<>
<span style={loaded ? inVisibleStyle : visibleStyle}>
<Loader />
</span>
<span style={loaded ? visibleStyle : inVisibleStyle}>
<Component {...pageProps} />
</span>
</>
</Container>
);
}
}
This works perfectly fine but I feel like there may be a better solution more elegant solution. Is this the only way which isn't cumbersome to implement this loading feature or is there an alternative ?
Using the new hook api,
this is how I would do it..
function Loading() {
const router = useRouter();
const [loading, setLoading] = useState(false);
useEffect(() => {
const handleStart = (url) => (url !== router.asPath) && setLoading(true);
const handleComplete = (url) => (url === router.asPath) && setLoading(false);
router.events.on('routeChangeStart', handleStart)
router.events.on('routeChangeComplete', handleComplete)
router.events.on('routeChangeError', handleComplete)
return () => {
router.events.off('routeChangeStart', handleStart)
router.events.off('routeChangeComplete', handleComplete)
router.events.off('routeChangeError', handleComplete)
}
})
return loading && (<div>Loading....{/*I have an animation here*/}</div>);
}
Now <Loading/> is going to show up whenever the route will change...
I animate this using react-spring, but you can use any library you prefer to do this.
You can even take a step further and modify when the component shows up by modifying the handleStart and handleComplete methods that gets a url.
Why not use nprogress as follows in _app.js
import React from 'react';
import Router from 'next/router';
import App, { Container } from 'next/app';
import NProgress from 'nprogress';
NProgress.configure({ showSpinner: publicRuntimeConfig.NProgressShowSpinner });
Router.onRouteChangeStart = () => {
// console.log('onRouteChangeStart triggered');
NProgress.start();
};
Router.onRouteChangeComplete = () => {
// console.log('onRouteChangeComplete triggered');
NProgress.done();
};
Router.onRouteChangeError = () => {
// console.log('onRouteChangeError triggered');
NProgress.done();
};
export default class MyApp extends App { ... }
Link to nprogress.
You also need to include style file as well. If you put the css file in static directory, then you can access the style as follows:
<link rel="stylesheet" type="text/css" href="/static/css/nprogress.css" />
Make sure the CSS is available in all pages...
It will work for all your routes changing.
For anyone coming across this in 2021, the package nextjs-progressbar makes this super easy. In your Next.js _app.js, simply add:
import NextNProgress from 'nextjs-progressbar';
export default function MyApp({ Component, pageProps }) {
return (
<>
<NextNProgress />
<Component {...pageProps} />;
</>
);
}
And done!
Demo and screenshot:
New Update with NProgress:
import Router from 'next/router'
import Link from 'next/link'
import Head from 'next/head'
import NProgress from 'nprogress'
Router.events.on('routeChangeStart', (url) => {
console.log(`Loading: ${url}`)
NProgress.start()
})
Router.events.on('routeChangeComplete', () => NProgress.done())
Router.events.on('routeChangeError', () => NProgress.done())
export default function App({ Component, pageProps }) {
return (
<>
<Head>
{/* Import CSS for nprogress */}
<link rel="stylesheet" type="text/css" href="/nprogress.css" />
</Head>
<Component {...pageProps} />
</>
)
}
If you use Tailwind CSS, copy the code from here: https://unpkg.com/nprogress#0.2.0/nprogress.css and paste the code into your global CSS file.
if you want to disable the spinner add the below code in your _app.tsx/jsx file and remove the spinner styles from CSS.
NProgress.configure({ showSpinner: false });
Source Links:
https://github.com/rstacruz/nprogress
https://nextjs.org/docs/api-reference/next/router
Progress bar like NProgress in 90 lines of code (vs NProgress v0.2.0 is 470 lines .js + 70 lines .css):
import { useEffect, useReducer, useRef } from 'react';
import { assert } from './assert';
import { wait } from './wait';
import { getRandomInt } from './getRandomNumber';
let waitController: AbortController | undefined;
// https://gist.github.com/tkrotoff/db8a8106cc93ae797ea968d78ea28047
export function useProgressBar({
trickleMaxWidth = 94,
trickleIncrementMin = 1,
trickleIncrementMax = 5,
dropMinSpeed = 50,
dropMaxSpeed = 150,
transitionSpeed = 600
} = {}) {
// https://stackoverflow.com/a/66436476
const [, forceUpdate] = useReducer(x => x + 1, 0);
// https://github.com/facebook/react/issues/14010#issuecomment-433788147
const widthRef = useRef(0);
function setWidth(value: number) {
widthRef.current = value;
forceUpdate();
}
async function trickle() {
if (widthRef.current < trickleMaxWidth) {
const inc =
widthRef.current +
getRandomInt(trickleIncrementMin, trickleIncrementMax); // ~3
setWidth(inc);
try {
await wait(getRandomInt(dropMinSpeed, dropMaxSpeed) /* ~100 ms */, {
signal: waitController!.signal
});
await trickle();
} catch {
// Current loop aborted: a new route has been started
}
}
}
async function start() {
// Abort current loops if any: a new route has been started
waitController?.abort();
waitController = new AbortController();
// Force the show the JSX
setWidth(1);
await wait(0);
await trickle();
}
async function complete() {
assert(
waitController !== undefined,
'Make sure start() is called before calling complete()'
);
setWidth(100);
try {
await wait(transitionSpeed, { signal: waitController.signal });
setWidth(0);
} catch {
// Current loop aborted: a new route has been started
}
}
function reset() {
// Abort current loops if any
waitController?.abort();
setWidth(0);
}
useEffect(() => {
return () => {
// Abort current loops if any
waitController?.abort();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return {
start,
complete,
reset,
width: widthRef.current
};
}
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useProgressBar } from './useProgressBar';
const transitionSpeed = 600;
// https://gist.github.com/tkrotoff/db8a8106cc93ae797ea968d78ea28047
export function RouterProgressBar(
props?: Parameters<typeof useProgressBar>[0]
) {
const { events } = useRouter();
const { width, start, complete, reset } = useProgressBar({
transitionSpeed,
...props
});
useEffect(() => {
events.on('routeChangeStart', start);
events.on('routeChangeComplete', complete);
events.on('routeChangeError', reset); // Typical case: "Route Cancelled"
return () => {
events.off('routeChangeStart', start);
events.off('routeChangeComplete', complete);
events.off('routeChangeError', reset);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return width > 0 ? (
// Use Bootstrap, Material UI, Tailwind CSS... to style the progress bar
<div
className="progress fixed-top bg-transparent rounded-0"
style={{
height: 3, // GitHub turbo-progress-bar height is 3px
zIndex: 1091 // $zindex-toast + 1 => always visible
}}
>
<div
className="progress-bar"
style={{
width: `${width}%`,
//transition: 'none',
transition: `width ${width > 1 ? transitionSpeed : 0}ms ease`
}}
/>
</div>
) : null;
}
How to use:
// pages/_app.tsx
import { AppProps } from 'next/app';
import Head from 'next/head';
import { RouterProgressBar } from './RouterProgressBar';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<title>My title</title>
<meta name="description" content="My description" />
</Head>
<RouterProgressBar />
<Component {...pageProps} />
</>
);
}
More here: https://gist.github.com/tkrotoff/db8a8106cc93ae797ea968d78ea28047
Related
I am currently working on a PWA in Next JS where I'm using CleverTap for notification campaigning & Segment for logging customer events.
I am facing errors in malfunctioning of CleverTap or loss of user events in Segment everytime I initialise both on app load. After root cause analysis, I suspect that the issue arises due to the scripts for CleverTap & Segment interfering with each other's initialization.
Here's some sample code regarding the initialization and usage of app, CleverTap, Segment, and pushing Clevertap notifications
For initializing CleverTap
import { Clevertap } from 'data/types/Clevertap';
import { getWindow } from './browserUtils';
import { getClevertapAccountId } from './settings';
const clevertap: Clevertap = {
event: [],
profile: [],
account: [],
onUserLogin: [],
notifications: [],
privacy: [],
};
clevertap.account.push({ id: getClevertapAccountId() });
clevertap.privacy.push({ optOut: false });
clevertap.privacy.push({ useIP: false });
const w = getWindow();
if (w) {
w.clevertap = clevertap;
}
const init = (): void => {
if (!w || !getClevertapAccountId()) return;
const wzrk = w.document.createElement('script');
wzrk.type = 'text/javascript';
wzrk.async = true;
wzrk.src = `${
w.document.location.protocol === 'https:'
? 'https://d2r1yp2w7bby2u.cloudfront.net'
: 'http://static.clevertap.com'
}/js/a.js`;
const s = w.document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(wzrk, s);
};
export default clevertap;
export { init };
For initializing Segment
import { min as segmentMinSnippet } from '#segment/snippet';
import { getWindow } from 'utils/browserUtils';
import { getSegmentKey } from 'utils/settings';
const w = getWindow();
const jsSnippet = segmentMinSnippet({
apiKey: getSegmentKey(),
page: true,
load: true,
});
const initSegment = (): void => {
if (!w || !getSegmentKey()) return;
const script = document.createElement('script');
script.innerHTML = jsSnippet;
document.head.appendChild(script);
};
// eslint-disable-next-line import/prefer-default-export
export { initSegment };
Calling both initialization functions on _app.tsx start
import CssBaseline from '#material-ui/core/CssBaseline';
import AppContent from 'features/core/AppContent';
import NavProgress from 'features/core/AppContent/NavProgress';
import WithTheme from 'features/core/theme/WithTheme';
import buildEnvConfig from 'features/core/useEnvConfig/buildEnvConfig';
import EnvConfigContext from 'features/core/useEnvConfig/EnvConfigContext';
import EnvConfigVariantContext from 'features/core/useEnvConfig/EnvConfigVariantContext';
import getEnvConfigVariantFromCookie from 'features/core/useEnvConfig/getEnvConfigVariantFromCookie';
import { setEnvConfig } from 'features/core/useEnvConfig/staticEnvConfig';
import 'i18n';
import type { AppProps } from 'next/app';
import NextHead from 'next/head';
import React, { useEffect, useMemo } from 'react';
import 'styles/global.css';
import { init as initClevertap } from 'utils/clevertap';
import { init as initHotjar } from 'utils/hotjar-manager';
import { initSegment } from 'utils/segment';
const BizMartApp = (props: AppProps): React.ReactNode => {
const { Component, pageProps } = props;
useEffect(() => {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) {
jssStyles.parentElement?.removeChild(jssStyles);
}
}, []);
useEffect(() => {
setTimeout(() => {
initHotjar();
initClevertap();
initSegment();
});
}, []);
const envConfigVariant = useMemo(() => getEnvConfigVariantFromCookie(), []);
const envConfig = useMemo(() => {
const ec = buildEnvConfig(envConfigVariant);
setEnvConfig(ec);
return ec;
}, [envConfigVariant]);
return (
<>
<NextHead>
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width"
/>
</NextHead>
<WithTheme>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<NavProgress />
<EnvConfigVariantContext.Provider value={envConfigVariant}>
<EnvConfigContext.Provider value={envConfig}>
<AppContent>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<Component {...pageProps} />
</AppContent>
</EnvConfigContext.Provider>
</EnvConfigVariantContext.Provider>
</WithTheme>
</>
);
};
export default BizMartApp;
Calling Clevertap notification modal
As a hack, I ran them in a non-blocking mode. I simply initialized CleverTap => added delay of 1 second => initialized Segment.
This seemed to work just fine, except that the user events in the 1st second were not logged by Segment.
I implemented the following hacky solution by calling initSegment after a 1 second delay as opposed to the earlier version in _app.tsx:
Changes in _app.tsx
....
useEffect(() => {
setTimeout(() => {
initHotjar();
initClevertap();
});
setTimeout(() => {
initSegment();
}, 1000);
}, []);
....
While the above hacked worked great in initializing & making CleverTap & Segment work in a non-blocking way & we can even use a debounce on all user Segment events happening in the 1st second, to not lost them; is there a way in which we can achieve both of this without this setTimeout hack ?
I want to use react suspense.
The suspense behavior I want is to not show the fallback until a certain amount of time.
Using react with next works as intended. However, react alone flickers.
Why does this only work when using next ?
What's the difference?
After changing the react dom generation code, it worked as expected.
How can I do this in react-native as well?
Example
I made a simple todo app.
We made a 100ms delay to get the todos list with an asynchronous request.
recoil
import { selector, atom } from "recoil";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export const todosDelayState = selector({
key: "todosDelayState",
get: async ({ get }) => {
await sleep(100);
const todos = get(todosState);
return todos;
}
});
export const todosState = atom({
key: "todosState", // unique ID (with respect to other atoms/selectors)
default: [{ id: 0, text: "fasfasdf", done: false }] // default value (aka initial value)
});
Home
export default function Home({ accounts }) {
return (
<RecoilRoot>
<React.Suspense fallback={<span>Loading</span>}>
<Todo />
</React.Suspense>
</RecoilRoot>
);
}
Todo
import TodoForm from "./TodoForm";
import TodoList from "./TodoList";
import React from "react";
import { useRecoilState, useRecoilValue } from "recoil";
import { todosDelayState, todosState } from "./todos";
function Todo() {
const [_, setTodos] = useRecoilState(todosState);
const todos = useRecoilValue(todosDelayState);
const onToggle = (id, done) => {
const _todos = todos.map((todo) => {
if (todo.id === id) {
return { ...todo, done: !done };
} else {
return todo;
}
});
setTodos(_todos);
};
const onRemove = (id) => {
const _todos = todos.filter((todo) => {
return todo.id === id ? false : true;
});
setTodos(_todos);
};
const onInsert = (value) => {
const id = todos.length === 0 ? 1 : todos[todos.length - 1].id + 1;
const todo = {
id,
text: value,
done: false
};
const _todos = todos.concat([todo]);
setTodos(_todos);
};
return (
<React.Fragment>
<TodoForm onInsert={onInsert} />
<TodoList todos={todos} onToggle={onToggle} onRemove={onRemove} />
</React.Fragment>
);
}
export default Todo;
1. Sandbox with next, react 18, recoil
live demo
In case it behaves exactly as I expected.
2. Sandbox with react 18, recoil
live demo
~~blinks~~ --> nice work
After changing the react dom generation code, it worked as expected.
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import App from "./App";
// const rootElement = document.getElementById("root");
// ReactDOM.render(
// <StrictMode>
// <App />
// </StrictMode>,
// rootElement
// );
const container = document.getElementById("root");
const root = ReactDOM.createRoot(container);
root.render(<App />);
3. Expo with react-native, react, recoil
live demo solved live demo
~~blinks~~ -> work
Unlike react-dom, react-native could not find a code that directly affects it.
So, I made a custom hook, Suspense, and ErrorBoundary using the loadable of recoil.
export const useCustomRecoilValue = (state, initialValue) => {
const data = useRecoilValueLoadable(state)
const prevData = useRef(initialValue)
const _data = useMemo(() => {
if (data.state === 'hasValue') {
prevData.current = data.contents
return [data.contents, false, undefined]
} else if (data.state === 'hasError') {
return [prevData.current, false, data.contents]
} else {
return [prevData.current, true, undefined]
}
}, [data])
return _data
}
// const todos = useRecoilValue(todosDelayState)
const [todos, loading, error] = useCustomRecoilValue(todosDelayState, [])
// return (
// <VStack space={8} w="100%">
// <TodoForm onInsert={onInsert} />
// <TodoList todos={todos} onToggle={onToggle} onRemove={onRemove} />
// </VStack>
// )
return (
<VStack space={8} w="100%">
<TodoForm onInsert={onInsert} />
<ErrorBoundary error={error} fallback={<Box>Error</Box>}>
<Suspense
delay={250}
loading={loading}
fallback={<Box>Loading...</Box>}>
<TodoList todos={todos} onToggle={onToggle} onRemove={onRemove} />
</Suspense>
</ErrorBoundary>
</VStack>
)
I have used ReactDOM.createPortal inside the render method of a stateful component like so:
class MyComponent extends Component {
...
render() {
return (
<Wrapper>
{ReactDOM.createPortal(<FOO />, 'dom-location')}
</Wrapper>
)
}
}
... but can it also be used by a stateless (functional) component?
Will chime in with an option where you dont want to manually update your index.html and add extra markup, this snippet will dynamically create a div for you, then insert the children.
export const Portal = ({ children, className = 'root-portal', el = 'div' }) => {
const [container] = React.useState(() => {
// This will be executed only on the initial render
// https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
return document.createElement(el);
});
React.useEffect(() => {
container.classList.add(className)
document.body.appendChild(container)
return () => {
document.body.removeChild(container)
}
}, [])
return ReactDOM.createPortal(children, container)
}
It can be done like this for a fixed component:
const MyComponent = () => ReactDOM.createPortal(<FOO/>, 'dom-location')
or, to make the function more flexible, by passing a component prop:
const MyComponent = ({ component }) => ReactDOM.createPortal(component, 'dom-location')
can it also be used by a stateless (functional) component
?
yes.
const Modal = (props) => {
const modalRoot = document.getElementById('myEle');
return ReactDOM.createPortal(props.children, modalRoot,);
}
Inside render :
render() {
const modal = this.state.showModal ? (
<Modal>
<Hello/>
</Modal>
) : null;
return (
<div>
<div id="myEle">
</div>
</div>
);
}
Working codesandbox#demo
TSX version based on #Samuel's answer (React 17, TS 4.1):
// portal.tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
interface IProps {
className? : string
el? : string
children : React.ReactNode
}
/**
* React portal based on https://stackoverflow.com/a/59154364
* #param children Child elements
* #param className CSS classname
* #param el HTML element to create. default: div
*/
const Portal : React.FC<IProps> = ( { children, className, el = 'div' } : IProps ) => {
const [container] = React.useState(document.createElement(el))
if ( className )
container.classList.add(className)
React.useEffect(() => {
document.body.appendChild(container)
return () => {
document.body.removeChild(container)
}
}, [])
return ReactDOM.createPortal(children, container)
}
export default Portal
IMPORTANT useRef/useState to prevent bugs
It's important that you use useState or useRef to store the element you created via document.createElement because otherwise it gets recreated on every re-render
//This div with id of "overlay-portal" needs to be added to your index.html or for next.js _document.tsx
const modalRoot = document.getElementById("overlay-portal")!;
//we use useRef here to only initialize el once and not recreate it on every rerender, which would cause bugs
const el = useRef(document.createElement("div"));
useEffect(() => {
modalRoot.appendChild(el.current);
return () => {
modalRoot.removeChild(el.current);
};
}, []);
return ReactDOM.createPortal(
<div
onClick={onOutSideClick}
ref={overlayRef}
className={classes.overlay}
>
<div ref={imageRowRef} className={classes.fullScreenImageRow}>
{renderImages()}
</div>
<button onClick={onClose} className={classes.closeButton}>
<Image width={25} height={25} src="/app/close-white.svg" />
</button>
</div>,
el.current
);
Yes, according to docs the main requirements are:
The first argument (child) is any renderable React child, such as an element, string, or fragment. The second argument (container) is a DOM element.
In case of stateless component you can pass element via props and render it via portal.
Hope it will helps.
Portal with SSR (NextJS)
If you are trying to use any of the above with SSR (for example NextJS) you may run into difficulty.
The following should get you what you need. This methods allows for passing in an id/selector to use for the portal which can be helpful in some cases, otherwise it creates a default using __ROOT_PORTAL__.
If it can't find the selector then it will create and attach a div.
NOTE: you could also statically add a div and specify a known id in pages/_document.tsx (or .jsx) if again using NextJS. Pass in that id and it will attempt to find and use it.
import { PropsWithChildren, useEffect, useState, useRef } from 'react';
import { createPortal } from 'react-dom';
export interface IPortal {
selector?: string;
}
const Portal = (props: PropsWithChildren<IPortal>) => {
props = {
selector: '__ROOT_PORTAL__',
...props
};
const { selector, children } = props;
const ref = useRef<Element>()
const [mounted, setMounted] = useState(false);
const selectorPrefixed = '#' + selector.replace(/^#/, '');
useEffect(() => {
ref.current = document.querySelector(selectorPrefixed);
if (!ref.current) {
const div = document.createElement('div');
div.setAttribute('id', selector);
document.body.appendChild(div);
ref.current = div;
}
setMounted(true);
}, [selector]);
return mounted ? createPortal(children, ref.current) : null;
};
export default Portal;
Usage
The below is a quickie example of using the portal. It does NOT take into account position etc. Just something simple to show you usage. Sky is limit from there :)
import React, { useState, CSSProperties } from 'react';
import Portal from './path/to/portal'; // Path to above
const modalStyle: CSSProperties = {
padding: '3rem',
backgroundColor: '#eee',
margin: '0 auto',
width: 400
};
const Home = () => {
const [visible, setVisible] = useState(false);
return (
<>
<p>Hello World <a href="#" onClick={() => setVisible(true)}>Show Modal</a></p>
<Portal>
{visible ? <div style={modalStyle}>Hello Modal! <a href="#" onClick={() => setVisible(false)}>Close</a></div> : null}
</Portal>
</>
);
};
export default Home;
const X = ({ children }) => ReactDOM.createPortal(children, 'dom-location')
Sharing my solution:
// PortalWrapperModal.js
import React, { useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
import $ from 'jquery';
const PortalWrapperModal = ({
children,
onHide,
backdrop = 'static',
focus = true,
keyboard = false,
}) => {
const portalRef = useRef(null);
const handleClose = (e) => {
if (e) e.preventDefault();
if (portalRef.current) $(portalRef.current).modal('hide');
};
useEffect(() => {
if (portalRef.current) {
$(portalRef.current).modal({ backdrop, focus, keyboard });
$(portalRef.current).modal('show');
$(portalRef.current).on('hidden.bs.modal', onHide);
}
}, [onHide, backdrop, focus, keyboard]);
return ReactDOM.createPortal(
<>{children(portalRef, handleClose)}</>,
document.getElementById('modal-root')
);
};
export { PortalWrapperModal };
How to add class in element on scroll React js, I want to add class in on scroll and remove that class if on top of the page.
import React from "react"
import { Link } from "react-router"
import { prefixLink } from "gatsby-helpers"
import Helmet from "react-helmet"
import { config } from "config"
module.exports = React.createClass({
propTypes() {
return {
children: React.PropTypes.any,
}
},
render() {
window.addEventListener('scroll', (event) => {
});
return (
<div>
<header className="header">
<div className="top-bar">
<span>678-827-2782 </span>
<span> hellohello#knotel.com</span>
<button>Login</button>
</div>
</header>
{this.props.children}
</div>
)
},
})
If you want to use React Hooks in 2020 stateless component
const [scroll, setScroll] = useState(false);
useEffect(() => {
window.addEventListener("scroll", () => {
setScroll(window.scrollY > 50);
});
}, []);
and use it anywhere in your code
className={scroll ? "bg-black" : "bg-white"}
setScroll(window.scrollY > 50); here 50 specify the height.
Use state to manage classnames and update state in the scroll event. Also, you should move the scroll event binding into the componentDidMount function instead of render.
import React from "react"
import { Link } from "react-router"
import { prefixLink } from "gatsby-helpers"
import Helmet from "react-helmet"
import { config } from "config"
module.exports = React.createClass({
propTypes() {
return {
children: React.PropTypes.any,
}
},
componentDidMount(){
window.addEventListener('scroll', () => {
let activeClass = 'normal';
if(window.scrollY === 0){
activeClass = 'top';
}
this.setState({ activeClass });
});
}
render() {
return (
<div>
<header className="header">
<div className={`top-bar ${this.state.activeClass}`}>
<span>678-827-2782 </span>
<span> hellohello#knotel.com</span>
<button>Login</button>
</div>
</header>
{this.props.children}
</div>
)
},
})
import React, { useState, useEffect } from "react"
import { Link } from "react-router"
import { prefixLink } from "gatsby-helpers"
import Helmet from "react-helmet"
import { config } from "config"
export default function Header(props) {
const [scroll, setScroll] = useState(false)
useEffect(() => {
window.addEventListener("scroll", () => {
setScroll(window.scrollY > 10)
})
}, [])
return (
<header className={scroll ? "main-header scrolled" : "main-header"}>
</header>
)
}
One more variant with hooks and cleanup to avoid memory leaks, if the browser is not handled it properly.
const [isScrollValueMoreThanHeaderHeight, setIsScrollValueMoreThanHeaderHeight] = useState(false);
//here 96(px) - height of current header
useEffect(() => {
const handleScroll = () => {
setIsScrollValueMoreThanHeaderHeight(window.scrollY > 96);
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll);
},[])
And then used it in a ternary way to change class
className={isScrollValueMoreThanHeaderHeight ? "class1" : "class2"}
I want to build a chat system and automatically scroll to the bottom when entering the window and when new messages come in. How do you automatically scroll to the bottom of a container in React?
As Tushar mentioned, you can keep a dummy div at the bottom of your chat:
render () {
return (
<div>
<div className="MessageContainer" >
<div className="MessagesList">
{this.renderMessages()}
</div>
<div style={{ float:"left", clear: "both" }}
ref={(el) => { this.messagesEnd = el; }}>
</div>
</div>
</div>
);
}
and then scroll to it whenever your component is updated (i.e. state updated as new messages are added):
scrollToBottom = () => {
this.messagesEnd.scrollIntoView({ behavior: "smooth" });
}
componentDidMount() {
this.scrollToBottom();
}
componentDidUpdate() {
this.scrollToBottom();
}
I'm using the standard Element.scrollIntoView method here.
I just want to update the answer to match the new React.createRef() method, but it's basically the same, just have in mind the current property in the created ref:
class Messages extends React.Component {
const messagesEndRef = React.createRef()
componentDidMount () {
this.scrollToBottom()
}
componentDidUpdate () {
this.scrollToBottom()
}
scrollToBottom = () => {
this.messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}
render () {
const { messages } = this.props
return (
<div>
{messages.map(message => <Message key={message.id} {...message} />)}
<div ref={this.messagesEndRef} />
</div>
)
}
}
UPDATE:
Now that hooks are available, I'm updating the answer to add the use of the useRef and useEffect hooks, the real thing doing the magic (React refs and scrollIntoView DOM method) remains the same:
import React, { useEffect, useRef } from 'react'
const Messages = ({ messages }) => {
const messagesEndRef = useRef(null)
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}
useEffect(() => {
scrollToBottom()
}, [messages]);
return (
<div>
{messages.map(message => <Message key={message.id} {...message} />)}
<div ref={messagesEndRef} />
</div>
)
}
Also made a (very basic) codesandbox if you wanna check the behaviour https://codesandbox.io/s/scrolltobottomexample-f90lz
Do not use findDOMNode
Class components with ref
class MyComponent extends Component {
componentDidMount() {
this.scrollToBottom();
}
componentDidUpdate() {
this.scrollToBottom();
}
scrollToBottom() {
this.el.scrollIntoView({ behavior: 'smooth' });
}
render() {
return <div ref={el => { this.el = el; }} />
}
}
Function components with hooks:
import React, { useRef, useEffect } from 'react';
const MyComponent = () => {
const divRef = useRef(null);
useEffect(() => {
divRef.current.scrollIntoView({ behavior: 'smooth' });
});
return <div ref={divRef} />;
}
Thanks to #enlitement
we should avoid using findDOMNode,
we can use refs to keep track of the components
render() {
...
return (
<div>
<div
className="MessageList"
ref={(div) => {
this.messageList = div;
}}
>
{ messageListContent }
</div>
</div>
);
}
scrollToBottom() {
const scrollHeight = this.messageList.scrollHeight;
const height = this.messageList.clientHeight;
const maxScrollTop = scrollHeight - height;
this.messageList.scrollTop = maxScrollTop > 0 ? maxScrollTop : 0;
}
componentDidUpdate() {
this.scrollToBottom();
}
reference:
https://facebook.github.io/react/docs/react-dom.html#finddomnode
https://www.pubnub.com/blog/2016-06-28-reactjs-chat-app-infinite-scroll-history-using-redux/
The easiest and best way I would recommend is.
My ReactJS version: 16.12.0
For Class Components
HTML structure inside render() function
render()
return(
<body>
<div ref="messageList">
<div>Message 1</div>
<div>Message 2</div>
<div>Message 3</div>
</div>
</body>
)
)
scrollToBottom() function which will get reference of the element.
and scroll according to scrollIntoView() function.
scrollToBottom = () => {
const { messageList } = this.refs;
messageList.scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"});
}
and call the above function inside componentDidMount() and componentDidUpdate()
For Functional Components (Hooks)
Import useRef() and useEffect()
import { useEffect, useRef } from 'react'
Inside your export function, (same as calling a useState())
const messageRef = useRef();
And let's assume you have to scroll when page load,
useEffect(() => {
if (messageRef.current) {
messageRef.current.scrollIntoView(
{
behavior: 'smooth',
block: 'end',
inline: 'nearest'
})
}
})
OR if you want it to trigger once an action performed,
useEffect(() => {
if (messageRef.current) {
messageRef.current.scrollIntoView(
{
behavior: 'smooth',
block: 'end',
inline: 'nearest'
})
}
},
[stateVariable])
And Finally, to your HTML structure
return(
<body>
<div ref={messageRef}> // <= The only different is we are calling a variable here
<div>Message 1</div>
<div>Message 2</div>
<div>Message 3</div>
</div>
</body>
)
for more explanation about Element.scrollIntoView() visit developer.mozilla.org
More detailed explanation in Callback refs visit reactjs.org
react-scrollable-feed automatically scrolls down to the latest element if the user was already at the bottom of the scrollable section. Otherwise, it will leave the user at the same position. I think this is pretty useful for chat components :)
I think the other answers here will force scroll everytime no matter where the scrollbar was. The other issue with scrollIntoView is that it will scroll the whole page if your scrollable div was not in view.
It can be used like this :
import * as React from 'react'
import ScrollableFeed from 'react-scrollable-feed'
class App extends React.Component {
render() {
const messages = ['Item 1', 'Item 2'];
return (
<ScrollableFeed>
{messages.map((message, i) => <div key={i}>{message}</div>)}
</ScrollableFeed>
);
}
}
Just make sure to have a wrapper component with a specific height or max-height
Disclaimer: I am the owner of the package
I could not get any of below answers to work but simple js did the trick for me:
window.scrollTo({
top: document.body.scrollHeight,
left: 0,
behavior: 'smooth'
});
If you want to do this with React Hooks, this method can be followed. For a dummy div has been placed at the bottom of the chat. useRef Hook is used here.
Hooks API Reference : https://reactjs.org/docs/hooks-reference.html#useref
import React, { useEffect, useRef } from 'react';
const ChatView = ({ ...props }) => {
const el = useRef(null);
useEffect(() => {
el.current.scrollIntoView({ block: 'end', behavior: 'smooth' });
});
return (
<div>
<div className="MessageContainer" >
<div className="MessagesList">
{this.renderMessages()}
</div>
<div id={'el'} ref={el}>
</div>
</div>
</div>
);
}
There are two major problems with the scrollIntoView(...) approach in the top answers:
it's semantically incorrect, as it causes the entire page to scroll if your parent element is scrolled outside the window boundaries. The browser literally scrolls anything it needs to in getting the element visible.
in a functional component using useEffect(), you get unreliable results, at least in Chrome 96.0.4665.45. useEffect() gets called too soon on page reload and the scroll doesn't happen. Delaying scrollIntoView with setTimeout(..., 0) fixes it for page reload, but not first load in a fresh tab, at least for me. shrugs
Here's the solution I've been using, it's solid and is more compatible with older browsers:
function Chat() {
const chatParent = useRef<HTMLDivElement(null);
useEffect(() => {
const domNode = chatParent.current;
if (domNode) {
domNode.scrollTop = domNode.scrollHeight;
}
})
return (
<div ref={chatParent}>
...
</div>
)
}
You can use refs to keep track of the components.
If you know of a way to set the ref of one individual component (the last one), please post!
Here's what I found worked for me:
class ChatContainer extends React.Component {
render() {
const {
messages
} = this.props;
var messageBubbles = messages.map((message, idx) => (
<MessageBubble
key={message.id}
message={message.body}
ref={(ref) => this['_div' + idx] = ref}
/>
));
return (
<div>
{messageBubbles}
</div>
);
}
componentDidMount() {
this.handleResize();
// Scroll to the bottom on initialization
var len = this.props.messages.length - 1;
const node = ReactDOM.findDOMNode(this['_div' + len]);
if (node) {
node.scrollIntoView();
}
}
componentDidUpdate() {
// Scroll as new elements come along
var len = this.props.messages.length - 1;
const node = ReactDOM.findDOMNode(this['_div' + len]);
if (node) {
node.scrollIntoView();
}
}
}
Reference your messages container.
<div ref={(el) => { this.messagesContainer = el; }}> YOUR MESSAGES </div>
Find your messages container and make its scrollTop attribute equal scrollHeight:
scrollToBottom = () => {
const messagesContainer = ReactDOM.findDOMNode(this.messagesContainer);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
};
Evoke above method on componentDidMount and componentDidUpdate.
componentDidMount() {
this.scrollToBottom();
}
componentDidUpdate() {
this.scrollToBottom();
}
This is how I am using this in my code:
export default class StoryView extends Component {
constructor(props) {
super(props);
this.scrollToBottom = this.scrollToBottom.bind(this);
}
scrollToBottom = () => {
const messagesContainer = ReactDOM.findDOMNode(this.messagesContainer);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
};
componentDidMount() {
this.scrollToBottom();
}
componentDidUpdate() {
this.scrollToBottom();
}
render() {
return (
<div>
<Grid className="storyView">
<Row>
<div className="codeView">
<Col md={8} mdOffset={2}>
<div ref={(el) => { this.messagesContainer = el; }}
className="chat">
{
this.props.messages.map(function (message, i) {
return (
<div key={i}>
<div className="bubble" >
{message.body}
</div>
</div>
);
}, this)
}
</div>
</Col>
</div>
</Row>
</Grid>
</div>
);
}
}
I created a empty element in the end of messages, and scrolled to that element. No need of keeping track of refs.
Working Example:
You can use the DOM scrollIntoView method to make a component visible in the view.
For this, while rendering the component just give a reference ID for the DOM element using ref attribute. Then use the method scrollIntoView on componentDidMount life cycle. I am just putting a working sample code for this solution. The following is a component rendering each time a message received. You should write code/methods for rendering this component.
class ChatMessage extends Component {
scrollToBottom = (ref) => {
this.refs[ref].scrollIntoView({ behavior: "smooth" });
}
componentDidMount() {
this.scrollToBottom(this.props.message.MessageId);
}
render() {
return(
<div ref={this.props.message.MessageId}>
<div>Message content here...</div>
</div>
);
}
}
Here this.props.message.MessageId is the unique ID of the particular chat message passed as props
import React, {Component} from 'react';
export default class ChatOutPut extends Component {
constructor(props) {
super(props);
this.state = {
messages: props.chatmessages
};
}
componentDidUpdate = (previousProps, previousState) => {
if (this.refs.chatoutput != null) {
this.refs.chatoutput.scrollTop = this.refs.chatoutput.scrollHeight;
}
}
renderMessage(data) {
return (
<div key={data.key}>
{data.message}
</div>
);
}
render() {
return (
<div ref='chatoutput' className={classes.chatoutputcontainer}>
{this.state.messages.map(this.renderMessage, this)}
</div>
);
}
}
thank you 'metakermit' for his good answer, but I think we can make it a bit better,
for scroll to bottom, we should use this:
scrollToBottom = () => {
this.messagesEnd.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });
}
but if you want to scroll top, you should use this:
scrollToTop = () => {
this.messagesEnd.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" });
}
and this codes are common:
componentDidMount() {
this.scrollToBottom();
}
componentDidUpdate() {
this.scrollToBottom();
}
render () {
return (
<div>
<div className="MessageContainer" >
<div className="MessagesList">
{this.renderMessages()}
</div>
<div style={{ float:"left", clear: "both" }}
ref={(el) => { this.messagesEnd = el; }}>
</div>
</div>
</div>
);
}
As another option it is worth looking at react scroll component.
I like doing it the following way.
componentDidUpdate(prevProps, prevState){
this.scrollToBottom();
}
scrollToBottom() {
const {thing} = this.refs;
thing.scrollTop = thing.scrollHeight - thing.clientHeight;
}
render(){
return(
<div ref={`thing`}>
<ManyThings things={}>
</div>
)
}
This is how you would solve this in TypeScript (using the ref to a targeted element where you scroll to):
class Chat extends Component <TextChatPropsType, TextChatStateType> {
private scrollTarget = React.createRef<HTMLDivElement>();
componentDidMount() {
this.scrollToBottom();//scroll to bottom on mount
}
componentDidUpdate() {
this.scrollToBottom();//scroll to bottom when new message was added
}
scrollToBottom = () => {
const node: HTMLDivElement | null = this.scrollTarget.current; //get the element via ref
if (node) { //current ref can be null, so we have to check
node.scrollIntoView({behavior: 'smooth'}); //scroll to the targeted element
}
};
render <div>
{message.map((m: Message) => <ChatMessage key={`chat--${m.id}`} message={m}/>}
<div ref={this.scrollTarget} data-explanation="This is where we scroll to"></div>
</div>
}
For more information about using ref with React and Typescript you can find a great article here.
This works for me
messagesEndRef.current.scrollTop = messagesEndRef.current.scrollHeight
where const messagesEndRef = useRef(); to use
Using React.createRef()
class MessageBox extends Component {
constructor(props) {
super(props)
this.boxRef = React.createRef()
}
scrollToBottom = () => {
this.boxRef.current.scrollTop = this.boxRef.current.scrollHeight
}
componentDidUpdate = () => {
this.scrollToBottom()
}
render() {
return (
<div ref={this.boxRef}></div>
)
}
}
This is modified from an answer above to support 'children' instead of a data array.
Note: The use of styled-components is of no importance to the solution.
import {useEffect, useRef} from "react";
import React from "react";
import styled from "styled-components";
export interface Props {
children: Array<any> | any,
}
export function AutoScrollList(props: Props) {
const bottomRef: any = useRef();
const scrollToBottom = () => {
bottomRef.current.scrollIntoView({
behavior: "smooth",
block: "start",
});
};
useEffect(() => {
scrollToBottom()
}, [props.children])
return (
<Container {...props}>
<div key={'child'}>{props.children}</div>
<div key={'dummy'} ref={bottomRef}/>
</Container>
);
}
const Container = styled.div``;
In order to scroll down to the bottom of the page first we have to select an id which resides at the bottom of the page. Then we can use the document.getElementById to select the id and scroll down using scrollIntoView(). Please refer the below code.
scrollToBottom= async ()=>{
document.getElementById('bottomID').scrollIntoView();
}
I have face this problem in mweb/web.All the solution is good in this page but all the solution is not working while using android chrome browser .
So for mweb and web I got the solution with some minor fixes.
import { createRef, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'redux/store';
import Message from '../Message/Message';
import styles from './MessageList.module.scss';
const MessageList = () => {
const messagesEndRef: any = createRef();
const { messages } = useSelector((state: AppState) => state?.video);
const scrollToBottom = () => {
//this is not working in mWeb
// messagesEndRef.current.scrollIntoView({
// behavior: 'smooth',
// block: 'end',
// inline: 'nearest',
// });
const scroll =
messagesEndRef.current.scrollHeight -
messagesEndRef.current.clientHeight;
messagesEndRef.current.scrollTo(0, scroll);
};
useEffect(() => {
if (messages.length > 3) {
scrollToBottom();
}
}, [messages]);
return (
<section className={styles.footerTopSection} ref={messagesEndRef} >
{messages?.map((message: any) => (
<Message key={message.id} {...message} />
))}
</section>
);
};
export default MessageList;
This is a great usecase for useLayoutEffect as taught by Kent C. Dodds.
https://kentcdodds.com/blog/useeffect-vs-uselayouteffect
if your effect is mutating the DOM (via a DOM node ref) and the DOM mutation will change the appearance of the DOM node between the time that it is rendered and your effect mutates it, then you don't want to use useEffect.
In my case i was dynamically generating elements at the bottom of a div so i had to add a small timeout.
const bottomRef = useRef<null | HTMLDivElement>(null);
useLayoutEffect(() => {
setTimeout(function () {
if (bottomRef.current) bottomRef.current.scrollTop = bottomRef.current.scrollHeight;
}, 10);
}, [transactionsAmount]);
const scrollingBottom = () => {
const e = ref;
e.current?.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "start",
});
};
useEffect(() => {
scrollingBottom();
});
<span ref={ref}>{item.body.content}</span>
Full version (Typescript):
import * as React from 'react'
export class DivWithScrollHere extends React.Component<any, any> {
loading:any = React.createRef();
componentDidMount() {
this.loading.scrollIntoView(false);
}
render() {
return (
<div ref={e => { this.loading = e; }}> <LoadingTile /> </div>
)
}
}