React Router: Component is not rendering but the URL is updated - javascript

This kind of "component not rendering in React Router" type of questions seem to be a very frequently asked question. I have looked through everything but I could not find a solution for my problem.
Here is how my code look like:
render(){
return(
<div>
<SearchBar searchBody={this.state.body}/>
<Route path = "/ranked/" component ={Ranked}></Route>
</div>
);
}
Above, the component Ranked is created which, depending on the subpath renders different things.
(For example, localhost:3000/ranked/NBA)
function SearchDropDown(props){
return(
<div className = "searchDropDownItem">
<Link to={"/ranked/"+props.item.url}>{props.item.name}</Link>
</div>
)
}
Above is a different component with the Link tag, which, depending on the url, links to different subpath of /ranked/.
The problem is that let say I am on localhost:3000/ranked/NBA.
If I get redirected to localhost:3000/ranked/WNBA through the linked tag, the url is updated correctly but the component is refreshed to itself.
From the solutions from previous related posts, I have tried
<Route exact path = "/ranked" ...
But it didn't work.
What could be the problem here? How do I solve it?

I would recommend your route look something like this instead /route/:org if you expect to receive props at the end of that specified route. Then inside your Ranked component you would use this.props.match.params.org to get the organization you want ie. (NBA, WNBA). After you have received these props in your Ranked component then you can render what ever you need for that specified organization. Hopefully this makes some sense.

Related

HashRouter, base root "/" [duplicate]

