Is React Router causing my component to render twice? - javascript

Background
I have a list of items. If you click on a detail link under an item, you are taken to its page where it allows you to read and add comments about that particular item. Currently, the comments component is rendering both on the index page and on the item details page. It should only render on the latter.
Can you help me figure out why?
Troubleshooting
I checked my routes and my Comments and Comment components but did not see any obvious errors. I saw this post about rendering components twice, but my situation is different because I have to use render= to pass down props.
I'll admit, I have never tried to build a route like this, so maybe I am not approaching it properly. The one is question is the 2nd from the bottom in this list below.
Routes
render(){
return(
<div>
<Switch>
<Route exact path='/' render={(routerProps) => <Programs {...routerProps} programs={this.props.programs}/>} /> />
<Route exact path='/programs' render={(routerProps) => <Programs {...routerProps} programs={this.props.programs} />} />
<Route path='/programs/new' render={(routerProps) => <ProgramInput {...routerProps}/>}/>
<Route path='/programs/:id' render={(routerProps) => <Program {...routerProps} programs={this.props.programs}/>}/>
<Route exact path='/programs/:id/comments' render={(routerProps) => <Program {...routerProps} program={this.props.program}/>}/>
<Route exact path='/watchlist' render={(routerProps) => <Program {...routerProps} programs={this.props.programs} />} />
</Switch>
</div>
)
}
CommentsContainer
class CommentsContainer extends React.Component {
render(){
return(
<div>
<Comments comments={this.props.program && this.props.program.comments}/>
</div>
)
}
}
export default CommentsContainer
Program (where CommentsContainer is rendered)
I took out some of the Card code for brevity. There is More link inside the card that takes you to the item page. That part works fine.
return(
<Fragment>
<Grid.Column>
<Card as='div'>
</Card>
</Grid.Column>
<CommentsContainer program={program}/>
</Fragment>
)
}
Comments
const Comments = (props) => {
// console.log(props.comments && props.comments.map(comment => console.log(comment))
return(
<Comment.Group>
{props.comments && props.comments.map(comment => <CommentCard key={comment.id} comment={comment}/>)}
</Comment.Group>
)
}
export default Comments
ComentCard
I don't think the issue lies here, but here's a link if you need to see the file.
Backend
I am using Rails API for my backend. Here is a link to my comments_controller if you want to take a peek.
Thank you for your time and advice!

If you only want the CommentsContainer to show when there are comments, you can just replace the line where it's rendered with:
{program.comments && <CommentsContainer program={program}/>}

As Matt Oestreich pointed out earlier you always render comments when you render the Program component. The Programs (plural) component is basically a list that consists of Program components. So when you render the Programs component you always render the CommentsContainer with the Comment Component.
So maybe you can make another component, the ProgramDetails Component where you include the CommentsContainer. In the Program Component you can link to the ProgramDetails Component. I think this would prevent that the comments render in the Programs (plural) is rendered.

This did end up being a routes issue. Both my /programs and my / routes were displaying my CommentsContainer because I needed to add a ternary to essentially filter those routes out.
I added this code in my Programs (plural) component right after my Card component. It uses props.match to check if the route being rendered was NOT /programs and NOT /. When that statement evaluated to true, the CommentsContainer and associated comments were rendered on the /programs/:id. If either of those statements evaluated to true, it didn't render anything.
{this.props.match.url !== '/programs' && this.props.match.url !== '/'? <CommentsContainer program={program}/> : null}
Since I only passed down routerProps in my Programs.js file, I had to pass down props.match from Programs to my Program component (which renders the cards and CommentContainer). Doing this allows the ternary above to work.
{props.programs && props.programs.map((program) => <Program key={program.name} program={program} match={props.match}/>)}

Related

React router DOM : Switch not working if child element isnt a route

