How to change scroll behavior while going back in next js? - javascript

I fetch a list of posts in index js like this :
const Index = (props) => {
return (
<div>
{props.posts.map((each) => {
return (
<Link scroll={false} as={`/post/${each.id}`} href="/post/[id]" key={each.id}>
<a>
<h1>{each.title}</h1>
</a>
</Link>
);
})}
</div>
);
};
export async function getStaticProps() {
const url = `https://jsonplaceholder.typicode.com/posts`;
const res = await axios.get(url);
return {
props: { posts: res.data }
};
}
And when user clicks on any link it goes to post page which is :
function post({ post }) {
return (
<h1>{post.id}</h1>
);
}
export async function getServerSideProps({ query }) {
const { id } = query;
const res = await Axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`);
return {
props: { post: res.data}
};
}
The problem is when I click back the scroll position resets to top and it fetches all posts .
I included scroll={false} in Link but it doesn't work .
How can I prevent scroll resetting when user clicks back from the post page ?

Next.js in fact has built-in support for restoring scroll position when back to the previous page. We can simply enable it by editing the next.config.js:
module.exports = {
experimental: {
scrollRestoration: true,
},
}

scroll={false} doesn't maintain the scroll of the previous page; it doesn't change the scroll at all which means the scroll would be that of the page you are linking from. You can use scroll={false} to override the default behavior of setting the scrollY to 0, so you can implement your own behavior.
Here's how I implemented restoring the scroll position. This is very similar to Max william's answer, but using useRef instead of useState. We are using useRef instead of useState because useRef does not cause a re-render whenever its value is mutated, unlike useState. We are going to be updating the value to the current scroll position every time the scroll is changed by the user, which would mean a ton of useless re-renders if we were to use useState.
First, define a UserContext component to easily pass the scroll data from the _app.js component to wherever you need:
import { createContext } from 'react';
const UserContext = createContext();
export default UserContext;
Then, in your _app.js component, wrap your pages with the UserContext and create a useRef attribute to store the scroll position.
import { useRef } from 'react';
import UserContext from '../components/context'
function MyApp({ Component, pageProps }) {
const scrollRef = useRef({
scrollPos: 0
});
return (
<Layout>
<UserContext.Provider value={{ scrollRef: scrollRef }}>
<Component {...pageProps} />
</UserContext.Provider>
</Layout>
)
}
export default MyApp
Then inside whichever page component you are wanting to restore the scroll position (that is, the page that you want to return to and see the same scroll position as when you left), you can put this code to set the scroll position of the page and bind a scroll event to a function to update the stored scroll position.
import UserContext from '../components/context'
import { useContext } from 'react';
export default function YourPageComponent() {
const { scrollRef } = useContext(UserContext);
React.useEffect(() => {
//called when the component has been mounted, sets the scroll to the currently stored scroll position
window.scrollTo(0, scrollRef.current.scrollPos);
const handleScrollPos = () => {
//every time the window is scrolled, update the reference. This will not cause a re-render, meaning smooth uninterrupted scrolling.
scrollRef.current.scrollPos = window.scrollY
};
window.addEventListener('scroll', handleScrollPos);
return () => {
//remove event listener on unmount
window.removeEventListener('scroll', handleScrollPos);
};
});
return (
//your content
)
}
The last little thing is to use scroll={false} on your Link component that links back to YourPageComponent. This is so next.js doesn't automatically set the scroll to 0, overriding everything we've done.
Credit to Max william's answer to the majority of the structure, my main change is using useRef. I also added some explanations, I hope it's helpful!

I solved the problem with the help of context and window scroll position like this :
import UserContext from '../context/context';
function MyApp({ Component, pageProps }) {
const [ scrollPos, setScrollPos ] = React.useState(0);
return (
<UserContext.Provider value={{ scrollPos: scrollPos, setScrollPos: setScrollPos }}>
<Component {...pageProps} />
</UserContext.Provider>
);
}
export default MyApp;
index js file :
import Link from 'next/link';
import UserContext from '../context/context';
import { useContext } from 'react';
import Axios from 'axios';
export default function Index(props) {
const { scrollPos, setScrollPos } = useContext(UserContext);
const handleScrollPos = () => {
setScrollPos(window.scrollY);
};
React.useEffect(() => {
window.scrollTo(0, scrollPos);
}, []);
React.useEffect(() => {
window.addEventListener('scroll', handleScrollPos);
return () => {
window.removeEventListener('scroll', handleScrollPos);
};
}, []);
if (props.err) {
return <h4>Error bro</h4>;
}
return (
<div>
{props.res.map((each) => {
return (
<div key={each.id}>
<Link scroll={true} as={`/post/${each.id}`} href="/post/[id]">
{each.title}
</Link>
</div>
);
})}
</div>
);
}
export async function getServerSideProps() {
let res;
let err;
try {
res = await Axios.get('https://jsonplaceholder.typicode.com/posts');
err = null;
} catch (e) {
err = 'Error bro';
res = { data: [] };
}
return {
props: {
res: res.data,
err: err
}
};
}
post js file :
import Axios from 'axios';
function Post(props) {
if (props.err) {
return <h4>{props.err}</h4>;
}
return <h1>{props.post.title}</h1>;
}
export async function getServerSideProps(ctx) {
const { query } = ctx;
let err;
let res;
try {
res = await Axios.get(`https://jsonplaceholder.typicode.com/posts/${query.id}`);
err = null;
} catch (e) {
res = { data: [] };
err = 'Error getting post';
}
return {
props: {
post: res.data,
err: err
}
};
}
export default Post;
So when you click back from post js page , the first useEffect in index js will run and you will be scrolled to that position .
Also after that the second useEffect will capture the user's scroll position by listening to the scroll event listener so it will always save the latest scroll y position in context so next time you comeback to index js the first useEffect will run and set scroll position to that value in context .

