I am trying to find a working example of low level animations using the React Router v4 with React Transition Group v2. I have looked at the example on the React Router docs but they only use CSS Animation with one route.
This is how I currently use the React Router:
export const App = () => (
<div className="app-container">
<main className="app-container__content">
<Switch>
<Route exact path="/projects/:slug" component={ProjectPage} />
<Route exact path="/" component={StartPage} />
</Switch>
</main>
</div>
);
And this is my Root.jsx:
const Root = ({ store, history }) => (
<Provider store={store}>
<ConnectedRouter history={history}>
<Switch>
<Route path="/" component={App} />
</Switch>
</ConnectedRouter>
</Provider>
);
I have tested the solution here: https://hackernoon.com/animated-page-transitions-with-react-router-4-reacttransitiongroup-and-animated-1ca17bd97a1a - but it doesn't work. Could some one point me in the right direction?
Update
I have tried like this, but the callback doesn't get called. So I end up with to pages in the dom.
export const App = ({ location }) => {
console.log(location);
return (
<div className="app-container">
<main className="app-container__content">
<ScrollToTop />
<TransitionGroup>
<Switch key={location.pathname} location={location}>
<Route exact path="/projects/:slug" component={ProjectPage} />
<Route exact path="/" component={StartPage} />
</Switch>
</TransitionGroup>
</main>
</div>
);
};
you are missing the Transition component itself
it should look something like this:
<TransitionGroup>
<CSSTransition
key={location.key}
classNames="page-animation"
timeout={{ enter: PAGE_ENTER_ANIMATION_SPEED, exit: PAGE_EXIT_ANIMATION_SPEED }}>
<Switch location={location}>
<Route exact path="/projects/:slug" component={ProjectPage} />
<Route exact path="/" component={StartPage} />
</Switch>
</CSSTransition>
</TransitionGroup>
notice that the key is on the CSSTransition itself and not on the Switch
Update
If you want to implement it by yourself
here is a basic implementation of CSSTransition
class MyTransition extends React.Component {
constructor(props) {
super(props);
this.state = {
inTransition: true,
playAnimation: false,
};
}
componentDidMount() {
setTimeout(() => {
this.setState({playAnimation: true});
}, 1);
this.removeClassesAndFireOnExited();
}
removeClassesAndFireOnExited() {
setTimeout(() => {
this.setState({playAnimation: false, inTransition: false}, () => {
if (!this.props.in) {
this.props.onExited(this);
}
});
}, this.props.timeout[this.props.in ? 'enter' : 'exit']);
}
componentWillReceiveProps(nextProps) {
if (nextProps.in !== this.props.in) {
this.setState({inTransition: true});
setTimeout(() => {
this.setState({playAnimation: true});
}, 1);
this.removeClassesAndFireOnExited();
}
}
render() {
const baseClassName = this.props.in ? 'page-animation-enter' : 'page-animation-exit';
const {inTransition, playAnimation} = this.state;
const transitionClasses = [...(inTransition ? [baseClassName] : []), ...(playAnimation ? [baseClassName + '-active'] : [])];
return (
<div className={transitionClasses.join(' ')}>
{this.props.children}
</div>
);
}
}
you get this.props.in from TransitionGroup to indicate whether you are entering or leaving.
this.props.onExited is another prop that you get from TransitionGroup, you must call it when the exit animation completes, so TransitionGroup will know that you should be unmounted.
Here is an article explaining how to setup react-router v4 and react-transition-group with multiple routes and with transitions depending of the next state: https://medium.com/lalilo/dynamic-transitions-with-react-router-and-react-transition-group-69ab795815c9
Related
In the following code, the url changes but the content doesn't rerender until manual refresh. What am I doing wrong here? I could use props.children or something but don't really want to. My understanding of is that it should render the content of the nested elements under .
const LandingPage = () => {
return (
<div>
<div>
buttons
<Button>
<Link to="/team1">team1</Link>
</Button>
<Button>
<Link to="/team2">team2</Link>
</Button>
<Button>
<Link to="/team3">team3</Link>
</Button>
</div>
<Outlet />
</div>
)
}
export default class Router extends Component<any> {
state = {
teams: [team1, team2, team3] as Team[]
}
public render() {
return (
<BrowserRouter>
<Routes>
<Route path='/' element={<LandingPage />} >
{
this.state.teams.map(team => {
const path = `/${team.name.toLowerCase()}`
return (
<Route path={path} element={
<BaseTeam
name={team.name}
TL={team.TL}
location={team.location}
members={team.members}
iconPath={team.iconPath}
/>
} />)
})
}
</Route>
</Routes>
</BrowserRouter>
)
}
}
Maybe signal to react library that a key has changed so that it needs to rerender the outlet
const LandingPage = () => {
const location = useLocation(); // react-router-dom
return (
<div>
<div>
buttons
<Button>
<Link to="/team1">team1</Link>
</Button>
<Button>
<Link to="/team2">team2</Link>
</Button>
<Button>
<Link to="/team3">team3</Link>
</Button>
</div>
<Outlet key={location.pathname}/>
</div>
)}
It seems the mapped routes are missing a React key. Add key={path} so each route is rendering a different instance of BaseTeam.
The main issue is that the BaseTeam component is the same "instance" for all the routes rendering it.
It should either also have a key prop specified so when the key changes BaseTeam is remounted and sets the name class property.
Example:
<BrowserRouter>
<Routes>
<Route path="/" element={<LandingPage />}>
{this.state.teams.map((team) => {
const path = `/${team.name.toLowerCase()}`;
return (
<Route
key={path} // <-- add missing React key
path={path}
element={(
<BaseTeam
key={path} // <-- add key to trigger remounting
name={team.name}
/>
)}
/>
);
})}
</Route>
</Routes>
</BrowserRouter>
Or BaseTeam needs to be updated to react to the name prop updating. Use the componentDidUpdate lifecycle method to check the name prop against the current state, enqueue a state update is necessary.
Example:
class BaseTeam extends React.Component {
state = {
name: this.props.name
};
componentDidUpdate(prevProps) {
if (prevProps.name !== this.props.name) {
this.setState({ name: this.props.name });
}
}
render() {
return <div>{this.state.name}</div>;
}
}
...
<BrowserRouter>
<Routes>
<Route path="/" element={<LandingPage />}>
{this.state.teams.map((team) => {
const path = `/${team.name.toLowerCase()}`;
return (
<Route
key={path}
path={path}
element={<BaseTeam name={team.name} />}
/>
);
})}
</Route>
</Routes>
</BrowserRouter>
As you've found out in your code though, just rendering the props.name prop directly is actually the correct solution. It's a React anti-pattern to store passed props into local state. As you can see, it requires extra code to keep the props and state synchrononized.
I have a Link in a page and I want it to pass the props to a route component which is a class component, how can I do that?? here is the code I have done so far:
the file where the Link is :
<List
itemLayout="horizontal"
dataSource={classes}
renderItem={item => (
<List.Item>
<List.Item.Meta
title = {
<Link
to={{
pathname : "/Class",
state: {Name: item.Name}
}}
className="ListItem"
>
{item.Name}
</Link>
}
/>
</List.Item>
)}
/>
this is the index.js where I stored all the routes and I also have a context. Provider:
ReactDOM.render(
<Provider store= {store}>
<Router>
<Layout>
<Switch>
<Route component={LandingPage} exact path="/" />
<Route path="/dashboard" >
<UserProvider>
<Dashboard/>
</UserProvider>
</Route>
<Route path="/Class" >
<UserProvider>
<Class/>
</UserProvider>
</Route>
<Route component={NewMessage} path="/New-Message" />
<Route path="/Inbox" >
<UserProvider>
<Inbox/>
</UserProvider>
</Route>
<Route path="/Outbox" >
<UserProvider>
<Outbox/>
</UserProvider>
</Route>
<Route component={ResetPassword} path="/reset-password" />
<Route component={ResetPasswordConfirmed} path="/reset/password/confirm/:uid/:token" />
<Route component={Signup} path="/signup" />
<Route component={Activate} path="/activate/:uid/:token" />
</Switch>
</Layout>
</Router>
</Provider>,
document.getElementById('root')
);
and this is the file that the link is pointing at (Class.js) :
class Class extends React.Component {
static contextType = userContext
state = {
Curriculum: [],
Classworks: [],
Homeworks: [],
activeClass: ""
}
componentDidMount() {
const { state } = props.location.state
this.setState({
activeClass: state
});
console.log(this.state.activeClass);
}
render()
{
const { user_data, classes } = this.context
return(
<div className="back">
<Navbar/>
{this.state.activeClass}
<main className= "MainContent">
<div className="Class">
<h2 className="class">
</h2>
</div>
</main>
<br/>
<footer className="Footer">Designed by Eng. Omar Redwan</footer>
</div>
)}
}
export default Class;
Issue
The Class component isn't directly rendered by a Route component so it doesn't receive the route props.
<Route path="/Class" >
<UserProvider>
<Class/>
</UserProvider>
</Route>
Solution
Wrap the export of the Class component with the withRouter Higher Order Component so it has the route props injected to it.
export default withRouter(Class);
Now the Class component should be able to access route state via the location prop.
const { location: { state } } = this.props;
Side Note
You can't log react state right after an enqueued update as react state updates are asynchronously processed between render cycles, so you'll only ever see the current state from the current render cycle.
this.setState({
activeClass: state
});
console.log(this.state.activeClass); // <-- current state!
Use the componentDidUpdate lifecycle method to log state/prop updates.
<Link
to={{
pathname: "/Class",
state: item.Name
}}
className="ListItem">
{item.Name}
</Link>
and access like this in navigated component
componentDidMount() {
const { state } = this.props.location;
this.setState({
activeClass: state
}, ()=> {
this.state.activeClass
});
}
Don't console state value immediately after setting to the state because it's asynchronous. If you want that console to check do as a callback to setState({...}, ()=> { console.log('check here') })
New to React:
I have a <Header /> Component that I want to hide only when the user visit a specific page.
The way I designed my app so far is that the <Header /> Component is not re-rendered when navigating, only the page content is, so it gives a really smooth experience.
I tried to re-render the header for every route, that would make it easy to hide, but I get that ugly re-rendering glitch each time I navigate.
So basically, is there a way to re-render a component only when going in and out of a specific route ?
If not, what would be the best practice to achieve this goal ?
App.js:
class App extends Component {
render() {
return (
<BrowserRouter>
<div className="App">
<Frame>
<Canvas />
<Header />
<Main />
<NavBar />
</Frame>
</div>
</BrowserRouter>
);
}
}
Main.js:
const Main = () => (
<Switch>
<Route exact activeClassName="active" path="/" component={Home} />
<Route exact activeClassName="active" path="/art" component={Art} />
<Route exact activeClassName="active" path="/about" component={About} />
<Route exact activeClassName="active" path="/contact" component={Contact} />
</Switch>
);
I'm new to React too, but came across this problem. A react-router based alternative to the accepted answer would be to use withRouter, which wraps the component you want to hide and provides it with location prop (amongst others).
import { withRouter } from 'react-router-dom';
const ComponentToHide = (props) => {
const { location } = props;
if (location.pathname.match(/routeOnWhichToHideIt/)){
return null;
}
return (
<ComponentToHideContent/>
)
}
const ComponentThatHides = withRouter(ComponentToHide);
Note though this caveat from the docs:
withRouter does not subscribe to location changes like React Redux’s
connect does for state changes. Instead, re-renders after location
changes propagate out from the component. This means that
withRouter does not re-render on route transitions unless its parent
component re-renders.
This caveat not withstanding, this approach seems to work for me for a very similar use case to the OP's.
Since React Router 5.1 there is the hook useLocation, which lets you easily access the current location.
import { useLocation } from 'react-router-dom'
function HeaderView() {
let location = useLocation();
console.log(location.pathname);
return <span>Path : {location.pathname}</span>
}
You could add it to all routes (by declaring a non exact path) and hide it in your specific path:
<Route path='/' component={Header} /> // note, no exact={true}
then in Header render method:
render() {
const {match: {url}} = this.props;
if(url.startWith('/your-no-header-path') {
return null;
} else {
// your existing render login
}
}
You can rely on state to do the re-rendering.
If you navigate from route shouldHide then this.setState({ hide: true })
You can wrap your <Header> in the render with a conditional:
{
!this.state.hide &&
<Header>
}
Or you can use a function:
_header = () => {
const { hide } = this.state
if (hide) return null
return (
<Header />
)
}
And in the render method:
{this._header()}
I haven't tried react-router, but something like this might work:
class App extends Component {
constructor(props) {
super(props)
this.state = {
hide: false
}
}
toggleHeader = () => {
const { hide } = this.state
this.setState({ hide: !hide })
}
render() {
const Main = () => (
<Switch>
<Route exact activeClassName="active" path="/" component={Home} />
<Route
exact
activeClassName="active"
path="/art"
render={(props) => <Art toggleHeader={this.toggleHeader} />}
/>
<Route exact activeClassName="active" path="/about" component={About} />
<Route exact activeClassName="active" path="/contact" component={Contact} />
</Switch>
);
return (
<BrowserRouter>
<div className="App">
<Frame>
<Canvas />
<Header />
<Main />
<NavBar />
</Frame>
</div>
</BrowserRouter>
);
}
}
And you need to manually call the function inside Art:
this.props.hideHeader()
{location.pathname !== '/page-you-dont-want' && <YourComponent />}
This will check the path name if it is NOT page that you DO NOT want the component to appear, it will NOT display it, otherwise is WILL display it.
I'm pulling my hair out trying to render multiple layouts with React Router v4.
For instance, I'd like pages with the following paths to have layout 1:
exact path="/"
path="/blog"
path="/about"
path="/projects"
and the following paths to have layout 2:
path="/blog/:id
path="/project/:id
Effectively what's being answered here but for v4: Using multiple layouts for react-router components
None of the other answers worked so I came up with the following solution. I used the render prop instead of the traditional component prop at the highest level.
It uses the layoutPicker function to determine the layout based on the path. If that path isn't assigned to a layout then it returns a "bad route" message.
import SimpleLayout from './layouts/simple-layout';
import FullLayout from './layouts/full-layout';
var layoutAssignments = {
'/': FullLayout,
'/pricing': FullLayout,
'/signup': SimpleLayout,
'/login': SimpleLayout
}
var layoutPicker = function(props){
var Layout = layoutAssignments[props.location.pathname];
return Layout ? <Layout/> : <pre>bad route</pre>;
};
class Main extends React.Component {
render(){
return (
<Router>
<Route path="*" render={layoutPicker}/>
</Router>
);
}
}
simple-layout.js and full-layout.js follow this format:
class SimpleLayout extends React.Component {
render(){
return (
<div>
<Route path="/signup" component={SignupPage}/>
<Route path="/login" component={LoginPage}/>
</div>
);
}
}
So, for this you should use render function (https://reacttraining.com/react-router/core/api/Route/render-func)
A really good article that helped me: https://simonsmith.io/reusing-layouts-in-react-router-4/
In the end you will be use something like this:
<Router>
<div>
<DefaultLayout path="/" component={SomeComponent} />
<PostLayout path="/posts/:post" component={PostComponent} />
</div>
</Router>
I solved this problem utilizing a bit of both of your solutions:
My Routes.js file
import BaseWithNav from './layouts/base_with_nav';
import BaseNoNav from './layouts/base_no_nav';
function renderWithLayout(Component, Layout) {
return <Layout><Component /></Layout>
}
export default () => (
<Switch>
{/* Routes with Sidebar Navigation */}
<Route exact path="/" render={() => renderWithLayout(Home, BaseWithNav)} />
{/* Routes without Sidebar Navigation */}
<Route path="/error" render={() => renderWithLayout(AppErrorMsg, BaseNoNav)} />
<Route path="/*" render={() => renderWithLayout(PageNotFound, BaseNoNav)} />
</Switch>
)
Base.js (where routes get imported)
export default class Base extends React.Component {
render() {
return (
<Provider store={store}>
<Router>
<Routes />
</Router>
</Provider>
)
}
}
Layouts
BaseWithNav.js
class BaseWithNav extends Component {
constructor(props) {
super(props);
}
render() {
return <div id="base-no-nav">
<MainNavigation />
<main>
{this.props.children}
</main>
</div>
}
}
export default BaseWithNav;
BaseNoNav.js
class BaseNoNav extends Component {
constructor(props) {
super(props);
}
render() {
let {classes} = this.props;
return <div id="base-no-nav">
<main>
{this.props.children}
</main>
</div>
}
}
export default BaseNoNav;
I hope this helps!
I know i am replying late but it's easy to do that, i hope it will helps to newbie.
i am using React 4
Layout.js
export default props => (
<div>
<NavMenu />
<Container>
{props.children}
</Container>
</div>
);
LoginLayout.js
export default props => (
<div>
<Container>
{props.children}
</Container>
</div>
);
Now finally we have our App
App.js
function renderWithLoginLayout(Component, Layout) {
return <LoginLayout><Component /></LoginLayout>
}
function renderWithLayout(Path, Component, Layout) {
return <Layout><Route path={Path} component={Component} /></Layout>
}
export default () => (
<Switch>
<Route exact path='/' render={() => renderWithLayout(this.path, Home, Layout)} />
<Route path='/counter' render={() => renderWithLayout(this.path, Counter, Layout)} />
<Route path='/fetch-data/:startDateIndex?' render={() => renderWithLayout(this.path, FetchData, Layout)} />
<Route path='/login' render={() => renderWithLoginLayout(Login, LoginLayout)} />
</Switch>
);
My app is currently separated into 3 parts:
Frontend
Administration
Error
Frontend, Administration and the Error component have their own styling.
The Frontend and Administration component are also have their own Switch component to navigate through them.
The problem I am facing is that I can't hit the NoMatch path without a Redirect component. But when I do this I lose the wrong path in the browser URL.
Is there a chance when the inner Switch component has no matching route that it keeps searching in its parent Switch component?
Then I would be able to hit the NoMatch route and also keep the wrong path in the URL.
Edit: I updated my answer below with the final solution that is working like intended.
const Frontend = (props) => {
const { match } = props;
return (<div>
<h1>Frontend</h1>
<p><Link to={match.path}>Home</Link></p>
<p><Link to={`${match.path}users`}>Users</Link></p>
<p><Link to="/admin">Admin</Link></p>
<p><Link to={`${match.path}not-found-page`}>404</Link></p>
<hr />
<Switch>
<Route exact path={match.path} component={Home} />
<Route path={`${match.path}users`} component={Users} />
{
// Workaround
}
<Redirect to="/error" />
</Switch>
</div>);
};
const Admin = (props) => {
const { match } = props;
return (<div>
<h1>Admin</h1>
<p><Link to={match.path}>Dashboard</Link></p>
<p><Link to={`${match.path}/users`}>Edit Users</Link></p>
<p><Link to="/">Frontend</Link></p>
<p><Link to={`${match.path}/not-found-page`}>404</Link></p>
<hr />
<Switch>
<Route exact path={match.path} component={Home} />
<Route path={`${match.path}/users`} component={Users} />
{
// Workaround
}
<Redirect to="/error" />
</Switch>
</div>);
};
const ErrorPage = () =>
<div>
<h1>404 not found</h1>
<p><Link to="/">Home</Link></p>
</div>;
const App = () => (
<div>
<AddressBar />
<Switch>
<Route path="/error" component={ErrorPage} />
<Route path="/admin" component={Admin} />
<Route path="/" component={Frontend} />
{
// this should render the error page
// instead of redirecting to /error
}
<Route component={ErrorPage} />
</Switch>
</div>
);
Here is the final solution for this kind of requirement.
To make it work we use the location's state property. On the redirect in the inner routes we set the state to error: true.
On the GlobalErrorSwitch we check the state and render the error component.
import React, { Component } from 'react';
import { Switch, Route, Redirect, Link } from 'react-router-dom';
const Home = () => <div><h1>Home</h1></div>;
const User = () => <div><h1>User</h1></div>;
const Error = () => <div><h1>Error</h1></div>
const Frontend = props => {
console.log('Frontend');
return (
<div>
<h2>Frontend</h2>
<p><Link to="/">Root</Link></p>
<p><Link to="/user">User</Link></p>
<p><Link to="/admin">Backend</Link></p>
<p><Link to="/the-route-is-swiggity-swoute">Swiggity swooty</Link></p>
<Switch>
<Route exact path='/' component={Home}/>
<Route path='/user' component={User}/>
<Redirect to={{
state: { error: true }
}} />
</Switch>
<footer>Bottom</footer>
</div>
);
}
const Backend = props => {
console.log('Backend');
return (
<div>
<h2>Backend</h2>
<p><Link to="/admin">Root</Link></p>
<p><Link to="/admin/user">User</Link></p>
<p><Link to="/">Frontend</Link></p>
<p><Link to="/admin/the-route-is-swiggity-swoute">Swiggity swooty</Link></p>
<Switch>
<Route exact path='/admin' component={Home}/>
<Route path='/admin/user' component={User}/>
<Redirect to={{
state: { error: true }
}} />
</Switch>
<footer>Bottom</footer>
</div>
);
}
class GlobalErrorSwitch extends Component {
previousLocation = this.props.location
componentWillUpdate(nextProps) {
const { location } = this.props;
if (nextProps.history.action !== 'POP'
&& (!location.state || !location.state.error)) {
this.previousLocation = this.props.location
};
}
render() {
const { location } = this.props;
const isError = !!(
location.state &&
location.state.error &&
this.previousLocation !== location // not initial render
)
return (
<div>
{
isError
? <Route component={Error} />
: <Switch location={isError ? this.previousLocation : location}>
<Route path="/admin" component={Backend} />
<Route path="/" component={Frontend} />
</Switch>}
</div>
)
}
}
class App extends Component {
render() {
return <Route component={GlobalErrorSwitch} />
}
}
export default App;
All child component routes are wrapped in the <Switch> the parent (the switch inside the app component) you don't actually the switch in the child components.
Simply remove child switch.component and let the 404 in the <App <Switch> catch any missing.