I'm facing an issue im not able to comprehend. I'm working on a React codebase and we started having too much Routes, so i decided to refactor it a little bit. There is some logic to determine if we should redirect to a route that will check if you're authenticated, and if not will redirect you to the sign in page. We also have basic routes without that mechanism that just display a component ( for every page that doesnt need authentication like sign in, forgot password etc. )
I've decided to abstract that logic in a different component, that will ultimatley render a Route. As a first step in that direction, i decided to wrap the rendering of every route in a single component, instead of having the whole logic just laying down there.
Here's the code for the routes:
<Switch>
{Object.values(userRoutes).map((route: SanitizedRoute) => (
<RouteController route={route} key={route.PATH} />
))}
</Switch>
RouterController.tsx
return (
<Route
path={props.route.PATH}
exact={props.route.EXACT}
render={() => {
return <Layout>{props.route.COMPONENT}</Layout>;
}}
/>
);
All the information for the Route component is passed down as a prop. This is where i start getting problems. If i try to access /path_b what is rendered is the first element of my userRoute array, eventhough the Route doesn't match; /path_b or /path_c or /path_whatever will always render the compononent defined for /path_a.
Accessing any path actually returns the /path_a component as if it was the only one present in my Switch component.
If i were to replace the RouteController component by its content as such :
<Switch>
{Object.values(userRoutes).map((route: SanitizedRoute) => (
<Route
path={route.PATH}
exact={route.EXACT}
key={route.PATH}
render={() => {
return <Layout>{route.COMPONENT}</Layout>;
}}
/>
))}
</Switch>
Then everything would work fine as expected. Wrapping my RouteController with a Switch component is also working fine - although im not sure about the side effect of having a Switch per route ?
{Object.values(userRoutes).map((route: SanitizedRoute) => (
<Switch key={route.PATH}>
<RouteController route={route} />
</Switch>
))}
My questions are:
Why isn't it possible to wrap all of those routes in a single ?
Is there some props from the component i should manually pass down to the component via my ?
I know that the is used to render the first route that match the path. Does having multiple kind of defeat the purpose of using a in the first place ?
Here's a Sandbox to display this behavior : https://codesandbox.io/s/react-router-dom-switch-test-forked-sqc98
I wrapper 1 route in a component. When using the wrapper nothing work. If i copy paste the wrapper content inside the Switch, it does.
Why isn't it possible to wrap all of those routes in a single
<Switch>?
This is because of how the Switch component works, it renders the first child <Route> or <Redirect> that matches the location.
Switch
The RouteController component is neither, so it's not involved in matching, and in fact only first component will be rendered. This is why you see only the first route rendered regardless of path.
Is there some props from the <Switch> component I should manually
pass down to the <Route> component via my <RouterController>?
It's possible to move/define a path prop (and any other "route" prop) on the RouteController the Switch may use for path matching, but this is ill-advised and not a documented use case of the Switch and Route components.
I know that the <Switch> is used to render the first route that
match the path. Does having multiple <Switch> kind of defeat the
purpose of using a <Switch> in the first place?
Yes, if you are rendering multiple Switch components, each with only a single Route component then they aren't really switching routes at this point and you're relying on the inclusive route matching of any wrapping router, or exclusively matching in the case of any other wrapping switches.
Solution
Instead of trying to map routes into your RouterController component and dealing with the issues of what is directly composing what, have your RouterController consume all the routes as a prop, and render them into a single Switch component.
RouterController.tsx
const RouterController = ({ routes }) => {
// any component business logic
return (
<Switch>
{routes.map((route: SanitizedRoute) => (
<Route
path={route.PATH}
exact={route.EXACT}
key={route.PATH}
render={() => <Layout>{route.COMPONENT}</Layout>}
/>
))}
</Switch>
);
};
...
<RouterController routes={Object.values(userRoutes)} />

React router rendering

I have a piece of code that I can not understand. Any explanation is appreciated. My question is what (props) actually is doing here and where it comes from? If I take "props" out, my code is not working.
<Route
exact
path='/'
render={(props) =>
!isAuthenticated ? (
<Landing {...props} />
) : (
<Redirect to='/admin' />
)
}
/>
Route is a component of react-router , which matches the url and routes , The code you have given is used for Authentication to redirect user if he is not Authorized ,whatever that is passed from parent has to be passed to the children in this case , the reason they are passing props is to pass the properties of React Router which includes things like history,location and etc. A simple console.log will show you what it is passing and why it is required
Refer to Image below to see what all is getting passed

Understanding how we can avoid passing props through many levels of component hierarchy in ReactJS