Related

There is a problem with the delete function of the react component implemented through the map method

I am developing using react.
It is in the process of fetching the information contained in the db and displaying it on the web page through the map method.
If you delete one piece of information using onclick or the onClose method provided by antd, the info is also deleted from the db.
in the db, the function worked successfully. but the information at the bottom is deleted, not the deleted information in the web page.
If I refresh website, it is displayed normally, but I don't want to use the window reload function.
I wonder why this is happening and what is the solution.
thank you!
AlertPage
import React, { useState } from "react";
import useSWR from "swr";
import axios from "axios";
import AlertComponent from "./Sections/AlertComponent";
const fetcher = async (url) =>
await axios.get(url).then((response) => JSON.parse(response.data.alerts));
function AlertPage() {
const { data = [], error } = useSWR("/api/streaming/getAlerts", fetcher, {
refreshInterval: 1000,
});
const onClose = (data) => {
axios.post(`/api/streaming/removeAlerts/${data._id.$oid}`).then(() => {
console.log(`${data._id.$oid} deleted`);
});
};
const renderAlerts = data.map((alert, index) => {
return (
<div key={index}>
<AlertComponent alert={alert} index={index} onClose={onClose} />
</div>
);
});
if (error) return <div>failed to load</div>;
if (data === []) return <div>loading...</div>;
return <div>{renderAlerts}</div>;
}
export default AlertPage;
AlertComponent
import React, { useState } from "react";
import { Alert } from "antd";
import Marquee from "react-fast-marquee";
function AlertComponent(props) {
const [alert, setalert] = useState(props.alert);
const [index, setindex] = useState(props.index);
return (
<div
className="alert"
key={index}
style={{ display: "flex" }}
onClick={() => {
props.onClose(alert);
}}
>
<Alert
message={`${alert.data.timestamp.$date.substr(0, 19)}`}
description={
<Marquee pauseOnHover speed={40} gradient={false}>
{`<${alert.data.location}> <${alert.data.name}> <${alert.data.contents}> detected`}
</Marquee>
}
banner
/>
</div>
);
}
export default AlertComponent;
This could be happening due the local cache maintained by swr and since you're not refetching the data after the deletion the changes are not reflected in the DOM.
One options is to trigger a manual refetch to retrieve the most up-to-date data. We could achieve that by changing the following lines:
const { data = [], error, mutate } = useSWR("/api/streaming/getAlerts", fetcher, {
refreshInterval: 1000
});
...
axios.post(`/api/streaming/removeAlerts/${data._id.$oid}`).then(() => {
mutate("/api/streaming/getAlerts");
});
another approach would be to rely on the optimistic update strategy from swr, there is an example here

