React Router with dynamic path and dynamic component - reactjs

I'm building the app with hierarchical structure and ability to set any slug url the user wants by the admin panel. For example, if the top page has url news and the page inside has url new_telescope, the whole url would be /news/new_telescope/. I wrote the url controller to handle this, so it really doesn't matter how many levels are there in the url - I get the data of the needed page from the server. So my React Router wouldn't have predefined urls - I need to take the current url, send it to server, get the data, get the component and render it. Now it looks like this:
function getRoute() {
url.splice(0, 1);
adminRequest(
'get',
constants.SERVER_ADDRESS + `/system/url/?url=` + JSON.stringify(url) + '&version=1',
).then(response => {
const CurrentComponent = lazy(() => import('./' + response.data.template));
return <Route path={window.location.pathname} element={<CurrentComponent page={response.data}/> } />
});
}
return (
<>
<Suspense fallback={<div className='preloader'><CircularProgress size='75px' /></div>}>
<Routes>
// trying to get the current url with proper component
{ getRoute() }
<Route path="/admin/" element={<Admin />} />
<Route path="/admin/login/" element={<Login />} />
...
</Routes>
</Suspense>
</>
);
So the problem is with the getRoute function. As you can see I make a request, get the component name from the DB and use lazy to import it. In that function I make a new Route to put it to Routes. But I get errors No routes matched location with any url. And of course the component doesn't render. I thought that Suspense will wait for the first route to appear but it doesn't work that way. How can I make a Router to wait for a dynamic route?

Issue
The getRoute function doesn't return anything. Sure, the Promise chain started from adminRequest returns some JSX, but that resolved value isn't returned by the other function.
React render functions are 100% synchronous functions. You can't call an asynchronous function and expect React to wait for any asynchronous code to resolve to render any content.
Solution
Use a useEffect hook to issue the side-effect of fetching the route. Unconditionally render the Route but conditionally render the element value.
const [data, setData] = useState();
const [CurrentComponent, setCurrentComponent] = useState();
useEffect(() => {
const getRoute = async () => {
url.splice(0, 1);
const response = await adminRequest(
"get",
constants.SERVER_ADDRESS +
`/system/url/?url=` +
JSON.stringify(url) +
"&version=1"
);
const CurrentComponent = lazy(() =>
import("./" + response.data.template)
);
setData(response.data);
setCurrentComponent(CurrentComponent);
};
getRoute();
}, [url]);
...
<Suspense fallback={<div className="preloader">Loading...</div>}>
<Routes>
<Route
path={/* whatever the path is */}
element={CurrentComponent ? <CurrentComponent page={data} /> : null}
/>
<Route path="/admin/" element={<Admin />} />
<Route path="/admin/login/" element={<Login />} />
</Routes>
</Suspense>
Demo

Related

React router route loader not working on nested components

