Can't use React useEffect and also build failed using Gatsby - react-hooks

I am building a headless eCommerce website using Nacelle, Gatsby, and Shopify plus.
My problem is that I integrated Okendo API to fetch product reviews and can't build the project.
Actually, as you know, headless eCommerce is a new technology to us, but it is mostly close to Gatsby and SSR.
I tried to go 2 ways, one is to include the script to head using gatsby-react-helmet, and another one is to call window api inside useEffect or useLayoutEffect.
1. Including the script to head tag using gatsby-plugin-react-helmet.
ProductReview.js
import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet';
import transformProductId from '../../utils/transformProductId';
import { PRODUCT_REVIEW_METAFIELD_KEY, OKENDO_SUBSCRIBER_ID } from '../../constants';
const ProductReview = ({
product
}) => {
const OkendoSettings = {
filtersEnabled: true,
omitMicrodata: true,
subscriberId: OKENDO_SUBSCRIBER_ID,
widgetTemplateId: "default"
}
return (
<>
<Helmet>
<script type="application/javascript" src="../plugins/okendo/index.js" />
<script type="application/json" id="oke-reviews-settings">
{JSON.stringify(OkendoSettings)}
</script>
<script type="application/javascript" src="../plugins/okendo/initAPI.js" />
</Helmet>
<div
data-oke-reviews-widget
data-oke-reviews-product-id={transformProductId(product.id)}
/>
</>
);
};
export default React.memo(ProductReview);
/plugin/okendo/index.js
(function () {
function asyncLoad() {
var urls = ['https:\/\/d3hw6dc1ow8pp2.cloudfront.net\/reviewsWidget.min.js?shop=example.myshopify.com'];
for (var i = 0; i < urls.length; i++) {
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = urls[i];
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
}
}
if (window.attachEvent) {
window.attachEvent('onload', asyncLoad);
} else {
window.addEventListener('load', asyncLoad, false);
}
})();
/plugin/okendo/initAPI.js
window.okeReviewsWidgetOnInit = function (okeInitApi) {};
If I include the Okendo scripts to head tag, it works all fine.
But when I try to build on vercel, it says "error Building static HTML failed for path /products/example-product-slug".
2. Calling window.init api inside useEffect.
ProductReview.js
import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet';
import transformProductId from '../../utils/transformProductId';
import { PRODUCT_REVIEW_METAFIELD_KEY, OKENDO_SUBSCRIBER_ID } from '../../constants';
const ProductReview = ({
product
}) => {
const OkendoSettings = {
filtersEnabled: true,
omitMicrodata: true,
subscriberId: OKENDO_SUBSCRIBER_ID,
widgetTemplateId: "default"
}
useEffect(() => {
if (typeof window !== `undefined` && window.okendoInitApi) {
const reviewsWidget = window.document.querySelector('#oke-reviews-widget');
window.okendoInitApi.initReviewsWidget(reviewsWidget);
}
}, [product.id]);
return (
<>
<Helmet>
<script type="application/javascript" src="../plugins/okendo/index.js" />
<script type="application/json" id="oke-reviews-settings">
{JSON.stringify(OkendoSettings)}
</script>
{/* <script type="application/javascript" src="../plugins/okendo/initAPI.js" /> */}
</Helmet>
<div
id="oke-reviews-widget"
data-oke-reviews-widget
data-oke-reviews-product-id={transformProductId(product.id)}
/>
</>
);
};
export default React.memo(ProductReview);
While I am using useEffect to initialize Okendo api, it works only when the page refresh, not work if I open a page.
And if I try to build it, it says "error "window" is not available during server side rendering.".
I know useEffect doesn’t run unless it’s in the browser, but still I don't get what the solution is.
Hope to hear a good news.
Thank you.
UPDATE: The product id is generated from Shopify product graphql data named handle.
gatsby-node.js
exports.createPages = async ({ graphql, actions: { createPage } }) => {
// Fetch all products
const products = await graphql(`
{
allNacelleProduct (filter: { availableForSale: {eq: true} }) {
edges {
node {
handle
}
}
}
}
`);
products.data.allNacelleProduct.edges.forEach((product) =>
createPage({
// Build a Product Detail Page (PDP) for each product
path: `/products/${product.node.handle}`,
component: path.resolve('./src/templates/product-detail.js'),
context: {
handle: product.node.handle
}
})
);
...

Related

How to add npm ckeditor4 in stenciljs?

I have installed npm i ckeditor4 to my stencil project and I have used it like this. But Im not getting the ckeditor, tell me where to add the script tag I am completely new to stencil
ui-editor.tsx
import { Component, h } from '#stencil/core';
#Component({
tag: 'ui-editor',
styleUrl: 'style.scss',
shadow: true
})
export class UiEditor {
render() {
return (
<div id="editor">
<p>This is the editor content.</p>
</div>
)
}
}
As said in the documentation https://www.npmjs.com/package/ckeditor4 where should I add the scripts
<script src="./node_modules/ckeditor4/ckeditor.js"></script>
<script>
CKEDITOR.replace( 'editor' );
</script>
Try removing the script tag from your index.html file. The following component will automatically add the script tag from unpkg.
Example on webcomponents.dev
import { h, Component, State, Host } from "#stencil/core";
#Component({
tag: "ck-editor"
})
export class CkEditor {
_textarea: HTMLTextAreaElement;
componentWillLoad() {
return this.appendScript();
}
componentDidLoad() {
//#ts-ignore
let editor = CKEDITOR.replace(this._textarea, {
width: "99%",
height: "300px",
});
}
private async submit() {
// #ts-ignore
console.log(
CKEDITOR.instances[
this._textarea.nextSibling.id.replace("cke_", "")
].getData()
);
}
appendScript() {
return new Promise((resolve) => {
if (document.getElementById("ckeditor-script")) {
resolve();
return;
}
const ckeditorScript = document.createElement("script");
ckeditorScript.id = "ckeditor-script";
ckeditorScript.src = "https://unpkg.com/ckeditor4#4.14.1/ckeditor.js";
ckeditorScript.onload = () => resolve();
document.body.appendChild(ckeditorScript);
});
}
render() {
return (
<Host>
<textarea ref={(el) => (this._textarea = el)}></textarea>
<button onClick={() => this.submit()}>Submit</button>
</Host>
);
}
}
You should be able to import ckeditor but I haven't tested to see how that handles in rollup. The project I was recently working on was already loading ckeditor from unpkg so we went that direction instead.

