Next.js Persistent component - Youtube embed that plays even after changing page - javascript

I am trying to create a Next.js react app. One of the requirements is that a youtube player must persist when changing pages. Only issue is that I'm not sure if it's possible with the way Next works. It seems that pages will aways re-mount regardless of the structure.
Here in my app I export Index into page.js which acts as a parent component.
<Media/> //Youtube player
page.js will always remount thus the player will reload.
page.js:
import React from 'react';
import { Provider } from 'react-redux';
import { checkLang } from '../helpers/langSupport';
import { changeLang } from '../store/actions/langAction';
import store from '../store/store';
import { Router } from '../config/router';
import Media from './youtube';
import Head from './head';
import Nav from './nav';
const childPage = (ChildPage) => {
return (
class Page extends React.Component {
componentDidMount(){
this.checkLanguage()
}
checkLanguage() {
checkLang(this.props.url, (status, result) => {
if(status){
store.dispatch(changeLang(result))
}else{
Router.pushRoute('/en'+this.props.url.asPath)
}
})
}
render() {
return (
<Provider store={store}>
<div>
<Head />
<Nav />
<ChildPage {...this.props} />
<Media/>
</div>
</Provider>
)
}
}
)
}
export default childPage;
index.js:
import React from 'react';
import { connect } from 'react-redux'
import { add, minus } from '../store/actions/countAction';
import Page from '../components/page';
export class Index extends React.Component {
render() {
return (
<div>
<div className="hero">
Count: {this.props.count.count}
<br/><br/>
<button onClick={()=>this.props.dispatch(add(1))}>Add</button>
<button onClick={()=>this.props.dispatch(minus(1))}>Minus</button>
</div>
<style jsx>{`
.hero{
margin-left: 50px;
}
`}</style>
</div>
)
}
}
export default Page(connect(state=>state)(Index));

This has now been implemented as _app.js. Check out the next.js readme to learn more.

Unfortunately this is something that does not exist in Next.js yet. Though it is actively discussed and will probably be implemented in the near future.
ReactDOM.render is called on every page change, so this is not possible today.
See discussions on:
https://github.com/zeit/next.js/issues/88
https://github.com/zeit/next.js/pull/2440
https://github.com/zeit/next.js/pull/3288

Related

How can I customize the style of a React component shared between lazy-loaded pages?