React Router v6 doesn't support usePrompt and useBlock anymore [duplicate]

I am basically trying to intercept route changes. Maybe something equivalent of vue's beforeEach in React Router v6 could be useful as React Router v.6 does not include usePrompt.
BEFORE each route change I want to do some logic - the logic might need to interrupt or even change the end route based on the result.
I have searched around but I really can't find something that solves this specific problem.
Thanks in advance.
Currently they have removed the usePrompt from the react-router v6.
I found a solution from ui.dev and added TypeScript support, and am now using that until the react-router will bring back the usePrompt/useBlocker hooks
import { History, Transition } from 'history';
import { useCallback, useContext, useEffect } from "react";
import { Navigator } from 'react-router';
import { UNSAFE_NavigationContext as NavigationContext } from "react-router-dom";
type ExtendNavigator = Navigator & Pick<History, "block">;
export function useBlocker(blocker: (tx: Transition) => void, when = true) {
const { navigator } = useContext(NavigationContext);
useEffect(() => {
if (!when) return;
const unblock = (navigator as ExtendNavigator).block((tx) => {
const autoUnblockingTx = {
...tx,
retry() {
unblock();
tx.retry();
},
};
blocker(autoUnblockingTx);
});
return unblock;
}, [navigator, blocker, when]);
}
export default function usePrompt(message: string, when = true) {
const blocker = useCallback((tx: Transition) => {
if (window.confirm(message)) tx.retry();
}, [message]);
useBlocker(blocker, when);
}
This can then be used in any view/component where you would like a "A you sure you want to leave?"-message displayed when the condition is true.
usePrompt("Do you want to leave?", isFormDirty());
Yes usePrompt and useBlock has been removed, but you can achieve same thing using history.block, here is the working example for blocking navigation using history.block with custom modal in React Router Dom V5
import { useHistory } from "react-router-dom";
import { UnregisterCallback } from "history";
...
type Prop = {
verify?: {
blockRoute?: (nextRoute: string) => boolean;
}
};
...
// in the component where you want to show confirmation modal on any nav change
const history = useHistory();
const unblock = useRef<UnregisterCallback>();
const onConfirmExit = () => {
/**
* if user confirms to exit, we can allow the navigation
*/
// Unblock the navigation.
unblock?.current?.();
// Proceed with the blocked navigation
goBack();
};
useEffect(() => {
/**
* Block navigation and register a callback that
* fires when a navigation attempt is blocked.
*/
unblock.current = history.block(({ pathname: to }) => {
/**
* Simply allow the transition to pass immediately,
* if user does not want to verify the navigate away action,
* or if user is allowed to navigate to next route without blocking.
*/
if (!verify || !verify.blockRoute?.(to)) return undefined;
/**
* Navigation was blocked! Let's show a confirmation dialog
* so the user can decide if they actually want to navigate
* away and discard changes they've made in the current page.
*/
showConfirmationModal();
// prevent navigation
return false;
});
// just in case theres an unmount we can unblock if it exists
return unblock.current;
}, [history]);
Here is a JS example of the react-route-dom v6 usePrompt if you're not using TS.
import { useContext, useEffect, useCallback } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
export function useBlocker( blocker, when = true ) {
const { navigator } = useContext( NavigationContext );
useEffect( () => {
if ( ! when ) return;
const unblock = navigator.block( ( tx ) => {
const autoUnblockingTx = {
...tx,
retry() {
unblock();
tx.retry();
},
};
blocker( autoUnblockingTx );
} );
return unblock;
}, [ navigator, blocker, when ] );
}
export function usePrompt( message, when = true ) {
const blocker = useCallback(
( tx ) => {
// eslint-disable-next-line no-alert
if ( window.confirm( message ) ) tx.retry();
},
[ message ]
);
useBlocker( blocker, when );
}
Then the implementation would be...
const MyComponent = () => {
const formIsDirty = true; // Condition to trigger the prompt.
usePrompt( 'Leave screen?', formIsDirty );
return (
<div>Hello world</div>
);
};
Here's the article with the example