React.js using context api to implement the dark/light theme, is it possible to get data in App component when I am using contextprovider?

I am using the context api to give my application the toggling ability between the dark/light mode, I managed to toggle the mode in all the children components of App component but when I tried to implement it to the component itself I failed I guess this related the fact the I am using the contextProvider within this component, code below for :
import React from 'react'
import styles from './App.module.css'
import { Card, CountryPicker, Chart } from './components/index'
import { fetchData } from './api/index'
import ThemeContextProvider, { ThemeContext } from './contexts/ThemeContext'
import ToggleTheme from './components/ToggleTheme/ToggleTheme'
export default class App extends React.Component {
static contextType = ThemeContext
state = {
data: {},
country: '',
}
handleCountryChange = async (country) => {
// console.log(country)
const Data = await fetchData(country)
// console.log(Data)
this.setState({ data: Data, country })
// console.log(this.state.data, this.state.country)
}
async componentDidMount() {
const data = await fetchData();
this.setState({ data })
}
render() {
const { data, country } = this.state;
// problem here
const { isLightTheme, dark, light } = this.context;
return (
<ThemeContextProvider>
<div className={styles.container} >
<ToggleTheme />
<Card data={data} />
<CountryPicker handleCountryChange={this.handleCountryChange} />
<Chart data={data} country={country} />
</div>
</ThemeContextProvider>
)
}
}
I figured it out,the solution was very simple, just importing the App component into other componentMain and wrapping it with <ContextProvider></ContextProvider> and import in the index.js

react + redux + saga + server side rendering + how to stop additional ajax calls for server side rendered page?

