Nested Routing on exported components with React Router - javascript

I was following a CRUD tutorial that used nested Routes like the code below. I tried to omit most of the code that doesn't concern routing.
After looking up several other tutorials on nested routing I noticed they don't use exported components like I was. I also noticed that the tutorial code below exported its components using withRouter.
index.js:
...imports
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root'),
);
App.js:
const App = ({ classes }) => (
<CssBaseline />
<AppHeader />
<main className={classes.main}>
<Home />
<Route exact path="/" component={Home} />
<Route exact path="/posts" component={PostManager} /> //This renders a list of posts
</main>
</Fragment>
PostsManager.js:
...imports
...constructor and other functions (this.savePost)
renderPostEditor = ({ match: { params: { id } } }) => {
if (this.state.loading) return null;
const post = find(this.state.posts, { id: Number(id) });
if (!post && id !== 'new') return <Redirect to="/posts" />;
return <PostEditor post={post} onSave={this.savePost} />;
};
render() { //component render funciton
...
<Button
variant="fab"
color="secondary"
aria-label="add"
className={classes.fab}
component={Link}
to="/posts/new"
>
<Route exact path="/posts/:id" render={this.renderPostEditor} />
}
...exporting component withRouter()
The problem I got was that when I tried to access /posts/new or /posts/2 which both should match /posts/:id, I didn't get any match. The method this.renderPostEditor obviously didn't get called and PostEditor wasn't rendered.
I tried to solve the problem by removing the Route in PostsManager.js and putting in App.js. That way I got a match but it didn't render the way I wanted because this.renderPostEditor dependended on PostManager.js
My question is why I didn't get a match inside PostsManager.js but got match in App.js?

Try removing the exact prop from <Route ... /> definition.

Related

React Recoil persist state between routes

I want to use Recoil to share a state between to React Router Routes.
My setup looks something like this:
ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="/checkin" element={<Checkin />} />
<Route path="/checkin/confirm" element={<ConfirmCheckin />} />
</Routes>
</BrowserRouter>
</RecoilRoot>
</React.StrictMode>,
document.getElementById("root")
);
My atom setup looks like this:
const checkinItemsState = atom<InventoryItem[]>({
key: "checkinItemsState",
default: [],
});
In both components i use useRecoilState(checkinItemsState)
Now when i open localhost:3000/checkin it shows my state and i can modify it. When i then navigate to localhost:3000/checkin/confirm my state is just an empty array.
What am I missing here? is there something i misunderstand?
So fare I've tried to wrap my Routes with a component like:
const Index: React.FC<{ children: ReactNode }> = ({ children }) => {
useRecoilState(checkinItemsState);
return (
<>
<span>Hello World</span>
{children}
</>
);
};
I guessed maybe it was because the state was nowhere referenced when moving between urls. This does not work.
Any help would be appreciated.

React Router v5: Route defined in child component not working

My application is using react-router-dom v5.3 and I'm having trouble routing from the root url of my application to a child component (called the "See All" Page) while also passing props down. Currently, my code just renders an empty page whenever I navigate to the child component.
RootRouter.js:
export default function RootRouter() {
return (
<BrowserRouter>
<Switch>
<Route
path="/"
exact
render={() => <HomeView />}
/>
</Switch>
</BrowserRouter>
);
}
Homeview.js:
function HomeView() {
const seeAllViewTitle = "some_title_here"
return (
<div>
<div>Some content here!</div>
<Link to={`/seeall/${seeAllViewTitle}`}}>
<Button/>
</Link>
<Route path={`/seeall/${seeAllViewTitle}`}>
<SeeAllView
groupTitle={""}
pageData={[]}
eventHandler={some_function_here}
/>
</Route>
</div>
);
}
If I were to put the Route that is currently in homeview.js inside of Rootrouter.js, the component shows up, but I can't pass any props into it from there.
Issue
The HomeView component is rendered only when the path is exactly "/". When the link is clicked and navigates to "/seeall/some_title_here " the path no longer exactly matches and the HomeView component unmounts.
Solution
Remove the exact prop from the root route so nested/sub routes can also be matched and rendered.
export default function RootRouter() {
return (
<BrowserRouter>
<Switch>
<Route path="/" component={HomeView} />
</Switch>
</BrowserRouter>
);
}
If you did not intend for these components to be rendered at the same time then move the nested route out to the RootRouter component.
export default function RootRouter() {
return (
<BrowserRouter>
<Switch>
<Route path="/seeall/:title">
<SeeAllView
groupTitle={""}
pageData={[]}
eventHandler={some_function_here}
/>
</Route>
<Route path="/" component={HomeView} />
</Switch>
</BrowserRouter>
);
}
...
function HomeView() {
const seeAllViewTitle = "some_title_here"
return (
<div>
<div>Some content here!</div>
<Link to={`/seeall/${seeAllViewTitle}`}}>
<Button/>
</Link>
</div>
);
}
Are you remembering to receive props in the function declaration for HomeView? usually, you'll need to explicitly define that you are receiving props, either with a props variable or by defining specific prop names in an object syntax

