I am trying to implement custom scrolling with nextjs. I want to be able to save the scroll position of the page when a user leaves so when they return with popstate I can reset the scroll position.
The issue is that when I try to use replaceState with the history api next/router overrides what I put there with their own data. Next/router doesn't provide any type of history for their router and they don't put an id or anything that I can reference to use my own state. So I have no way to reference what page of the history stack I'm actually on. Next/router api docs are located here Nextjs Router API.
Here is a quick example of what I'm trying to do:
const setHistory = () => {
const { state } = window.history;
const newState = {
...state,
scroll: 'my custom scroll value'
};
history.replaceState(newState, '', state.url);
}
const handleRouteChangeStart = (url) => {
setHistory();
}
const handlePopstate = () => {
console.log(window.history)
}
window.addEventListener('popstate', handlePopstate);
Router.events.on('routeChangeStart', handleRouteChangeStart);
I have also tried to set the value to the options key that nextjs sets but I have inconsistent results with that as well meaning sometimes the value isn't set and it feels a little hacky.
Does NextJs really not allow you to have any kind of interaction with browser history or the ability to manipulate the window.history object?
Related
I am developing a nuxt app which needs to render different components based on my window width. And also will send a request to create a session, in which one of the header parameter is device width. Here is my approach to do that:
Code in my store:
//index.js
export const state = () => ({
device_width: null,
session_data : {},
})
export const mutations = {
set_device_width(state, payload) {
return state.device_width = payload;
},
set_session_data(state, payload){
return state.session_data = payload
}
}
export const actions = {
//server init action to initialize the app with some necessary data
async nuxtServerInit({ commit }) {
if (process.browser) {
commit('set_device_width', window.innerWidth);
}
var device_type = "";
if (this.state.device_width < 768) {
device_type = "mobile";
} else if (this.state.device_width > 768) {
device_type = "desktop";
}
var sessionData= await axios(...//api request with 'device_type' in header//....)
commit('set_session_data', sessionData)
}
then in my templates I am using computed method to get the value.
computed:{
isMobile(){
return this.$store.state.device_width<768
}
}
Now when I am keeping the screen size under 768, it works just fine. But in the case of desktop mode, full screen, it is always starting as isMobile==true for a few sec. Then isMobile is becoming false and automatically the desktop component is getting loaded. But for that few second I can see the mobile UI, and mainly my session data is always getting created with "device_type: mobile". Also as the page is initially starting with mobile UI, the SEO elements of my desktop UI is not working. How can I solve this? I just need to determine the screen size and store it before the initialization of the app. I thought serverInit action was the way to do that.
you cannot get any device info on serverside, nuxtServerInit executes at serverside and it has no clue about the user device, therefore you cannot use window object in it.
alternative would be getting the device's width in a mounted or created cycle of your layouts and committing it to the store, but these will happen after initializing your app.
As tony said, it should be more of a CSS solution for the biggest part.
If you still want to have the device somewhere, you can use fevid solution, here is some code to make it more visual.
/layouts/LayoutDefault.vue
<script>
import { throttle } from 'lodash-es'
const watchResize = throttle(function () {
console.log('resized properly')
// here you should mutate your `device_type` via a Vuex mutation/action
// and make your axios call by preferably fetching either a const/let variable
// or a global vuex state
}, 1000)
export default {
mounted() {
window.addEventListener('resize', watchResize)
},
}
</script>
I do recommend using throttle, install it with yarn add -D lodash-es because watching for a resize is a heavy watcher and it may trigger many times, hence make your UI sluggish.
PS: do not use this in a middleware, otherwise the eventListener will be plugged on each route navigation.
If you are using several layouts, it would be a good idea to unmount the watcher in a beforeDestroy() hook.
If you want to have more details on Nuxt's lifecycle, you can check this page: https://nuxtjs.org/docs/2.x/concepts/nuxt-lifecycle/#server
Here, you can see that nuxtServerInit is indeed only server side hence the name.
EDIT: this module looks nice for your use-case: https://github.com/nuxt-community/device-module
I've a form with a drop down on it, when I select the drop down I push the value onto the querystring. When the url with that querystring is first hit it stores the querystring param into state and then uses it to populate the drop down. This all works as intended. My issue is triggering the form to see changes to the querystring while still on the same page.
If I'm already on the page but then click a react router Link to the same page but with a different query string parameter it doesn't catch/see that it has changed. I'm wondering how to watch the querystring for changes or if there is a better way to handle this.
I've found that you can listen to the history object that React-Router is meant to use under the hood but I've had little success with this and again I'm not sure if this is the right way to go https://github.com/ReactTraining/history/blob/master/docs/getting-started.md
I've tried adding the following to the page but the listener never seems to fire when I change the querystring (aka search) of the url. Wonder what I'm missing?
useEffect(() => {
// Listen for changes to the current location.
console.info("wiring up history listener");
let unlisten = history.listen(({ location, action }) => {
console.info("HISTORY CHANGED", location.search);
console.log(action, location.pathname, location.state);
// Plan was to update my state here if needed
});
return unlisten;
}, []);
Using react-router-dom v6.
If you want to listen for changes on the path's query string parameters you need to "listen" for changes to them from the location object. Use the useLocation hook to get the location object.
location
{
key: 'ac3df4', // not with HashHistory!
pathname: '/somewhere',
search: '?some=search-string',
hash: '#howdy',
state: {
[userDefined]: true
}
}
Listen for changes using effect hook.
const { search } = useLocation();
useEffect(() => {
// search query string changed
}, [search]);
Your code doesn't work because react-router uses a different instance of history so your custom listeners aren't fired when the change is made through react-router's Link (it gets handled by a different version of history). However, clicking on the browser's forward or back buttons should trigger your listener since this notifies all history instances.
The best tool for your use case is useSearchParams hook
const [searchParams] = useSearchParams();
useEffect(() => {
// code for handling search query change
}, [searchParams]);
Also, you might not need to use useEffect but treat searchParams as a source of truth for search data without creating another entity in the state.
react-router comes with all the hooks you need. Sounds to me like you want: useParams
My React app uses CRA with react-router for routing.
After rendering this component if the user goes to another page this component gets un-mounted. When the user navigates back to it using the browser back button, the component is re-mounted and fetches the data form the server again.
I fully understand why this is happening and that it's the expected behaviour.
However, what's the best way to change my setup so that:
(a) the state persists after leaving the page (un-mounting the component), and
(b) the state is reloaded from server if user comes to the page through other than the browser back button
(c) the app knows whether a render is triggered by browser back button or not
const TableView = ({ view, module }) => {
const [isLoading, setIsLoading] = useState(true)
const [records, setRecords] = useState([])
useEffect(() => {
console.log("Mounting TableView")
return () => {
console.log("Un-Mounting TableView")
}
}, [])
useEffect(() => {
let mounted = true;
module.getAll() // gets data from server
.then(res => {
if (mounted) {
setRecords(res);
setIsLoading(false);
}
})
.catch(alert)
return () => mounted = false
}, [module])
As far as I understood Redux supports this use case? What would be the best way to reflect this without implementing Redux?
Well, for (a) you can use Context API. It's similar to Redux so you can store state even when components are unmounted.
For (b) and (c), a possible approach would be to verify if the user went to the page by pressing back button. You can do something like:
const history = useHistory()
const [cameByBackButton, setCameByBackButton] = useState(false)
useEffect(() => {
if (history.action === "POP") {
setCameByBackButton(true);
};
}, [history.action]);
So with cameByBackButton state you can identify if the back button was pressed and then you can use it to decide if a http call should be made or even if a component should be rendered.
I want to change a URL query for the current page in Next JS without triggering the page change event. My use case is to simply remember the week being viewed in a calendar, similar to Google Calendar. Here's what I've tried:
import Calendar from '../components/Calendar'
import { formatDate } from '../utils'
class CalendarDashboardPage extends React.Component {
refreshQuery(date) {
const { pathname, asPath } = this.props.router
const d = formatDate(date) // YYYY-MM-DD
this.props.router.push(pathname, asPath, { query: { d }, shallow: true })
}
render() {
return <Calendar onDateChange={this.refreshQuery) />
}
}
export default withRouter(CalendarDashboardPage)
This almost seems to work OK, but because this.props.router.push triggers a page change in Next, it causes the page loading bar that I'm using (nprogress) to appear on screen. I've tried this.props.router.push({ query: { d } });, but no luck.
So my question: is there any way to change the router query without triggering events such as routeChangeStart(url)?
You can keep track of the previous pathname and do a conditional check in the routeChangeStart event handler to see if the pathname(without query) changes:
// _app.js
import Router from "next/router";
let previousPathname = null;
function handleRouteChange(url) {
const pathname = url.split('?')[0];
if (previousPathname && previousPathname !== pathname) {
console.log(`App is changing from ${previousPathname} to ${pathname}...`);
}
previousPathname = pathname;
};
Router.events.on("routeChangeStart", handleRouteChange);
...
This may not answer your question directly since it will still trigger Router.events but hopefully can help you with your use case.
Short version: you can't (at least AFAIK).
The first requirement to change the URL in the browser without reload the page is to do that in a single page application.
To achieve I'm afraid you need to drop next/router and start to use react-router, in this article you can find details about how to do a SPA with next using react router.
Once you have done, with this.props.history.push("/new/url") react router feature you can do what you are looking for.
Hope this helps.
My solution was to just use the browser's API.
refreshQuery(date) {
const dateString = formatDate(date) // YYYY-MM-DD
window.history.pushState('', '', `?d=${dateString}`)
}
This changes the date URL parameter when the user flips through the calendar, which can be snagged by componentDidMount() when the users refreshes/shares the URL.
I have a listener to onpopstate event. I want to get record of history which was popped without travelling in history. I could do history.forward() and get state, but that will cause side-effects I don't want to see.
window.addEventListener('popstate', (event) => {
prevHistoryRecord = // How to get it without history.forward()
});
I assume you want to access a state added/modified by a framework or other part of your application. In that case, you can overwrite history functions and listen, store state changes in an object, which is accessible to you
const historyState = []
const fn = history.pushState.bind(history)
history.pushState = function() {
console.log('Push state', arguments)
historyState.push({ args: arguments })
fn.apply(history, arguments)
}
history.pushState('arg1', 'arg2')
You should overwrite history object before it has been used in your case. So, you will have access to a copy of history state.
It might also be accessible in History as well but I have no idea if you can reach it.