I'm building a React application and I started using CRA. I configured the routes of the app using React Router. Pages components are lazy-loaded.
There are 2 pages: Home and About.
...
const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));
...
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route path="/about" component={About} />
<Route path="/" component={Home} />
</Switch>
</Suspense>
...
Each page uses the Button component below.
import React from 'react';
import styles from './Button.module.scss';
const Button = ({ children, className = '' }) => (
<button className={`${styles.btn} ${className}`}>{children}</button>
);
export default Button;
The Button.module.scss file just sets the background color of the button to red.
.btn {
background: red;
}
The Button component accepts a className prop which is then added to the rendered button. This is because I want to give freedom to the consumer of the component. For example, in some pages margins could be needed or the background should be yellow instead of red.
To make it simple, I just want to have a different background color for the Button based on the current page, so that:
Home page => Blue button
About page => Yellow button
Each page is defined as below:
import React from 'react';
import Button from './Button';
import styles from './[PageName].module.scss';
const [PageName] = () => (
<div>
<h1>[PageName]</h1>
<Button className={styles.pageBtn}>[ExpectedColor]</Button>
</div>
);
export default [PageName];
where [PageName] is the name of the page and [ExpectedColor] is the corresponding expected color based on the above bullet list (blue or yellow).
The imported SCSS module, exports a class .pageBtn which sets the background property to the desired color.
Note: I could use a prop on the Button component which defines the variant to display (Blue/Yellow) and based on that prop add a class defined in the SCSS file. I don't want to do that since the change could be something that doesn't belong to a variant (e.g. margin-top).
The problem
If I run the application using yarn start, the application works fine. However, if I build the application (yarn build) and then I start serving the application (e.g. using serve -s build), the behavior is different and the application doesn't work as expected.
When the Home page is loaded, the button is correctly shown with a blue background. Inspecting the loaded CSS chunk, it contains:
.Button_btn__2cUFR {
background: red
}
.Home_pageBtn__nnyWK {
background: blue
}
That's fine. Then I click on the navigation link to open the About page. Even in this case, the button is shown correctly with a yellow background. Inspecting the loaded CSS chunk, it contains:
.Button_btn__2cUFR {
background: red
}
.About_pageBtn__3jjV7 {
background: yellow
}
When I go back to the Home page, the button is now displayed with a red background instead of yellow. That's because the About page has loaded the CSS above which defines again the Button_btn__2cUFR class. Since the class is now after the Home_pageBtn__nnyWK class definition, the button is displayed as red.
Note: the Button component is not exported on the common chunk because its size is too small. Having that in a common chunk could solve the problem. However, my question is about small shared components.
Solutions
I have thought to 2 solutions which, however, I don't like too much:
Increase selectors specificity
The classes specified in the [PageName].module.scss could be defined as:
.pageBtn.pageBtn {
background: [color];
}
This will increase the selector specificity and will override the default Button_btn__2cUFR class. However, each page chunk will include the shared components in case the component is quite small (less than 30kb). Also, the consumer of the component has to know that trick.
Eject and configure webpack
Ejecting the app (or using something like react-app-rewired) would allow specifying the minimum size for common chunk using webpack. However, that's not what I would like for all the components.
To summarize, the question is: what is the correct working way of overriding styles of shared components when using lazy-loaded routes?
You can use the following logic with config file for any pages. Also, You can send config data from remote server (req/res API) and handle with redux.
See Demo: CodeSandBox
create components directory and create files like below:
src
|---components
|---Button
| |---Button.jsx
| |---Button.module.css
Button Component:
// Button.jsx
import React from "react";
import styles from "./Button.module.css";
const Button = props => {
const { children, className, ...otherProps } = props;
return (
<button className={styles[`${className}`]} {...otherProps}>
{children}
</button>
);
};
export default Button;
...
// Button.module.css
.Home_btn {
background: red;
}
.About_btn {
background: blue;
}
create utils directory and create AppUtils.js file:
This file handle config files of pages and return new object
class AppUtils {
static setRoutes(config) {
let routes = [...config.routes];
if (config.settings) {
routes = routes.map(route => {
return {
...route,
settings: { ...config.settings, ...route.settings }
};
});
}
return [...routes];
}
static generateRoutesFromConfigs(configs) {
let allRoutes = [];
configs.forEach(config => {
allRoutes = [...allRoutes, ...this.setRoutes(config)];
});
return allRoutes;
}
}
export default AppUtils;
create app-configs directory and create routesConfig.jsx file:
This file lists and organizes routes.
import React from "react";
import AppUtils from "../utils/AppUtils";
import { pagesConfig } from "../pages/pagesConfig";
const routeConfigs = [...pagesConfig];
const routes = [
...AppUtils.generateRoutesFromConfigs(routeConfigs),
{
component: () => <h1>404 page not found</h1>
}
];
export default routes;
Modify index.js and App.js files to:
// index.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>,
rootElement
);
...
react-router-config: Static route configuration helpers for React
Router.
// App.js
import React, { Suspense } from "react";
import { Switch, Link } from "react-router-dom";
import { renderRoutes } from "react-router-config";
import routes from "./app-configs/routesConfig";
import "./styles.css";
export default function App() {
return (
<div className="App">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
</ul>
<Suspense fallback={<h1>loading....</h1>}>
<Switch>{renderRoutes(routes)}</Switch>
</Suspense>
</div>
);
}
create pages directory and create files and subdirectory like below:
src
|---pages
|---about
| |---AboutPage.jsx
| |---AboutPageConfig.jsx
|
|---home
|---HomePage.jsx
|---HomePageConfig.jsx
|
|---pagesConfig.js
About Page files:
// AboutPage.jsx
import React from "react";
import Button from "../../components/Button/Button";
const AboutPage = props => {
const btnClass = props.route.settings.layout.config.buttonClass;
return (
<>
<h1>about page</h1>
<Button className={btnClass}>about button</Button>
</>
);
};
export default AboutPage;
...
// AboutPageConfig.jsx
import React from "react";
export const AboutPageConfig = {
settings: {
layout: {
config: {
buttonClass: "About_btn"
}
}
},
routes: [
{
path: "/about",
exact: true,
component: React.lazy(() => import("./AboutPage"))
}
]
};
Home Page files:
// HomePage.jsx
import React from "react";
import Button from "../../components/Button/Button";
const HomePage = props => {
const btnClass = props.route.settings.layout.config.buttonClass;
return (
<>
<h1>home page</h1>
<Button className={btnClass}>home button</Button>
</>
);
};
export default HomePage;
...
// HomePageConfig.jsx
import React from "react";
export const HomePageConfig = {
settings: {
layout: {
config: {
buttonClass: "Home_btn"
}
}
},
routes: [
{
path: "/",
exact: true,
component: React.lazy(() => import("./HomePage"))
}
]
};
...
// pagesConfig.js
import { HomePageConfig } from "./home/HomePageConfig";
import { AboutPageConfig } from "./about/AboutPageConfig";
export const pagesConfig = [HomePageConfig, AboutPageConfig];
Edited section:
With HOC Maybe this way: CodeSandBox
create hoc dir and withPage.jsx file:
src
|---hoc
|---withPage.jsx
...
// withPage.jsx
import React, { useEffect, useState } from "react";
export function withPage(Component, path) {
function loadComponentFromPath(path, setStyles) {
import(path).then(component => setStyles(component.default));
}
return function(props) {
const [styles, setStyles] = useState();
useEffect(() => {
loadComponentFromPath(`../pages/${path}`, setStyles);
}, []);
return <Component {...props} styles={styles} />;
};
}
And then pages like below:
src
|---pages
|---about
| |---About.jsx
| |---About.module.css
|
|---home
|---Home.jsx
|---Home.module.css
About.jsx file:
// About.jsx
import React from "react";
import { withPage } from "../../hoc/withPage";
const About = props => {
const {styles} = props;
return (
<button className={styles && styles.AboutBtn}>About</button>
);
};
export default withPage(About, "about/About.module.css");
About.module.css file:
// About.module.css
.AboutBtn {
background: yellow;
}
Home.jsx file:
// Home.jsx
import React from "react";
import { withPage } from "../../hoc/withPage";
const Home = props => {
const { styles } = props;
return <button className={styles && styles.HomeBtn}>Home</button>;
};
export default withPage(Home, "home/Home.module.css");
Home.module.css file:
// Home.module.css
.HomeBtn {
background: red;
}
I would suggest instead of adding both the default styles and the consumer styles, use the consumer's styles over yours and use your as a callback if not supplied. The consumer can still compose your defaults with the composes keyword.
Button.js
import React from 'react';
import styles from './Button.module.scss';
const Button = ({ children, className}) => (
<button className={className ?? styles.btn}>{children}</button>
);
export default Button;
SomePage.module.scss
.pageBtn {
// First some defaults
composes: btn from './Button.module.scss';
// And override some of the defautls here
background: yellow;
}
If you wish, use sass #extends or #mixin instead
EDIT: Haven't tested it, but could it be that just by using composes webpack will make sure to bundle the defaults only once? Thus you're no longer needed to change your Button.js code with the ??
Solution 1
I know this is very obvious, but would work anyway:
Set !important on your overwriting css rules, thus bypassing specificity:
[PageName].module.scss:
.btn {
color: yellow !important;
}
However, most of the strict devs I know would avoid this keyword at all cost.
Why ?
Because when you start to have a lot of !important your css is a nightmare to debug. If you start writing !important rules with higher specificity, you know you have gone too far
It is only meant for corner-cases like yours, you might as well use it.
Solution 2
fix CRA config to enforce style tags order.
It is open-source after all :)
You can give your input on this bug here (upvote might give it more visibility):
https://github.com/facebook/create-react-app/issues/7190
Solution 3 (Update)
You could create a SCSS mixin in a new customButton.scss file, to generate css rules with higher specificity:
// customButton.scss
#mixin customBtn() {
:global {
.customBtn.override {
#content;
}
}
}
We will use two static class names (using the :global selector), because that way their name won't change based on where they are imported from.
Now use that mixin in your pages' SCSS:
// [pageName].module.scss
#import 'customButton.scss';
#include customBtn {
color: yellow;
}
css output should be:
.customBtn.override {
// put everything you want to customize here
color: yellow;
}
In Button.jsx: apply both class names to your button in addition to styles.btn:
// Button.jsx
const Button = ({ children, className = '' }) => (
<button className={`${styles.btn} customBtn override ${className}`}>
{children}
</button>
);
(Note that these are not referenced through the styles object, but the classname directly)
The main drawback is these are not dynamic class names, so you have to watch out to avoid conflicts yourself like we use to do before.
But I think it should do the trick

