Clevertap & Segment initialization conflict in NextJS PWA - reactjs

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 ?

Related

Wallet get's disconnected from next.js solana application

I have followed a YouTube tutorial to create a solana application, all my code works fine and the wallet gets connected successfull, but when I refresh the page the wallet gets disconnected and the function getWallet returns nothing (connected = false and the PublicKey is null)
here's my walletConnectionProvider:
import React, { FC, useMemo } from 'react';
import { ConnectionProvider, WalletProvider } from '#solana/wallet-adapter-react';
import { WalletAdapterNetwork } from '#solana/wallet-adapter-base';
import {
GlowWalletAdapter,
PhantomWalletAdapter,
SlopeWalletAdapter,
SolflareWalletAdapter,
SolletExtensionWalletAdapter,
SolletWalletAdapter,
TorusWalletAdapter,
} from '#solana/wallet-adapter-wallets';
import {
WalletModalProvider,
WalletDisconnectButton,
WalletMultiButton
} from '#solana/wallet-adapter-react-ui';
import { clusterApiUrl } from '#solana/web3.js';
require('#solana/wallet-adapter-react-ui/styles.css');
export const WalletConnectionProvider = ({children}) => {
const network = WalletAdapterNetwork.Devnet;
const endpoint = useMemo(() => clusterApiUrl(network), [network]);
const wallets = useMemo(
() => [
new PhantomWalletAdapter(),
new GlowWalletAdapter(),
new SlopeWalletAdapter(),
new SolflareWalletAdapter({ network }),
new TorusWalletAdapter(),
],
[network]
);
return (
<ConnectionProvider endpoint={endpoint}>
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>
{children}
</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
};
export default WalletConnectionProvider
thanks
You have to add AutoConnectProvider, create a file called AutoConnectProvider and add this code
import { useLocalStorage } from '#solana/wallet-adapter-react';
import { createContext, FC, ReactNode, useContext } from 'react';
export interface AutoConnectContextState {
autoConnect: boolean;
setAutoConnect(autoConnect: boolean): void;
}
export const AutoConnectContext = createContext<AutoConnectContextState>({} as AutoConnectContextState);
export function useAutoConnect(): AutoConnectContextState {
return useContext(AutoConnectContext);
}
export const AutoConnectProvider: FC<{ children: ReactNode }> = ({ children }:any) => {
// TODO: fix auto connect to actual reconnect on refresh/other.
// TODO: make switch/slider settings
// const [autoConnect, setAutoConnect] = useLocalStorage('autoConnect', false);
const [autoConnect, setAutoConnect] = useLocalStorage('autoConnect', true);
return (
<AutoConnectContext.Provider value={{ autoConnect, setAutoConnect }}>{children}</AutoConnectContext.Provider>
);
};
then in whichever file you have your WalletContext wrap it with the AutoContextProvider
export const ContextProvider: FC<{ children: ReactNode }> = ({ children }) => {
return (
<AutoConnectProvider>
<WalletContextProvider>{children}</WalletContextProvider>
</AutoConnectProvider>
)
}
Hopw this helps :)
You can also specify the autoconnect attribute on the WalletProvider component to automatically attempt to reconnect a user wallet upon a page refresh.
import { ConnectionProvider, WalletProvider } from '#solana/wallet-adapter-react'
<WalletProvider wallets={wallets} autoconnect>
<WalletModalProvider>
<App />
</WalletModalProvider>
</WalletProvider>

How to mock a module import with Sinon and ReactJS

