Render Child Component based on route param - javascript

I have a react router implemented as such:
ReactDOM.render((
<Router history={createBrowserHistory()}>
<Route path="/" component={App}>
<IndexRoute component={IndexPage} />
<Route name="listView" path="things" component={ThingsList}>
<Route name="expandedView" path=":_id" component={ThingExpanded} />
</Route>
</Route>
</Router>
), document.getElementById("body"));
I'm rendering the list, and registering the route with no problem. with below component.
let ThingsList = React.createClass({
render() {
return (
<div>
<ul>
{things.map((thing) => {
return (
<li key={things._id}>
<Link to={`/things/${thing._id}`}>
<span>{thing.firstName}</span>
</Link>
</li>
)
}
</ul>
</div>
)
}
})
But how can I show additional data, when one clicks the link and registers the route for expanded view of a particular item? I need to read the router info, and accordingly render the additional info. I mean it should not be based on onClick, based on the route. Something like this?:
componentDidMount () {
let rName = Router.getName;
if (rName == "expandedView") {
let rSlug = Router.name("expandedView").slug("_id");
this.setState({expand: true, which: rSlug});
}
}
render() {
let extra;
if (this.state.expand) {
extra = <ThingExpanded />
}
}
and then in the List component:
render() {
return (
...
<Link to={`/things/${thing._id}`}>
<span>{thing.name}</span>
{extra}
</Link>
...)
}
Note: This answer is very close to what I need, but didn't work. :/
Conditional handler based on slug in React Router?

Related

Rendering two different components with Reactjs

I've 2 different components, and I would like to render them separately. Logic is simple when user is logged in render 1st component, if it's not render default component.
Please review my code, and help me with this, I'm not good at react, so please help me
export default function App(props) {
const Authenticated = props.Authenticated
const RoutingCabinet = (
<React.Fragment>
<Header/>
<Switch>
<Route exact path = '/' component = { () => TempBody} />
<Route exact path = '/new' component = { () => <ResponsiveDrawer />}/>
</Switch>
<Footer/>
</React.Fragment>
);
const RoutingContent = (
<React.Fragment>
<Cabinet />
<Switch>
<Route exact path = '/user.test'component = { () => <div>Element</div> }/>
</Switch>
</React.Fragment>
);
return(
<ThemeProvider theme = {Theme}>
<BrowserRouter>
{
Authenticated
? RoutingCabinet
: RoutingContent
}
</BrowserRouter>
</ThemeProvider>
);
}
Currently I can see only RoutingCabinet
Here is the index.js
ReactDOM.render(<App Authenticated = {false} />, document.getElementById('__body__', '__root__'));
I may be wrong here, but is <Cabinet /> the same as <RoutingCabinet />? If so, it might work ok. The <div>Element</div> will be visible only if url matches user.test. If you are on any other page, this won't be rendered, so it'll look like <RoutingCabinet /> is rendered, while in fact it's <RoutingContent />, it just renders the same output.

How to detect matched route from a component outside of the <Route/> component that was matched using react-router?

I've got the following structure in my React app, using react-router-dom.
<Router>
<Header/>
<Main>
<AllRoutes> // THIS HANDLES THE SWITCH WITH ALL THE ROUTES
<Switch>
<Route exact path={ROUTES.HOME} component={Home}/>
<Route exact path={ROUTES.ABOUT} component={About}/>
<Route exact path={ROUTES.PRIVACY} component={Privacy}/>
// ETC
</Switch>
</AllRoutes>
</Main>
<Footer/> // <==== FOOTER NEEDS TO KNOW WHICH ROUTE HAS BEEN MATCH
<Router>
QUESTION
Footer needs to know what <Route/> has been match. What is the best pattern to achieve that?
OPTION #1
I found the useRouteMatch hook over on react router docs:
This would kind of work, but I don't think it is good enough for my situation. Because a URL string can match a route and still don't be a valid route at the same time.
For example:
Route: /:language/privacy
Valid route: /en/privacy
Not valid route that would also match: /notALanguage/privacy
Once a route has match, I usually need to check if it is valid before rendering a component page or the 404 page.
Like:
<Route exact path={"/:language/privacy"} render={(routeProps) => {
const possibleLanguage = routeProps.match.params.language;
if (possibleLanguage in LANGUAGES) {
return(
<PrivacyPage lang={possibleLanguage}/>
);
}
else {
return(
<Page404/>
);
}
}}/>
OPTION #2
What I'm thinking about doing is:
App.js calls useLocation. So it always re-render when there is a route change.
I could add a detectRoute function in App.js to do all the route checking beforehand.
And my AllRoutes component wouldn't need a component. I would implement a native JS switch and render the corresponding route.
This way I know upfront which <Route/> is going to match and I can pass it on to <Footer/> or any component that lives outside of the matched <Route/>.
Something like this:
SandBox Link
export default function App() {
console.log("Rendering App...");
const location = useLocation();
// THIS WOULD BE THE detectRoute FUNCTION
// I COULD EVEN USE THE useRouteMatch HOOK IN HERE
const matchedRoute =
location.pathname === ROUTE1
? "ROUTE1"
: location.pathname === ROUTE2
? "ROUTE2"
: "404";
return (
<div>
<div className="App">
<Link to={ROUTE1}>Route 1</Link>
<Link to={ROUTE2}>Route 2</Link>
<Link to={"/whatever"}>Route 404</Link>
</div>
<div>
<AllRoutes matchedRoute={matchedRoute} />
</div>
</div>
);
}
function AllRoutes(props) {
switch (props.matchedRoute) {
case "ROUTE1":
return <Route exact path={ROUTE1} component={Page1} />;
case "ROUTE2":
return <Route exact path={ROUTE2} component={Page2} />;
default:
return <Route exact path={"*"} component={Page404} />;
}
}
It works. But I would like to know if there's a proper way of doing this, 'cause this seems a bit weird and there might be something out there that was specifically designed for this.
Generally you want to either:
Wrap the components together
Create another switch to route them (and pass match params)
I put together a somewhat comprehensive example of the options. Hope that helps!
import React from "react";
import "./styles.css";
import { Switch, Link, Route, BrowserRouter } from "react-router-dom";
const hoc = (Component, value) => () => (
<>
<main>
<Component />
</main>
<Footer value={value} />
</>
);
const Wrapper = ({ component: Component, value }) => (
<>
<main>
<Component />
</main>
<Footer value={value} />
</>
);
const WrapperRoute = ({ component, value, ...other }) => (
<Route
{...other}
render={props => <Wrapper component={component} value={value} {...props} />}
/>
);
const Footer = ({ value }) => <footer>Footer! {value}</footer>;
const Header = () => <header>Header!</header>;
const Another = () => <Link to="/onemore">One More!</Link>;
const Home = () => <Link to="/other">Other!</Link>;
const OneMore = () => <Link to="/">Home!</Link>;
const Other = () => <Link to="/another">Another!</Link>;
export default () => (
<BrowserRouter>
<Header />
<Switch>
{/* You could inline it! */}
<Route
path="/another"
render={() => (
<>
<main>
<Another />
</main>
<Footer value="" />
</>
)}
/>
{/* You could use a custom route component (that uses an HOC or a wrapper) */}
<WrapperRoute
component={OneMore}
path="/onemore"
value="I got one more!"
/>
{/* You could use a Higher-Order Component! */}
<Route path="/other" component={hoc(Other, "I got other!")} />
{/* You could use a wrapper component! */}
<Route
path="/"
render={() => <Wrapper component={Home} value="I got home!" />}
/>
</Switch>
{/* You could have another switch for your footer (inline or within the component) */}
<Switch>
<Route
path="/another"
render={() => <Footer value="Switch footer another!" />}
/>
<Route
path="/other"
render={() => <Footer value="Switch footer other!" />}
/>
<Route
path="/onemore"
render={() => <Footer value="Switch footer onemore!" />}
/>
<Route path="/" render={() => <Footer value="Switch footer home!" />} />
</Switch>
</BrowserRouter>
);
Note the WrapperRoute would allow you to do validation on your match params before passing them through. You could do a Redirect if needed.
What I've ended up doing:
Since I'm using Redux, I added a piece of global state to keep track of the matched route.
And I dispatch actions to update that state from the render prop from the <Route/>'s component.
<Switch>
<Route key={index} exact path={"/some-route"} render={(routeProps) => {
// HERE I DISPATCH AN ACTION TO CHANGE THE STATE FOR THE CURRENT ROUTE
dispatch({
type: UPDATE_CURRENT_ROUTE,
payload: { name: "SOME_ROUTE_NAME" }
});
return (
<PrivacyPage
{...routeProps}
/>
);
}}/>
</Switch>
And now I can do on Footer.js:
function Footer() {
const currentRoute = useSelector((state) => state.currentRoute);
// RENDER FOOTER ACCORDINGLY TO THE CURRENT ROUTE
}

How to change NavBar text on login/logout in React.JS?

I have a Navigation bar in my project which I call from inside App.js. Based on if I am logged in or not, I want to render different views of NavBar. If logged in, I want the NavBar to have a logout button. And if logged out, I want the NavBar to have login button. I use a token in localStorage to check if I am logged in or not. When logged in, token is present in localStorage. On logout/before login, there is no token key in localStorage. I pass this token as a state to NavBar as shown:
export default function App() {
const [loggedIn, setLoggedIn] = useState(localStorage.getItem("token"));
return (
<div>
<Router>
<Navbar isAuth={loggedIn} />
<Route exact path="/" exact component={Home} />
<Route path="/login" component={Login} />
<PrivateRoute path="/dashboard" component={Dashboard} />
</Router>
</div>
);
}
Now from NavBar component, I use this prop to render different views of NavBar as shown below:
const NavBar = props => {
const classes = useStyles();
if (props.isAuth !== null) {
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" className={classes.title}>
<Link
href="/"
style={{ textDecoration: "none", color: "white" }}
>
Timetracker
</Link>
</Typography>
<Link href="/" style={{ color: "white" }}>
<Button color="inherit" onClick={auth.logout}>
Logout
</Button>
</Link>
</Toolbar>
</AppBar>
</div>
);
} else {
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" className={classes.title}>
<Link
href="/"
style={{ textDecoration: "none", color: "white" }}
>
Timetracker
</Link>
</Typography>
<Link href="/login" style={{ color: "white" }}>
<Button color="inherit">Login</Button>
</Link>
</Toolbar>
</AppBar>
</div>
);
}
};
export default NavBar;
The problem is that, the NavBar does not update itself as soon as I login. I have to manually refresh the page in order to render the new NavBar. Similarly on logout too, It does not update itself and updates only on manual refresh. What is the issue and how to solve this?
I found a simple solution:
use a componentDidMount() or useEffect() function which will render automatically upon loading the NavBar page.
Inside this function, use a setInterval() function to continually retrieve the auth status (say, an interval of 5000). This will continually refresh the NavBar, and change the button immediately.
I imagine you would have to put the auth check in the NavBar component itself, instead of using props. I put the specific buttons I wanted to change in a separate component called NavBarUser, which changes 'login | signup' to 'logout' and contains a user avatar. I then inserted this component into the NavBar itself at the appropriate place.
This is what my code looks like:
```
import React, { useState, useEffect } from 'react';
import Avatar from './Avatar';
import { BrowserRouter as Router, Link } from 'react-router-dom';
const NavBarUser = () => {
const [user, setUser] = useState({});
useEffect(() => {
{ /*
setInterval was used in order to refresh the page constantly
in order to have the "logout" button show immediately in place of
"login", as soon as user logs out.
*/}
setInterval(() => {
const userString = localStorage.getItem("user");
const user = JSON.parse(userString);
setUser(user);
}, [])
}, 5000);
const logout = () => {
return localStorage.removeItem("user");
}
if (!user) {
return (
<div className="navbar-nav ml-auto">
<Link to="/login" className="nav-item nav-link">Login</Link> <span
className="nav-item nav-link">|</span> <Link to="/SignUp" className="nav-item nav-
link">Sign Up</Link>
</div>
)
}
if (user) {
return (
<div className="navbar-nav ml-auto">
<Link to="/" className="nav-item nav-link" onClick={logout}>Logout</Link>
<Avatar img="/images/Eat-healthy.jpg" />
</div>
)
}
}
export default NavBarUser;
```
You need to add <Switch> as well. From the documentation:
Renders the first child or that matches the location.
<Switch> is unique in that it renders a route exclusively. In contrast, every <Route> that matches the location renders inclusively.
Just like the following:
<Router>
<Switch>
<Navbar isAuth={loggedIn} />
<Route exact path="/" exact component={Home} />
<Route path="/login" component={Login} />
<PrivateRoute path="/dashboard" component={Dashboard} />
</Switch>
</Router>
Read further here: Router
I hope that helps!
Your app's state won't update if you change the value of the token in localStorage.
You need to make sure you update the state, I've added a sandbox if it helps.
Here's how I solved this issue:
To start, I created a isLoggedIn state in my App class. I gave it a componentDidMount() method that would fetch the login state from a cookie on app start. Then I created globalLogin and globalLogout methods as arrow functions, which set the isLoggedIn state to true or false accordingly. I passed my Nav component the isLoggedIn state as a prop and passed the Login and Nav routes the globalLogin and globalLogout methods. These methods can then be called from Login or Nav with this.props.globalLogout(); or this.props.globalLogin();.
This is a simplified version of my App.js.
class App extends Component {
constructor(props){
super(props);
this.state = {
isLoggedIn: false,
}
}
componentDidMount() {
const token = Cookie.get("token") ? Cookie.get("token") : null;
if (token) {
this.setState({ "isLoggedIn": true });
}
}
globalLogin = () => {
this.setState({ "isLoggedIn": true });
}
globalLogout = () => {
this.setState({ "isLoggedIn": false });
}
render() {
return (
<Router>
<div className="App">
<Nav isLoggedIn={this.state.isLoggedIn} globalLogout={this.globalLogout}/>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/login" exact>
<Login globalLogin={this.globalLogin}/>
</Route>
</Switch>
</div>
</Router>
);
}
}
EDIT: using history.push didn't work in login module above so I added an intermediate to handle props
render() {
const LoginIntermediate = (props) => {
return (
<Login {...props} globalLogin={this.globalLogin}/>
)
}
return (
<Router>
<div className="App">
<Nav isLoggedIn={this.state.isLoggedIn} globalLogout={this.globalLogout}/>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/login" exact component={LoginIntermediate} />
</Switch>
</div>
</Router>
);
}