conditional rendering with toast and usestate does not work with react

I have my state and I want to display the component if the value is true but in the console I receive the error message Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state my code
import React, { useState} from "react";
import { useToasts } from "react-toast-notifications";
const Index = () => {
const [test, setTest]= useState(true);
const { addToast } = useToasts();
function RenderToast() {
return (
<div>
{ addToast('message') }
</div>
)}
return (
<div>
{test && <RenderToast /> }
</div>
)
}
You cannot set state during a render. And I'm guessing that addToast internally sets some state.
And looking at the docs for that library, you don't explicitly render the toasts. You just call addToast and then the <ToastProvider/> farther up in the tree shows them.
So to make this simple example works where a toast is shown on mount, you should use an effect to add the toast after the first render, and make sure your component is wrapped by <ToastProvider>
const Index = () => {
const { addToast } = useToasts();
useEffect(() => {
addToast('message')
}, [])
return <>Some Content here</>
}
// Example app that includes the toast provider
const MyApp = () => {
<ToastProvider>
<Index />
</ToastProvider>
}
how i can display the toast based on a variable for exemple display toast after receive error on backend?
You simply call addToast where you are handling your server communication.
For example:
const Index = () => {
const { addToast } = useToasts();
useEffect(() => {
fetchDataFromApi()
.then(data => ...)
.catch(error => addToast(`error: ${error}`))
}, [])
//...
}

How can I know my current route in react-navigation 5?

I am using this https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html to access my navigation from any source, my file look as follow:
import { createRef } from 'react';
export const navigationRef = createRef();
export function navigate(name, params) {
return navigationRef.current?.navigate(name, params);
}
export function goBack() {
return navigationRef.current?.goBack();
}
export function getRootState() {
return navigationRef.current?.getRootState();
}
This is perfect for my #navigation/drawer, which is outside my stack navigation.
Only one problem the last method is not synchronized and I want to have an active state on my item menu that is the current route.
How is that possible with react navigation 5?
I am using the following approach to get the current route name in react-navigation v5.
https://reactnavigation.org/docs/navigation-prop/#dangerouslygetstate
const {index, routes} = this.props.navigation.dangerouslyGetState();
const currentRoute = routes[index].name;
console.log('current screen', currentRoute);
The NavigationContainer has onStateChange prop, useful for this case, check react-navigation docs Screen Tracking for analytics and if you need access without navigation prop see Navigating without the navigation prop
I share the code to get only active routeName
function App(){
const routeNameRef = React.useRef();
// Gets the current screen from navigation state
const getActiveRouteName = (state)=> {
const route = state.routes[state?.index || 0];
if (route.state) {
// Dive into nested navigators
return getActiveRouteName(route.state);
}
return route.name;
};
return (<NavigationContainer
onStateChange={(state) => {
if (!state) return;
//#ts-ignore
routeNameRef.current = getActiveRouteName(state);
}}
>
...
</NavigationContainer>)
}
If you want to know the current screen from a component you can also use this:
export const HomeScreen = ({ navigation, route }) => {
console.log(route.name);
return (
{...}
);
};
It is possible to get this from the navigationRef attached to the navigation container. Where navigationRef is a ref.
export const navigationRef = React.createRef()
and
<NavigationContainer
ref={navigationRef}
>
<Navigator />
</NavigationContainer>
Then use: const currentRouteName = navigationRef.current.getCurrentRoute().name to get the current route name.
Alternatively in a functional component you can useRef const navigationRef = React.useRef()
There is a util function called getFocusedRouteNameFromRoute(route) which the docs recommends.
BUT - it seems its working only for child screen, so I defined the following function to get the active route name:
const getCurrentRouteName = (navigation, route) => {
if (route.state)
return getFocusedRouteNameFromRoute(route);
return route.name;
};
This works for me. In navigationRef.js
let navigator;
export const setNavigator = (nav) => {
navigator = nav;
};
export const getCurrentRoute = () => {
const route = navigator.getCurrentRoute();
return route.name;
};
This can be referred from any source like this
import { getCurrentRoute } from '../navigationRef';
const currentScene = getCurrentRoute();