I am working on a web project.
In this project we use server side rendering with node , express and react.
For fetching data and Redux , we use Redux-Saga.
There is only one problem.
Every page is rendered from server and when we get to browser, our app acts like a client side application.
If a page has some ajax calls, and that page renders from server, although it has all data it needs from server , in client side it makes the ajax calls.
I want to stop the additional ajax calls.
I want to skip the ajax calls for the requested page from server only.
this is index.jsx file for client side.
import App from './App'
import ReactDOM from 'react-dom'
import React from 'react';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'react-router-redux'
import getStore from './getStore';
import createHistory from 'history/createBrowserHistory';
import './styles/styles.scss';
const history = createHistory();
const store = getStore(history);
if (module.hot) {
module.hot.accept('./App', () => {
const NextApp = require('./App').default;
render(NextApp);
});
}
const render = (_App) => {
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<_App />
</ConnectedRouter>
</Provider>
, document.getElementById("AppContainer"));
};
store.subscribe(() => {
const state = store.getState();
if (state.items.length > 0) {
render(App);
}
});
const fetchDataForLocation = location => {
if (location.pathname === "/") {
store.dispatch({ type: `REQUEST_FETCH_ITEMS` });
}
};
fetchDataForLocation(history.location);
history.listen(fetchDataForLocation);
this is the index.js file for server
import path from 'path';
import express from 'express';
import webpack from 'webpack';
import yields from 'express-yields';
import fs from 'fs-extra';
import App from '../src/App';
import { renderToString } from 'react-dom/server';
import React from 'react'
import { argv } from 'optimist';
import { ConnectedRouter } from 'react-router-redux';
import getStore from '../src/getStore'
import { Provider } from 'react-redux';
import createHistory from 'history/createMemoryHistory';
import open from 'open';
import { get } from 'request-promise';
const port = process.env.PORT || 4000;
const app = express();
const useServerRender = argv.useServerRender === 'true';
const inDebugMode = argv.inDebugMode == 'true';
let indexPath = inDebugMode ? '../public/index.html' : './public/index.html';
let mediaPath = inDebugMode ? '../src/styles/media' : './src/styles/media';
app.use('/media', express.static(mediaPath));
function* getItems() {
let data = yield get("http://localhost:3826/api/item/getall", { gzip: true });
return JSON.parse(data);
}
if (process.env.NODE_ENV === 'development') {
const config = require('../webpack.config.dev.babel.js').default;
const compiler = webpack(config);
app.use(require('webpack-dev-middleware')(compiler, {
noInfo: true,
stats: {
assets: false,
colors: true,
version: false,
hash: false,
timings: false,
chunks: false,
chunkModules: false
}
}));
app.use(require('webpack-hot-middleware')(compiler));
} else {
app.use(express.static(path.resolve(__dirname, '../dist')));
}
app.get(['/', '/aboutus'], function* (req, res) {
let index = yield fs.readFile(indexPath, "utf-8");
const initialState = {
items: []
};
if (req.path == '/') {
const items = yield getItems();
initialState.items = items.data.items;
}
const history = createHistory({
initialEntries: [req.path]
});
const store = getStore(history, initialState);
if (useServerRender) {
const appRendered = renderToString(
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>
);
index = index.replace(`<%= preloadedApplication %>`, appRendered)
} else {
index = index.replace(`<%= preloadedApplication %>`, `Please wait while we load the application.`);
}
res.send(index);
});
app.listen(port, '0.0.0.0', () => {
console.info(`Listening at http://localhost:${port}`);
if (process.env.NODE_ENV === 'development') {
open(`http://localhost:${port}`);
}
});
I think if somehow we are able to use server side store in client side, we may overcome this problem.
It's all about store data.
we should send store data somehow to client side, And use this data for store in client side.
For me the best solution is via html:
<script id='app-props' type='application/json'>
<![CDATA[<%= initialData %>]]>
</script>
And in store file i retrieve it like this:
if (typeof window !== 'undefined') {
let initialDataEl = document.getElementById('app-props');
try {
let props = initialDataEl.textContent;
props = props.replace("<![CDATA[", "").replace("]]>", "");
defaultState = JSON.parse(props);
}
catch (err) {
console.log(err);
}
}

Redux server side rendering breaks when using an apollo graphql client