Is there a way to force a React-Router <Link> to load a page from path, even when the current location is already that page? I can't seem to find any mention of this in the react-router documentations.
We have a page on a route for "apply" that loads up a landing page with a hero image, some explanatory text, etc., and an "apply for this program" button that swaps in content that acts as an application form. This all happens on the same "apply" route, because users should not be able to directly navigate to this form without first hitting the landing page.
However, when they have this form open, and they click on the apply link in the nav menu again, the entire page should reload as it would on first mount, getting them "back" (but really, forward) to the landing page again.
Instead, clicking the <Link> does nothing, because react-router sees we're already on the "apply" page, and so does not unmount the current page to then mount a different one.
Is there a way to force it to unmount the current page before then mounting the requested page, even if it's for the page users are supposedly already on? (via a <Link> property for instance?)
Note: this question was posted when React-Router meant v5, and while the problem in this post is independent of a specific React-Router versions, but the solutions are not. As such, the accepted answer is the solution for React-Router v6, so if you're still using v5, first and foremost upgrade your version of React-Router, but if you absolutely can't, the accepted answer won't work for you and you'll want this answer instead.
In the Route component, specify a random key.
<Route path={YOURPATH} render={(props) => <YourComp {...props} keyProp={someValue} key={randomGen()}/>} />
when react see a different key, they will trigger rerender.
A fix I used to solve my little need around this was to change the location that React-Router looks at. If it sees a location that we're already on (as in your example) it won't do anything, but by using a location object and changing that, rather than using a plain string path, React-Router will "navigate" to the new location, even if the path looks the same.
You can do this by setting a key that's different from the current key (similar to how React's render relies on key) with a state property that allows you to write clear code around what you wanted to do:
render() {
const linkTarget = {
pathname: "/page",
key: uuid(), // we could use Math.random, but that's not guaranteed unique.
state: {
applied: true
}
};
return (
...
<Link to={linkTarget}>Page</Link>
...
);
}
Note that (confusingly) you tell the Link which values you need pass as a state object, but the link will pass those values on into the component as props. So don't make the mistake of trying to access this.state in the target component!
We can then check for this in the target component's componentDidUpdate like so:
componentDidUpdate(prevProps, prevState, snapshot) {
// Check to see if the "applied" flag got changed (NOT just "set")
if (this.props.location.state.applied && !prevProps.location.state.applied) {
// Do stuff here
}
}
Simple as:
<Route path="/my/path" render={(props) => <MyComp {...props} key={Date.now()}/>} />
Works fine for me. When targeting to the same path:
this.props.history.push("/my/path");
The page gets reloaded, even if I'm already at /my/path.
Based on official documentation for 'react-router' v6 for Link component
A is an element that lets the user navigate to another page by clicking or tapping on it. In react-router-dom, a renders an accessible element with a real href that points to the resource it's linking to. This means that things like right-clicking a work as you'd expect. You can use to skip client side routing and let the browser handle the transition normally (as if it were an ).
So you can pass reloadDocument to your <Link/> component and it will always refresh the page.
Example
<Link reloadDocument to={linkTo}> myapp.com </Link>
At least works for me!
Not a good solution because it forces a full page refresh and throws an error, but you can call forceUpdate() using an onClick handler like:
<Link onClick={this.forceUpdate} to={'/the-page'}>
Click Me
</Link>
All I can say is it works. I'm stuck in a similar issue myself and hope someone else has a better answer!
React router Link not causing component to update within nested routes
This might be a common problem and I was looking for a decent solution to have in my toolbet for next time. React-Router provides some mechanisms to know when an user tries to visit any page even the one they are already.
Reading the location.key hash, it's the perfect approach as it changes every-time the user try to navigate between any page.
componentDidUpdate (prevProps) {
if (prevProps.location.key !== this.props.location.key) {
this.setState({
isFormSubmitted: false,
})
}
}
After setting a new state, the render method is called. In the example, I set the state to default values.
Reference: A location object is never mutated so you can use it in the lifecycle hooks to determine when navigation happens
I solved this by pushing a new route into history, then replacing that route with the current route (or the route you want to refresh). This will trigger react-router to "reload" the route without refreshing the entire page.
<Link onClick={this.reloadRoute()} to={'/route-to-refresh'}>
Click Me
</Link>
let reloadRoute = () => {
router.push({ pathname: '/empty' });
router.replace({ pathname: '/route-to-refresh' });
}
React router works by using your browser history to navigate without reloading the entire page. If you force a route into the history react router will detect this and reload the route. It is important to replace the empty route so that your back button does not take you to the empty route after you push it in.
According to react-router it looks like the react router library does not support this functionality and probably never will, so you have to force the refresh in a hacky way.
I got this working in a slightly different way that #peiti-li's answer, in react-router-dom v5.1.2, because in my case, my page got stuck in an infinite render loop after attempting their solution.
Following is what I did.
<Route
path="/mypath"
render={(props) => <MyComponent key={props.location.key} />}
/>
Every time a route change happens, the location.key prop changes even if the user is on the same route already. According to react-router-dom docs:
Instead of having a new React element created for you using the
component prop, you can pass in a function to be called when the
location matches. The render prop function has access to all the same
route props (match, location and history) as the component render
prop.
This means that we can use the props.location.key to obtain the changing key when a route change happens. Passing this to the component will make the component re-render every time the key changes.
I found a simple solution.
<BrowserRouter forceRefresh />
This forces a refresh when any links are clicked on. Unfortunately, it is global, so you can't specify which links/pages to refresh only.
From the documentation:
If true the router will use full page refreshes on page navigation. You may want to use this to imitate the way a traditional server-rendered app would work with full page refreshes between page navigation.
Here's a hacky solution that doesn't require updating any downstream components or updating a lot of routes. I really dislike it as I feel like there should be something in react-router that handles this for me.
Basically, if the link is for the current page then on click...
Wait until after the current execution.
Replace the history with /refresh?url=<your url to refresh>.
Have your switch listen for a /refresh route, then have it redirect back to the url specified in the url query parameter.
Code
First in my link component:
function MenuLink({ to, children }) {
const location = useLocation();
const history = useHistory();
const isCurrentPage = () => location.pathname === to;
const handler = isCurrentPage() ? () => {
setTimeout(() => {
if (isCurrentPage()) {
history.replace("/refresh?url=" + encodeURIComponent(to))
}
}, 0);
} : undefined;
return <Link to={to} onClick={handler}>{children}</Link>;
}
Then in my switch:
<Switch>
<Route path="/refresh" render={() => <Redirect to={parseQueryString().url ?? "/"} />} />
{/* ...rest of routes go here... */}
<Switch>
...where parseQueryString() is a function I wrote for getting the query parameters.
There is a much easier way now to achieve this, with the reloadDocument Link prop:
<Link to={linkTarget} reloadDocument={true}>Page</Link>
you can use BrowserRouter forceRefresh={true}
I use react-router-dom 5
Example :
<BrowserRouter forceRefresh={true}>
<Link
to={{pathname: '/otherPage', state: {data: data}}}>
</Link>
</BrowserRouter>
Solved using the Rachita Bansal answer but with the componentDidUpdate instead componentWillReceiveProps
componentDidUpdate(prevProps) {
if (prevProps.location.pathname !== this.props.location.pathname) { window.location.reload();
}
}
You can use the lifecycle method - componentWillReceiveProps
When you click on the link, the key of the location props is updated. So, you can do a workaround, something like below,
/**
* #param {object} nextProps new properties
*/
componentWillReceiveProps = (nextProps)=> {
if (nextProps.location.pathname !== this.props.location.pathname) {
window.location.reload();
}
};
To be honest, none of these are really "thinking React". For those that land on this question, a better alternative that accomplishes the same task is to use component state.
Set the state on the routed component to a boolean or something that you can track:
this.state = {
isLandingPage: true // or some other tracking value
};
When you want to go to the next route, just update the state and have your render method load in the desired component.
Try just using an anchor tag a href link. Use target="_self" in the tag to force the page to rerender fully.

