Getting an error on using setRouteLeaveHook (withRouter) - reactjs

Currently I'm building a Web App using ReactJS. The app has a registration form.
Now consider, user has started with the registration process. But before submitting the form user leaves this registration page. At this point, say form contains unsaved data and I would like to display a confirmation message saying that Save Changes you have made before leaving this screen.
Below is my code to achieve this
componentDidMount () {
this.props.router.setRouteLeaveHook('/enterprise/enterprise-area/enterprise-details', this.routerWillLeave);
}
routerWillLeave(nextLocation) {
// return false to prevent a transition w/o prompting the user,
// or return a string to allow the user to decide:
if (true) {
return 'Your work is not saved! Are you sure you want to leave?';
}
}
export default withRouter(connect(
mapStateToProps,{
initializeVendorDetails
})(VendorRegistration));
I get the error shown below:
Uncaught TypeError: Cannot assign to read only property '__id__' of /enterprise/enterprise-area/enterprise-details
I went through official documentation and github issues but found nothing. Thanks in anticipation.

#NobuhitoKurose Thanks for your reply. Finally I manage to figure out the problem here.
Yes, My component was not directly connected to route.
I went through withRouter doc where I found that I actually need to provide a route object(this.props.route) as a first parameter instead of route as a string(as I mentioned in above code).
Since my component is not directly connected to route I was getting this.props.route as undefined.
I checked parent component (which is connected to route) and this component has its route prop. So I just pass this route prop from parent component to this current component (where I'm using withRouter) and everything has worked well.
Below is my an updated code
In parent component (which is connected to route)
<VendorRegistration route={this.props.route}/>
Component where I'm using withRouter
componentDidMount() {
this.props.router.setRouteLeaveHook(this.props.route, this.routerWillLeave);
}
routerWillLeave(nextLocation) {
// return false to prevent a transition w/o prompting the user,
// or return a string to allow the user to decide:
// FIXME: update condition as per requirement
if (true) {
return 'You have unsaved information, are you sure you want to leave this page?';
}
}
export default withRouter(connect(
mapStateToProps,{
initializeVendorDetails
})(VendorRegistration));

Related

React / Context API / TypeScript : How to init web app and avoid UI flickering at statup?

When a user navigate to my site, I need to initialize the React web app as follow:
If the incoming user has never requested the web app, I want to set the UI language to browser default (navigator.language).
If the incoming user has already visited the site and chosen a prefered language (lang stored in the localStorage), I want to init the UI with this language.
If the incoming user has an account and is already connected (token available in localStorage), I want to auto-connect him and render the app accordingly : login button transformed into a welcome message, UI language set to user preference.
To do so, I'm using React Context API and a defaultUser object.
defaultUser: init a default user
const defaultUser = {
language: 'en_EN',
isConnected: false
}
Context: create a default context
export const AppContext = createContext({
connectedUser: defaultUser,
})
Provider: create the provider with default context
export function AppProvider({ children }: any) {
[...]
const provider = {
connectedUser
}
return (
<AppContext.Provider value={provider}>
{children}
</AppContext.Provider>
)
}
App: init the provider during app start up
export class App extends Component {
static contextType = AppContext
render() {
return (
<AppProvider>
<AppContainer />
</AppProvider>
)
}
}
AppContainer: render the app
export class AppContainer extends Component {
static contextType = AppContext
componentDidMount() {
/** If user is not connected, verify if a stored session exists and use it to connect user */
if (!this.context.connectedUser.isConnected) {
[...do things...]
}
}
The whole mecanism works well except an annoying thing : the web app is systematically initialized with default user values, until the AppContainer::componentDidMount() do the real init job.
This is causing a sort of flickering effect.
I'm struggeling for 2 days on how to fix that, trying to perform Context init before <AppContainer /> rendering, and I'm stuck.
Any recommandations?
EDIT :
For clarity, I'm adding a diagram. Currently :
React App is rendered at start.
Context is initialized at start with default value.
Context is updated when end is reached.
React App is rendered again when end.
Any layout change during these two steps (UI language, UI modification based on user permissions) are clearly visible to the user and generate a sort of flickering.
I found sort of a solution by simply conditionning <AppContainer/> loading, postponing it to the end of the sequence. However instead of having flickering I have now a lag and other unwanted side effects.
The goal would be to differ all the sequence before React Act is rendered, and after Window is available. Then dynamically create the Context, then render all.
I think the point would be resolved if I could dynamically create the AppContext and pass a variable to createContext() during App constructor() or maybe componentWillMount() (not sure when Window is available), but then TypeScript get into play with types issues and I'm still stuck.
You didn't share the code that initializes the context, but I suspect you put the default value to be either a hardcoded value, or navigator.language and therefore experience the flickering. I'd like to suggest two ways to solve this:
Solution 1
Perhaps instead of having a hardcoded default context you could generate the default context programmatically by accessing localStorage.get('lang') or similar? There is a slight drawback to this solution though: You will be mixing concerns of react and the browser, but I think in this case it's an alternative to consider, because it's very simple and obvious to the reader.
Solution 2
Alternatively, when calling ReactDOM.render you could pass down whatever you need from localStorage as a prop to your application and so you keep the browser related logic separate from the pure React stuff.
I hope this makes sense.
Here's my follow-up after Amit suggestions, in case it can help anyone else.
Init Context with functions
Instead of initializing defaultUser with hard-coded values and update it later, I set directly it with a function returning navigator.lang as suggested. This solved the flickering issue on UI labels.
Init data before RectDOM.render
However I still had flickering on UI components for which I have to get the appropriate state from an API call.
Eg, if the incoming user has a valid session token stored in localStorage, the Login button must be disabled. Before doing so, I need to make sure the session token is valid by an async call to the API. I didn't find a way to have it «awaited» by the Context init which seems to be synchronous.
That's where Amit second suggestion get into play. Instead of struggling finding a solution inside React, I did necessary processing before ReactDOM.render, then passing stuffs as props to <Apps/>.
This works pretty well to get and pass the data...
Except that Context API didn't setSate anymore as soon as any of its data was refering to an object from outside the Context. In other word using function calls is ok to init (probably by val), but reference to external objects breaks setState.
Conclusion
As my project is still in early stage, this gave me the chance to get rid of Context API, do the proper init as required, and code the props/states progagation with basic React.

Better way to update react component on event

I'm making a web-app in which I use React, together with Firebase Authentication. I have a login page and I want it to redirect to a different page when a user is already signed in.
To check if the user is logged in I use a method to retrieve the User object, it returns null when a user is not signed in and a user object with all sorts of data if they are.
I have an if statement in my render method (of the login page) which checks if the user is logged in, but I ran into an issue. When a user first loads up the page it takes around half a second for the Firebase API to retrieve the user object and until that is completed my user object will return null.
This makes for an issue where it seems as though my user isn't logged in even if they are. That causes them not to redirect and stay on the login page until the state is updated in some way after the user object is initialized.
Firebase offers a way to fix this by giving us an onAuthStateChanged() method which allows me to execute a function when a user signs in or logs out.
What I'm doing now is using this method in the constructor method of my Login page class to manually re-render the component, thus redirecting the user when Firebase logs them in. It looks something like this:
export default class Login extends React.Component<Props, State> {
constructor(props:Props) {
super(props)
this.props.firebase.auth().onAuthStateChanged((user) => {
const oldState = this.state
this.setState(oldState)
})
}
render () {
if (this.props.firebase.auth().currentUser) {
return (
<Redirect to="/earn" />
)
} else {
return (
// Login page
)
}
}
}
(I omitted some irrelevant code)
Now this works fine and all but I'm not sure if this is the correct way to do it and I feel like I could make it look a lot nicer, I just don't know how.
Any suggestions or official ways to achieve what I'm doing right now are very much appreciated.

Using this.props.history.push("/path") is re-rendering and then returning

Edited the question after further debugging
I am having a strange issue, tried for a while to figure it out but I can't.
I have a React Component called NewGoal.jsx, after a user submits their new goal I attempt to reroute them to my "goals" page.
The problem: After they submit the browser loads in my goal page, but only for one second. It then continues and goes BACK to the NewGoal page!!
I am trying to understand why this is happening, I am beginning to feel that this might be an async issue.
Here is my code, currently it is using async-await, I also tried the same idea using a .then() but it also didn't work:
async handleSubmit(event)
{
const response = await axios.post("http://localhost:8080/addGoal",
{
goalID: null,
duration: this.state.days,
accomplishedDays: 0,
isPublic: this.state.isPublic,
description: this.state.name,
dateCreated: new Date().toISOString().substring(0,10),
}) */
// push to route
this.props.history.push("/goals");
}
While debugging, I tried taking out the functionality where I post the new message, and just did a history.push, code is below - and this completely worked.
// THIS WORKS
async handleSubmit(event)
{
// push to route
this.props.history.push("/goals");
}
But as soon as I add anything else to the function, whether before the history.push or after, it stops working.
Any advice would be very very appreciated!
Thank you
In the React Router doc's the developers talk about how the history object is mutable. Their recommendation is not to alter it directly.
https://reacttraining.com/react-router/web/api/history#history-history-is-mutable
Fortunately there are few ways to programmatically change the User's location while still working within the lifecycle events of React.
The easiest I've found is also the newest. React Router uses the React Context API to make the history object used by the router available to it's descendents. This will save you passing the history object down your component tree through props.
The only thing you need to do is make sure your AddNewGoalPage uses the history object from context instead of props.
handleSubmit(event)
...
//successful, redirect to all goals
if(res.data)
{
this.context.history.push("/goals")
}
...
})
}
I don't know if you're using a class component or a functional component for the AddNewGoalPage - but your handleSubmit method hints that it's a member of a Class, so the router's history object will be automatically available to you within your class through this.context.history.
If you are using a functional component, you'll need to make sure that the handleSubmit method is properly bound to the functional component otherwise the context the functional component parameter is given by React won't not be available to it.
Feel free to reply to me if this is the case.