React Lazyload as soon as page interacted with

I'm building a React site which uses live chat and am using the react-livechat package. I've paired this up with the react-lazyload plugin to try to prevent it from adversely affect page load times.
I'm now trying to work out a way to load the livechat component in as soon as the page is interacted with. Currently it only renders when the page is scrolled to within a set distance of the component which by default is the footer of the page. This does prevent the page load being impacted but requires a user to scroll a certain distance before the component loads. Ideally it would load form any interaction with the page.
import React, { Component } from 'react';
import LiveChat from 'react-livechat';
import LazyLoad from 'react-lazyload';
class App extends Component {
render() {
return (
<div className="App">
...
<LazyLoad once>
<LiveChat license={'xxxxxx'} />
</LazyLoad>
...
</div>
);
};
}
export default App;
I managed to get this behavior with a workaround and loading the component after a certain period of time. I found that 10 seconds worked well to ensure even on mobile everything had entirely rendered.
// App.js
import React, { Component } from 'react';
import LazyLiveChat from './components/utils/lazyLiveChat';
class App extends Component {
constructor() {
super();
this.state = { loadLiveChat: false };
}
componentDidMount() {
setTimeout(() => this.setState({loadLiveChat: true}), 10000);
}
render() {
return (
<div className="App">
...
<LazyLiveChat loadChat={this.state.loadLiveChat} />
...
</div>
);
};
}
export default App;
// lazyLiveChat.js
import React, { Component } from 'react';
import LiveChat from 'react-livechat';
class LazyLiveChat extends Component {
render() {
if (this.props.loadChat) {
return (
<LiveChat license={xxxxxx} />
);
}
return null;
}
}
export default LazyLiveChat;