When I'm using an apollo provider with redux server side rendering,
https://github.com/reactjs/redux/blob/master/docs/recipes/ServerRendering.md
I get the following warning and it breaks the server side output
Warning: Failed context type: The context `client` is marked as required in `Apollo(Home)`, but its value is `undefined`.
in Apollo(Home) (created by Connect(Apollo(Home)))
in Connect(Apollo(Home)) (created by RouterContext)
in RouterContext
in Provider
However this renders fine client side.
app
window.webappStart = () => {
const initialState = window.__PRELOADED_STATE__;
const store = createStore(rootReducer, initialState);
const client = new ApolloClient({
networkInterface: createNetworkInterface({ uri: 'https://api.graph.cool/simple/v1/foo' }),
});
render(
<ApolloProvider store={store} client={client}>
<Router>{routes}</Router>
</ApolloProvider>,
document.querySelector(".js-content")
);
};
Here's the boilerplate apollo code
import React from 'react';
import gql from 'graphql-tag';
import { graphql } from 'react-apollo';
// The data prop, which is provided by the wrapper below contains,
// a `loading` key while the query is in flight and posts when it is ready
function PostList({ data: { loading, posts } }) {
if (loading) {
return <div>Loading</div>;
} else {
return (
<ul>
{posts.map(post =>
<li key={post.id}>
{post.title} by {' '}
{post.author.firstName} {post.author.lastName} {' '}
({post.votes} votes)
</li>
)}
</ul>
);
}
}
// The `graphql` wrapper executes a GraphQL query and makes the results
// available on the `data` prop of the wrapped component (PostList here)
export default graphql(gql`
query allPosts {
posts {
id
title
votes
author {
id
firstName
lastName
}
}
}
`)(PostList);
The PostList component looks alright to me, as does the client-side initiation of your app.
If you're getting that error in your server logs, then I think you'll want to check your routing middleware to ensure you're passing the client to ApolloProvider before rendering your app.
I'm using Express v4.* and react-router v4. My setup looks like this:
import React from 'react'
import { renderToString } from 'react-dom/server'
import { match, RouterContext } from 'react-router'
import ApolloClient, { createNetworkInterface } from 'apollo-client'
import { ApolloProvider, renderToStringWithData } from 'react-apollo'
import routes from '../app/routes.js'
import { store } from 'app/store/index.js'
const Html = ({ title = 'App', content }) => (
<html>
<head>
<title>{title}</title>
<link href="/main.css" rel="stylesheet"/>
</head>
<body>
<div id="root" dangerouslySetInnerHTML={{ __html: content }} />
<script src='/index.js'/>
</body>
</html>
)
module.exports = (req, res) => {
match(
{
location: req.originalUrl,
routes,
},
(error, redirectLocation, renderProps) => {
if (redirectLocation) {
res.redirect(redirectLocation.pathname + redirectLocation.search)
} else if (error) {
console.error('ROUTER ERROR:', error)
res.status(500)
} else if (renderProps) {
const client = new ApolloClient({
ssrMode: true,
networkInterface: createNetworkInterface({
uri: 'http://localhost:8888/graphql',
}),
})
/**
* Make sure client is added here. Store is optional */
const App = (
<ApolloProvider client={client} store={store}>
<RouterContext {...renderProps} />
</ApolloProvider>
)
/**
* Render and send response
*/
renderToStringWithData(App).then(content => {
const html = <Html content={content}/>
res.status(200).send(`<!DOCTYPE html>\n${ renderToString(html) }`)
}).catch((err) => console.log(`INITIAL RENDER (SSR) ERROR:`, err))
} else {
res.status(404).send('Not found')
}
}
)
}

Scroll to the top of the page after render in react.js