Component not receiving props when pushed from same path - react router

This is the code that works ...for pushing to different routes
sendToMessage = (item)=>{
let path = '/messages';
this.props.history.push({
pathname:path,
state:{
clientid:item.clientid,
clientname:item.clientname
}
});
}
So, when I click on the item from a different route it works perfectly and the component receives the props sent
But when I am on the /messages page already and click on the item, I am not receiving the props so I can update the page with the new data.
why is this happening?
I'd like it to work for all situations,
whether from same route or from different route.
Now, I do receive this warning
Hash history cannot PUSH the same path; a new entry will not be added to the history stack
This is how I am trying to access the props in the destination component
componentWillReceiveProps(nextProps) {
console.log('nextPRops : ',nextProps);
}
but shouldnt I be able to still receive the props that I am sending??
is there any other way to send props to other component? these components are not parent and child component, so cant really pass props like that.
More details : it's a feature where a user gets a notification of message and gets taken to the messages page to that specific message he/she clicked on. Now, it may happen that the user is already on that page when the notification appears. in that case, we still need to show the new message when the user clicks on it. although it was made to bring to different route but it should also work when client is already on that page hope this makes it a little more clear
Method 1:
You can use redux to store clientid and clientname in redux and always access it from redux store, so instead of trying to route, you can update the data in redux store and that will trigger your componentWillReceiveProps function
Method 2:
One more solution would be to change the routing path, in case you have pathname like client - change the path to client/:clientid - if you use the routing in this way then you wont get the warning when you try to route from same component to same component
As what the warning said by react-router, you can't push to the same page.
Alternatively, you can/should use component's state, by updating the component state, it will rerender the component.
state = {
clientid: '',
clientname: ''
}
sendToMessage = (item) => {
this.setState({
clientid: item.clientid,
clientname: item.clientname
});
}

