Set up Storybook to work with Next.js's Link tag - next.js

I'm trying to set up Storybook for a Next.js project. I have a component that render the Link tag from Next.js. My problem is that when I load this component, Storybook throws the following error:
Cannot read property 'pageLoader' of null
at Link.handleRef
What does one have to do to get Storybook working with Next.js Routing, specifically rendering the Link tag?
Update: Code that causes the error:
// button-component.js
import Link from 'next/link.js';
import t from 'prop-types';
import React from 'react';
function Button({ as, children, href, ...props }) {
const isExternal = href && href.startsWith('http');
const a = (
<a href={href} {...props}>
{children}
</a>
);
if (href) {
return isExternal ? (
a
) : (
<Link href={href} as={as}>
{a}
</Link>
);
}
return (
<button type="button" {...props}>
{children}
</button>
);
}
Button.propTypes = {
as: t.string,
children: t.node,
href: t.string,
};
export default React.memo(Button);
// button.stories.js
import React from 'react';
import Button from './button-component';
export default {
title: 'Button',
};
export const standardButton = () => <Button>Standard Button</Button>;
export const internalLink = () => <Button href='/buy'>
Internal Link
</Button>;
export const externalLink = () => (
<Button target="_blank" href="https://www.hopin.to">
External Link
</Button>
);

I found an issue reported about this on Next.js's github: https://github.com/zeit/next.js/issues/9951
It was reported only 5 days ago, so you could be having the same issue. The resolution is to upgrade to nextjs v9.1.8-canary.6. Reading more about this and looking at the source code, this is likely your problem. Also, there are more recent canary builds of nextjs, if you want to try something newer.
If that doesn't resolve it, my other guess is that you're getting errors because you're using Link outside of a Next.js page. Next.js may include dependencies for pages, behind the scenes. Link may rely on those dependencies and is throwing an error when they aren't found. If you want to test your components outside of Next.js pages, you could create a custom Link component that tests whether you're in Next.js and only renders Link if you are. For example:
import Link from 'next/link'
import Router from 'next/router'
const CustomLink = ({children, ...otherProps}) => {
const isPage = () => {
// if we're in a next.js route, then Router.router will be set
return Boolean(Router.router)
}
return isPage()
? (<Link {...otherProps}>{children}</Link>)
: children
}
Then use CustomLink instead of Link.

Another solution I found works similar as with next/image. To your .storybook/preview.js add following:
import Link from "next/link";
Object.defineProperty(Link, "default", {
configurable: true,
value: (props) => <a {...props} />,
});

Related

Unhandled Runtime Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary

I am making a website using Next.js and the above error is shown every time.
Don't know what is wrong in my code.
next-dev.js?3515:20
Warning: Expected server HTML to contain a matching <a> in <div>.
Another error shown is due to some suspense boundary causing root to switch to client side rendering :
import React from 'react';
import Link from 'next/Link';
import { urlFor } from "../lib/client";
const HeroBanner = ({heroBanner}) => {
return (
<div className='hero-banner-container'>
<div>
<p className='beats-solo'>{heroBanner.smallText}</p>
<h3>{heroBanner.midText}</h3>
<h1>{heroBanner.discount}</h1>
<img src={urlFor(heroBanner.image)} alt ="headphones"
className="hero-banner-image"/>
<div>
<Link href ={ `/product/${heroBanner.product}`}>
<button type='button'>{heroBanner.buttonText}</button>
</Link>
<div className='desc'>
<h5>Description</h5>
<p>{heroBanner.desc}</p>
</div>
</div>
</div>
</div>
)
}
export default HeroBanner
Unhandled Runtime Error Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.
The website is working fine but this error keeps popping everytime i reload my browser.
import React from 'react'
import { client } from "../lib/client";
import { Product, FooterBanner , HeroBanner } from '../components'
const Home = ({products,bannerData}) => (
<div>
<HeroBanner heroBanner = {bannerData.length && bannerData[0]}/>
<div className='products-heading'>
<h2>Best Selling Products</h2>
<p>Speakers of many variations</p>
</div>
<div className='products-container'>
{products?.map(
(product)=>product.name)}
</div>
<FooterBanner/>
</div>
);
export const getServerSideProps = async () => {
const query = '*[_type == "product"]';
const products = await client.fetch(query);
const bannerQuery = '*[_type == "banner"]';
const bannerData = await client.fetch(bannerQuery);
return {
props: {products, bannerData}
}
}
export default Home
Here is my client.js code:
import sanityClient from '#sanity/client';
import imageUrlBuilder from "#sanity/image-url";
export const client = sanityClient({
projectId: '1elz6lsg',
dataset: 'production',
apiVersion : '2022-03-10',
useCdn : 'true',
token: process.env.NEXT_PUBLIC_SANITY_TOKEN,
});
const builder = imageUrlBuilder(client);
export const urlFor = (source)=>builder.image(source);
I have no idea what the problem was, probably some conflicts between the dependencies versions...
Try deleting node_modules and package-lock.json and running npm install again.
I was able to get rid of the error by doing this.

