How to Jump to Redux State with Browser Back Button - reactjs

I’m a react beginner and I have a project using React Router (4.2.2), Redux and I’ve recently added react-router-redux hoping that it would solve my problem. I have components whose redux stored states need to change based on interactions with the browser back and forward buttons.
For example, I have a screen containing details for an element the user clicked. If I use the browser back button and navigate to another point in history in which that details view was open, it will only show the most recent element info (due to that the only information in the store) or sometimes no information at all will be passed.
I thought react-router-redux would help me keep these two in sync but maybe I’m missing a step that enables that to happen. I’ve installed Redux Debug Tools and I can see in there the state that I want to jump to, but how do I enable it when the user uses the back button? The url for the details page is the same each time it’s viewed, perhaps I need to add a hash tag with specific information but even then, how would I do a look up for the correct information in the store?
I open the details view like this:
<Link to='/activityDetails'>
<ActivityButton
id={this.props.someData.someId}
/>
</Link>
And inside ActivityButton:
openActivityDetails = () => {
this.props.showActivityDetailsScreen(this.props.id);
};
function matchDispatchToProps(dispatch) {
return bindActionCreators({
showActivityDetailsScreen: activityDetailsActions.showActivityDetails
}, dispatch)
}
render(){
return (
<div className={'activity-button'} onClick={this.openActivityDetails}>
<img
onClick={this.handleClick}
src={imgPath}
/>
</div>
);
}

So I figured it out on my own. Essentially what you need to do is set the state information inside the when you create it, like so:
<Link to={{
pathname: '/activityDetails',
state: { "activityData": activityData }
}}>
<ActivityButton
id={this.props.someData.someId}
/>
</Link>
then in the class you need the information ActivityDetails in my case you can query it like this:
var actData = this.props.location.state.activityData;
or if you're using withRouter and history it's also in the following location:
const { history: { push } } = this.props;
history.state.state.activityData
I ended up removing react-router-redux because it wasn't necessary. It made an entry in the state with the current route, but in my case that wasn't useful. Hope this helps someone in the future.

Related

React Context always returns EMPTY

I have a Search parent component and a SideBar child component, I am trying to get context in SideBar, but everytime it returns empty.
I followed the tutorial exactly like: https://itnext.io/manage-react-state-without-redux-a1d03403d360
but it never worked, anyone know what I did wrong?
Here is the codesandbox link to the project: https://codesandbox.io/s/vigilant-elion-3li7v
I wrote that article.
To solve your specific problem:
When using the HOC withStore you're injecting the prop store into the wrapped component: <WrappedComponent store={context}.
The value of the prop store is an object that contains 3 functions: get, set, and remove.
So, instead of printing it, you should use it. For example this.props.store.get("currentAlbums") or this.props.store.set("currentAlbums", [album1, album2]).
This example is forked by your code: https://codesandbox.io/s/nameless-wood-ycps6
However
Don't rewrite the article code, but use the library: https://www.npmjs.com/package/#spyna/react-store which is already packed, tested, and has more features.
An event better solution is to use this library: https://www.npmjs.com/package/react-context-hook. That is the new version of the one in that article.
This is an example of a sidebar that updates another component content: https://codesandbox.io/s/react-context-hook-sidebar-xxwkm
Be careful when using react context API
Using the React Context API to manage the global state of an application has some performance issues, because each time the context changes, every child component is updated.
So, I don't recommend using it for large projects.
The library https://www.npmjs.com/package/#spyna/react-store has this issue.
The library https://www.npmjs.com/package/react-context-hook does not.
You pass the store as a prop, so to access it, you need this.props.store in your SideBar.
Not this.state.store
Create a wrapping App component around Search and Sidebar:
const App = props => (
<div>
<Search />
<SideBar />
</div>
);
export default createStore(App);
Now you can manipulate state with set and get that you have available in child components Search and Sidebar.
In Search component you can have something like:
componentDidMount() {
this.props.store.set("showModal", this.state.showModal);
}
also wrapped with withStore(Search) ofc.
and in SideBar you can now call:
render() {
return (
<div>
{"Sidebar: this.state.store: ---> " +
JSON.stringify(this.props.store.get("showModal"))}
}
</div>
);
}
and you will get the output.