React router v5 renders only index page on all routes

ISSUE
any route written manually in URL or triggered by button renders only index page (route name is changing, content of the page is always content of index)
App.js (handling all the main functions from the app)
// Exporting my router and passing all states/functions from App
<div className="App">
<Routes
........
........
........
onRemove={onRemove}
setCartOpen={setCartOpen}
cartOpen={cartOpen}
.......
.......
/>
</div>
Router (example of how my routes work)
export default function Routes({....., ....., onRemove,setCartOpen,cartOpen,..... })
<Router>
<Header
countCartItems={cartItems.length}
cartItems={cartItems}
onAdd={onAdd}
onRemove={onRemove}
setCartOpen={setCartOpen}
/>
<Suspense fallback={<Spinner />}>
<Switch>
<Route
path={baseRouteUrl + '/'}
component={(props) => (
<IndexPage
{...props}
.......
.......
onRemove={onRemove}
setCartOpen={setCartOpen}
cartOpen={cartOpen}
.......
.......
/>
)}>
</Route>
<Route
path={baseRouteUrl + '/about'}
component={About}
></Route>
<Route
path={baseRouteUrl + ''}
component={NotFound}
></Route>
.......
And Index.js
ReactDOM.render(
<React.StrictMode>
<QueryClientProvider client={client}>
<ChakraProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</ChakraProvider>
</QueryClientProvider>
</React.StrictMode>,
document.getElementById('root')
);
NOTE
Before trying to pass props in the routes the router was working just fine
The reason I decided to structure my app this way is because I need my Header to be exported on all pages with all the functionalities.
Is there anything I should do in a different way? I know it might be a little messy, so I'll be grateful to learn how to handle it cleaner and most important, how to make it work so all routes will render the correct pages.
Just like in most languages, a switch will match and runs the first thing it runs into.
In this case, it's matching your default path and rendering that one first.
One of the way to deal with this is to set exact to true
<Route exact path={...}... />
react router does partial matching so '/' is partial from '/about' when using exact it will disable the partial matching and will get the correct path
<Route
exact
path={baseRouteUrl + '/'}
component={(props) => (
<IndexPage
{...props}
.......
.......
onRemove={onRemove}
setCartOpen={setCartOpen}
cartOpen={cartOpen}
.......
.......
/>
)}>
</Route>

How to conditionally render wrapping components in React Router 4/5

I am migrating an app from react-router version 3 to 5, and am a little stuck trying to figure out the best approach to the following problem.
I have some routes that make use of a wrapping component.
in routerv3 I could do things like
const Container = props =>
<div>
<header>container1</header>
{ props.children }
</div>
const Container2 = props =>
<div>
<header>container2</header>
{ props.children }
</div>
These containers have other function
<Route component={Container}>
<Route path='/container1' component={Page1} />
</Route>
<Route component={Container2}>
<Route path='/container2' component={Page1} />
</Route>
When navigating to /container1 it would render the Page1 component with a Container1 wrapping it, and then on /container2 it would render Page1 with Container2 wrapping it.
However when moving to router v4+ all matching routes are rendered, so what happens is that for example on /container2 the result is
<div>
<header>container1</header>
</div>
<div>
<header>container2</header>
<Page1 />
</div>
Does anyone know of a way to write these "wrapping components" such that they are only rendered if routes match? Or perhaps a better way to do this kind of thing if that's not how react-router 4/5 works.
here is a link to a stackblitz of me trying to figure this out
https://stackblitz.com/edit/react-jvsdsn?file=index.js
You can do something like this
<Route
path="/container1"
render={(routeProps) => (
<Container>
<Page1 {...routeProps} />
</Container>
)}
/>
<Route
path="/container2"
render={(routeProps) => (
<Container2>
<Page1 {...routeProps} />
</Container2>
)}
/>