Hitting Back button in React app doesn't reload the page

I have a React app (16.8.6) written in TypeScript that uses React Router (5.0.1) and MobX (5.9.4). The navigation works fine and data loads when it should, however, when I click the browser's Back button the URL changes but no state is updated and the page doesn't get re-rendered. I've read endless articles about this issue and about the withRouter fix, which I tried but it doesn't make a difference.
A typical use case is navigating to the summary page, selecting various things which cause new data to load and new history states to get pushed and then going back a couple of steps to where you started. Most of the history pushes occur within the summary component, which handles several routes. I have noticed that when going back from the summary page to the home page the re-rendering happens as it should.
My index.tsx
import { Provider } from 'mobx-react'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import App from './App'
import * as serviceWorker from './serviceWorker'
import * as Utils from './utils/Utils'
const rootStore = Utils.createStores()
ReactDOM.render(
<Provider {...rootStore }>
<App />
</Provider>,
document.getElementById('root') as HTMLElement
)
serviceWorker.unregister()
My app.tsx
import * as React from 'react'
import { inject, observer } from 'mobx-react'
import { Route, Router, Switch } from 'react-router'
import Home from './pages/Home/Home'
import PackageSummary from './pages/PackageSummary/PackageSummary'
import ErrorPage from './pages/ErrorPage/ErrorPage'
import { STORE_ROUTER } from './constants/Constants'
import { RouterStore } from './stores/RouterStore'
#inject(STORE_ROUTER)
#observer
class App extends React.Component {
private routerStore = this.props[STORE_ROUTER] as RouterStore
public render() {
return (
<Router history={this.routerStore.history}>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/summary/:packageId" component={PackageSummary} />
<Route exact path="/summary/:packageId/:menuName" component={PackageSummary} />
<Route exact path="/summary/:packageId/:menuName/:appName" component={PackageSummary} />
<Route component={ErrorPage} />
</Switch>
</Router>
)
}
}
export default App
My router store
import { RouterStore as BaseRouterStore, syncHistoryWithStore } from 'mobx-react-router'
import { createBrowserHistory } from 'history'
export class RouterStore extends BaseRouterStore {
constructor() {
super()
this.history = syncHistoryWithStore(createBrowserHistory(), this)
}
}
How I create the MobX stores
export const createStores = () => {
const routerStore = new RouterStore()
const packageListStore = new PackageListStore()
const packageSummaryStore = new PackageSummaryStore()
const packageUploadStore = new PackageUploadStore()
return {
[STORE_ROUTER]: routerStore,
[STORE_SUPPORT_PACKAGE_LIST]: packageListStore,
[STORE_SUPPORT_PACKAGE_SUMMARY]: packageSummaryStore,
[STORE_SUPPORT_PACKAGE_UPLOAD]: packageUploadStore
}
}
So my questions are:
How can I get the page to load the proper data when the user goes back/forward via the browser?
If the solution is being able to get MobX to observe changes to the location, how would I do that?
You could implement something like this in your component:
import { inject, observer } from 'mobx-react';
import { observe } from 'mobx';
#inject('routerStore')
#observer
class PackageSummary extends React.Component {
listener = null;
componentDidMount() {
this.listener = observe(this.props.routerStore, 'location', ({ oldValue, newValue }) => {
if (!oldValue || oldValue.pathname !== newValue.pathname) {
// your logic
}
}, true)
}
componentWillUnmount() {
this.listener();
}
}
Problem with this approach is that if you go back from /summary to other page (e.g. '/'), callback will initiate, so you would also need some kind of check which route is this. Because of these kind of complications I would suggest using mobx-state-router, which I found much better to use with MobX.
React router monitors url changes and renders associated component defined for the route aka url.
You have to manually refresh or call a window function to reload.
If I remember correctly, using a browser back function does not reload the page (I might be wrong).
Why not try to detect the back action by a browser and reload the page when detected instead?
You can try the following code to manually reload the page when the browser back button is clicked.
$(window).bind("pageshow", function() {
// Run reload code here.
});
Also out of curiosity, why do you need so many different stores?
In App.js
useEffect(() => {
window.onpageshow = function(event) {
if (event.persisted) {
window.location.reload();
}
};
}, []);