Will JSX conditional rendering out of an object code split?

Will conditional rendering out of an object code split and lazy load as expected? Here's a short example of what I'm talking about.
const Component1 = lazy(() => import('some path'));
const Component2 = lazy(() => import('some path'));
const Component3 = lazy(() => import('some path'));
render () {
const { selectionIndex } = this.state;
<Suspense fallback={<div>Loading...</div>}>
{{
one: <Component1 />,
two: <Component2 />,
three: <Component3 />,
}[selectionIndex]}
</Suspense>
}
I want to know whether all three components will load on render, or just the one selected by selectionIndex. I'm trying to use this to conditionally select something to display based on a menu set by state, but I don't want to load everything at once.
They will not get rendered all at once. You can experiment by yourself, put console.log inside components is an easy way to find out.
React for web consists of two libs, "react" and "react-dom". "react" is in charge of encapsulating your logic intention into declarative data structures, while "react-dom" consumes these data structures and handles the actual "rendering" part of job.
The JSX element creation syntax <Component {…props} /> translates to plain JS as an API call to React.createElement(Component, props). The return value of this API call is actually just a plain object of certain shape that roughly looks like:
{
type: Component,
props: props
}
This is the aforementioned "declarative data structure". You can inspect it in console.
As you can see, calling React.createElement just return such data structure, it will not directly call the .render() method or functional component’s function body. The data structure is submitted to "react-dom" lib to be eventually "rendered".
So your example code just create those data structures, but the related component will not be rendered.
seems like its conditionally loaded based on selectionIndex. all the other components are not loaded at once.
P.S.: if you ever feel like which will get load first, just put a console log in that component and debug easily
conditionally load demo link - if you open this, the components are being loaded initially based on selectionIndex value being "one".
I'm not going to go into too much technical detail, because I feel like #hackape already provided you with a great answer as to why, point of my answer is just to explain how (to check it)
In general, I'd recommend you to download download the React Developer Tools
chrome link
firefox link
and then you can check which components are being rendered if you open the components tab inside your developer console. Here's a sandboxed example, best way to find out is to test it yourself afterall :-)
As you can see in the developer tools (bottom right), only the currently set element is being rendered

UI does not render after state is changed

