ReactJS: save state when navigate with browser button - javascript

My app has two pages, first, the home page, which is a list of items, loaded with ajax, and support pagination. when you click an item in the list, it will render a new page, shows the detail of the item. I'm using react-router and it works fine.
However, when I press back button, I'll get back to the home page, and all the previous state is lost, I have to wait for ajax load again, and lost the pagination too.
So, how can I improve this, let the page act as an ordinary one (when you press back button you can return to the previous state instantly, even with the same scroll position), thanks.

I've solved this myself by providing a module Store, which is like this:
// Store.js
var o = {};
var Store = {
saveProductList: function(state) {
o['products'] = state;
},
getProductList: function() {
return o['products'];
}
};
module.exports = Store;
then in the home page component, load and save the state:
componentDidMount: function() {
var state = Store.getProductList();
if (state) {
this.setState(state);
} else {
// load data via ajax request
}
},
componentWillUnmount: function() {
Store.saveProductList(this.state);
}
this works for me.

You should use dynamic routes like this example:
<Route name="inbox" handler={Inbox}>
<Route name="message" path=":messageId" handler={Message}/>
<DefaultRoute handler={InboxStats}/>
</Route>
If you change the messageId(child component) you will still have the same state in the Inbox(parent) Component.
It will look something like this:
<Route name="search" path="/search/:query?/:limit?/:offset?" handler={Search} />

Related

How do I stop the re-rendering of my react component? It rerenders for every object in a drop down

After building the homepage of my website I finally figured out how to dynamically navigate to other pages. I wanted the browser to render the State homepage when a user clicked on a dropdown and selected a state. The navigation works, but it re-renders the component 50 times which I do not understand. I suspect it is due to the map function that is creating the menuitems. I could build out 50 individual menuitems but that is really ugly.
I am just starting out learning React. I have 7 YRS experience in backend development, but I am still trying to get a handle on React development. I have created a wepage with Material UI that has a dropdown that looks like this
<FormControl>
<InputLabel>Select a State</InputLabel>
<Select value={location} onChange={selectionChangeHandler}>
{locations.map((value) => (
<MenuItem value={value.toLowerCase()} key={value.toLowerCase()} component={Link} to={`${value.toLowerCase()}/home`} >
{value}
</MenuItem>
))}
</Select>
</FormControl>
This returns a dropdown with the 50 states in it. When the user clicks on a state I want the program to route to that page on click. This dynamic routing works BUT. It re-renders my component 50 times. I think this is happening because the dropdown is being built inside of a .map functions and there are 50 entries in that list.
I can remove the map function and hardcode in 50 menuitems but that is ugly.
Here is my onChange Function
const selectionChangeHandler = (event) => {
console.log(event.target.value)
}
I have also tried removing the component={Link} and using the useNavigate hook in the selectionChangeHandler like so
const selectionChangeHandler = (event) => {
console.log(event.target.value)
setlocation(event.target.value)
link = `${event.target.value}/home`
navigate(link)
}
This works but also renders 50 times. I am at a loss.
I cross posted the above to reddit and then I researched a little more. It turns out in React. When a parent component's state is updated it re-renders all child components. This may be what is going on here, but I do not know how to fix it. Even if I pass the state as a prop to the child component I still have to link to the correct page.
I am kind of nervous about posting I really tried to put work into solving my own problem before reaching out for help, and I might be reaching out for help a lot as I learn. I am committed to learning, but some problems I just cannot figure out on my own.
Link to Code Link to Code
The problem here is inside StateHome.js. You use a naked axios.get call directly in your component so it's going to re-render anytime the component changes from state change.
When you get the results you set state inside the same component which re-renders the component, which then re-runs the axios.get call, causing the loop.
I understand you want to call that endpoint when someone lands on the /alabama/home page. To tell React "Do this one time when the component loads" you must use a useEffect with an empty dependency array. So instead of this:
const StateHome = () => {
const { location } = useParams();
const [PageData, SetPageData] = useState();
axios.get(`http://localhost:4000/${location}/home`).then((response) => {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
SetPageData(response.data);
});
return <h1>This is my State Home Page Component for State {location}</h1>;
};
You need to use this:
const StateHome = () => {
console.log("StateHome");
const { location } = useParams();
const [PageData, SetPageData] = useState();
useEffect(() => {
axios.get(`http://localhost:4000/${location}/home`).then((response) => {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
SetPageData(response.data);
});
}, []); // <--- indicates "on mount", loads once regadless of other side-effects (setState)
return <h1>This is my State Home Page Component for State {location}</h1>;
};