Organizing React routes into separate components

I'm trying to find a way to organize my routes to assist the dev who might be taking over my work in the future. I thought of separating my <Route /> entries into separate components and then just load those into a main component similar to how users are assigned groups.
The issue is that when using more than one component only the first one works. This might not be the most react way of doing this so I'm also open to alternatives.
Original route arrangement
const AllRoutes = () => {
return (
<Switch>
{/* public routes*/}
<Route path={'/about'} component={AboutView} />
<Route path={'/project'} component={ProjectView} />
<Route path={'/contact'} component={ContactView} />
{/* auth routes */}
<Route path={'/login'} component={LoginView} />
<Route path={'/logout'} component={LogoutView} />
<Route component={Error404View} />
</Switch>
)
}
Separating the public routes from the auth ones:
const PublicRouteGroup = () => {
return (
<>
<Route path={'/about'} component={AboutView} />
<Route path={'/project'} component={ProjectView} />
<Route path={'/contact'} component={ContactView} />
</>
)
}
const AuthRouteGroup = () => {
return (
<>
<Route path={'/login'} component={LoginView} />
<Route path={'/logout'} component={LogoutView} />
</>
)
}
This way I can use it as such:
const AllRoutes = () => {
return (
<Switch>
<PublicRouteGroup /> {/* This works */}
<AuthRouteGroup /> {/* This doesn't */}
{/* This 404 is not a route group */}
<Route component={Error404View} />
</Switch>
)
}
Flipping <PublicRouteGroup /> and <AuthRouteGroup /> only changes the order:
const AllRoutes = () => {
return (
<Switch>
<AuthRouteGroup /> {/* This works */}
<PublicRouteGroup /> {/* This doesn't */}
{/* This 404 is not a route group */}
<Route component={Error404View} />
</Switch>
)
}
Update #1
This is thanks to #skyboyer. By moving the <Switch> to the child components and removing it from the AllRoutes component each component started to show. It appears adding the <Switch> in AllRoutes is allowing only the first hit to show which is as <Switch> does. But now by removing it it shows the 404 at the end of each page as well.
Basically, it looks like this:
const AllRoutes = () => {
return (
<>
<Route component={AuthRouteGroup} /> {/* This works */}
<Route component={PublicRouteGroup} /> {/* This also works */}
{/* This 404 is not a route group */}
<Route component={Error404View} /> {/* Always shown at the bottom */}
{/* Even putting the 404 in its own RouteGroup yields the same issue */}
</>
)
}
It appears this current set up of treating components like OOP classes you can extend from is the wrong approach. I've instead made use of arrays since these can be acted upon by the spread operator. It still accomplishes the same goal of organizing routes across an infinite number of groups which was what I was after.
Create the array for each group
const public_route_group = [
{path: '/about', component: AboutView},
{path: '/project', component: ProjectView},
{path: '/contact', component: ContactView},
]
const auth_route_group = [
{path: '/login', component: LoginView},
{path: '/logout', component: LogoutView},
]
const error_route_group = [
{component: Error404View} // No path required
]
const user_routes = [
...public_route_group,
...auth_route_group,
...error_route_group
]
Create the routes
const AllRoutes = () => {
return (
<Switch>
{user_routes.map((route, idx) => {
return <Route key={idx} {...route} />
})}
</Switch>
)
}
I figure this can also be modified further if you're using nested objects in your array.
I'd like to thank #skyboyer for providing an insight into this problem.
How about having it without Swtich at top-level
<Route component={PublicRouteGroup} />
<Route component={AuthRouteGroup} />
so they are rendered unconditionally. And then having extra Switch in your components like
const AuthRouteGroup = () => {
return (
<Switch>
<Route path={'/login'} component={LoginView} />
<Route path={'/logout'} component={LogoutView} />
<Switch/>
)
}
But why id did not work?
The reason is how Switch works:
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
const path = child.props.path || child.props.from;
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});
See, even if AuthRouteGroup is not a Route, Switch anyway looks to its props.path. And once undefined for props.path matches any path and Switch renders only first matching Route you are getting only first component rendered.
[UPD] "does-not-match-any-route" View will work only at top level of Switch. Also there are no way to know if some nested children of sibling element has matched current route or not. So only way I see is listing all routes in single place.
Alternative that looks rather poor is having special route "/error404" and redirect user to it from inside of other components(but who should decide? and where? and when?).