I have a problem, which I have no ideas, how to solve.
In my react component I display a long list of data and few links at the bottom.
After clicking on any of this links I fill in the list with new collection of the links and need to scroll to the top.
The problem is - how to scroll to the top after new collection is rendered?
'use strict';
// url of this component is #/:checklistId/:sectionId
var React = require('react'),
Router = require('react-router'),
sectionStore = require('./../stores/checklist-section-store');
function updateStateFromProps() {
var self = this;
sectionStore.getChecklistSectionContent({
checklistId: this.getParams().checklistId,
sectionId: this.getParams().sectionId
}).then(function (section) {
self.setState({
section,
componentReady: true
});
});
this.setState({componentReady: false});
}
var Checklist = React.createClass({
mixins: [Router.State],
componentWillMount: function () {
updateStateFromProps.call(this);
},
componentWillReceiveProps(){
updateStateFromProps.call(this);
},
render: function () {
if (this.state.componentReady) {
return(
<section className='checklist-section'>
<header className='section-header'>{ this.state.section.name } </header>
<Steps steps={ this.state.section.steps }/>
<a href=`#/${this.getParams().checklistId}/${this.state.section.nextSection.Id}`>
Next Section
</a>
</section>
);
} else {...}
}
});
module.exports = Checklist;
Finally.. I used:
componentDidMount() {
window.scrollTo(0, 0)
}
EDIT: React v16.8+
useEffect(() => {
window.scrollTo(0, 0)
}, [])
Since the original solution was provided for very early version of react, here is an update:
constructor(props) {
super(props)
this.myRef = React.createRef() // Create a ref object
}
componentDidMount() {
this.myRef.current.scrollTo(0, 0);
}
render() {
return <div ref={this.myRef}></div>
} // attach the ref property to a dom element
You could use something like this. ReactDom is for react.14. Just React otherwise.
componentDidUpdate = () => { ReactDom.findDOMNode(this).scrollIntoView(); }
Update 5/11/2019 for React 16+
constructor(props) {
super(props)
this.childDiv = React.createRef()
}
componentDidMount = () => this.handleScroll()
componentDidUpdate = () => this.handleScroll()
handleScroll = () => {
const { index, selected } = this.props
if (index === selected) {
setTimeout(() => {
this.childDiv.current.scrollIntoView({ behavior: 'smooth' })
}, 500)
}
}
In React Routing there is the problem that if we redirect to the new route, then it won't automatically take you to the top of the page.
Even I did have the same issue.
I just added the single line to my component and it worked like butter.
componentDidMount() {
window.scrollTo(0, 0);
}
Refer: react training
Hook solution:
Create a ScrollToTop hook
import { useEffect } from "react";
import { withRouter } from "react-router-dom";
const ScrollToTop = ({ children, location: { pathname } }) => {
useEffect(() => {
window.scrollTo({
top: 0,
left: 0,
behavior: "smooth"
});
}, [pathname]);
return children || null;
};
export default withRouter(ScrollToTop);
Wrap your App with it
<Router>
<ScrollToTop>
<App />
</ScrollToTop>
</Router>
Documentation : https://reacttraining.com/react-router/web/guides/scroll-restoration
For those using hooks, the following code will work.
React.useEffect(() => {
window.scrollTo(0, 0);
}, []);
Note, you can also import useEffect directly: import { useEffect } from 'react'
This could, and probably should, be handled using refs:
"... you can use ReactDOM.findDOMNode as an "escape hatch" but we don't recommend it since it breaks encapsulation and in almost every case there's a clearer way to structure your code within the React model."
Example code:
class MyComponent extends React.Component {
componentDidMount() {
this._div.scrollTop = 0
}
render() {
return <div ref={(ref) => this._div = ref} />
}
}
You can do this in the router like that:
ReactDOM.render((
<Router onUpdate={() => window.scrollTo(0, 0)} history={browserHistory}>
<Route path='/' component={App}>
<IndexRoute component={Home}></IndexRoute>
<Route path="/about" component={About}/>
<Route path="/work">
<IndexRoute component={Work}></IndexRoute>
<Route path=":id" component={ProjectFull}></Route>
</Route>
<Route path="/blog" component={Blog}/>
</Route>
</Router>
), document.getElementById('root'));
The onUpdate={() => window.scrollTo(0, 0)} put the scroll top.
For more information check: codepen link
This works for me.
import React, { useEffect } from 'react';
useEffect(() => {
const body = document.querySelector('#root');
body.scrollIntoView({
behavior: 'smooth'
}, 500)
}, []);
Here's yet another approach that allows you to choose which mounted components you want the window scroll position to reset to without mass duplicating the ComponentDidUpdate/ComponentDidMount.
The example below is wrapping the Blog component with ScrollIntoView(), so that if the route changes when the Blog component is mounted, then the HOC's ComponentDidUpdate will update the window scroll position.
You can just as easily wrap it over the entire app, so that on any route change, it'll trigger a window reset.
ScrollIntoView.js
import React, { Component } from 'react';
import { withRouter } from 'react-router';
export default WrappedComponent => {
class ResetWindowScroll extends Component {
componentDidUpdate = (prevProps) => {
if(this.props.location !== prevProps.location) window.scrollTo(0,0);
}
render = () => <WrappedComponent {...this.props} />
}
return withRouter(ResetWindowScroll);
}
Routes.js
import React from 'react';
import { Route, IndexRoute } from 'react-router';
import App from '../components/App';
import About from '../components/pages/About';
import Blog from '../components/pages/Blog'
import Index from '../components/Landing';
import NotFound from '../components/navigation/NotFound';
import ScrollIntoView from '../components/navigation/ScrollIntoView';
export default (
<Route path="/" component={App}>
<IndexRoute component={Index} />
<Route path="/about" component={About} />
<Route path="/blog" component={ScrollIntoView(Blog)} />
<Route path="*" component={NotFound} />
</Route>
);
The above example works great, but if you've migrated to react-router-dom, then you can simplify the above by creating a HOC that wraps the component.
Once again, you could also just as easily wrap it over your routes (just change componentDidMount method to the componentDidUpdate method example code written above, as well as wrapping ScrollIntoView with withRouter).
containers/ScrollIntoView.js
import { PureComponent, Fragment } from "react";
class ScrollIntoView extends PureComponent {
componentDidMount = () => window.scrollTo(0, 0);
render = () => this.props.children
}
export default ScrollIntoView;
components/Home.js
import React from "react";
import ScrollIntoView from "../containers/ScrollIntoView";
export default () => (
<ScrollIntoView>
<div className="container">
<p>
Sample Text
</p>
</div>
</ScrollIntoView>
);
This solution is working for the Functional component as well as the Class Base.
First of all, I do not like the idea of Scroll to top on every re-render, instead, I like of attache function to the particular event.
Step #1: Create a function to ScrollToTop
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
Step #2: Call this function on an event e.g onClick
onRowClick={scrollToTop()}
// onClick={scrollToTop()}
// etc...
I'm using react-router ScrollToTop Component which code described in react-router docs
https://reacttraining.com/react-router/web/guides/scroll-restoration/scroll-to-top
I'm changing code in single Routes file and after that no need of change code in every component.
Example Code -
Step 1 - create ScrollToTop.js Component
import React, { Component } from 'react';
import { withRouter } from 'react-router';
class ScrollToTop extends Component {
componentDidUpdate(prevProps) {
if (this.props.location !== prevProps.location) {
window.scrollTo(0, 0)
}
}
render() {
return this.props.children
}
}
export default withRouter(ScrollToTop)
Step 2 - In App.js file, add ScrollToTop Component after <Router
const App = () => (
<Router>
<ScrollToTop>
<App/>
</ScrollToTop>
</Router>
)
If all want to do is something simple here is a solution that will work for everybody
add this mini function
scrollTop()
{
window.scrollTo({
top: 0,
behavior: "smooth"
});
}
call the function as following from the footer of the page
<a className="scroll-to-top rounded" style={{display: "inline"}} onClick={this.scrollTop}>TOP</a>
if you want to add nice styles here is the css
.scroll-to-top {
position: fixed;
right: 1rem;
bottom: 1rem;
display: none;
width: 2.75rem;
height: 2.75rem;
text-align: center;
color: #fff;
background: rgba(90, 92, 105, 0.5);
line-height: 46px;
}
This is the only thing that worked for me (with an ES6 class component):
componentDidMount() {
ReactDOM.findDOMNode(this).scrollIntoView();
}
All of the above didn't work for me - not sure why but:
componentDidMount(){
document.getElementById('HEADER').scrollIntoView();
}
worked, where HEADER is the id of my header element
I have tried #sledgeweight solution but it does not work well for some of the views. But adding a setTimeout seems to work perfectly. In case someone facing the same issue as me. Below is my code.
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
const ScrollToTop = () => {
const { pathname } = useLocation()
useEffect(() => {
console.log(pathname)
/* settimeout make sure this run after components have rendered. This will help fixing bug for some views where scroll to top not working perfectly */
setTimeout(() => {
window.scrollTo({ top: 0, behavior: 'smooth' })
}, 0)
}, [pathname])
return null
}
export default ScrollToTop
Use it in AppRouter.js as
<Router>
<ScrollToTop/>
<App>
</Router>
Using Hooks in functional components,
assuming the component updates when theres an update in the result props
import React, { useEffect } from 'react';
export const scrollTop = ({result}) => {
useEffect(() => {
window.scrollTo(0, 0);
}, [result])
}
The page that appears after clicking, just write into it.
componentDidMount() {
window.scrollTo(0, 0);
}
Smooth scroll to top . In hooks you can use this method inside lifecycle mounting state for once render
useEffect(() => {
window.scrollTo({top: 0, left: 0, behavior: 'smooth' });
}, [])
Looks like all the useEffect examples dont factor in you might want to trigger this with a state change.
const [aStateVariable, setAStateVariable] = useState(false);
const handleClick = () => {
setAStateVariable(true);
}
useEffect(() => {
if(aStateVariable === true) {
window.scrollTo(0, 0)
}
}, [aStateVariable])
I tried everything, but this is the only thing that worked.
useLayoutEffect(() => {
document.getElementById("someID").scrollTo(0, 0);
});
This is what I did:
useEffect(() => ref.current.scrollTo(0, 0));
const ref = useRef()
return(
<div ref={ref}>
...
</div>
)
I was doing a SPA in React 17.0 using functional components and window.scroll, window.scrollTo and all of this variants doesn't work for me. So I made a solution using useRef hook. I created a span tag in the top of the component with Ref and then I used and effect with ref.current.scrollIntoView()
There is a short example:
import React, { useEffect,useRef} from 'react';
export const ExampleComponent = () => {
const ref = useRef();
useEffect(() => {
ref.current.scrollIntoView()
}, []);
return(
<>
<span ref={ref}></span>
<YourCodeHere />
<MoreCode />
</>
)
}
For React v18+ my recommendation will be to use wrapper component, will be the easiest way to execute.
Step 1: Create a ScrollToTop component (component/ScrollToTop.js)
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
export function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
Step 2: Wrap your App with it (index.js)
<React.StrictMode>
<BrowserRouter>
<ScrollToTop />
<App />
</BrowserRouter>
</React.StrictMode>
Explanation: Every time pathname changes useEffect will be called to scroll the page to the top.
I'm using React Hooks and wanted something re-usable but also something I could call at any time (rather than just after render).
// utils.js
export const useScrollToTop = (initialScrollState = false) => {
const [scrollToTop, setScrollToTop] = useState(initialScrollState);
useEffect(() => {
if (scrollToTop) {
setScrollToTop(false);
try {
window.scroll({
top: 0,
left: 0,
behavior: 'smooth',
});
} catch (error) {
window.scrollTo(0, 0);
}
}
}, [scrollToTop, setScrollToTop]);
return setScrollToTop;
};
Then to use the hook you can do:
import { useScrollToTop } from 'utils';
const MyPage = (props) => {
// initialise useScrollToTop with true in order to scroll on page load
const setScrollToTop = useScrollToTop(true);
...
return <div onClick={() => setScrollToTop(true)}>click me to scroll to top</div>
}
I ran into this issue building a site with Gatsby whose Link is built on top of Reach Router. It seems odd that this is a modification that has to be made rather than the default behaviour.
Anyway, I tried many of the solutions above and the only one that actually worked for me was:
document.getElementById("WhateverIdYouWantToScrollTo").scrollIntoView()
I put this in a useEffect but you could just as easily put it in componentDidMount or trigger it any other way you wanted to.
Not sure why window.scrollTo(0, 0) wouldn't work for me (and others).
I had the same for problem for a while. Adding window.scrollTo(0, 0);to every page is painful and redundant. So i added a HOC which will wrap all my routes and it will stay inside BrowserRouter component:
<ScrollTop>
<Routes />
</ScrollTop>
Inside ScrollTopComponent we have the following:
import React, { useEffect } from "react";
import { useLocation } from "react-router-dom";
const ScrollTop = (props) => {
const { children } = props;
const location = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [location]);
return <main>{children}</main>;
};
export default ScrollTop;
Solution for functional component - Using useEffect() hook
useEffect(() => {
window.history.scrollRestoration = 'manual';}, []);
If you are doing this for mobile, at least with chrome, you will see a white bar at the bottom.
This happens when the URL bar disappears. Solution:
Change the css for height/min-height: 100% to height/min-height: 100vh.
Google Developer Docs
None of the above answers is currently working for me. It turns out that .scrollTo is not as widely compatible as .scrollIntoView.
In our App.js, in componentWillMount() we added
this.props.history.listen((location, action) => {
setTimeout(() => { document.getElementById('root').scrollIntoView({ behavior: "smooth" }) }, 777)
})
This is the only solution that is working universally for us. root is the ID of our App. The "smooth" behavior doesn't work on every browser / device. The 777 timeout is a bit conservative, but we load a lot of data on every page, so through testing this was necessary. A shorter 237 might work for most applications.

Resources