Am using react router v6 and i would like to use the new loader to load the data before the component loads. So i have the following
In my index.js
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="*"
loader={async ({ params }) => {
console.log("index loader log"); //this logs
return true;
}}
element={<App />}
> </Route>
)
);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<RouterProvider router={router} />
);
in my app component i have nested routes like
const App = () => {
return (
<>
<Routes>
<Route path="auth/*" element={<AuthLayout/>}/>
<Route path="about"
loader={async ({ params }) => {
console.log("about child loader log"); //this doesnt log
return true;
}}
element={<AboutPage/>}/>
</Routes>
<h1>Testing app</h1>
</>
);
}
On the app component the loader on the Route path="about" does not console.log when i visit the about route but the component is rendered. What am i missing for the loader to work on the child route.
Based on some basic testing it seems that in order for the new RRDv6.4 data APIs to work you need to specify the complete routing configuration in the createBrowserRouter function.
There does however appear to already be an issue filed with #remix-run/react-router for this behavior as a reported bug, so you may want to follow it if it ever addressed/resolved. (I suspect it was you since the name is "geoffrey" and the timing is coincidentally about an hour ago around the same time as this post)
This above issue has since been closed with comment:
Descendant <Routes> trees do not participate in data loading
(https://reactrouter.com/en/main/components/routes) since they cannot
be known ahead of render-time. You'll need to lift your descendant
route definitions up into the routes you pass to createBrowserRouter.
The relevant information regarding the descendent routes and the new Data API can be found in the Routes documentation in a note.
Note:
If you're using a data router like createBrowserRouter it is
uncommon to use this component as it does not participate in data
loading.
Hoist the entire route declaration to the parent creating the data router. The following does work with the loader function for the "/about" route and About component.
const router = createBrowserRouter(
createRoutesFromElements(
<Route
path="*"
loader={({ params }) => {
console.log("index loader log");
return "This is the App";
}}
element={<App />}
>
<Route path="auth/*" element={<AuthLayout />} />
<Route
path="about"
loader={({ params }) => {
console.log("about child loader log");
return "this is the about page";
}}
element={<AboutPage />}
/>
</Route>
)
);
The App component should render an Outlet for the nested routes to render their content into.
import { Outlet } from 'react-router-dom';
const App = () => {
return (
<>
<h1>Testing app</h1>
<Outlet />
</>
);
};

I get 'undefined' when i try to fetch data from an API but when i access it again it works. why?

I'm developing a Tv shows website using https://api.themoviedb.org API.
Here are the routes I set up :
<>
<Router>
<Navbar/>
<Routes>
<Route path='/' element={<Home/>}/>
<Route path='/signup' element={<Signup/>}/>
<Route path='/movieOverview/:id' element={<MovieOverview/>}/>
<Route path='*' element={<Error/>}/>
</Routes>
</Router>
</>
In one of them i set a dynamic path prop on the Route component that renders the movieOverview component - <Route path="/movieOverview/:id" element={}
So I can use the useParams hook to get the ID when someone clicks on any of the shows to see more details navigate to to={/movieOverview/${props.id}}
Which is here:
export default function MovieOverview(){
const [moviedetails, setMovieDetails] = React.useState()
const {id} = useParams()
React.useEffect(() => {
getData()
window.scrollTo(0,0)
}, [])
const getData = () => {
fetch(`https://api.themoviedb.org/3/tv/${id}?api_key=172f725b29bb276f5c4b6e294a988fc5`)
.then(res => res.json())
.then(data => setMovieDetails(data))
}
return(
<div className='overview-1'>
<p className='detail'>{moviedetails.name}</p>
</div>
)
}
The issue is the getData function returns 'undefined', but when I delete {moviedetails.name} and try it again, it delivers the data. i want it to deliver the data immediately.
TL;DR
change your code as below:
<p className="detail">{moviedetails?.name}</p>
Why my code is not working ?
Initially moviedetails is undefined , so when you are trying to access name property , it is throwing error.
And once the API response is received the value is getting set , so you are able to read the name property.
Another way of solving it would to be initialize your state with empty object as below :
const [moviedetails, setMovieDetails] = React.useState({})
Here is a working example : Demo

React router does not reload component if same element is used for two paths

I have the following two paths set up
<Route path="bookings/:bookingnumber" element={<Bookings />} />
<Route path="bookings" element={<Bookings />} />
In Bookings component I have conditional code written to check if a parameter :bookingnumber is passed, and then it only shows results relevant to that booking number.
Otherwise, if no parameter is passed, it shows all the results for all the bookings.
I am using Outlet to display these components in the main area, and the two paths can be chosen from a sidebar.
Let's say I have 10 results if I go to /bookings, and 2 results if I go to /bookings/123.
However, if I go to /bookings/123 first and then to /bookings it keeps on showing only 2 results (the component does not reload, however I can see the URL changing in the browser)
Issue
<Route path="bookings" element={<Bookings />} />
<Route path="bookings/:bookingnumber" element={<Bookings />} />
With the same component rendered on more than 1 route, if you navigate from one route to another for the same routes component, the routed component remains mounted.
Example: If navigating from "/bookings" to "/bookings/123", or "/bookings/123" to "/bookings", or "/bookings/123" to "/bookings/456" the Bookings component remains mounted. If you've any logic that depends on the bookingnumber route path parameter then the Bookings component needs to handle this in a useEffect hook with a proper dependency on the bookingnumber param.
Solution
Use a useEffect hook to "listen" for changes to the bookingnumber param value.
Example:
const Bookings = () => {
const { bookingnumber } = useParams();
useEffect(() => {
// initial render or booking number value updated
// run logic depending on bookingnumber value to update "results"
...
}, [bookingnumber]);
...
};
I've been struggling too with this issue, the solution is to add the key prop, which is used by react to understand when 2 components are different (otherwise it assumes they're the same and they will not be re-rendered).
Hence the solution in your case may look like:
<Route key={'single'} path="bookings/:bookingnumber" element={<Bookings />} />
<Route key={'all'} path="bookings" element={<Bookings />} />
<Route path="bookings" element={<BookingList />}>
<Route path=":bookingnumber" element={<ViewBooking />} />
</Route>
make two pages, first one will show all list of bookings and second one will show ID data
Hello I believe you are using react-router-dom v6, in this case you can wrap the child routes inside your main path route. And then give them the index(true|false) attribute to make sure the router understand if it is the exact index page or should render a page based on params.
Note: I copied the example from above, and added some lines to fix it!
<Route path="bookings">
<Route index={false} path=":bookingnumber" element={<ViewBooking />} />
<Route index={true} element={<BookingList />} />
</Route>
UPDATE:
You can handle a single component route with useParams hook.
Here what you're routing file should look like:
<Route path="bookings">
<Route index={false} path=":bookingnumber" element={<ViewBooking />} />
<Route index={true} element={<BookingList />} />
</Route>
And this is what your component should look like:
const MyComponent = () => {
const { bookingnumber } = useParams()
// if the path doesn't include the booking number then we should just render a normal page
if( !bookingnumber ) {
return <h1>This is a simple page</h1>
}
// if the page path does include the booking number then we can fetch data and return another result
// example you can fetch data based on this number
const [username, setUsername] = useState("")
useEffect(() => {
const fetchData = async () => {
const req = await fetch(`https://629c770de9358232f75b55dc.mockapi.io/api/v1/users/${bookingnumber}`)
const data = await req.json()
setUsername(data.name)
}
fetchData()
}, [username])
return (
<h1>Welcome mr.{username}</h1>
)
}