I have an app (project in Udacity) in React which display books on my shelves according to categories: Currently Reading, Want to Read and Read. Every time I change the category say from Want to Read to Currently Reading the book will move to the right category and in this case it would be Currently Reading. My code works on this one with no problem. However, you can also search from the vast library of books wherein you could move to your shelf, by default the category is None, although you could include the existing books in your shelf as part of being search (aside from the main library of books). Now, my problem is this, if I move from None to Want To Read category for example my UI does not change after I click the back button that brought me back to the main page (i.e. App.js). When I do however, change of category in the main, I have no problem. Also my function for updating the Book Shelf in App.js when called does not show any error in the console.
I have the following components:
App
|
|--BooksSearch
|--BooksList
| |--BookShelf
|--BooksSearchPage
|--BookShelf
The BooksList and BooksSearch displays the books and the search button respectively in the main page (i.e. App.js). The BooksSearchPage allows user to search books from the library to move into the shelves. The BookShelf displays the list of books whether they are in the shelves or in the library.
This is my App.js
class App extends React.Component {
state = {
mybooks : [],
showSearchPage: false
}
componentDidMount() {
BooksAPI.getAll().then( (mybooks)=> {
this.setState({mybooks})
})}
toCamelShelf(Shelf) {
if (Shelf==="currentlyreading") return "currentlyReading"
if (Shelf==="wanttoread") return "wantToRead"
return Shelf
}
updateBookShelf = (mybook, shelf) => {
shelf=this.toCamelShelf(shelf)
BooksAPI.update(mybook, shelf).then(
this.setState((state)=>({
mybooks: state.mybooks.map((bk)=>bk.id === mybook.id ?
{...bk, shelf:shelf} : bk)
})))}
render() {
return (
<div className="app">
{this.state.showSearchPage ? (
<Route path='/search' render={({history})=>(
<BooksSearchPage mybooks={this.state.mybooks} onSetSearchPage={
()=>{ this.setState({showSearchPage:false});
history.push("/");
}}
onUpdateBookShelf={this.updateBookShelf}
/>
)} />
) : (
<Route exact path='/' render={()=>(
<div className="list-books">
<div className="list-books-title">
<h1>My Reads</h1>
</div>
<BooksList mybooks={this.state.mybooks}
onUpdateBookShelf={this.updateBookShelf}/>
<BooksSearch onSetSearchPage={()=>this.setState({showSearchPage:true})}/>
</div>
)} />
)}
</div>
)
}
}
export default App
And since the code is too long, I included my repo in Github. I am very new to ReactJS and have been debugging this problem for the last 3 days but to no avail.
I'm having a hard time understanding the app enough to know why exactly, but it sounds like its a state issue.
If you navigate away and come back, or click something and it doesn't update properly, the state isn't being updated at that moment (that event) or the state wasn't saved correctly right before that event.
As soon as you reproduce the problem event, ask yourself "what was the state right before I did this?" and "why is the state how it is now?"
Did you forget to update the state?
Is it getting the wrong state from somewhere?
Did you call this.setState({ something })?
Did you overwrite the state instead of adding to it?
Is there a missing state update?
On both pages, right before and right after, add in the render method: console.log(this.state) and if needed, console.log(this.props). I think you will see the problem if you look there. The question is how exactly did it get like that? Re-visit all your state updates.
If you navigate away and come back, where does it get that state from? Why is that data in there?
Remember, React is a state machine. State is an object that has a snapshot of data every time you look at it. It's like looking at a piece of paper with all your data on it. If you leave the room and come back and the data isn't there, what updated your state and made it go away? or why didn't it get added to your state? That mechanism there is causing your problem.
I see a few spots in your code to focus on:
BooksAPI.update(mybook, shelf).then(
this.setState((state)=>({
mybooks: state.mybooks.map((bk)=>bk.id === mybook.id ?
{...bk, shelf:shelf} : bk)
})))}
and
<BooksSearchPage mybooks={this.state.mybooks} onSetSearchPage={
()=>{ this.setState({showSearchPage:false});
history.push("/");
}}
onUpdateBookShelf={this.updateBookShelf}
and
<BooksList mybooks={this.state.mybooks}
onUpdateBookShelf={this.updateBookShelf}/>
<BooksSearch onSetSearchPage={()=>this.setState({showSearchPage:true})}/>
also right up here:
class App extends React.Component {
state = {
mybooks : [],
showSearchPage: false
}
componentDidMount() {
BooksAPI.getAll().then( (mybooks)=> {
this.setState({mybooks})
})}
One of them is acting too strongly or one of them isn't updating at the right time, or data is getting overwritten, I suspect.
The console.log() should be most helpful. If your data is missing. Make it show up there at that time and the problem will go away :) (P.S. that setState on componentDidMount looks a little suspect).

How can I make my React child components render even if a prop is missing