I'm trying to write a unit test for one of my React components written in TS:
import React, { useContext } from 'react';
import Lottie from 'lottie-react-web';
import { ConfigContext } from '../ConfigProvider';
import type { UIKitFC } from '../../types/react-extensions';
// interfaces
export interface LoadingOverlayProps {
size: 'large' | 'medium' | 'small';
testId?: string;
}
interface LoaderProps {
size: 'large' | 'medium' | 'small';
}
const G3Loader: React.FC<LoaderProps> = ({ size }) => {
const options = { animationData };
const pxSize =
size === 'small' ? '100px' : size === 'medium' ? '200px' : '300px';
const height = pxSize,
width = pxSize;
return (
<div className="loader-container">
<Lottie options={options} height={height} width={width} />
<div className="loader__loading-txt">
<div>
<h4>Loading...</h4>
</div>
</div>
</div>
);
};
/**
* Description of Loading Overlay component
*/
export const LoadingOverlay: UIKitFC<LoadingOverlayProps> = (props) => {
const { testId } = props;
const { namespace } = useContext(ConfigContext);
const { baseClassName } = LoadingOverlay.constants;
const componentClassName = `${namespace}-${baseClassName}`;
const componentTestId = testId || `${namespace}-${baseClassName}`;
return (
<div id={componentTestId} className={componentClassName}>
<G3Loader size={props.size} />
</div>
);
};
LoadingOverlay.constants = {
baseClassName: 'loadingOverlay',
};
LoadingOverlay.defaultProps = {
testId: 'loadingOverlay',
};
export default LoadingOverlay;
The component uses an imported module "Lottie" for some animation, but I'm not interested in testing it, I just want to test my component and its props.
The problem is, when I run my unit test, I get an error:
Error: Not implemented: HTMLCanvasElement.prototype.getContext (without installing the canvas npm package)
After some research, I've concluded that the error is caused by the Lottie import so I would like to mock it for the purpose of my test. I'm using Mocha and Sinon's stub functionality to try and mock the library import, but the same error persists, making me feel like I'm not stubbing the module out correctly. Here's my latest attempt at a unit test:
import React from 'react';
import * as Lottie from 'lottie-react-web';
import { render } from '#testing-library/react';
import { expect } from 'chai';
import * as sinon from 'sinon';
import LoadingOverlay from '../src/components/LoadingOverlay';
const TEST_ID = 'the-test-id';
const FakeLottie: React.FC = (props) => {
return <div>{props}</div>;
};
describe('Loading Overlay', () => {
// beforeEach(function () {
// sinon.stub(Lottie, 'default').callsFake((props) => FakeLottie(props));
// });
console.log('11111');
it('should have a test ID', () => {
sinon.stub(Lottie, 'default').callsFake((props) => FakeLottie(props));
console.log(Lottie);
const { getByTestId, debug } = render(
<LoadingOverlay testId={TEST_ID} size="small" />
);
debug();
expect(getByTestId(TEST_ID)).to.not.equal(null);
});
});
I'm not really sure what else to try, unit tests are not my forte... If anyone can help, that would be great.
I answered my own questions... Posting in case somebody else runs into the same issue.
The error was complaining about HTMLCanvasElement. It turns out the component I was trying to stub out was using the Canvas library itself which wasn't required when running in the browser, but since I was building a test, I just added the Canvas library to my package and the issue was solved. Full code below:
import React from 'react';
import { render, cleanup } from '#testing-library/react';
import { expect, assert } from 'chai';
import * as lottie from 'lottie-react-web';
import { createSandbox } from 'sinon';
import LoadingOverlay from '../src/components/LoadingOverlay';
// test ID
const TEST_ID = 'the-test-id';
// mocks
const sandbox = createSandbox();
const MockLottie = () => 'Mock Lottie';
describe('Loading Overlay', () => {
beforeEach(() => {
sandbox.stub(lottie, 'default').callsFake(MockLottie);
});
afterEach(() => {
sandbox.restore();
cleanup();
});
it('should have test ID', () => {
const { getByTestId } = render(
<LoadingOverlay testId={TEST_ID} size="medium" />
);
expect(getByTestId(TEST_ID)).to.not.equal(null);
});
});

React freezing when updating context

using react hooks
Experiencing a weird bug that seems to cause some sort of infinite loop or something. Has anyone run into something like this before.
Here's what I have:
import { createContext, useState, useCallback } from "react";
export type ModalValue = string | null;
const DEFAULT_ACTION = null;
export const useModalContext = (): ModalContextContents => {
const [
modal,
setModalInner,
] = useState<ModalValue>(DEFAULT_ACTION);
const setModal = useCallback((nv: ModalValue) => {
setModalInner(nv);
}, []);
return {
modal,
setModal,
};
};
interface ModalContextContents {
modal: ModalValue;
setModal: (nv: ModalValue) => void;
}
export const ModalContext = createContext<ModalContextContents>({modal: null, setModal: () => {}});
modal.tsx
import React, {useContext, useCallback} from 'react';
import {Box, Button, Aligner} from 'components';
import {ModalContext} from './modalContext';
import './modal.scss';
export const Modal = () => {
const modalApi = useContext(ModalContext);
if (modalApi.modal === null) {
return <></>
}
return <div key="lobby-modal" className='modal'>
<Aligner>
<Box style={{background: 'rgba(0, 0, 0, 0.72)'}}>
{modalApi.modal || ''}
<Button text="close" onClick={() => {modalApi.setModal(null)}}/>
</Box>
</Aligner>
</div>;
}
For some reason when I call:
modalApi.setModal('some-text');
then:
modalApi.setModal(null);
the entire page freezes.
Anyone have any idea what's going on here?
elsewhere in the app I had:
const callbackMapping: {[key: string]: Action} = useMemo(() => {
return {
callback: () => modelApi.setModal('wardrobe')
}
}, [room, modelApi])
then I was invoking it in a useEffect.
so I was causing an infinite in the app by changing the context value and then re-changing it.