How to pass this.props to js file using react-native

I am making auth component using react-native.
And the code below sends to 'MainTab' of 'this.props.navigation' depends on the result of axios.
<TouchableOpacity onPress={this.handleSubmit}>
<Text>Save</Text>
</TouchableOpacity>
handleSubmit = () => {
const result = await axios.post(
'http://192.0.0.1:4000/clients',
users
);
if (result.data.success) {
return this.props.navigation.navigate('MainTab');
}
return false
};
But I want to use handleSubmit at an other 'js' file to avoid doing repeatedly.
Thus, I edit a code like below.
import { saveSettings } from '../../storage/settingsStorage'
handleSubmit(): void {
saveSettings(this.state);
}
// in 'settingsStorage.js'
export const saveSettings = async users => {
try {
const result = await axios.post(
'http://192.0.0.1:4000/clients/token',
users
);
if (result.data.success) {
return this.props.navigation.navigate('MainTab');
}
return false
} catch (e) {
console.log(e);
}
};
And in this case, I know 'this.props' can't be passed in normal Js file without passing props. But I don't know how can I pass the props?
Thank you so much for reading this.
Based on your description I think you can just add a second parameter to saveSettings and pass the navigation object through:
import { saveSettings } from '../../storage/settingsStorage'
handleSubmit(): void {
saveSettings(this.state, this.props.navigation);
}
// in 'settingsStorage.js'
export const saveSettings = async (users, navigation) => {
try {
const result = await axios.post(
'http://192.0.0.1:4000/clients/token',
users
);
if (result.data.success) {
return navigation.navigate('MainTab');
}
return false
} catch (e) {
console.log(e);
}
};
Instead of passing navigation prop you can use a technique Navigating without the navigation prop as described in official site .
App.js
import { createStackNavigator, createAppContainer } from 'react-navigation';
import NavigationService from './NavigationService';
const TopLevelNavigator = createStackNavigator({
/* ... */
});
const AppContainer = createAppContainer(TopLevelNavigator);
export default class App extends React.Component {
// ...
render() {
return (
<AppContainer
ref={navigatorRef => {
NavigationService.setTopLevelNavigator(navigatorRef);
}}
/>
);
}
}
we define NavigationService which is a simple module with functions that dispatch user-defined navigation actions.
/ NavigationService.js
import { NavigationActions } from 'react-navigation';
let _navigator;
function setTopLevelNavigator(navigatorRef) {
_navigator = navigatorRef;
}
function navigate(routeName, params) {
_navigator.dispatch(
NavigationActions.navigate({
routeName,
params,
})
);
}
// add other navigation functions that you need and export them
export default {
navigate,
setTopLevelNavigator,
};
Now use it every where without navigation prop
// any js module
import NavigationService from 'path-to-NavigationService.js';
// ...
NavigationService.navigate('MainTab');

Categories