React Router v5.2 - Blocking route change with createBrowserHistory and history.block

My app has two pages: Step1 and Step2. Step1 has a checkbox that blocks navigation if it is checked and a Next button that navigates to Step2 when clicked. Step2 has a Previous button that navigates back to Step1 when clicked.
Link to demo
As per this tutorial, I'm using the block method of the createBrowserHistory object to block route changes if the checkbox in Step1 is checked:
const unblock = useRef();
useEffect(() => {
unblock.current = history.block((tx) => {
if (block.current.checked) {
const promptMessage = "Are you sure you want to leave?";
if (window.confirm(promptMessage)) {
unblock.current();
tx.retry();
}
} else {
console.log("tfgx");
unblock.current();
tx.retry();
}
});
}, []);
I also had to set the history prop in the low-level <Router> (not <BrowserRouter>) to the createBrowserHistory object, like so:
<Router history={createBrowserHistory()}>
...
</Router>
But this prevents the routes from being rendered properly. I think this may have something to do with <Switch> not being able to read the location object properly. If I use <BrowserRouter>, the location object looks like this: {pathname: "/step1", ... key: "7sd45"}. But when I use <Router={createBrowserHistory()}>, the location object looks like this {action: "PUSH", location: {pathname: "/step1", ... key: "7sd45"}}. (I'm also getting the warning You cannot change <Router history>.)
My desired result is to block navigation if the 'Block navigation' checkbox is checked and unblock it otherwise. If the location changes when navigation is unblocked, I would like the corresponding route to be rendered correctly.
The section on createBrowserHisory in the React Router v5 docs is sparse and there aren't many examples that make use of it, so I'd be grateful if someone could shed some light on this.
EDIT: Passing location.location to <Switch> seems to fix it (Updated demo). But if I call useLocation inside Step1 and print the result (line 17-18), I get {pathname: "/step1", ... key: "7sd45"} and not {action: "PUSH", location: {pathname: "/step1", ... key: "7sd45"}}. Why is this?
Also, if the user attempts to go to another location when navigation is blocked, my custom prompt appears as expected ("Are you sure you want to leave" with "OK" and "Cancel" buttons). However, if they dismiss this by clicking Cancel, then the browser's own dialog box appears -
In Chrome:
In Firefox:
Is it possible to suppress the browser prompt after my prompt has been dismissed?
The router context's history object also has a block function but it works a little differently. It takes a callback that consumes location and action arguments.
history.block((location, action) => {...});
Returning false from the callback blocks the navigation transition, returning true allows the transition to go through.
React.useEffect(() => {
const unblock = history.block((location, action) => {
if (checkBlockingCondition) {
return window.confirm("Navigate Back?");
}
return true;
});
return () => {
unblock();
};
}, []);
Alternatively, react-router-dom suggests using the Prompt component to conditionally block route transitions. Your code is very close to their preventing transitions example.
Updates to your last codesandbox:
Use blocking state versus react ref so the prompt rerenders and reevaluates the condition.
Render a Prompt component.
Prevent the default form submit action, i.e. to prevent the page from reloading.
code
import {
BrowserRouter as Router,
Prompt, // <-- import Prompt
Redirect,
Switch,
Route,
useLocation
} from "react-router-dom";
const Step1 = ({ id, history }) => {
const [isBlocking, setIsBlocking] = useState(false);
return (
<form
id={id}
onSubmit={(e) => {
e.preventDefault(); // <-- prevent default form action, i.e. page reload
history.push("/step2");
}}
>
<label>
Block navigation
<input
type="checkbox"
onChange={(e) => setIsBlocking(e.target.checked)}
/>
</label>
<br />
<br />
<button type="submit">Next</button>
<Prompt
when={isBlocking} // <-- blocking condition
message="Are you sure you want to leave?"
/>
</form>
);
};
To do this in typescript, you could use a ternary based on your "dirty" condition to show the prompt before navigating to your next route.
As long as the component this goes in is inside of a a routing context, it will work.
isChecked is a psuedo-code variable to be replaced by whatever condition you want to evaluate on leaving the page. I recommend having it in the dependencies because when the state variable updates, so will the block condition.
const history = useHistory();
useEffect(() => {
const unblock = history.block(
!isChecked
? "You’ve got unsaved changes. Are you sure you want to navigate away from this page?"
: true
);
return function cleanup() {
unblock();
};
}, [isChecked]);

change profile picture when click in link to the same page

I have an user profile (/someuser) that I open using :
<Route path="/:user" component={Profile} />
The profile is like this:
export default function App(props) {
const [nick, setNick] = useState(props.match.params.user); //get the nick using props.
const [profile, setProfile_picture] = useState(profile_picture); // default one
useEffect(() => {
// get current user profile picture:
firebase.database().ref('/users/').orderByChild('user').equalTo(props.match.params.user).once('value').then(snapshot => {
if (snapshot.exists()){
var img = snapshot.val().img;
setProfile_picture(img);
}
});
}, []);
return(
<>
<img className={classes.profile} src={profile} />
<h1>{nick}</h1>
<Link to="/user2">Go to profile 2</Link>
</>
);
}
);
Supose I'm in /user profile and I click in the Link at the end of the code to go to the /user2. What I'd like to do is change the profile picture and the nick (without reload). Force after click to update this informations... Any ideas how can I do this?
Thanks.
You need to trigger the useEffect based on params change because when the params change component is re-rendered by react-router and hence the state will not be re-initialized.
Triggering the useEffect on params change will ensure data is updated when params change.
Also make sure to set nick state too ni useEffect as that too needs to update on params change
useEffect(() => {
// set nick state
setNick(props.match.params.user);
// get current user profile picture:
firebase.database().ref('/users/').orderByChild('user').equalTo(props.match.params.user).once('value').then(snapshot => {
if (snapshot.exists()){
var img = snapshot.val().img;
setProfile_picture(img);
}
});
}, [props.match.params.user]);
Do you mean without reloading the page? If so then I think you can make a boolean that toggles when you go to a different route and initiates then useEffect hook by putting it in the dependency array. Or you could move the code to a separate component and pass it in as a prop. Now to do it completely without redirecting the browser would be to use the toggle boolean as mentioned before, but using fetch api, axios, etc to send that nick to a backend and change it accordingly which I think is similar to an Ajax request I hope this helps.

