I'm following the second answer to implement a scroll to top every time there's a route change. The answer suggests creating a new component, <ScrollToTop />, which I did
import { useEffect } from 'react';
import { withRouter } from 'react-router-dom';
function ScrollToTop({ history }) {
useEffect(() => {
const unlisten = history.listen(() => {
window.scrollTo(0, 0);
});
return () => {
unlisten();
}
}, []);
return (null);
}
export default withRouter(ScrollToTop);
I'm confused as to where to place such component, since my app is structured a bit differently. See the index.js and App.js files below.
index.js
ReactDOM.render(
<>
<EWLoader />
<ContextProvider>
<Router> //tried placing below <Router>, didn't work
<App />
</Router>
</ContextProvider>
</>,
document.getElementById('root'),
)
App.js
function App() {
return (
<> //tried placing above <Switch>, didn't work
<Switch>
<Route path="/EWadmin">
<Admin />
</Route>
<Route path="/ewadmin-allbookings">
<AdminAllBookings />
</Route>
<Route path="/">
<EWHeader />
<Middleware />
{/* <EWPopular /> */}
</Route>
</Switch>
</>
)
}
I've also noticed <ScrollToTop /> is destructuring an object's property history but I'm not importing or declaring it anywhere in index.js or App.js. Perhaps something should be done with regards to that?
There are two ways to achieve scrolling to top on page change:
First solution
Modify your ScrollToTop component to this:
function ScrollToTop({ history }) {
useEffect(() => {
const unlisten = history.listen(() => window.scrollTo(0, 0));
return unlisten;
}, [history]); // make sure to list history as a dependency
return null;
}
export default withRouter(ScrollToTop);
Then add ScrollToTop just under your router. Now it scrolls to top anytime the history changes, for any page.
Second solution
Modify your ScrollToTop component:
function ScrollToTop() {
useEffect(() => {
window.scrollTo(0, 0);
}, []); // No dependency
return null;
}
// No need to use withRouter
Then add ScrollToTop under every Route component where you want this behaviour.
Related
When I change pages, the application is being kept at the same point it was on the previous page. I want to show the component from the top when I change pages. To achieve that, I am trying to implement React Router ScrollToTop.
I found the documentation and implemented it, but I am using react router v6, so it is a bit different.
https://v5.reactrouter.com/web/guides/scroll-restoration
Everything inside the ScrollToTop component doesn't get rendered, and I end up with a blank page.
App.js:
import { Router, Routes, Route } from "react-router-dom";
import './App.scss';
import Main from './pages/Main';
import Projects from './pages/Projects';
import NavBar from './components/NavBar';
import Footer from './components/Footer';
import ScrollToTop from './components/scrollToTop';
function App() {
return (
<div className="app" id="app">
<NavBar />
<div className='app-body'>
<Router>
<ScrollToTop>
<Routes>
<Route path="/portfolio" element={<Main />} />
<Route path="/portfolio/projects" element={<Projects />} />
</Routes>
</ScrollToTop>
</Router>
</div>
<Footer />
</div>
);
}
export default App;
ScrollToTop.js:
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
export default function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
As others have pointed out, you are wrapping your Routes component with the ScrollToTop component, but instead of editing it to render its implicit children prop I suggest converting it to a React hook, especially considering since it doesn't actually render anything, you want it to run as a side-effect of navigation.
function useScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
}
...
function App() {
useScrollToTop();
return (
<div className="App">
<div className="app-body">
<NavBar />
<Routes>
<Route path="/portfolio" element={<Main />} />
<Route path="/portfolio/projects" element={<Projects />} />
</Routes>
</div>
</div>
);
}
This necessarily requires you to lift the Router higher in the ReactTree to wrap the App component so it has a routing context to use for the useScrollToTop hook.
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<Router>
<App />
</Router>
</StrictMode>,
rootElement
);
You put the Routes component as a descendant of ScrollToTop component, so you should return children instead of null.
ScrollToTop.js:
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
export default function ScrollToTop({ children }) {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return children;
}
The blank page is because you are returning null from <ScrollToTop> component. Instead if you return <></> or take the {children} prop and return that from <ScrollToTop >, it should work :)
I have the following problem: I have a general component that contains some data from the redux store and I want to clear this data once the user visits another route.
<Route path="/create/gallery" element={<CreatePage type={ContentType.gallery}/>} />
also I have some more code that saves my entered data to the store
saveGeneralInfo = (field: string, value: string) => {
const data = {};
data[field] = value;
this.props.dispatch(saveGeneralInfo(data));
}
How I can clear the state if the user leave the page or visit any other link? (For example from header)
if(this.state.keycloak) {
if(this.state.authenticated) return (
<div className="App">
<Header/>
<Routes>
<Route path="/" element={<Content />} />
<Route path="/sites"/>
<Route path="/users"/>
<Route path="/create/gallery" element={<CreatePage type={ContentType.gallery}/>}/>
<Route path="/create/article" element={<CreatePage type={ContentType.article} />} />
<Route path="/create/quiz" element={<CreatePage type={ContentType.quiz} />} />
</Routes>
</div>
);
else return (
<div>Can't authorize</div>
)
}
You will have to provide functionality for store clearing that fires on unMount lifecycle event in every route root component.
If you are using functional components:
export const Component = () => {
const dispatch = useDispatch();
useEffect(() => {
return () => {
dispatch(yourActionThatCleansReduxStore())
}
}, [])
//rest of your code
}
In my case I reset parts of my store for every page URL like /info or /user where store looks like
{
user: {
id: ...
},
info: ...
}
You can create a route controller using children component
import { useDispatch } from "react-redux";
import { useLocation } from "react-router-dom";
import { cleanState } from "Your-reducer.js";
function CleanState({ children }) {
const location = useLocation();
const dispatch = useDispatch();
useEffect(() => {
dispatch(cleanState()); // every time the route changes clean the state
// if you don't use redux-toolkit you can use action.payload etc....
},[location.pathname])
return <>{children}</>;
}
export default CleanState;
then you have to wrap the main component
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import YourComponents from "./YourComponents"; // Your components
import CleanState from "./CleanState"; // where did you save it
function App() {
return (
<Router>
<CleanState> // This is the previous component, this will listen the movements of the routes
<Routes>
<Route path="/main" element={<YourComponents />} />
<Route path="*" element={<YourComponents />} />
</Routes>
</CleanState>
</Router>
);
}
export default App;
I have the following scrollToTop component following this github question: https://github.com/ReactTraining/react-router/issues/6665
Here is the component:
import React from "react";
import { useEffect } from "react";
import { useLocation, withRouter } from "react-router-dom";
const ScrollToTop: React.FC = (props: any) => {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return props.children;
};
export default withRouter(ScrollToTop);
here is my App Component:
function App() {
const dispatch = useDispatch();
// fetches necessary configurations for elements img size etc.
const configMbdApiState = useSelector(
(state: RootStore) => state.postApiConfigurationReducer
);
const storeSearchValueHandlerState = useSelector(
(state: RootStore) => state.searchValueFromInputHandlerR
);
useEffect(() => {
dispatch(
postMDBConfigurationApi(
`https://api.themoviedb.org/3/configuration?api_key=${configMbdApiState.apiKey}`
)
);
dispatch(
postMoviesGenresFetchResponse(
`https://api.themoviedb.org/3/genre/movie/list?api_key=${configMbdApiState.apiKey}`
)
);
dispatch(
postTvshowsGenresFetchResponse(
`https://api.themoviedb.org/3/genre/tv/list?api_key=${configMbdApiState.apiKey}`
)
);
}, []);
// if user clicks outside the instant results div, it removes the div from the UI
const resetsUserSearchHandler = () => {
if (storeSearchValueHandlerState.userSearchValue.length > 0) {
dispatch(storesUserSearchValueHandler(""));
}
};
return (
<BrowserRouter>
<div className="App" onClick={resetsUserSearchHandler}>
<Switch>
<ScrollToTop>
<Route path="/" exact component={WelcomePage} />
<Route path="/home" component={Home} />
<Route
path="/details/movie/:title"
component={SingleMovieDetails}
/>
<Route path="/details/tv/:title" component={TvShowDetails} />
<Route path="/details/actor/:name" component={ActorDetails} />
<Route path="/results" component={ResultsPage} />
</ScrollToTop>
</Switch>
</div>
</BrowserRouter>
);
}
export default App;
Things I have tried:
Console logged the pathname to see if it changes in every render cycle and it does.
I have tried to move scrollToTop and use it directly on the components and it doesn't work either.
At the moment I'm using "any" for the props type in the scroll component. Could this be the problem ?
I'm using NavLink from route-react-dom to direct to other paths, so I don't really know what the issue could be.
I followed the documentation on how to implement ScrollToTop with React Router but I am unable to get the function to work. I created a component ScrollToTop.js with the following code:
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
export default function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
In my App.js I add the wrapper after Router but all I get is a white screen whenever I call ScrollToTop. Here is a snippet from App.js.
<Router history={createBrowserHistory()}>
<AuthProvider>
<GlobalStyles />
<GoogleAnalytics />
<CookiesNotification />
<ScrollToTop>
{renderRoutes(routes)}
</ScrollToTop>
</AuthProvider>
</Router>
I am told to wrap <ScrollToTop around <App /> but that is not located in App.js but instead inside index.js in the root directory.
Your ScrollToTop component does its job. I've set up a working example here.
I have not changed ScrollToTop at all. The app looks like this:
function App() {
return (
<Router>
<ScrollToTop />
<Routes />
<Links />
</Router>
);
}
Now anytime the Router's pathname changes, the app will scroll to the top.
I'm testing if my components render with Redux successfully with React Testing Library. I'm having trouble having my utility component to pass the renderWithRedux test. This is my App component.
function App() {
return (
<>
<Router>
<NavBar />
<div className="container">
<Switch>
<Route exact path='/' component={Home}/>
<AuthRoute exact path='/login' component={Login} />
<AuthRoute exact path='/signup' component={Signup} />
<Route exact path='/users/:handle' component={UserProfile} />
<Route exact path='/users/:handle/post/:postId' component={UserProfile} />
</Switch>
</div>
</Router>
</>
);
}
Here is my AuthRoute utility component.
const AuthRoute = ({ component: Component, authenticated, ...rest }) => (
// if authenticated, redirect to homepage, otherwise redirect to signup or login
<Route
{...rest}
render={(props) =>
authenticated === true ? <Redirect to='/' /> : <Component {...props} />
}
/>
);
AuthRoute.test.js
const renderWithRedux = () => render(
<Provider store={myStore}>
<AuthRoute />
</Provider>
);
it('renders with Redux', () => {
const {} = renderWithRedux(<AuthRoute />);
});
I've attempted the solutions from Invariant failed: You should not use <Route> outside a <Router>, but to no avail. I appreciate any help, thank you.
Render the component under test into a router
import { MemoryRouter } from 'react-router-dom';
const renderWithRedux = ({ children }) => render(
<Provider store={myStore}>
{children}
</Provider>
);
it('renders with Redux', () => {
const {} = renderWithRedux(
<MemoryRouter>
<AuthRoute />
</MemoryRouter>
);
});
Just like the Provider to wrap redux things you have to wrap your components with routes using MemoryRouter for the tests.
import { MemoryRouter } from 'react-router';
Basically, you have two wrapper elements. It should go something like this, for example, renderWithReduxWrapp => renderWithRouter => YourTestingComponent.
I had a similar issue when trying to test Button render (which has a Link) depending on props, and was able to solve it by creating some helper functions.
Here is the example:
This is the main component, UserCard.js, which renders user data from redux, and only shows a button if withButton props is passed.
import React from "react";
import { Link } from "react-router-dom";
import { Button } from "react-bootstrap";
const CardComponent = ({ withButton }) => {
const userInfo = useSelector((state) => getUserSelector(state));
return (
<div>
<div>{userInfo}</div>
{withButton && (
<Link to="/settings" className="button-link">
<Button block>EDIT CONTACT INFO</Button>
</Link>
)}
</div>
);
};
export default CardComponent;
This is a CardComponent.test.js file.
First, you need to add these lines of code
const ReduxWrapper = ({ children }) => {
<Provider store={store}>{children} </Provider>;
}
const AppWrapper = ({ children }) => (
<BrowserRouter>
<ReduxWrapper>{children}</ReduxWrapper>
</BrowserRouter>
);
const renderWithRouter = (ui, { route = '/' } = {}) => {
window.history.pushState({}, 'Test page', route);
return render(ui, { wrapper: AppWrapper });
};
After that, you need to start your test with renderWithRouter instead of just render method.
it('should render settings button if prop withButton is passed', () => {
renderWithRouter(<CardComponent withButton />, { wrapper: ReduxWrapper });
// apply you code here. I only needed to check if the button is renederd or not.
const settingsButton = screen.queryByText(/edit contact info/i);
expect(settingsButton).toBeInTheDocument();
});