Console.Log Not Being Called Inside React Constructor

I'm trying to add a component to a default .NET Core MVC with React project. I believe I have everything wired up to mirror the existing "Fetch Data" component, but it doesn't seem like it's actually being called (but the link to the component in my navbar does move to a new page).
The component itself...
import React, { Component } from 'react';
export class TestComponent extends Component {
static displayName = TestComponent.name;
constructor (props) {
super(props);
console.log("WHO NOW?");
this.state = { message: '', loading: true, promise: null };
this.state.promise = fetch('api/SampleData/ManyHotDogs');
console.log(this.state.promise);
}
static renderForecastsTable (message) {
return (
<h1>
Current Message: {message}
</h1>
);
}
render () {
let contents = this.state.loading
? <p><em>Loading...</em></p>
: TestComponent.renderForecastsTable(this.state.message);
return (
<div>
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
{contents}
</div>
);
}
}
The App.js
import React, { Component } from 'react';
import { Route } from 'react-router';
import { Layout } from './components/Layout';
import { Home } from './components/Home';
import { FetchData } from './components/FetchData';
import { Counter } from './components/Counter';
import { TestComponent } from './components/TestComponent';
export default class App extends Component {
static displayName = App.name;
render () {
return (
<Layout>
<Route exact path='/' component={Home} />
<Route path='/counter' component={Counter} />
<Route path='/fetch-data' component={FetchData} />
<Route path='/test-controller' component={TestComponent} />
</Layout>
);
}
}
That console.log("Who now") is never called when I inspect, and the page remains totally blank. I can't find a key difference between this and the functioning components, and google has not been much help either. Any ideas what is missing?
Edit
While troubleshooting this, I ended up creating a dependency nightmare that broke the app. Since I'm only using the app to explore React, I nuked it and started over--and on the second attempt I have not been able to reproduce the not-rendering issue.
It is advisable to use componentDidMount to make the call to the REST API with the fetch or axios.
class TestComponent extends Component{
constructor(props){
state = {promise: ''}
}
async componentDidMount () {
let promise = await fetch ('api / SampleData / ManyHotDogs');
this.setState ({promise});
console.log (promise);
}
render(){
return(
<div>{this.state.promise}</div>
);
}
}

How can you import portfolio projects in react?

I'm making a portfolio with react. How can I navigate to my projects through my localhost:3000? They are in my includes folder and the projects aren't made with react. It seems not to be the same as using XAMPP as my sever.
My file structure is:
-src
-componets
-sectionB.js
-imports
-myproject1
-index.php
I'm trying to go to my project with an <a> tag with this code:
import React, { Component } from 'react';
import {BrowserRouter as Router, Route, Link} from 'react-router-dom'
class Card extends Component {
render() {
return (
<a className="proj-card" href={this.props.href}>
<h2>{this.props.projName}</h2>
<img src={this.props.img}/>
<span>Technolgies: ({this.props.tech})</span>
<p>{this.props.projdesc}</p>
</a>
)
}
}
class SectionB extends Component {
render() {
return (
<div className="sectionB">
<Card projdesc={...} img={...} tech="..." projName="..." href="./imports/calendar2/index.php"/>
</div>
)
}
}
export default SectionB;
Why is this not directing the url to my project?
I think you're supposed to be using Link from react-router-dom. That may be a place to start. And what happens when you change:
<Card projdesc={...} img={...} tech="..." projName="..." href="./imports/calendar2/index.php"/>
to
<Card projdesc={...} img={...} tech="..." projName="..." href="../imports/calendar2/index.php"/>

Categories

Resources