React-tooltip and Next.js SSR issue

I use the react-tooltip library in my Next.js app.
I noticed that every time I refresh a website while visiting a page that uses the tooltip I get an error:
react-dom.development.js:88 Warning: Prop `dangerouslySetInnerHTML` did not match.
CSS classes are different on the client and on the server
The weird part is I do not get that error while navigating from a random page to a page that uses the react-tooltip.
The tooltip related code:
<StyledPopularityTooltipIcon src="/icons/tooltip.svg" alt="question mark" data-tip="hello world" />
<ReactTooltip
effect="solid"
className="tooltip"
backgroundColor="#F0F0F0"
arrowColor="#F0F0F0"
clickable={true}
/>
I had the same issue, I had to use state to detect when component has been mounted, and show the tooltip only after that.
P.S. You don't see the error when navigating, because the page is not rendered on server when you navigate, it's all front-end :)
In case you are using any server-side rendering (like Next.js) - you will need to make sure your component is mounted first before showing the react-tooltip.
I fixed this by using the following:
import React, { useEffect, useState } from 'react';
const [isMounted,setIsMounted] = useState(false); // Need this for the react-tooltip
useEffect(() => {
setIsMounted(true);
},[]);
return (<div>
{isMounted && <ReactTooltip id={"mytip"} effect={"solid"} />}
<span data-tip={"Tip Here"} data-for={"mytip"}>Hover me</span>
</div>)
You should wrap your JSX in the following component:
import React, { useEffect, useState } from 'react';
const NoSsr = ({ children }): JSX.Element => {
const [isMounted, setMount] = useState(false);
useEffect(() => {
setMount(true);
}, []);
return <>{isMounted ? children : null}</>;
};
export default NoSsr;
Like this:
<NoSsr>
<YourJSX />
</NoSsr>
If you are working with NEXTJS this might be a good approach, you can check the documentation here as well, also if you are working with data-event, globalEventOff or any other prop and is not hiding or not working in your localhost, this only occurs in Development Strict Mode. ReactTooltip works fine in Production code with React 18. So you can set reactStrictMode : false, in your next.config.js to test it locally and then set it back to true, hope this helps :) info reference here
import dynamic from 'next/dynamic'
const ReactTooltip = dynamic(() => import('react-tooltip'), { ssr : false });
function Home() {
return (
<div>
<Button
data-tip
data-event="click focus"
data-for="toolTip"
onClick={():void => ()}
/>
<ReactTooltip id="toolTip" globalEventOff="click"/>
</div>
)
}
export default Home

VIDEOJS: ERROR: (CODE:4 MEDIA_ERR_SRC_NOT_SUPPORTED) with Azure Media Services