react router Link doesn't cause rerender when visited on the same path

I'm using react router v4, had some issue reloading the page (not window.location.reload). I better give a real use case to explain the issue, we use a social network app as the example:
user A commented a post by user B, a notification appear in user B page.
user B clicked on the notification, we do this.props.history.push('/job/' + id'), it worked, hence user B went to job/123 page.
user A commented again, new notification appear in user B page, while user B still remain on the job/123 page, he clicked on the notification link and triggered this.props.history.push('/job' + id'). But he won't see the page rerender, he DID NOT see the latest comment because the page does nothing.
It seems to be a common scenario in many cases. It can be tackled using many different approaches. Check this stackoverflow question. There are some good answers and findings. Personally this approach made more sense to me.
location.key changes every single time whenever user tries to navigate between pages, even within the same route. To test this place below block of code in you /jod/:id component:
componentDidUpdate (prevProps) {
if (prevProps.location.key !== this.props.location.key) {
console.log("... prevProps.key", prevProps.location.key)
console.log("... this.props.key", this.props.location.key)
}
}
I had this exact same situation. Updated state in componentDidUpdate. After that worked as expected. Clicking on items within the same route updates state and displays correct info.
I assume (as not sure how you're passing/updating comments in /job/:id) if you set something like this in your /job/:id component should work:
componentDidUpdate (prevProps) {
if (prevProps.location.key !== this.props.location.key) {
this.setState({
comments: (((this.props || {}).location || {}).comments || {})
})
}
}
You are describing 2 different kinds of state changes.
In the first scenario, when user B is not at the /job/:id page and he clicks a link you get a URL change, which triggers a state change in the router, and propagates that change through to your component so you can see the comment.
In the second scenario, when user B is already at the /job/:id page and a new comment comes through, the URL doesn't need to change, so clicking on a link won't change the URL and won't trigger a state change in the router, so you won't see the new content.
I would probably try something like this (pseudo code because I don't know how you're getting new comments or subscribing via the websocket):
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
class Home extends React.Component {
render() {
return (
<div>
<h1>The home page</h1>
{/* This is the link that the user sees when someone makes a new comment */}
<Link to="/job/123">See the new comment!</Link>
</div>
);
}
}
class Job extends React.Component {
state = { comments: [] };
fetchComments() {
// Fetch the comments for this job from the server,
// using the id from the URL.
fetchTheComments(this.props.params.id, comments => {
this.setState({ comments });
});
}
componentDidMount() {
// Fetch the comments once when we first mount.
this.fetchComments();
// Setup a listener (websocket) to listen for more comments. When
// we get a notification, re-fetch the comments.
listenForNotifications(() => {
this.fetchComments();
});
}
render() {
return (
<div>
<h1>Job {this.props.params.id}</h1>
<ul>
{this.state.comments.map(comment => (
<li key={comment.id}>{comment.text}</li>
))}
</ul>
</div>
);
}
}
ReactDOM.render(
<BrowserRouter>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/job/:id" component={Job} />
</Switch>
</BrowserRouter>,
document.getElementById("app")
);
Now the page will get updated in both scenarios.
this solved the problem for me :
import { BrowserRouter as Router,Route,Switch,Redirect} from "react-router-dom";
<Router>
<Switch>
<Redirect from="/x_walls/:user_Id" to='/walls/:user_Id'/>
<Route path="/walls/:user_Id" exact render={(props) => <Wall {...props}/> }/>
</Switch>
</Router>
and when you want to call "walls" you just call "x_walls" instead
You should watch updating postId in componentWillUpdate life cycle and do something from here like this:
componentWillUpdate(np) {
const { match } = this.props;
const prevPostId = match.params.postId;
const nextPostId = np.match.params.postId;
if(nextPostId && prevPostId !== nextPostId){
// do something
}
}

How to find Popup state and refresh state in React-router?

I know this question kind a stupid But I am pretty confused with this site producthunt how they are doing this.When clicking the product list popup with react router is done like this..
But When I refresh that page it render like this..How this is done using React-router
My bet would be that they use the state property when pushing a page to give an indication to the component about how to render the page. More specifically, to indicate the component where it comes from. For example:
router.push({
pathname: '/posts/origami-studio-by-facebook',
state: { fromPosts: true }
})
And then you can read the router's state in the route's component to check what page to show.
const Post = (productName) => {
if(this.context.router.location.state.fromPosts) {
return <Posts productPopup{productName} />
// open the posts page with a popup for the product
} else {
return <PostPage productName={productName} />
}
}
So when you open the page in your browser, the state.fromPosts is not set and you get redirected to the PostPage. In the end, even if the route is the same, what you end up seing is completely different.

Categories