Route Redirect not Happening when Reducers update State

I'm receiving a User model using reducers. The first value is null (because this is how Reducers behave -- it means it's pending and hasn't received the model yet), and the second is the User model itself.
When I get the first null value, the component redirects to the "IsNotSetUp" path, but then when I get the second value, the component shows the isUserSetUp prop as TRUE but it doesn't Redirect to the "IsSetUp" path? What is going on?
class App extends Component {
state = { isUserSetUp: false }
componentWillReceiveProps(nextProps) {
if (nextProps.auth && nextProps.auth.isProfileSetUp) {
this.setState({ isUserSetUp: true }, function() {
console.log("This is when we receive the User model from the reducer");
});
}
else {
this.setState({ isUserSetUp: false });
}
}
componentDidMount() {
this.props.fetchUser();
}
render() {
return (
<div className="container">
<BrowserRouter>
<div>
<Header />
<Route exact path="/" component={Landing} />
<Route exact path="/dashboard" component={Dashboard} />
{/* Profile Set up */}
<Route exact path="/setup" render={() =>
(
this.state.isUserSetUp ? (
<Redirect to={'/IsSetUp'} />
) : (
<Redirect to={'/IsNotSetUp'} />
)
)}
/>
</div>
</BrowserRouter>
</div>
);
}
};
What's going wrong? What's a workaround? What's a better, suggested path I take?
Thanks in advance, beautifuls ;)
You can use hashHistory to implement this.
import {
hashHistory
} from 'react-router';
/**
* [update route of the application]
* #param {String} route [new route of the application]
*/
const updateRoute = (route) => {
hashHistory.push(route);
};
updateRoute('/IsSetUp');
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react-dom.min.js"></script>
Let me know if it helps.

Categories

Resources