I have a Next app where I want to embed a player. To do so, I decided to use the video.js library.
Everything works fine with, eg, youtube videos.
However, Video.js player doesn't play videos hosted on Azure Media Service
My player code:
import { useCallback, useEffect, useState } from 'react';
import videojs from 'video.js';
import 'videojs-youtube';
import 'videojs-flash';
import 'videojs-vimeo';
const Player = (props) => {
const [videoEl, setVideoEl] = useState(null);
const onVideo = useCallback((el) => {
setVideoEl(el);
}, []);
useEffect(() => {
if (videoEl == null) return;
const player = videojs(videoEl, props);
console.log('quality', player.getVideoPlaybackQuality());
return () => {
player.dispose();
};
}, [props, videoEl]);
return (
<>
{/* wrap the player in a div with a `data-vjs-player` attribute
so videojs won't create additional wrapper in the DOM */}
<div data-vjs-player>
<video
ref={onVideo}
className="video-js"
style={{ width: '100%', height: '100%' }}
playsInline
/>
</div>
</>
);
};
export default Player;
The options for videos are the following:
const videoJsOptions = {
techOrder: ['html', 'youtube', 'flash', 'other supported tech'],
autoplay: true,
controls: true,
usingNativeControls: true,
sources: [
{
src:
'https://my-video.streaming.media.azure.net/49a94f-122/manifest',
type: 'application/vnd.ms-sstr+xml'
}
]
};
In the head of the app I injected the following links from the Azure docs:
<link
href="//amp.azure.net/libs/amp/2.3.5/skins/amp-default/azuremediaplayer.min.css"
rel="stylesheet"
/>
<script src="//amp.azure.net/libs/amp/2.3.5/azuremediaplayer.min.js"></script>
I get this error
VIDEOJS: ERROR: (CODE:4 MEDIA_ERR_SRC_NOT_SUPPORTED) No compatible source was found for this media.
Not sure exactly about the specific issue noted above, I think that you may be trying to use Smooth Streaming "ms-sstr+xml" which is not going to be the best choice on Video.js.
We do have some samples in this repo that we just published that shows how to use Video.js with AMS - and also lists off the known issues.
https://github.com/Azure-Samples/media-services-3rdparty-player-samples
Take a look there and see if that helps with your use case scenario. Would welcome your feedback on this repo since it is new. Add Issues in the github if you see any.

NextJS dynamic title

Have been googling forever and found a way to change the <title>. That way is this: https://github.com/zeit/next.js/tree/master/examples/layout-component
The main problem with this is that everytime someone refresh the site/change page the title goes from http://localhost:3000 to the actual Title (eg. About us) and I'm a bit afraid of how this is affecting the SEO.
What is the correct way of chaning the page title dynamically?
My layout file:
import Link from 'next/link'
import Head from './../node_modules/next/head'
export default function Layout({ children, title = 'Welcome to my website' }) {
return (
<div>
<Head>
<title>{title}</title>
</Head>
{children}
</div>
)
}
Check out next-seo and install it in your next.js application.
yarn add next-seo
# or
npm install --save next-seo
And it will handle the page title and the meta description for you magically.
import React from 'react';
import { NextSeo } from 'next-seo'; // then add the `NextSeo` at any `pages/` that you wish
export default () => (
<>
<NextSeo
title="About Us, or just any title that you wish"
description="Then with a short description here."
/>
<p>Simple Usage</p>
</>
);
I have implemented the same tactic on my own web app here.
Well for me this works,
Import <Head> from next,
import Head from 'next/head'
And in return statement,
<>
<Head>
<title>Page Title</title>
</Head>
<section>
<Your_JSX_CODE>
</section>
</>
If you need a dynamic title/description, for example for the route parameters case, you can do this. (Consider that the page name is [id].js)
import React from 'react'
import { NextSeo } from 'next-seo' //as proposed by #xun
// import Head from "next/head" //alternative solution
const Detail = ({id}) => {
const title = `My ${id} title.`
const description = `My ${id} description.`
return (
<>
<NextSeo
title={title}
description={description}
/>
<p>It works!</p>
</>
)}
export default Detail
And at the end of your file:
export async function getServerSideProps({query}) {
const { id } = query
return {
props: {
id
},
};
}
Good luck!
I reninstalled "next" and "next-routes" in my dependencies and now this works.