You should call navigate() in a React.useEffect(), not when your component is first rendered. (React+ReactROuterDom v6)

So this might be hard to get at first but I'll try to explain everything possible. I'm rendering an App component which uses useNavigation hook from react-router-dom library. Inside AppRoutes I check, if I have $stateParams.redirect and also other values like param1 and param2.I get $stateParams from another custom hook defined in my app. While running the app, I get the log, should navigate now but it actually doesn't navigate to decider-route page instead it stays at / which is <Home /> component. Also I have this warning in console You should call navigate() in a React.useEffect(), not when your component is first rendered. I was wondering why doesn't the navigation takes place to decider-route and the warning is the reason why navigation does not take place?
const App = () => {
return (
<MemoryRouter>
<AppRoutes />
</MemoryRouter>
)
}
const AppRoutes = () => {
const navigate = useNavigate() // react-router-dom v6
if ($stateParams.redirect) {
if ($stateParams.param1 && $stateParams.param2) {
console.log('StateParams : ', $stateParams)
console.log('Should navigate now!')
navigate(`/decider-route/${$stateParams.param1}/${$stateParams.param2}`)
}
}
return (
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/decider-route/:param1/:param2"
element={<Component />}
/>
</Routes>
)
}
The error is preety much self-explanatory. You just need to wrap the navigate() in a useEffect() hook so that it gets executed after the component mounts.
But, in this case, it is being called as soon as the component is first rendered.
navigate() should be triggered by a user action or an useEffect hook in this case. But you're not playing by the rules :)
app.js
const App = () => {
return (
<MemoryRouter>
<AppRoutes />
</MemoryRouter>
);
};
const AppRoutes = () => {
const navigate = useNavigate(); // react-router-dom v6
useEffect(() => {
if ($stateParams.redirect) {
if ($stateParams.param1 && $stateParams.param2) {
console.log("StateParams : ", $stateParams);
console.log("Should navigate now!");
navigate(
`/decider-route/${$stateParams.param1}/${$stateParams.param2}`
);
}
}
}, []);
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/decider-route/:param1/:param2 " element={<Component />} />
</Routes>
);
};

How do I achieve conditional routing based on the parameter in the requested route?

I am developing a React js blog-like web site and I am finding some trouble managing the routes. My pages with articles' URLs are like this: website/pages/3
I would like to redirect to homepage when the page index is 1, since the homepage is the first page with articles by default.
My Router looks like this:
<Router>
<Switch>
<Route exact path="/" render={() => <Page postsPerPage={3}/>} />
<Route exact path="/Page/:activePage" render={() => <Page postsPerPage={3}/>} />
<Route path="/Login" component={Login} />
<PrivateRoute path="/AddPost" component={AddPost} />
<Route path="/:postLocation" component={Post} />
</Switch>
</Router>
I would like to route "/Page/:activePage" to the component the route "/" renders if the activePage is 1. So the component would be the same (Page), but the path would be different.
Could conditional rendering in the Router do the trick, and if so, how? I was thinking about something along these lines:
<Route exact path="/Page/:activePage" render={() =>
{
let {activePage} = useParams()
if (activePage == 1) return (<Redirect to="/"/>)
else return(<Page postsPerPage={3}/>)
}
}/>
However it seems React is not happy about me using useParams there (there's a compilation error: React Hook "useParams" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks)
I tested that snippet with a constant value of 1 instead of the activePage parameter and it does redirect so that basically leaves me with the question of how do I retrieve the parameter from the path?
Render function in this case The render prop function has access to all the same route props (match, location and history) as the component render prop.
so you can basically do something like.
<Route exact path="/Page/:activePage" render={(props) =>
{
if (props.match.params.activePage == 1) return (<Redirect to="/"/>)
else return(<Page postsPerPage={3}/>)
}
}
/>
Looking at your example case above, I will rather not redirect anything but carry the logic into the Page component. inside the componentDidMount or useEffect function that extracts the activePage param. I will check if it's 1(or whatever sentinal value you choose to use) Then I perform the logic that will have been performed by the home component else I proceed normally with the route. eg If you extracted it and do a fetch to the backend for the case where its, not 1, then when it's 1 you could just return from the function and it will work as if it were on the home page. alternatively, after the check, if it's 1 you could then redirect back to '/'.
You should probably handle the routing within your Pages component or if you prefer, create a separate component to handle the conditional routing.
for example:
function Pages (props) {
const {activePage} = useParams()
return activePage === 1 ? <Redirect to='/' /> : (
<div>
Your Pages component content here
</div>
)
}
export default Pages;
or
function ConditionalRoutingComponent(props) {
const {activePage} = useParams()
return activePage === 1 ? <Redirect to='/' /> : <Page postsPerPage={3}/>
}
export default ConditionalRoutingComponent;
Hope this helps
The render function of Component the is called with three parameters namely match, history and location. You can use them to perform the action you are trying to do with hooks.
<Route ... render={({match}) => {
if (match.params.activePage == 1) {
doYourStuff()
}
}}

Resources