I would like to create a "go back" button that only goes back one page if the page is within the site.
I've tried following this answer to add a return button:
import { useNavigate } from 'react-router-dom';
function YourApp() {
const navigate = useNavigate();
return (
<>
<button onClick={() => navigate(-1)}>go back</button>
</>
);
}
But it goes one page back even if the page is not within the site.
E.g.: if I open a tab, go to stackoverflow.com, then go to my page and click the "go back" button, I will come back to StackOverflow.
How can I make it send me to a default page if the previous page is not within the site?
I ended up solving this by creating a history array in Redux and feeding it the current location from useLocation whenever it changes:
export const MyRoutes = () => {
const location = useLocation();
const dispatch = useDispatch();
useEffect(() => {
dispatch(pushLocationToHistory(location));
}, [location]);
};
And then I can just use the path of the last item if available.
The basic gist is that you can keep and maintain your own history stack and check if there are history entries to go back to. If the stack is empty then redirect to the app's home page, otherwise, allow the normal navigation.
Example using a React Context to hold the stack state and provide a customized navigate function.
import {
createContext,
useCallback,
useContext,
useEffect,
useState
} from "react";
import {
useNavigate as useNavigateBase,
UNSAFE_NavigationContext,
NavigationType
} from "react-router-dom";
const NavigateContext = createContext({
navigate: () => {},
});
const useNavigate = () => useContext(NavigateContext);
const NavigateProvider = ({ children }) => {
const [historyStack, setHistoryStack] = useState([]);
const navigateBase = useNavigateBase();
const { navigator } = useContext(UNSAFE_NavigationContext);
useEffect(() => {
const listener = ({ location, action }) => {
switch (action) {
case NavigationType.Push:
return setHistoryStack((stack) => stack.concat(location));
case NavigationType.Replace:
return setHistoryStack((stack) =>
stack.slice(0, -1).concat(location)
);
case NavigationType.Pop:
return setHistoryStack((stack) => stack.slice(0, -1));
default:
// ignore
}
};
return navigator.listen(listener);
}, [navigator]);
useEffect(() => {
console.log({ historyStack });
}, [historyStack]);
const navigate = useCallback(
(arg, options) => {
if (typeof arg === "number" && arg < 0 && !historyStack.length) {
navigateBase("/", { replace: true });
} else {
navigateBase(arg, options);
}
},
[historyStack, navigateBase]
);
return (
<NavigateContext.Provider value={navigate}>
{children}
</NavigateContext.Provider>
);
};
Example Usage:
const GoBackButton = () => {
const navigate = useNavigate();
return <button onClick={() => navigate(-1)}>go back</button>
}
...
function App() {
return (
<NavigateProvider>
... app code ...
</NavigateProvider>
);
}
This is my current progress. But i am not able to make it work.Even if the history is changing internally within the buttons of my application.still it is getting trigerred.can anybody help me??
import { useEffect,useState } from 'react';
import { useHistory,useLocation } from 'react-router-dom';
export const useGoBack = ()=>{
const history = useHistory();
const location = useLocation();
useEffect(()=>{
const unlisten=(event)=>{
history.listen((location, action) => {
if(action === 'POP');
{
let result = window.confirm("Are you sure u want to leave?");
if(!result)
{
history.goForward();
}
}
});
}
window.addEventListener('popstate',unlisten);
return () => {window.removeEventListener('popstate',unlisten)};
},[location.pathname]);
}
You don't need to define any dependency in useEffect, an empty array will be enough, also I recommend you to use window.addEventListener("popstate", unlisten) or history.listen(), not both:
useEffect(() => {
const unlisten = (event) => {
console.log(event);
//...
};
window.addEventListener("popstate", unlisten);
return () => {
window.removeEventListener("popstate", unlisten);
};
}, []);
or:
useEffect(() => {
return history.listen((location) => {//...}
}, []);
I have a component like so
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { useParams } from 'react-router-dom';
import PodcastActions from '../../store/podcast/podcast.actions';
const selectPodcast = createSelector(
state => state.podcasts,
(_, id) => id,
(podcasts, id) => {
return podcasts
? podcasts.find(podcast => {
return podcast.id.toString() === id;
})
: null;
});
const Podcast = () => {
const dispatch = useDispatch();
const params = useParams();
const podcast = useSelector(state => selectPodcast(state, params.id));
useEffect(() => {
if (!podcast) {
dispatch(PodcastActions.getPodcastById(params.id));
}
}, [dispatch, podcast, params.id]);
return <h2>{podcast.title}</h2>;
};
//Here I need to get the id
Podcast.serverFetch = PodcastActions.getPodcastById(); //Server side render - this is on refresh of the page
export default Podcast;
So I need to get the podcastId outside of the component for my SSR to be able to fetch using this
const dataRequirements =
routes
.filter(route => matchPath(req.url, route)) // filter matching paths
.map(route => route.component) // map to components
.filter(comp => comp.serverFetch) // check if components have data requirement
.map(comp => store.dispatch(comp.serverFetch())); // dispatch data requirement
But how can I do this?
For reference the url looks like this
localhost:port/podcasts/:id
I eventually changed the datarequirements function like follows
app.get("/*", async (req, res) => {
const context = {};
const store = createStore();
const dataRequirements = routes
.filter(route => matchPath(req.url, route)) // filter matching paths
.map(route => {
console.dir(route);
if (route.component?.serverFetch) {
const params = {};
const parts = route.path.split(`/`);
const routeParts = req.url.split(`/`);
parts.forEach((part, index) => {
if (part.startsWith(`:`)) {
params[part.substr(1)] = routeParts[index];
}
});
return store.dispatch(route.component.serverFetch(params));
}
});
//REST OF THE LOGIC TO RENDER REACT SSR
hope this will help someone else.
For reference, my routes are built in a constants file like follows
{
path: "pathOfRoute",
exact: true,
component: ReactComponentReference
}
I'm using react-router-dom v5, and react 16
Whenever I navigate like this:
from "/products/:someId" to "/products/:someOtherId"
the url changes but the components do not update accordingly
it's the same with queries.
"/products?search=something" or "/products?search=someOtherThing"
it does work when I'm in a different url, like "/" or "/users",
I'm using Link for navigation, I also tried the useHistory hook: history.push, history.replace and withRouter(myComponent);
This is how I'm getting data from queries/params
async function searchProducts(searchValue) {
const response = await axios.post(
"http://localhost:8000/api/products/search",
{ search: searchValue });
return response.data.body;
}
const useFetchData = (query) => {
const [products, setProducts] = useState([]);
useEffect(() => {
if (products.length === 0) {
// Use searchProducts for the request
searchProducts(query).then((foundProducts) => {
setProducts(foundProducts);
});
}
}, [products, query]);
return products;
};
then I useFetchData in my component which goes:
const ProductList = () => {
const history = useHistory();
// parsing query to be -> { search: "value" }
const urlQuery = queryString.parse(history.location.search);
const products = useFetchData(urlQuery.search);
const getList = () => {
return products.map((product) => {
return (
<li key={product._id}>
<ProductItem product={product} />
</li>
);
});
};
return <div className="container">{getList()}</div>;
};
The search button is in a different component for the header, it's always there since it's in the layout
<button className="header-search-button" onClick={handleClick}>
Search
</button>
and the handleClick:
// searchvalue has it's own onChange handler
const [searchValue, setSearchValue] = useState("");
// code...
const handleClick = () => {
// .... some code
// I also tried with push and Link
history.replace(`/products?search=${searchValue}`);
};
It's really difficult to tell without the code.
But my guess is you are able to change the /products?search=bag to /products?search=watch.
But after that it's not able to update the state and hence no re render.When you reload then the render happens.
It would be easier if we could see the code.
I get this error:
Can't perform a React state update on an unmounted component. This is
a no-op, but it indicates a memory leak in your application. To fix,
cancel all subscriptions and asynchronous tasks in a useEffect cleanup
function.
when fetching of data is started and component was unmounted, but function is trying to update state of unmounted component.
What is the best way to solve this?
CodePen example.
default function Test() {
const [notSeenAmount, setNotSeenAmount] = useState(false)
useEffect(() => {
let timer = setInterval(updateNotSeenAmount, 2000)
return () => clearInterval(timer)
}, [])
async function updateNotSeenAmount() {
let data // here i fetch data
setNotSeenAmount(data) // here is problem. If component was unmounted, i get error.
}
async function anotherFunction() {
updateNotSeenAmount() //it can trigger update too
}
return <button onClick={updateNotSeenAmount}>Push me</button> //update can be triggered manually
}
The easiest solution is to use a local variable that keeps track of whether the component is mounted or not. This is a common pattern with the class based approach. Here is an example that implement it with hooks:
function Example() {
const [text, setText] = React.useState("waiting...");
React.useEffect(() => {
let isCancelled = false;
simulateSlowNetworkRequest().then(() => {
if (!isCancelled) {
setText("done!");
}
});
return () => {
isCancelled = true;
};
}, []);
return <h2>{text}</h2>;
}
Here is an alternative with useRef (see below). Note that with a list of dependencies this solution won't work. The value of the ref will stay true after the first render. In that case the first solution is more appropriate.
function Example() {
const isCancelled = React.useRef(false);
const [text, setText] = React.useState("waiting...");
React.useEffect(() => {
fetch();
return () => {
isCancelled.current = true;
};
}, []);
function fetch() {
simulateSlowNetworkRequest().then(() => {
if (!isCancelled.current) {
setText("done!");
}
});
}
return <h2>{text}</h2>;
}
You can find more information about this pattern inside this article. Here is an issue inside the React project on GitHub that showcase this solution.
If you are fetching data from axios(using hooks) and the error still occurs, just wrap the setter inside the condition
let isRendered = useRef(false);
useEffect(() => {
isRendered = true;
axios
.get("/sample/api")
.then(res => {
if (isRendered) {
setState(res.data);
}
return null;
})
.catch(err => console.log(err));
return () => {
isRendered = false;
};
}, []);
TL;DR
Here is a CodeSandBox example
The other answers work of course, I just wanted to share a solution I came up with.
I built this hook that works just like React's useState, but will only setState if the component is mounted. I find it more elegant because you don't have to mess arround with an isMounted variable in your component !
Installation :
npm install use-state-if-mounted
Usage :
const [count, setCount] = useStateIfMounted(0);
You can find more advanced documentation on the npm page of the hook.
Here is a simple solution for this. This warning is due to when we do some fetch request while that request is in the background (because some requests take some time.)and we navigate back from that screen then react cannot update the state. here is the example code for this. write this line before every state Update.
if(!isScreenMounted.current) return;
Here is Complete Example
import React , {useRef} from 'react'
import { Text,StatusBar,SafeAreaView,ScrollView, StyleSheet } from 'react-native'
import BASEURL from '../constants/BaseURL';
const SearchScreen = () => {
const isScreenMounted = useRef(true)
useEffect(() => {
return () => isScreenMounted.current = false
},[])
const ConvertFileSubmit = () => {
if(!isScreenMounted.current) return;
setUpLoading(true)
var formdata = new FormData();
var file = {
uri: `file://${route.params.selectedfiles[0].uri}`,
type:`${route.params.selectedfiles[0].minetype}`,
name:`${route.params.selectedfiles[0].displayname}`,
};
formdata.append("file",file);
fetch(`${BASEURL}/UploadFile`, {
method: 'POST',
body: formdata,
redirect: 'manual'
}).then(response => response.json())
.then(result => {
if(!isScreenMounted.current) return;
setUpLoading(false)
}).catch(error => {
console.log('error', error)
});
}
return(
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={styles.scrollView}>
<Text>Search Screen</Text>
</ScrollView>
</SafeAreaView>
</>
)
}
export default SearchScreen;
const styles = StyleSheet.create({
scrollView: {
backgroundColor:"red",
},
container:{
flex:1,
justifyContent:"center",
alignItems:"center"
}
})
This answer is not related to the specific question but I got the same Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. and as a React newcomer could not find a solution to it.
My problem was related to useState in an unmounted component.
I noticed that I was calling a set state function (setIsLoading) after the function that unmounted my component:
const Login = () => {
const [isLoading, setIsLoading] = useState(false);
const handleLogin = () => {
setIsLoading(true);
firebase.auth().then(
functionToUnMountLoginSection();
// the problem is here
setIsLoading(false);
)
}
}
The correct way is to call setIsLoading when the component is still mounted, before calling the function to unmount/process user login in my specific case:
firebase.auth().then(
setIsLoading(false);
functionToUnMountLoginSection();
)
You add the state related datas into the useEffect body for not rerunning them every rerendering process. This method will solve the problem.
useEffect(() => {
let timer = setInterval(updateNotSeenAmount, 2000)
return () => clearInterval(timer)
}, [notSeenAmount])
REF: Tip: Optimizing Performance by Skipping Effects
Custom Hook Solution (ReactJs/NextJs)
Create a new folder named 'shared' and add two folders named 'hooks', 'utils' in it. Add a new file called 'commonFunctions.js' inside utils folder and add the code snippet below.
export const promisify = (fn) => {
return new Promise((resolve, reject) => {
fn
.then(response => resolve(response))
.catch(error => reject(error));
});
};
Add a new file called 'fetch-hook.js' inside hooks folder and add the code snippet below.
import { useCallback, useEffect, useRef } from "react";
import { promisify } from "../utils/commonFunctions";
export const useFetch = () => {
const isUnmounted = useRef(false);
useEffect(() => {
isUnmounted.current = false;
return () => {
isUnmounted.current = true;
};
}, []);
const call = useCallback((fn, onSuccess, onError = null) => {
promisify(fn).then(response => {
console.group('useFetch Hook response', response);
if (!isUnmounted.current) {
console.log('updating state..');
onSuccess(response.data);
}
else
console.log('aborted state update!');
console.groupEnd();
}).catch(error => {
console.log("useFetch Hook error", error);
if (!isUnmounted.current)
if (onError)
onError(error);
});
}, []);
return { call }
};
Folder Structure
Our custom hook is now ready. We use it in our component like below
const OurComponent = (props) => {
//..
const [subscriptions, setSubscriptions] = useState<any>([]);
//..
const { call } = useFetch();
// example method, change with your own
const getSubscriptions = useCallback(async () => {
call(
payment.companySubscriptions(userId), // example api call, change with your own
(data) => setSubscriptions(data),
);
}, [userId]);
//..
const updateSubscriptions = useCallback(async () => {
setTimeout(async () => {
await getSubscriptions();
}, 5000);// 5 seconds delay
}, [getSubscriptions]);
//..
}
In our component, we call 'updateSubscriptions' method. It will trigger 'getSubscriptions' method in which we used our custom hook. If we try to navigate to a different page after calling updateSubscriptions method before 5 seconds over, our custom hook will abort state update and prevent that warning on the title of this question
Wanna see opposite?
Change 'getSubscriptions' method with the one below
const getSubscriptions = useCallback(async () => {
const response = await payment.companySubscriptions(userId);
setSubscriptions(response);
}, [userId]);
Now try to call 'updateSubscriptions' method and navigate to a different page before 5 seconds over
Try this custom hook:
import { useEffect, useRef } from 'react';
export const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
function Example() {
const isMounted = useIsMounted();
const [text, setText] = useState();
const safeSetState = useCallback((callback, ...args) => {
if (isMounted.current) {
callback(...args);
}
}, []);
useEffect(() => {
safeSetState(setText, 'Hello')
});
}, []);
return <h2>{text}</h2>;
}