I'm using Material-ui's Tabs, which are controlled and I'm using them for (React-router) Links like this:
<Tab value={0} label="dashboard" containerElement={<Link to="/dashboard/home"/>}/>
<Tab value={1} label="users" containerElement={<Link to="/dashboard/users"/>} />
<Tab value={2} label="data" containerElement={<Link to="/dashboard/data"/>} />
If I'm currenlty visting dashboard/data and I click browser's back button
I go (for example) to dashboard/users but the highlighted Tab still stays on dashboard/data (value=2)
I can change by setting state, but I don't know how to handle the event when the browser's back button is pressed?
I've found this:
window.onpopstate = this.onBackButtonEvent;
but this is called each time state is changed (not only on back button event)
Using react-router made the job simple as such:
import { browserHistory } from 'react-router';
componentDidMount() {
this.onScrollNearBottom(this.scrollToLoad);
this.backListener = browserHistory.listen((loc, action) => {
if (action === "POP") {
// Do your stuff
}
});
}
componentWillUnmount() {
// Unbind listener
this.backListener();
}
Using hooks you can detect the back and forward buttons
import { useHistory } from 'react-router-dom'
const [ locationKeys, setLocationKeys ] = useState([])
const history = useHistory()
useEffect(() => {
return history.listen(location => {
if (history.action === 'PUSH') {
setLocationKeys([ location.key ])
}
if (history.action === 'POP') {
if (locationKeys[1] === location.key) {
setLocationKeys(([ _, ...keys ]) => keys)
// Handle forward event
} else {
setLocationKeys((keys) => [ location.key, ...keys ])
// Handle back event
}
}
})
}, [ locationKeys, ])
here is how I ended up doing it:
componentDidMount() {
this._isMounted = true;
window.onpopstate = ()=> {
if(this._isMounted) {
const { hash } = location;
if(hash.indexOf('home')>-1 && this.state.value!==0)
this.setState({value: 0})
if(hash.indexOf('users')>-1 && this.state.value!==1)
this.setState({value: 1})
if(hash.indexOf('data')>-1 && this.state.value!==2)
this.setState({value: 2})
}
}
}
thanks everybody for helping lol
Hooks sample
const {history} = useRouter();
useEffect(() => {
return () => {
// && history.location.pathname === "any specific path")
if (history.action === "POP") {
history.replace(history.location.pathname, /* the new state */);
}
};
}, [history])
I don't use history.listen because it doesn't affect the state
const disposeListener = history.listen(navData => {
if (navData.pathname === "/props") {
navData.state = /* the new state */;
}
});
Most of the answers for this question either use outdated versions of React Router, rely on less-modern Class Components, or are confusing; and none use Typescript, which is a common combination. Here is an answer using Router v5, function components, and Typescript:
// use destructuring to access the history property of the ReactComponentProps type
function MyComponent( { history }: ReactComponentProps) {
// use useEffect to access lifecycle methods, as componentDidMount etc. are not available on function components.
useEffect(() => {
return () => {
if (history.action === "POP") {
// Code here will run when back button fires. Note that it's after the `return` for useEffect's callback; code before the return will fire after the page mounts, code after when it is about to unmount.
}
}
})
}
A fuller example with explanations can be found here.
Version 3.x of the React Router API has a set of utilities you can use to expose a "Back" button event before the event registers with the browser's history. You must first wrap your component in the withRouter() higher-order component. You can then use the setRouteLeaveHook() function, which accepts any route object with a valid path property and a callback function.
import {Component} from 'react';
import {withRouter} from 'react-router';
class Foo extends Component {
componentDidMount() {
this.props.router.setRouteLeaveHook(this.props.route, this.routerWillLeave);
}
routerWillLeave(nextState) { // return false to block navigation, true to allow
if (nextState.action === 'POP') {
// handle "Back" button clicks here
}
}
}
export default withRouter(Foo);
Using hooks. I have converted #Nicolas Keller's code to typescript
const [locationKeys, setLocationKeys] = useState<(string | undefined)[]>([]);
const history = useHistory();
useEffect(() => {
return history.listen((location) => {
if (history.action === 'PUSH') {
if (location.key) setLocationKeys([location.key]);
}
if (history.action === 'POP') {
if (locationKeys[1] === location.key) {
setLocationKeys(([_, ...keys]) => keys);
// Handle forward event
console.log('forward button');
} else {
setLocationKeys((keys) => [location.key, ...keys]);
// Handle back event
console.log('back button');
removeTask();
}
}
});
}, [locationKeys]);
I used withrouter hoc in order to get history prop and just write a componentDidMount() method:
componentDidMount() {
if (this.props.history.action === "POP") {
// custom back button implementation
}
}
in NextJs we can use beforePopState function and do what we want such close modal or show a modal or check the back address and decide what to do
const router = useRouter();
useEffect(() => {
router.beforePopState(({ url, as, options }) => {
// I only want to allow these two routes!
if (as === '/' ) {
// Have SSR render bad routes as a 404.
window.location.href = as;
closeModal();
return false
}
return true
})
}, [])
For giving warning on the press of browser back in react functional components. do the following steps
declare isBackButtonClicked and initialize it as false and maintain the state using setBackbuttonPress function.
const [isBackButtonClicked, setBackbuttonPress] = useState(false);
In componentdidmount, add the following lines of code
window.history.pushState(null, null, window.location.pathname);
window.addEventListener('popstate', onBackButtonEvent);
define onBackButtonEvent Function and write logic as per your requirement.
const onBackButtonEvent = (e) => {
e.preventDefault();
if (!isBackButtonClicked) {
if (window.confirm("Do you want to go to Test Listing")) {
setBackbuttonPress(true)
props.history.go(listingpage)
} else {
window.history.pushState(null, null, window.location.pathname);
setBackbuttonPress(false)
}
}
}
In componentwillmount unsubscribe onBackButtonEvent Function
Final code will look like this
import React,{useEffect,useState} from 'react'
function HandleBrowserBackButton() {
const [isBackButtonClicked, setBackbuttonPress] = useState(false)
useEffect(() => {
window.history.pushState(null, null, window.location.pathname);
window.addEventListener('popstate', onBackButtonEvent);
//logic for showing popup warning on page refresh
window.onbeforeunload = function () {
return "Data will be lost if you leave the page, are you sure?";
};
return () => {
window.removeEventListener('popstate', onBackButtonEvent);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onBackButtonEvent = (e) => {
e.preventDefault();
if (!isBackButtonClicked) {
if (window.confirm("Do you want to go to Test Listing")) {
setBackbuttonPress(true)
props.history.go(listingpage)
} else {
window.history.pushState(null, null, window.location.pathname);
setBackbuttonPress(false)
}
}
}
return (
<div>
</div>
)
}
export default HandleBrowserBackButton
If you are using React Router V5, you can try Prompt.
Used to prompt the user before navigating away from a page. When your application enters a state that should prevent the user from navigating away (like a form is half-filled out), render a <Prompt>
<Prompt
message={(location, action) => {
if (action === 'POP') {
console.log("Backing up...")
// Add your back logic here
}
return true;
}}
/>
just put in componentDidMount()
componentDidMount() {
window.onbeforeunload =this.beforeUnloadListener;
}
beforeUnloadListener = (event) => {
event.preventDefault();
return event.returnValue = "Are you sure you want to exit?";
};
Add these 2 lines in to your componentDidMount().This worked for me
window.history.pushState(null, null, document.URL);
window.addEventListener('popstate', function(event) {
window.location.replace(
`YOUR URL`
);
});
It depends on the type of Router you use in React.
If you use BrowserRouter from react-router (not available in react-router v4 though), as mentioned above, you can use the action 'POP' to intercept the browser back button.
However, if you use HashRouter to push the routes, above solution will not work. The reason is hash router always triggered with 'POP' action when you click browser back button or pushing the route from your components. You cant differentiate these two actions either with window.popstate or history.listen simply.
Upcoming version 6.0 introduces useBlocker hook - which could be used to intercept all navigation attempts.
import { Action } from 'history';
import { useBlocker } from 'react-router';
// when blocker should be active
const unsavedChanges = true;
useBlocker((transition) => {
const {
location, // The new location
action, // The action that triggered the change
} = transition;
// intercept back and forward actions:
if (action === Action.Pop) {
alert('intercepted!')
}
}, unsavedChanges);
You can use "withrouter" HOC and use this.props.history.goBack.
<Button onClick={this.props.history.goBack}>
BACK
</Button>
Related
Im trying to pass a state value into a component. Why is it working in one component and not working in another component in the same folder?
I have the hooks in here. Im trying to access "currentGuess". In this function I initialize the state of currentGuess to "", then the next part just sets the "currentGuess" to whatever you type in.
----------------------/src/hooks/useWordle.js----------------------
const useWordle = (solution) => {
const [currentGuess, setCurrentGuess] = useState("");
/* OTHER UNNECESSARY CODE TO QUESTION */
const handleInput = ({ key }) => {
if (key === "Enter") {
if (turn > 5) {
console.log("You used all your guesses!");
return;
}
if (history.includes(currentGuess)) {
console.log("You already tried that word!");
return;
}
if (currentGuess.length !== 5) {
console.log("Word must be 5 characters long!");
return;
}
const formatted = formatGuessWord();
console.log(formatted);
}
if (key === "Backspace") {
setCurrentGuess((state) => {
return state.slice(0, -1);
});
}
if (/^[a-zA-z]$/.test(key))
if (currentGuess.length < 5) {
setCurrentGuess((state) => {
return state + key;
});
}
};
return {
currentGuess,
handleInput,
};
};
export default useWordle;
I can use it in here like this and it works no problem:
----------------------src/components/Wordle.js----------------------
import React, { useEffect } from "react";
import useWordle from "../hooks/wordleHooks.js";
function Wordle({ solution }) {
const { currentGuess, handleInput } = useWordle(solution);
console.log("currentGuess=", currentGuess);
useEffect(() => {
window.addEventListener("keyup", handleInput);
return () => window.removeEventListener("keyup", handleInput);
});
return <div>Current guess: {currentGuess}</div>;
}
export default Wordle;
I thought this line was what allowed me to use "currentGuess". I destructured it.
const { currentGuess, handleInput } = useWordle(solution);
However when I place that line in this code, "currentGuess" comes out undefined or empty.
----------------------/src/components/Key.js----------------------
import React, { useContext } from "react";
import { AppContext } from "../App";
import useWordle from "../hooks/wordleHooks.js";
export default function Key({ keyVal, largeKey }) {
const { onSelectLetter, onDeleteKeyPress, onEnterKeyPress } =
useContext(AppContext);
const { currentGuess } = useWordle();
const handleTypingInput = () => {
console.log("Key.js - Key() - handleTypingInput() - {currentGuess}= ", {
currentGuess,
}); // COMES OUT "Object { currentGuess: "" }"
};
If you made it this far thank you very much. Im new to this and hoping someone who knows what they are doing can see some glaring flaw in the code I can fix. You don't even have to solve it for me but can you point me in the right direction? How do I make the "currentGuess" variable/state be accessible in the Key.js component?
Hooks are meant to share behavior, not state. The call to useWordle in your Wordle component creates a completely different variable in memory for currentGuess than the call to useWordle in the Key component (remember, after all, that hooks are just regular old functions!). In other words, you have two completely separate versions of currentGuess floating around, one in each component.
If you'd like to share state, use the Context API, use a state management library (such Redux, MobX, etc.), or just lift state up.
When I close browser or tab it should call log out api.
can anybody suggest any solution on it?
This is an illustration that I came about, link included but it does not factor in the refresh part so still figuring that out.
import {useEffect} from 'react';
const App = () => {
let logOut = () => {
localStorage.clear();
navigate('/login')
}
useEffect(() => {
const handleTabClose = event => {
event.preventDefault();
console.log('beforeunload event triggered');
return (event.returnValue = 'Are you sure you want to exit?');
};
window.addEventListener('beforeunload', handleTabClose);
return () => {
logOut()
window.removeEventListener('beforeunload', handleTabClose);
};
}, []);
return (
<div>
<h2>hello world</h2>
</div>
);
};
export default App;
[https://bobbyhadz.com/blog/react-handle-tab-close-event][1]
You need to call the logout API right before your component unmounts.
For functional components, the clean up function is the place:
const MyComponent = (props) => {
useEffect(() => {
return () => {
// Logout logic goes here
}
}, []);
}
For class-based components, it's the componentWillUnmount() function:
class MyComponent extends React.Component {
componentWillUnmount() {
// Logout logic goes here
}
}
But wait, that will trigger the logout every time the user moves away from the page, even without closing the browser/tab. For that, you could make use of a state variable, and handle outbound navigations based on its value.
For example, create an isOnlineNav state variable that is false by default, and you set it to true whenever the user moves away by interacting with the app (link clicks, navigation, form submission, redirects, etc.). When the user closes the browser tab, the value remains false, and you could use the state to trigger logout.
Example in a functional component:
const MyComponent = (props) => {
const [isOnlineNav, setIsOnlineNav] = useState(false);
const goHome = () => {
setIsOnlineNav(true);
window.location = "/";
}
useEffect(() => {
return () => {
if (!isOnlineNav) {
// Logout logic goes here
}
}
}, []);
return (
<button onClick={goHome}>Go Home</button>
)
}
It could cause unexpected bugs if you're not careful. Below are at least two of those possible scenarios.
You need to cover all in-app ways that your user could navigate away
from this page.
Some otherwise straightforward ways of navigation will get a little more complicated (e.g.: You won't be able to use a <Link> or <a> tag with a direct path to go to).
I'm making an authentication system and after backend redirects me to the frontend page I'm making API request for userData and I'm saving that data to localStorage. Then I'm trying to load Spinner or UserInfo.
I'm trying to listen for the localStorage value with useEffect, but after login I'm getting 'undefined'. When the localStorage value is updated useEffect does not run again and Spinner keeps spinning forever.
I have tried to do: JSON.parse(localStorage.getItem('userData')), but then I got a useEffect infinite loop.
Only when I'm refreshing the page does my localStorage value appear and I can display it instead of Spinner.
What I'm doing wrong?
Maybe there is a better way to load userData when it's ready?
I'm trying to update DOM in correct way?
Thanks for answers ;)
import React, { useState, useEffect } from 'react';
import { Spinner } from '../../atoms';
import { Navbar } from '../../organisms/';
import { getUserData } from '../../../helpers/functions';
const Main = () => {
const [userData, setUserData] = useState();
useEffect(() => {
setUserData(localStorage.getItem('userData'));
}, [localStorage.getItem('userData')]);
return <>{userData ? <Navbar /> : <Spinner />}</>;
};
export default Main;
It would be better to add an event listener for localstorage here.
useEffect(() => {
function checkUserData() {
const item = localStorage.getItem('userData')
if (item) {
setUserData(item)
}
}
window.addEventListener('storage', checkUserData)
return () => {
window.removeEventListener('storage', checkUserData)
}
}, [])
Event listener to 'storage' event won't work in the same page
The storage event of the Window interface fires when a storage area
(localStorage) has been modified in the context of another document.
https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event
The solution is to use this structure:
useEffect(() => {
window.addEventListener("storage", () => {
// When storage changes refetch
refetch();
});
return () => {
// When the component unmounts remove the event listener
window.removeEventListener("storage");
};
}, []);
"Maybe there is a better way to load userData when it's ready?"
You could evaluate the value into localStorage directly instead passing to state.
const Main = () => {
if (localStage.getItem('userData')) {
return (<Navbar />);
}
else {
return (<Spinner />);
}
};
If there is a need to retrieve the userData in more components, evaluate the implementation of Redux to your application, this could eliminate the usage of localStorage, but of course, depends of your needs.
So the thing is im using react-native-qrcode-scanner and when I switch between tabs in my app, the QR scanner gets black in Android. I read its because in Android the components do not unmount. I had to add an isFocused if statement but its causing the whole thing to rerender and its a horrible user experience. Is there a way to make this better without having the if statement? Thanks!
import { withNavigationFocus } from 'react-navigation';
class ScannerScreen extends Component {
...
const { isFocused } = this.props
...
{isFocused ?
<QRCodeScanner
showMarker={true}
vibrate={false}
ref={(camera) => {this.state.scanner = camera}}
cameraStyle={{overflow: 'hidden', height: QRHeight}}
onRead={read}
bottomContent={<BottomQRScanner/>}
/>
:
null
}
}
export default withNavigationFocus(ScannerScreen)
Okay You can add this in your componentdidmount/componentwillmount && remove listner in componentwillunmount or useeffecthook
useEffect(() => {
const unsubscribe = navigation.addListener('willFocus', () => {
//Your function that you want to execute
});
return () => {
unsubscribe.remove();
};
});
Or in newer version just do this
import {NavigationEvents} from 'react-navigation';
with this
<NavigationEvents onDidFocus={() => console.log('I am triggered')} />
onDidFocus event will be get called whenever the page comes to focus .
I have a parent component which maintains state for three 'form' components that render in sequence. It looks something like this:
<Parent>
{ renderFormBasedOnState() }
</Parent>
FormA renders, then when next is click FormB renders then FormC renders, all in the parent.
Previously I was using a React Router to do this, but the problem is, I don't want the user to be able to bookmark /formb or /formc, as that would be an invalid state.
I can do this with a switch statement, but then I lose forward / back button browser history ability - and I don't want to basically re-implement react-router in my component. What is the simplest way to go about this?
Haven't tried it for the back of the browser, but it could look something like this:
export default class tmp extends React.Component {
state = {
currentVisibleForm: 'A'
}
onBackButtonEvent = (e) => {
if(this.state.currentVisibleForm !== 'A') {
e.preventDefault();
//Go back to the previous visible form by changing the state
} else {
// Nothing to do
}
}
componentDidMount = () => {
window.onpopstate = this.onBackButtonEvent;
}
render() {
return (
<Parent>
{this.state.currentVisibleForm === 'A' &&
<FormA />
}
{this.state.currentVisibleForm === 'B' &&
<FormB />
}
{this.state.currentVisibleForm === 'C' &&
<FormC />
}
</Parent>
)
}
}
Tell me if it is of any help!
So I was able to get this working with the history api, however it may not be worth the effort to fine tune - I may revert. Managing state in two places is kind of dumb. Note this history object is the same from the application's 'Router' component, and doesn't conflict.
state = {
FormData: {},
action: 'Form_1'
}
componentWillMount() {
this.unlistenHistory = history.listen((location) => {
if (location.state) {
this.setState(() => ({
action: location.state.action
}));
}
});
history.push(undefined, {action: 'FORM_1'});
}
componentWillUnmount() {
this.unlistenHistory();
}
finishForm1 = () => {
const action = 'Form_2';
history.push(undefined, { action });
this.setState((prevState) => ({
// business stuff,
action
}));
};
renderCurrentState() {
switch(this.state.action) {
case 'FORM_1':
return <Form1 />
...
}
}
render() {
return (
<div>
{ this.renderCurrentState() }
</div>
);
}