Redirect to url on commponentDidMount in App.js

I am trying to implement a feature when the user logs into the app and they have not completed their profile, they should be redirected to a certain URL, So everywhere they try to go (except logout) they should be redirected to the complete-profile URL.
I'm handling routing with react-router-dom package.
App.js
class App extends Component {
async componentDidMount() {
const logged = isLoggedIn();
if (logged) {
const completed = await hasCompleted();
if (!completed) this.props.history.replace("/complete-profile"); //error showing here
}
}
render() {
return (
<React.Fragment>
<NavBar />
<main className="container">
<Switch>
<Route path="/complete-profile" component={CompleteProfile} />
<Route path="/login" component={Login} />
<Route path="/register" component={Register} />
<Route path="/logout" component={Logout} />
<Route path="/" exact component={Home} />
</Switch>
</main>
</React.Fragment>
);
}
}
So basically what I'm trying to do here in componentDidMount method is: I first check if the user is logged in. Then check if the user has completed their profile, and if it's not completed then it should redirect to /complete-profile URL.
But with this method I'm facing an error with this.props.history.replace because it does not take any props when this method is getting called I guess, and the error that is showing is this:
Unhandled Rejection (TypeError): Cannot read property 'replace' of undefined
Which is the proper way to implement this one?
Because I dont think that I should implement these 4 lines of code checking for completed profile in every single component.
In the App component the history prop is not available (undefined) because the following props:
history
location
match
are passed from Route component to its children (CompleteProfile, Home,....). So you can not use them in the App component.
Instead you can create your own Route component:
class CompleteProfile extends Component {
state = {
completed: false
};
async componentDidMount() {
const logged = isLoggedIn();
if (logged) {
const completed = await hasCompleted();
this.setState({completed});
//error showing here
}
}
render() {
const { component: Component, ...rest } = this.props;
const { completed } = this.state;
return (
<Route {...rest} render={(props) => (
completed
? <Component {...props} />
: <Redirect to='/complete-profile' />
)} />
)
}
}
and use it instead of Route like this:
<CompleteProfile path='/' exact component={Home} />
This is the general idea you can refactor the code as you want.
Have a look at the description of the Route component. You'll see that three props are injected for each component that's rendered using <Route /> in your code for example. <Route path="/login" component={Login} /> These props are match location history
In your code App is not rendered using Route. Because of this, you don't have access to these three injected props. That's the reason why you get the error that history is undefined.
To redirect the user, use something like the below instead, where you conditionally render a redirect or the UI depending if data is present.
class App extends Component {
constructor(props){
super(props);
this.state = {
fetchingStatus: true,
completed: false
};
}
async componentDidMount() {
const logged = isLoggedIn();
if (logged) {
const completed = await hasCompleted();
if (!completed) this.setState({ fetchingStatus: false, completed: true })
}
}
render() {
const { fetchingStatus, completed } = this.state;
// Render nothing, or a spinner as loading indicator
if (fetchingStatus) return null; // waiting for details...
// if data was fetched and registeration not completed, render redirect.
if (!fetchingStatus && !completed) return <Redirect to="/complete-profile" />
// Render switch and nav.
return (
<React.Fragment>
<NavBar />
<main className="container">
<Switch>
<Route path="/complete-profile" component={CompleteProfile} />
<Route path="/login" component={Login} />
<Route path="/register" component={Register} />
<Route path="/logout" component={Logout} />
<Route path="/" exact component={Home} />
</Switch>
</main>
</React.Fragment>
);
}
}
More on React Router's redirect component here
This sort of TypeError becomes intimately familiar when working with React. It occurs when you attempt to use methods from an object that is "undefined".
You can ensure that this.props.history is not undefined like this:
if (this.props.history && !completed)
this.props.history.replace("/complete-profile"); //error showing here
The npm package 'Lodash' may also be of use in avoiding these kinds of complications.
This just a cheap trick. If you want to go to any url (including react or any other front end framework), just do :
window.location.replace(str)
str can be absolute path like "https://abcd.com", or relative-path like ("/complete-profile" which will become baseurl + "/complete-profile")
if (logged) {
const completed = await hasCompleted();
if (!completed) window.location.replace("/complete-profile"); //error showing here
}
Also #zacks method also works

Categories

Resources