I was going through ReactJS docs. On the page explaining Contexts, I came across following:
If you only want to avoid passing some props through many levels, component composition is often a simpler solution than context.
For example, consider a Page component that passes a user and avatarSize prop several levels down so that deeply nested Link and Avatar components can read it:
<Page user={user} avatarSize={avatarSize} />
// ... which renders ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... which renders ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... which renders ...
<Link href={user.permalink}>
<Avatar user={user} size={avatarSize} />
</Link>
In above code, we are still passing user and avatarSize as props right? Then how it allows to "avoid passing some props through many levels" as stated in the first sentence of the above quote?
The code you quoted is from the "problem" part of the documentation. That code isn't meant to be the solution, it shows the problem with prop drilling.
Scrolling a little down shows how to solve the problem using Component Composition
Component Compostion:
function Page(props) {
const user = props.user;
const userLink = (
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
);
return <PageLayout userLink={userLink} />;
}
// Now, we have:
<Page user={user} avatarSize={avatarSize} />
// ... which renders ...
<PageLayout userLink={...} />
// ... which renders ...
<NavigationBar userLink={...} />
// ... which renders ...
{props.userLink}
The solution to prop drilling is we can use context api. Context provides a way to share values like these between components without having to explicitly pass a prop through every level of the tree.
you can check details here on official documentation!

Why aren't {...props} necessary when passing them to a component in a router in React?

I've started learning React recently and when I was following a course I stumbled upon something that confused me a lot. I decided to try it out separately to understand it and I don't get it.
I've looked it up and when you use a Route Tag in React, you do it like this:
<Route path="/" component={Component} />
But when you want to pass props, the syntax is the following:
<Route path='/dashboard' render={(props) => (
<Dashboard {...props} isAuthed={true} />
)}
/>
Which at first confused me, but then I got it. When testing, I did the following:
<Route path="/" render={() => (
<PropDrilling user={"Someone"} />
)}>
When I test this in the component, console.logging this.props, it works and I don't know why.
Thanks in advance!
React router uses React's render props where react router uses match, location and history. So, when you don't provide props in the render function, it will still be able to show you react component props. But it would not include match, location and history props provided by react router. To use them, you must provide props parameter in the render function.
Using an analogy, consider
function test(obj) {
console.log(obj);
}
test({a : 1});
function test() {
console.log({a : 1});
}
test();
These two functions log the same result, but one has no information about the data passed to console.log, while the other does.
Similarily, {...props} means you are forwarding the props received as a function parameter to a component, irrespective of what the value of props is.
Taking the example of <Route path="/" component={Component} />, you could write it like that :
const myProps = {
path: "/",
component: <YourComponent />
}
<Route {...myProps} />
Consider every props passed to a component as a key in an object. Here is a small repro on Stackblitz to show you how it works.

Dynamic Lazy Loading using React.Lazy (16.6.0)

I get an array like
{Path:xxx,
Component:"./xxx/ComPXX"}
from my API and based on that create my application's routes. At the moment I'm using React-Loadable (5.5.0) and React-Router (4.4.0-betax to avoid warnings in strict mode). See working example here.
Since React 16.6 has introduced React.lazy, I'm trying to migrate my solution, however I'm facing errors and difficulties however I try to do this. You can see migrated (failing) code here.
Any Idea why this isn't working? Can it be because React.Lazy doesn't accept variables?
You got 2 3 main issues:
In this line:
var c = <dynamicLoader component={prop.component} />;
User-Defined Components Must Be Capitalized. so change it to this:
var c = <DynamicLoader component={prop.component} />;
Obviously you'll need to change the declaration as well:
function DynamicLoader(props) {
return (
<Suspense fallback={<div>Loading...</div>}>
React.lazy(() => import(`${props.component}`))
</Suspense>
);
}
In this line
return <Route exact path={prop.path} component={c} key={key} />;
As the name of the prop component suggests, you need to pass a
component and not an element you can read more about the difference
in the DOCS.
So you'll need to change it to this:
return <Route exact path={prop.path} component={() => c} key={key} />;
You are right. I missed the children part, you are rendering a string. You can create a
variable and just render it as the child:
function DynamicLoader(props) {
const LazyComponent = React.lazy(() => import(`${props.component}`));
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent/>
</Suspense>
);
}

Categories

Resources