Dynamically load .css based on condition in reactJS application

I have a reactJS application that I want to make available to multiple clients. Each clients has unique color schemes. I need to be able to import the .css file that corresponds to the specific client.
For example, if client 1 logs into the application, I want to import client1.css. if client 2 logs into the application, I want to import client2.css. I will know the client number once I have validated the login information.
The application contains multiple .js files. Every .js file contains the following at the top of the file
import React from 'react';
import { Redirect } from 'react-router-dom';
import {mqRequest} from '../functions/commonFunctions.js';
import '../styles/app.css';
Is there a way to import .css files dynamically for this scenario as opposed to specifying the .css file in the above import statement?
Thank you
Easy - i've delt with similar before.
componentWillMount() {
if(this.props.css1 === true) {
require('style1.css');
} else {
require('style2.css');
}
}
Consider using a cssInJs solution. Popular libraries are: emotion and styled-components but there are others as well.
I generally recommend a cssInJs solution, but for what you are trying to do it is especially useful.
In Emotion for example they have a tool specifically build for this purpose - the contextTheme.
What cssInJs basically means is that instead of using different static css files, use all the power of Javascript, to generate the needed css rules from your javascript code.
A bit late to the party, I want to expand on #Harmenx answer.
require works in development environments only, once it goes to production you're likely to get errors or not see the css file at all. Here are some options if you, or others, encounter this:
Option 1: Using css modules, assign a variable of styles with the response from the import based on the condition.
let styles;
if(this.props.css1 === true) {
//require('style1.css');
import("./style1.module.css").then((res) => { styles = res;});
} else {
//require('style2.css');
import("./style2.module.css").then((res) => { styles = res;});
}
...
<div className={styles.divClass}>...</div>
...
Option 2: using Suspend and lazy load from react
// STEP 1: create components for each stylesheet
// styles1.js
import React from "react";
import "./styles1.css";
export const Style1Variables = (React.FC = () => <></>);
export default Style1Variables ;
// styles2.js
import React from "react";
import "./styles2.css";
export const Style2Variables = (React.FC = () => <></>);
export default Style2Variables ;
// STEP 2: setup your conditional rendering component
import React, {lazy, Suspense} from "react";
const Styles1= lazy(() => import("./styles1"));
const Styles2= lazy(() => import("./styles2"));
export const ThemeSelector = ({ children }) => {
return (
<>
<Suspense fallback={null} />}>
{isClient1() ? <Styles1 /> : <Styles2/>}
</Suspense>
{children}
</>
);
};
// STEP 3: Wrap your app
ReactDOM.render(
<ThemeSelector>
<App />
</ThemeSelector>,
document.getElementById('root')
);
Option 3: Use React Helm which will include a link to the stylesheet in the header based on a conditional
class App extends Component {
render() {
<>
<Helmet>
<link
type="text/css"
rel="stylesheet"
href={isClient1() ? "./styles1.css" : "./styles2.css"}
/>
</Helmet>
...
</>
}
}
Personally, I like option 2 because you can set a variable whichClientIsThis() then modify the code to:
import React, {lazy, Suspense} from "react";
let clientID = whichClientIsThis();
const Styles= lazy(() => import("./`${clientID}`.css")); // change your variable filenames to match the client id.
export const ThemeSelector = ({ children }) => {
return (
<>
<Suspense fallback={null} />}>
<Styles />
</Suspense>
{children}
</>
);
};
ReactDOM.render(
<ThemeSelector>
<App />
</ThemeSelector>,
document.getElementById('root')
);
This way you don't need any conditionals. I'd still recommend lazy loading and suspending so the app has time to get the id and make the "decision" on which stylesheet to bring in.

Resources