How to use a method in the Main component on another

I am trying to save notes into localStorage (or in this case localforage). I have a simple text area with a button called "save." The save button is located in another file indicated below.
I used an example found here to try to set the items.
This is the code I wrote:
SaveMessage() {
var message = <Notepad></Notepad>;
reactLocalforage
.SetItem("Message", message, function(message) {
console.log(message);
})
.catch(function(err) {
console.log(err);
});
}
The var message is something I'm not too sure of either. Notepad is another component with the code which contains the text area and buttons:
<div className="button-container">
<button
className="save-button"
onClick={() => {
props.onSaveMessage(saveMessage);
}}
>
Save
</button>
<button className="remove-button">Show all</button>
</div>
The area where it says onClick I was hoping there would be a way to use the SaveMessage method with localforage initially I tried creating it as a prop (from a tutorial) so in the main method I'd have:
render() {
return (
<div>
<Notepad onSaveMessage={this.SaveMessage}></Notepad>
</div>
);
}
and then on the Notepad component:
<button
className="save-button"
onClick={() => {
props.onSaveMessage();
}}
>
Save
</button>
When I click the save button on my application I am hoping something will be set within the local-storage on the browser, but I get an exception:
TypeError: Cannot call a class as a function
The error occurs when I set item on the save message code above and when I try to call it as a prop onSaveMessage(saveMessage).
You haven't added enough code to be able to fix your exact issue, but I can help you understand how you should proceed to fix it. In React when you want to share information between components, you need to lift that data into a parent component, and then share it through props. If your component tree is profound, then this task can get tedious. So several libraries have been developed to manage your app's state.
I suggest you start by reading "Lifting State Up" from the React docs page. To help you apply these concepts to your current situation, I've created a CodeSandbox in which I try to replicate your situation. You can find it here.
Once you understand the need to "lift" your state, and how you can share both data and actions through props you can migrate to state handler tool. Some of them are:
React Context
Redux
MobX
There are much more. It is not an extensive list, nor the best, just the ones I have used and can vouch that they can help you.
I hope this helps.

passing mapped data from an api to another component

so i have data that i get from an api in componentDidMount, and then i map over it in the render. lets say the map returns 4 objects. How do i create a button click event that captures that specific objects data, and passes it along a route to another component?
code:
clickEvent(){
???
}
this.example = this.state.data.slice(1).map((data, key) =>
<div key={item.Id}>
<div>{data.dataIWantToPass}</div>
<Link to='/next_component_path' onClick={clickEvent}}>Click</Link>
</div>
So lets say the above returns 4 objects. I want the third one, when the link is clicked to pass its data.
clickEvent(dataUWantToPass){
<NewComponent dataPassed={dataUWantToPass} />
}
this.example = this.state.data.slice(1).map((data, key) =>
<div key={data.Id}>
<div>{data.dataIWantToPass}</div>
//Link is a react-router-dom component, this helps you to redirect to other component (to which you have added the route for
<Link to='/next_component_path'>
<button onClick={()=>this.clickEvent(data.dataIWantToPass)} value="Click"/>
</Link>
</div>
))
You can receive the data in the NewComponent as props.
If you want to check than you can write the componentWillReceiveProps() method in the NewComponent as:
componentWillReceiveProps(reveivedProps){
console.log(reveivedProps);
}
Ok, I have solved this by piecing together various bits from comments, so thank you for your contributions. This is how it was resolved:
On the link tag i did this:
<Link to={{ pathname: '/path', query:{passed_id: data.id} }}></Link>
Then on the component that the path routes to:
this.setState({passed_id: this.props.location.query.passed_id});
And that gave me access to the data i was passing, in this case an ID. Now I just need to figure out how to compare that ID, with the looped data I also pass (via local storage), and map over it. Another question i suppose.
You need to use redux. This allows you to store data in global application store, and get the data from any component which is subscribed to the store. Today almost every react project needs redux to manage the data across the application

Handle back button with react router