TLDR; I need to be able to render child components in React even if a property of this.props is missing.
I have an React app built with Yahoo's Flxubile. The app fetches data from a Wordpress site with WP REST API. Sometimes an image might be missing or something else from the API, this causes the client to break. Here's an example:
I have a file called Articles.js which is connected to ArticlesStore which holds my articles. I then render one Article.js for every article I have and pass props like this:
{ this.props.articles.map(function(el, index) {
return <Article key={index} article={el} />
})
}
Everything is ok here, but then in my Article.js when I try to access properties that is not set I get the following:
Uncaught TypeError: Cannot read property 'sizes' of undefined
This is the line which causes the error:
<img src={this.props.article.meta_fields.image.sizes.large} />
This happens when a image is missing from an article. I understand the javascript error of course but I want to render the Article.js component event if a image url is missing from the API/Store. I have tried the following solutions, but it causes too much clutter and no control:
Conditional set if we have the prop i.e.
{this.props.article.meta_fields.image ? this.props.article.meta_fields.image.sizes.large : "imagemissing.png"}
Setting defaultProps. This does not work because passed properties from parent to child overwrites defaultProps.
Maybe I should try something else than passing props from parent to child? Have a ArticleStore where I can set default values for every article? How would you do it?
If you want to provide a nested structure as a prop (as with article) you'll want to be able to rely on the structure always being pretty much the same. In this case it won't be, sometimes the meta_fields does not have an image-attribute (as your TypeError suggests).
In your case I would consider pulling out the things you actually need/use in the Article component from the article object and pass those as props.
Say that your Article only uses title, body, and image. Then just pass those as props.
<Article title={ article.title } body={ article.body } image={ getImage(article) }/>
function getImage(article) {
if (article.meta_fields.image
&& article.meta_fields.image.sizes
&& article.meta_fields.image.sizes.large
) {
return article.meta_fields.image.sizes.large;
}
return 'default_image.jpg'; // Or whatever you want.
}
One might think that the extra props constitutes more "clutter" here, but given the choice between clutter and TypeError, I choose clutter.
And if you don't want to reinvent the wheel. The problem of accessing data in a nested structure like this have been solved before.
// Lodash
_.get(article, 'meta_fields.image.sizes.large', 'default_image.jpg')
// Ramda
_.pathOr('default_image.jpg', ['meta_fields', 'image', 'sizes', 'large'], article)

Declaring routes using an array

I want to move my route declarations for React Router to separate files and then complete the route specification automatically, like this:
// Central array for routes
const routes = [];
// Two routes in separate files
routes.push(<Route path="page1" component="Component1"/>);
routes.push(<Route path="page2" component="Component2"/>);
// Render the routes
<Router history={browserHistory}>
{routes}
</Router>
Although this works, I'm getting the warning that all children of the iterator, i.e., the routes, should have a key prop:
Each child in an array or iterator should have a unique "key" prop. But are keys really necessary in this case? From my understanding, the routes render only once and are not dynamic, even though I'm using an array.
Is it possible to do that in React?
Because you are using an expression (instead of hard-coded/pre-configured children) and because the expression is getting evaluated to an array, React thinks that this is a component with dynamic children and hence it starts warning you with that message.
And thinking more about it, it makes sense. Because as far as React is concerned, there's nothing stopping you from modifying that array and expecting the expression to resolve to a different data set. Therefore it thinks it's a component with dynamic children.
From my understanding, the routes render only once and are not dynamic, even though I'm using an array.
Even though <Router> will render only once, it still is a React component and it's not aware that it will "only render once". The component needs to be ready to 're-render' and do so efficiently.
In order to make sure the component is render efficiently, it asks for these "keys".
You can stop reading my response here but if you want to build more intuition around it, keep on reading :)
Imagine you witnessed a thief stealing an old-lady's purse...
The police brings you in to identify the bad guy in a lineup. There are 8 suspects outside the lineup room. The officer brings in the first 6 suspects inside the room. The other 2 are waiting outside due to room capacity issues.
Each one of them is holding an ID tag (the key). You take a good look at each one of them and write down on a piece of paper their IDs. You still need to see the other 2 suspects but by law, the line up room must always have 6 people. Which means 4 of the ones you just saw will remain in the lineup room.
Because you're very smart and wrote down the ID of the suspects you already looked at (look at = render), you can skip looking at the suspects whom's ID is already in your list.
I know it's a weird example but I hope it helps :)
If you want more details, the React docs are a good place to start: https://facebook.github.io/react/docs/reconciliation.html#list-wise-diff
If you are sending props down to the child components without specific ids you would see this warning.
So basically when you say run a loop and pass the parameters to child components you need to specify ids for each iteration in the props.
For example,
const a=[
{'id':1,'name':'x',age:'25'},
{'id':2,'name':'y',age:'26'},
]
Parent Component:-
class Parent extends React.Component{
constructor(){
super();
this.state={
details=[
{'id':1,'name':'x',age:'25'},
{'id':2,'name':'y',age:'26'},
]
}
}
_getDetails() {
return this.state.details.map((content)=>{
return (
<Child key={content.id} name={content.name} age={content.age}/>
);
});
}
}
In above Child is the component where you are passing props to.
One simple solution is to use Plain Routes instead of JSX:
const routes = [];
routes.push({
path: 'path1',
component: Component1
});
routes.push({
path: 'path2',
component: Component2
});
<Router routes={routes} history={browserHistory}/>

Categories

Resources