React : How to safely display a component according to user role

I am trying to display an admin Dashboard in react and Meteor only if the current user is an admin.
I am calling a server method that checks the user permission and rendering the admin component only if this method returns true.
This server call is asynchronous and thus the component is not rendering, usually I manage this asynchronous call with state, but I don't want to expose anything in state here (wouldn't like someone to change the state and access the admin dashboard).
Here is the code:
export default class AdminChecker extends Component {
isItAdmin() {
// Get the id of current user
const userId = Meteor.userId();
if (userId) {
// call a server method which returns true if current user is Admin
Meteor.call('checkAdminId', userId, (err, authorized) => {
if (err) {
console.log(err);
return null;
}
return (authorized) ? <AdminDashboard /> : null;
});
}
}
render() {
return (
<div className="admin-temp-container">
{this.isItAdmin()}
</div>
);
}
}
I think I can have this logic in a parent component and send the result of isItAdmin to adminDashboard as a prop (the adminDashboard component would display information only if its props is true).
But I am insure if this is safe. Could one change the props with Chrome react developer tools or something like that?
Thanks a lot
I think there are two parts to this (and the comments are hinting at both):
First, you should not expect to be able to enforce security on the client. You must implement access control logic on the server (i.e., any API that performs an admin action must check that the user performing the action is an admin).
Once you've done that (and perhaps you already have), then you likely need to use props or state to store whether or not the user is an admin (just like you would store any other data in your app).
The key point is that once you enforce security on the server, then it doesn't really matter if a user manipulates the state to get to the admin dashboard: the server will not let the user view any real data or take any actions anyway.
This is completely unrelated to server-side rendering. You can most certainly build a secure admin dashboard without server-side rendering, as long as the APIs used to fetch the admin data and perform admin actions are implementing their own access control checks.

Resources