If a user navigates to www.example.com/one and clicks the back button, I want to redirect them to www.example.com.
I think it's a common problem, but I haven't found a solution yet.
Hooks version (React 16.8+):
Minimal version.
import { useHistory } from "react-router-dom";
export const Item = () => {
let history = useHistory();
return (
<>
<button onClick={() => history.goBack()}>Back</button>
</>
);
};
In react-router-dom v6 useHistory() is replaced by useNavigate(). so use useNavigate() inplace of useHistory() this way.
import { useNavigate} from "react-router-dom";
export const Item = () => {
let navigate = useNavigate();
return (
<>
<button onClick={() => navigate(-1)}>Back</button>
</>
);
};
for more on useNavigate visit this: https://reactrouter.com/docs/en/v6/hooks/use-navigate
You can try with two options, either you can use push method or goBack method from history of the router. Normally history props will available if you directly route the component via Route method or pass the history props to child component and use it.
Sample Code given below
this.props.history.push('/') //this will go to home page
or
this.props.history.goBack() //this will go to previous page
For your problem you try with push method and give the exact url you to move on.
For more reference visit https://reacttraining.com/react-router/web/api/history
What you want is this:
Let's say a person goes to a single page in your website such as: www.yoursite.com/category/books/romeo-and-juliet
In this page, you want to show a "Back" button that links you to one upper directory which is: www.yoursite.com/category/books/
This is breadcrumb system that we famously had in vBulletin forums and such.
Here is a basic solution to this:
let url = window.location.href;
let backButtonUrl = "";
if (url.charAt(url.length - 1) === "/") {
backButtonUrl = url.slice(0, url.lastIndexOf("/"));
backButtonUrl = backButtonUrl.slice(0, backButtonUrl.lastIndexOf("/"));
} else {
backButtonUrl = url.slice(0, url.lastIndexOf("/"));
}
What it basically does is:
1. Get the current URL from browser
2. Check if there is a "/" (slash) at the end of the link.
a. If there is: remove the slash, and remove everything the last slash
b. If there is not: remove everything last slash.
You can use {backButtonUrl} as your Go Back button link.
Note: it does not have anything to do with React Router, history, etc.
Note 2: Assuming you are using a link architecture that goes like www.site.com/word/letter/character
If you want to use it with react-router-dom library, then you need to set your url variable like this:
let url = this.props.match.url;
I found a solution. It's not beautiful but it works.
class Restaurant extends Component {
constructor(props) {
super(props);
this.props.history.push('/');
this.props.history.push(this.props.match.url);
}
...
I've had the same problem today. I have the following flow in one of the applications I'm working on:
User fills out a registration form
User enters credit card "payment page"
When payment is successful, the user sees a "payment confirmation" page.
I want to prevent the users from navigating from the "payment confirmation" (3) page back to any previous steps in the payment flow (1 and 2).
The best thing to do would be not to use routes to control which content is displayed, and use state instead. If you cannot afford to do that,
I found two practical ways to solve the problem:
Using React-Router:
When you hit the back button, React Router's history object will look like this:
When you go to any page using history.push record the page you are visiting in the state
Create a decorator, HOC, or whatever type of wrapper you prefer around the React-Router's Route component. In this component: If history.action === "POP" and "history.state.lastVisited === <some page with back navigation disabled>", then you should redirect your user to the /home page using history.replace
Another way to do is is by going to the /home page directly:
Use history.action to detect the back button was used, then:
Change location.href to equal the page you want to visit
Call location.reload(). This will reload the application, and the history will be reset
Browser back button works on your routes history. It will not invoke your programmatically handled routing. That's the point where we should keep maintain history stack with react router. If you are at route '/' and push '/home'. On browser back button it will pop '/home and will go back to '/'.
Even If you implementButton component for go back functionality and use react router history props. Believe me you have to carefully manage your navigation to maintain browser history stack appropriately. So it behaves same like whether you press browser back button or your app Button to go back or go forward.
I hope this would be helpful.
We wanted something similar for our React app and unfortunately this was the best solution we came up with. This is particularly helpful when our users are on mobile devices and they land on a specific page on our site from an ad or a referrer.
This is in our main routes.tsx file.
useEffect(() => {
// The path that the user is supposed to go to
const destinationPath = location.pathname;
// If our site was opened in a new window or tab and accessed directly
// OR the page before this one was NOT a page on our site, then...
if (
document.referrer === window.location.href ||
document.referrer.indexOf(window.location.host) === -1
) {
// Replaces the current pathname with the homepage
history.replace("/");
// Then pushes to the path the user was supposed to go to
history.push(destinationPath);
}
}, []);
Now when a user presses the back button, it takes the user to our homepage instead of being stuck within the "nested" route they were in.
NOTE: There are some small quirks with this implementation. Our app is also a Cordova app so we NEED to have our own back button. This implementation works well with our own back button but does not seem to work with the native browser's back button; hence, it worked well for our needs.

How to prevent route change using react-router

There's a certain page in my React app that I would like to prevent the user from leaving if the form is dirty.
In my react-routes, I am using the onLeave prop like this:
<Route path="dependent" component={DependentDetails} onLeave={checkForm}/>
And my onLeave is:
const checkForm = (nextState, replace, cb) => {
if (form.IsDirty) {
console.log('Leaving so soon?');
// I would like to stay on the same page somehow...
}
};
Is there a way to prevent the new route from firing and keep the user on the same page?
It is too late but according to the React Router Documentation you can use preventing transition with helping of <prompt> component.
<Prompt
when={isBlocking}
message={location =>
`Are you sure you want to go to ${location.pathname}`
}
/>
if isBlocking equal to true it shows a message. for more information you can read the documentation.
I think the recommended approach has changed since Lazarev's answer, since his linked example is no longer currently in the examples folder. Instead, I think you should follow this example by defining:
componentWillMount() {
this.props.router.setRouteLeaveHook(
this.props.route,
this.routerWillLeave
)
},
And then define routerWillLeave to be a function that returns a string which will appear in a confirmation alert.
UPDATE
The previous link is now outdated and unavailable. In newer versions of React Router it appears there is a new component Prompt that can be used to cancel/control navigation. See this example
react-router v6 no longer supports the Prompt component (they say that they hope to add it back once they have an acceptable implementation). However, react-router makes use of the history package which offers the following example for how to block transitions.
Note that to actually make this work in react router you have to replace the createBrowserHistory call with some hackery to make sure you are using the same history object as react router (see bottom of answer).
const history = createBrowserHistory();
let unblock = history.block((tx) => {
// 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.
let url = tx.location.pathname;
if (window.confirm(`Are you sure you want to go to ${url}?`)) {
// Unblock the navigation.
unblock();
// Retry the transition.
tx.retry();
}
You'll need to put this inside the appropriate useEffect hook and build the rest of the functionality that would have otherwise been provided by prompt. Note that this will also produce an (uncustomizable) warning if the user tries to navigate away but closing the tab or refreshing the page indicating that unsaved work may not be saved.
Please read the linked page as there are some drawbacks to using this functionality. Specifically, it adds an event listener to the beforeunload event which makes the page ineligable for the bfcache in firefox (though the code attempts to deregister the handler if the navigation is cancelled I'm not sure this restores salvageable status) I presume it's these issues which caused react-router to disable the Prompt component.
WARING to access history in reactrouter 6 you need to follow something like the instructions here which is a bit of a hack. Initially, I assumed that you could just use createBrowserHistory to access the history object as that code is illustrated in the react router documentation but (a bit confusingly imo) it was intended only to illustrate the idea of what the history does.
We're using React Router V5, and our site needed a custom prompt message to show up, and this medium article helped me understand how that was possible
TLDR: the <Prompt/> component from react-router-dom can accept a function as the message prop, and if that function returns true you'll continue in the navigation, and if false the navigation will be blocked
React-router api provides a Transition object for such cases, you can create a hook in a willTransitionTo lifecycle method of the component, you are using. Something like (code taken from react-router examples on the github):
var Form = React.createClass({
mixins: [ Router.Navigation ],
statics: {
willTransitionFrom: function (transition, element) {
if (element.refs.userInput.getDOMNode().value !== '') {
if (!confirm('You have unsaved information, are you sure you want to leave this page?')) {
transition.abort();
}
}
}
},
handleSubmit: function (event) {
event.preventDefault();
this.refs.userInput.getDOMNode().value = '';
this.transitionTo('/');
},
render: function () {
return (
<div>
<form onSubmit={this.handleSubmit}>
<p>Click the dashboard link with text in the input.</p>
<input type="text" ref="userInput" defaultValue="ohai" />
<button type="submit">Go</button>
</form>
</div>
);
}
});

Resources