TLDR: I'm trying to figure out how to arrange nested routes and my components so that a component can be swapped out based on route.
In SUPER simple terms, I'm trying to build an application where teachers can see the classes they teach at colleges.
The routes of the application are:
/dashboard: call backend to check if the teacher has a default college set on their account, if not then present a college picker dialog where the teacher can select a default college. Once there's a default college (say COLLEGE1), re-route to next route (next bullet point)
/dashboard/college/COLLEGE1: fetch metadata of classes taught in college.
/dashboard/college/COLLEGE1/class/CLASS1: show metadata of a single class within COLLEGE1. This route is accessed by clicking a class in bullet 2.
Here are rough mocks of what this interaction would look like when static (I've colored each component so it's easier for you to refer to them when responding):
However, I am just not able to figure out the nested routes + component hierarchy structure that would get me this.
The hierarchy I have so far is:
<Home>
<Header/>
<!-- content in header -->
</Header>
<MainContent>
<!-- Either loading, for e.g. to fetch permissions -->
<Loading />
<!-- OR display navigation + content -->
<MainContentPage>
<!-- show navigation pane on left, and then choose from report / T and Cs / Contact -->
<Navigation>
<CurrentCollegeInfo />
<DashboardLink />
<TermsAndConditionsLink />
<ContactUsLink />
</Navigation>
<ReportPage>
<!-- Either Loading -->
<Loading />
<!-- OR have user select default college -->
<DefaultCollegePickerPopup />
<!-- OR show college report or class details -->
<CollegeReportPage />
<ClassDetailsPage />
<!-- OR error pages: Not Found, 500, etc. -->
<ErrorPage />
</ReportPage>
<TermsAndConditionsPage />
<ContactUsPage />
</MainContentPage>
</MainContent>
</Home>
How do I insert route management here (I'm using react-router library at the moment btw) so that in the ReportPage component:
either the route is /dashboard (when loading default college from backend or asking user to pick one)
or it is /dashboard/college/COLLEGE1 and fetch college report
or it is /dashboard/college/COLLEGE1/class/CLASS1 and fetch class details?
Or is this not possible and I should rather figure out another flow?
So if I understand correctly, you want to use the react-router to load different components based on which endpoint the user is on? This is 100% possible. You just pass the component you want to show for a specific route as a component property.
You can also use parameters in the paths, so in your example, you have /dashboard/COLLEGE1... I'm assuming you need that to be dynamic to allow for any college. This is done with placing parameters into the path like so... /dashboard/:somevariablename.
<Route
// exact
path={"/dashboard"}
// path={"/dashboard/:collegeId"}
// path={"/dashboard/:collegeId/classes/:classId"}
component={ComponentToPass}
/>
If you make a Route for every possible component/page that the user can visit, and wrap it in a <Switch> component, it will show only one component. You can however skip the <Switch> and add multiple routes to an endpoint as well.
I'm assuming you'll need to use the collegeId and classId in the corresponding components. If you are using functional react, use const { VARNAME } = useParams() to retrieve the parameters you are using. If you are using class-based react, all you need to do is call this.props.match.VARNAME. -- Both are obviously used inside the component that you want to show/use.
So to change your code up a little bit (could be done in a dedicated routes component), heres a light example..
import {HashRouter, Switch, Route} from "react-router-dom"
import DefaultCollegePickerPopup from './wherever'
import CollegeReportPage from './wherever'
import ClassDetailsPage from './wherever'
function RouterComponent(props) {
return (
<HashRouter>
<Switch>
<Route
exact
path={"/dashboard"}
component={DefaultCollegePickerPopup}
/>
<Route
exact
path={"/dashboard/:collegeId"}
component={CollegeReportPage}
/>
<Route
exact
path={"/dashboard/:collegeId/class/:classId"}
component={ClassDetailsPage}
/>
</Switch>
</HashRouter>
)
}
function CollegeReportPage(props) {
const { collegeId } = useParams();
return (
<div>College report for {collegeId}</div>
)
}
class CollegeReportPage extends React.Component {
render() {
const { collegeId } = this.props.match
return (
<div>College report for {collegeId}</div>
)
}
}
If you haven't already looked at this, I would. It gives a LOT of useful information.
https://reactrouter.com/web/guides/quick-start
Related
I have similar problem as in Refresh the page only once in react class component.
There are several pages in my application and I move between them using BrowserRouter and useNavigate (react-router-dom v6). One of pages has greater size div and when I go back to main page, it's(main's) css gets messed up(button position changes, some media file grows out of divs, hovers are not displayed) until I refresh page(main page). As soon as I refresh page, everything sets up well.
I used code snippet provided by #rommyarb in the link above. It works, but there is time delay (less 1sec, still visible). Which means when we navigate back(navigate(-1)), it first renders mainpage with broken css --> (0.2-0.5s) then it refreshes and css is recovered.
Time delay is not big, but still it would be unpleasant user experience. Is there any way to first refresh page (localhost/main) then render component with proper css.
Any help would be appreciated!
Code:
function App() {
return (
<Router>
<Routes>
<Route exact path='/' element={<MainPage props ={props}/>} />
<Route path='/UnderConstruction' element={<UnderConstruction/>}/>
</Routes>
</Router>
)
}
function UnderConstruction(props) {
let navigate = useNavigate();
return (
<div className='UnderConstruction' style={somestyles}>
<h2>This page is under construction</h2>
<div style={somestyles}>
<img src={under_construction.jpg'} width="100%" height="60%" />
<Button style={somestyles} onClick={() => {
navigate(-1)
}}> Go Back</Button>
</div>
</div>
);
I solved problem. The makeStyles return function(hook), when we call it within component, we can access only styles naming, which is string. So, every time we call a function, naming changes (makeStyles-element-number) due to global counter. I saved styles in state, which saved only string name. After re-rendering actual styles name was changed(makeStyles-element-number incremented) and the one I saved mismatched with actual styles. The simple solution was not storing makeStyles styles in state.
For more details read: Internal implementation of "makeStyles" in React Material-UI?
I am seeking to recreate a pattern with React Router. It is best described by the Twitter example: as you hit the Tweet button, the browser navigates to /compose/tweet, mounting the composer component. However, and that's the key, the previous route (/home, /explore, /notifications, /messages) is kept mounted despite the route change. How do you do that?
This could be called bidimensional routing: the /compose/tweet route is kept orthogonal with respect to the other routes that render the main view. The other routes are hidden (i.e., not in the address bar any longer) upon navigating to /compose/tweet, thereby rendering two independent routes (say, /notifications and /compose/tweet) at once.
My actual example: I need to show a user settings menu (/user/menu) as a large sidebar, but I do not want that to change whether the user was navigating / (the homepage), /faq, /contact, etc. Based on my current understanding of React Router, as soon as you hit /user/menu, any other route (take /faq as an example) would be unmounted based on route match.
Caching the previous route (e.g., Redux, which I'm using extensively already) does not seem feasible, since, even though I would be able to redirect the user to the previous route upon exiting /user/menu, React would still be unmounting components, in fact showing the homepage in the background until the user exits /user/menu & gets redirected to where they were at, which is not the intended behaviour. I would want the rest of the page to stay there with the rendered components, just the way Twitter does.
Am I overlooking anything? Is this an easy pattern and I am missing something?
Note: it's a SSR isomorphic app, but I guess/hope that won't change things.
Despite Adam Abramov's suggestion to keep React Router as the source of truth for whatever can be passed as route, and avoid deep integrations between Router and Redux, I found myself having to use Redux as the main source of truth in this (important) use case. I still wanted to have Route components for SSR and SEO purposes.
So, I created my own MultidimensionalSwitch and MultidimensionalRoute components to solve this use case. If a MultidimensionalSwitch is mounted, it will render the components at their subroutes, but if none of them is matched, it will render them based on another dimension, which is provided by Redux at an additional alt property of the corresponding MultidimensionalRoute.
Here below is some code, feel free to answer/ask should you need more info about it.
Main
class _Main extends Component {
render() {
const {menuOpen,selected} = this.props;
return (
<Fragment>
<Route exact path={exactRoutes.ROOT} component={() => <Redirect to={selected?exactRoutes[selected]:exactRoutes.HOME} />} />
<Header />
<Route path={nestedRoutes.AUTH+routeParams.LOGIN_SIGNUP.key} component={ScreenAuth} />
<Route path={exactRoutes.USER_MENU} component={ScreenUser} />
{!menuOpen?"":<ScreenMenu />}
<ScreenMain>
<MultidimensionalSwitch>
<MultidimensionalRoute path={exactRoutes.HOME} alt={selected===guestMenuOption.HOME} component={GuestHome} />
<MultidimensionalRoute path={exactRoutes.USER} alt={selected===guestMenuOption.USER} component={User} />
<MultidimensionalRoute path={exactRoutes.VISION} alt={selected===guestMenuOption.VISION} component={GuestVision} />
<MultidimensionalRoute path={exactRoutes.FAQ} alt={selected===guestMenuOption.FAQ} component={GuestFaq} />
<MultidimensionalRoute path={exactRoutes.INFOGRAPHICS} alt={selected===guestMenuOption.INFOGRAPHICS} component={GuestInfographics} />
<MultidimensionalRoute path={exactRoutes.BLOG} alt={selected===guestMenuOption.BLOG} component={GuestBlog} />
<MultidimensionalRoute path={exactRoutes.CONTACT} alt={selected===guestMenuOption.CONTACT} component={GuestContact} />
</MultidimensionalSwitch>
<Footer />
</ScreenMain>
<Flare />
</Fragment>
);
};
}
const mapStateToProps = state => ({
menuOpen: state.client.guest.menuOpen,
selected: state.client.guest.guestMenuOption,
});
const Main = withRouter(connect(mapStateToProps,{})(_Main));
export default Main;
MultidimensionalSwitch
class _MultidimensionalSwitch extends Component {
render() {
return (
<Fragment>
<Switch>
{this.props.children}
{this.props.children.map(child => !child.props.alt?"":<Route path={nestedRoutes.ROOT} component={child.props.component} />)}
</Switch>
</Fragment>
);
}
}
const mapStateToProps = state => ({
selected: state.client.guest.guestMenuOption,
});
const MultidimensionalSwitch = withRouter(connect(mapStateToProps,{})(_MultidimensionalSwitch));
export default MultidimensionalSwitch;
MultidimensionalRoute
class _MultidimensionalRoute extends Component {
render() {
const {path,component} = this.props;
return (
<Fragment>
<Route exact path={path} component={component} />
</Fragment>
);
}
}
const mapStateToProps = (state,ownProps) => ({
path: ownProps.path,
component: ownProps.component,
});
const MultidimensionalRoute = withRouter(connect(mapStateToProps,{})(_MultidimensionalRoute));
export default MultidimensionalRoute;
I'm using react hooks with context API for sharing data between multiple components and using reach router for routing. My code looks something like this:
function App() {
return (
<div className="App">
<ContextProvider>
<Router>
<Comp1 path='/comp1' >
<Comp2 path="/"/>
</Comp1>
<Comp3 path="/comp3" />
</Router>
</ContextProvider>
</div>
);
}
In this example, whatever data exposed by ContextProvider will be available to all the components. My question is, how can I expose the context only to Comp3 but not to Comp1 and Comp2?
Note: I'm using reach router.
#Tien Duong, we had the same thought as yours. But it turns out the reach router internally works by mapping the component to the location configured to render them, it does not even take div
Funny how client side routing it taking shape, also they say they would be passing some route props to the Component mapped with the location.
It's like opening your house for anyone to enter. Where is the isolation in this case.
I am using react routing for navigation among pages. This navigation having child link,
Parent Link 1
Parent Link 2
a. parent1/child link 1
b. parent1/child link 2
a. parent2/child link 1
b. parent2/child link 2
c. parent2/child link 3
How to implement this navigation in react-router.
Please see the attached image with this post to get clear understanding on my query.
class App extends Component {
render() {
return (
<Router>
<div style={{width: 1000, margin: '0 auto'}}>
<ul>
<li><Link to='/'>Home</Link></li>
<li><Link to='/topics'>Topics</Link></li>
</ul>
<hr />
<Route exact path='/' component={Home} />
<Route path='/topics' component={Topics} />
</div>
</Router>
)
}
}
At this point, ” takes in a path and a component. When your app’s current location matches the path, the component will be rendered. When it doesn’t, Route will render null.”
function Topics () {
return (
<div>
<h1>Topics</h1>
<ul>
{topics.map(({ name, id }) => (
<li key={id}>
<Link to={`/topics/${id}`}>{name}</Link>
</li>
))}
</ul>
<hr />
<Route path={`/topics/:topicId`} component={Topic}/>
</div>
)
}
when we go to /topics, the Topic component is rendered. Topics then renders a navbar and a new Route which will match for any of the Links in the navbar we just rendered (since the Links are linking to /topics/${id} and the Route is matching for /topics/:topicId). This means that if we click on any of the Links in the Topics component.
It’s important to note that just because we matched another Route component, that doesn’t mean the previous Routes that matched aren’t still rendered. This is what confuses a lot of people. Remember, think of Route as rendering another component or null. The same way you think of nesting normal components in React can apply directly to nesting Routes.
At this point we’re progressing along nicely. What if, for some reason, another member of your team who wasn’t familiar with React Router decided to change /topics to /concepts? They’d probably head over to the main App component and change the Route
// <Route path='/topics' component={Topics} />
<Route path='/concepts' component={Topics} />
The problem is, this totally breaks the app. Inside of the Topics component we’re assuming that the path begins with /topics but now it’s been changed to /concepts. What we need is a way for the Topics component to receive whatever the initial path as a prop. That way, regardless of if someone changes the parent Route, it’ll always just work. Good news for us is React Router does exactly this. Each time a component is rendered with React Router, that component is passed three props - location, match, and history. The one we care about is match. match is going to contain information about how the Route was matches (exactly what we need). Specifically, it has two properties we need, path and url. These are very similar, this is how the docs describe them -
path - The path pattern used to match. Useful for building nested Routes
url - The matched portion of the URL. Useful for building nested Links
Assume we were using an app that had nested route’s and the current URL was /topics/react-router/url-parameters.
If we were to log match.path and match.url in the most nested component, here’s what we would get.
render() {
const { match } = this.props // coming from React Router.
console.log(match.path) // /topics/:topicId/:subId
console.log(match.url) // /topics/react-router/url-parameters
return ...
}
Notice that path is including the URL parameters and url is just the full URL. This is why one is used for Links and the other used for Routes.
When you’re creating a nested link, you don’t want to use URL paramters. You want the user to literally go to /topics/react-router/url-parameters. That’s why match.url is better for nested Links. However, when you’re matching certain patters with Route, you want to include the URL parameters - that’s why match.path is used for nested Routes.
I have a container component with a modal in it that is opened and closed based on a state property.
I want to control this via the URL, i.e. I want to have
/projects - the modal is NOT open
/projects/add - the modal IS open
As well as being able to link directly to it, I want the URL to change when I click on links within the main container to open the modal.
Can someone explain how I could do this, or point me in the right direction of a good tutorial?
NOTE: This way is not perfect. Even more it's rather antipattern than pattern. The reason I publish it here is it works fine for me and I like the way I can add modals (I can add modal to any page and in general their components don't depends on the other app in any way. And I keep nice url's instead of ugly ?modal=login-form). But be ready to get problems before you find everything working. Think twice!
Let's consider you want following url's:
/users to show <Users /> component
/users/login to show <Users /> component and <Login /> modal over it
You want Login to not depend on Users in anyway, say adding login modal to other pages without pain.
Let's consider you have kinda root component which stands on top of other components. For example Users render may look something like this:
render() {
return (
<Layout>
<UsersList />
</Layout>
);
}
And Layout's render may look something like this:
render() {
return (
<div className="page-wrapper">
<Header />
<Content>
{this.props.children}
</Content>
<Footer />
</div>
);
}
The trick is to force modal's injection to <Layout /> every time we need it.
The most simple approach is to use flux for it. I'm using redux and have ui reducer for such page meta-information, you can create ui store if you use other flux implementation. Anyway, final goal is to render modal if <Layout />'s state (or even better props) contains modal equal to some string representing modal name. Something like:
render() {
return (
<div className="page-wrapper">
<Header />
<Content>
{this.props.children}
</Content>
{this.props.modal ?
<Modal key={this.props.modal} /> :
null
}
<Footer />
</div>
);
}
<Modal /> returns modal component depends on given key (In case of our login-form key we want to receive <Login /> component).
Okay, let's go to router. Consider following code snippet.
const modal = (key) => {
return class extends React.Component {
static displayName = "ModalWrapper";
componentWillMount() {
// this is redux code that adds modal to ui store. Replace it with your's flux
store.dispatch(uiActions.setModal(key));
}
componentWillUnmount() {
store.dispatch(uiActions.unsetModal());
}
render() {
return (
<div className="Next">{this.props.children}</div>
);
}
}
};
...
<Route path="users" component={Next}>
<IndexRoute component={Users}>
<Route path="login" component={modal('login-form')}>
<IndexRoute component={Users} />
</Route>
</Route>
(Don't care about Next - I add it here for simplicity. Imagine it just renders this.props.children)
modal() function returns react component that triggers change in ui store. So as soon as router gets /users/login it adds login-form to ui store, <Layout /> get it as prop (or state) and renders <Modal /> which renders corresponding for given key modal.
To programmatically assess to a new URL, pass the router to your component and use push. push for example will be call in the callback trigger by the user action.
When setting your router set a route to /projects/:status. then, in your component route, you can read the value of status using this.props.param.status. Read "whats-it-look-lik" from react-router for an example.