React hooks - setInterval: causing the render to start before state is updated

I'm having an issue using React Hooks, specifically when I'm trying to run "setInterval". Currently my application has 2 buttons, "Next" and "Play".
When "Next" is clicked: the function "onNext" function runs, and the application renders.
When I click "Play": the function "onPlay" function is ran, but the component renders before the state has updated. I believe the "setInterval" is messing up the timing, but I'm not sure how to fix it or cause the component to wait to render until state is updated.
My initial thought is to use "useEffect", but I don't know how to make it tie into the "Play" button (since you can't place "useEffect", or any hooks within a function). I did try:
useEffect(() => {setInterval(() => {onNext();}, 500)}), [state3D]}
but this caused it to start running when the app is open. Any thoughts on how to set this up better?
import React, { useState, useRef, useEffect } from "react";
import ThreePointVis from "./ThreePointVis.jsx";
// import ThreePointVis_Tut from "./ThreePointVis_Tut.jsx";
import Controls3D from "./Controls3D.jsx";
import Settings3D from "./Settings3D.jsx";
import {
createWorld,
create3DWorld,
nextGen,
randomFill,
} from "../files/game3D.jsx";
import { loadPreset } from "../files/presets3D.jsx";
import "./styles3D.css";
import React, { useState, useRef, useEffect } from "react";
import ThreePointVis from "./ThreePointVis.jsx";
// import ThreePointVis_Tut from "./ThreePointVis_Tut.jsx";
import Controls3D from "./Controls3D.jsx";
import Settings3D from "./Settings3D.jsx";
import {
createWorld,
create3DWorld,
nextGen,
randomFill,
} from "../files/game3D.jsx";
import { loadPreset } from "../files/presets3D.jsx";
import "./styles3D.css";
export const Game3D = (props) => {
const [state3D, setState3D] = useState({
// world3D: loadPreset("line"),
world3D: loadPreset("plane"),
generation: 0,
isPlaying: false,
colorStyle: "default",
});
const changeState = (props) => {
// console.log("3d changestate: ", props);
const { world3D, generation } = props;
setState3D({ ...state3D, world3D: world3D, generation: generation });
};
const onChange = (world) => {
// console.log("onChange: ", world);
changeState({ world3D: world, generation: state3D.generation + 1 });
};
const onPlay = () => {
console.log("3D onPlay: ", state3D.world3D);
setState3D({ ...state3D, isPlaying: true });
setInterval(() => {
onNext();
}, 500);
};
const onNext = () => {
// console.log("onNext: ", state3D.world3D);
onChange(nextGen(state3D.world3D));
};
const onStop = () => {
console.log("onStop...");
setState3D({ ...state3D, isPlaying: false });
clearInterval();
};
const onSettingStyle = (settings, rules) => {
// console.log("onsettings: ", settings, rules);
const { colorStyle, gridSize, preset, generationSpeed } = settings;
setState3D({
...state3D,
world3D: loadPreset(`${preset}`),
generation: generationSpeed,
colorStyle: `${colorStyle}`,
});
};
const onShuffle = () => {
changeState({ world3D: randomFill(state3D.world3D), generation: 0 });
};
const onClear = () => {
changeState({ world3D: createWorld(), generation: 0 });
};
// console.log("state3D:", state3D);
return (
<div className="container-3D">
<Settings3D isPlaying={state3D.isPlaying} load={onSettingStyle} />
<div className="vis-container">
<ThreePointVis world={state3D.world3D} />
</div>
<Controls3D
isPlaying={state3D.isPlaying}
play={onPlay}
next={onNext}
stop={onStop}
shuffle={onShuffle}
clear={onClear}
/>
</div>
);
};
Use react Life cycle methods to synchronize view instead of using set intervals. You can use componentDidMount() which can be used to set your state. I recommend reading this https://reactjs.org/docs/state-and-lifecycle.html

Loading Screen on Next.js page transition

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

Resources