Reach router navigate updates URL but not component - javascript

I'm trying to get Reach Router to navigate programmatically from one of my components. The URL is updated as expected however the route is not rendered and if I look at the React developer tools I can see the original component is listed as being displayed.
If I refresh the page once at the new URL then it renders correctly.
How can I get it to render the new route?
A simplified example is shown below and I'm using #reach/router#1.2.1 (it may also be salient that I'm using Redux).
import React from 'react';
import { navigate } from '#reach/router';
const ExampleComponent = props => {
navigate('/a/different/url');
return <div />;
};
export default ExampleComponent;

I was running into the same issue with a <NotFound defualt /> route component.
This would change the URL, but React itself didn't change:
import React from "react";
import { RouteComponentProps, navigate } from "#reach/router";
interface INotFoundProps extends RouteComponentProps {}
export const NotFound: React.FC<INotFoundProps> = props => {
// For that it's worth, neither of these worked
// as I would have expected
if (props.navigate !== undefined) {
props.navigate("/");
}
// ...or...
navigate("/", { replace: true });
return null;
};
This changes the URL and renders the new route as I would expect:
...
export const NotFound: React.FC<INotFoundProps> = props => {
React.useEffect(() => {
navigate("/", { replace: true });
}, []);
return null;
};

Could it be that you use #reach/router in combination with redux-first-history? Because I had the same issue and could solve it with the following configuration of my historyContext:
import { globalHistory } from "#reach/router";
// other imports
const historyContext = createReduxHistoryContext({
// your options...
reachGlobalHistory: globalHistory // <-- this option is the important one that fixed my issue
}
More on this in the README of redux-first-history

The same issue happens to me when I'm just starting to play around with Reach Router. Luckily, found the solution not long after.
Inside Reach Router documentation for navigate, it is stated that:
Navigate returns a promise so you can await it. It resolves after React is completely finished rendering the next screen, even with React Suspense.
Hence, use await navigate() work it for me.
import React, {useEffect} from 'react';
import {useStoreState} from "easy-peasy";
import {useNavigate} from "#reach/router";
export default function Home() {
const {isAuthenticated} = useStoreState(state => state.auth)
const navigate = useNavigate()
useEffect(()=> {
async function navigateToLogin() {
await navigate('login')
}
if (!isAuthenticated) {
navigateToLogin()
}
},[navigate,isAuthenticated])
return <div>Home page</div>
}

Try and use gatsby navigate. It uses reach-router. It solved my problem
import { navigate } from 'gatsby'

Related

Nextjs: useEffect only once (if I use Link component)

I use Link component for open pages without reload:
<Link href="/home"><a>Home</a></Link>
<Link href="/page"><a>Page</a></Link>
This in my home:
const loadedRef = useRef(false)
useEffect(() => {
if(!loadedRef.current){
console.log("run")
loadedRef.current = true;
}
}, []);
This work fine for first load.
If I click on page and click on home, useEffect run again!
I want only and only once load useEffect even click another pages and return to home
useRef creates a value for this specific instance of the home component. If the instance unmounts, then that value goes away. If you want to make a global variable that persists even after the component has unmounted, do something like this:
import React, { useEffect } from 'react';
// Deliberately making this *outside* the component
let loaded = false;
const Home = () => {
useEffect(() => {
loaded = true;
}, []);
// ...
}
export default Home;
It occurs because the feature "shallow" of next.js
https://nextjs.org/docs/routing/shallow-routing
The official documentation asks to listen to the variable in the query string that receives the request. Generally, this variable is the name of your page like [slug].js. You can inspect the Network tab (F12) to see what is variable used in the query string also.
The below example inspects the slug variable.
import { useEffect } from 'react'
import { useRouter } from 'next/router'
// Current URL is '/'
function Page() {
const router = useRouter()
useEffect(() => {
// in my case the page is [slug].jsx
}, [router.query.slug])
}
export default Page

Redirect in an exported function with react-router-dom without passing anything?

I have a very simple logout function that looks like this:
export const logout = () => {
localStorage.removeItem('_id')
localStorage.removeItem('token')
localStorage.removeItem('refresh_token')
return {}
}
I'd like this function to redirect using react-router-dom but I'd also like to avoid passing anything to it. This would allow me to call this function outside the scope of a React Component and would mean I don't have to clutter my application by passing the history object everywhere. I can't use redirect as this function must return an empty object.
In a perfect world I'd be able to import something from react-router-dom at the top of the document that would allow me to programatically redirect from within this function.
If you can import the history object in your react component:
import { useHistory } from 'react-router-dom';
const ReactComponent = () => {
const history = useHistory();
// ... React component to be continued
You can pass it into your function:
// ... React component continued
<button onClick={() => logout(history)}> log out </button>
And push a new route to it:
export const logout = (history) => {
localStorage.removeItem('_id')
localStorage.removeItem('token')
localStorage.removeItem('refresh_token')
history.push('/desiredRoute')
return {}
}
Then if you have a Route component with its path set up as /desiredRoute it should render its children.

Hitting Back button in React app doesn't reload the page

I have a React app (16.8.6) written in TypeScript that uses React Router (5.0.1) and MobX (5.9.4). The navigation works fine and data loads when it should, however, when I click the browser's Back button the URL changes but no state is updated and the page doesn't get re-rendered. I've read endless articles about this issue and about the withRouter fix, which I tried but it doesn't make a difference.
A typical use case is navigating to the summary page, selecting various things which cause new data to load and new history states to get pushed and then going back a couple of steps to where you started. Most of the history pushes occur within the summary component, which handles several routes. I have noticed that when going back from the summary page to the home page the re-rendering happens as it should.
My index.tsx
import { Provider } from 'mobx-react'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import App from './App'
import * as serviceWorker from './serviceWorker'
import * as Utils from './utils/Utils'
const rootStore = Utils.createStores()
ReactDOM.render(
<Provider {...rootStore }>
<App />
</Provider>,
document.getElementById('root') as HTMLElement
)
serviceWorker.unregister()
My app.tsx
import * as React from 'react'
import { inject, observer } from 'mobx-react'
import { Route, Router, Switch } from 'react-router'
import Home from './pages/Home/Home'
import PackageSummary from './pages/PackageSummary/PackageSummary'
import ErrorPage from './pages/ErrorPage/ErrorPage'
import { STORE_ROUTER } from './constants/Constants'
import { RouterStore } from './stores/RouterStore'
#inject(STORE_ROUTER)
#observer
class App extends React.Component {
private routerStore = this.props[STORE_ROUTER] as RouterStore
public render() {
return (
<Router history={this.routerStore.history}>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/summary/:packageId" component={PackageSummary} />
<Route exact path="/summary/:packageId/:menuName" component={PackageSummary} />
<Route exact path="/summary/:packageId/:menuName/:appName" component={PackageSummary} />
<Route component={ErrorPage} />
</Switch>
</Router>
)
}
}
export default App
My router store
import { RouterStore as BaseRouterStore, syncHistoryWithStore } from 'mobx-react-router'
import { createBrowserHistory } from 'history'
export class RouterStore extends BaseRouterStore {
constructor() {
super()
this.history = syncHistoryWithStore(createBrowserHistory(), this)
}
}
How I create the MobX stores
export const createStores = () => {
const routerStore = new RouterStore()
const packageListStore = new PackageListStore()
const packageSummaryStore = new PackageSummaryStore()
const packageUploadStore = new PackageUploadStore()
return {
[STORE_ROUTER]: routerStore,
[STORE_SUPPORT_PACKAGE_LIST]: packageListStore,
[STORE_SUPPORT_PACKAGE_SUMMARY]: packageSummaryStore,
[STORE_SUPPORT_PACKAGE_UPLOAD]: packageUploadStore
}
}
So my questions are:
How can I get the page to load the proper data when the user goes back/forward via the browser?
If the solution is being able to get MobX to observe changes to the location, how would I do that?
You could implement something like this in your component:
import { inject, observer } from 'mobx-react';
import { observe } from 'mobx';
#inject('routerStore')
#observer
class PackageSummary extends React.Component {
listener = null;
componentDidMount() {
this.listener = observe(this.props.routerStore, 'location', ({ oldValue, newValue }) => {
if (!oldValue || oldValue.pathname !== newValue.pathname) {
// your logic
}
}, true)
}
componentWillUnmount() {
this.listener();
}
}
Problem with this approach is that if you go back from /summary to other page (e.g. '/'), callback will initiate, so you would also need some kind of check which route is this. Because of these kind of complications I would suggest using mobx-state-router, which I found much better to use with MobX.
React router monitors url changes and renders associated component defined for the route aka url.
You have to manually refresh or call a window function to reload.
If I remember correctly, using a browser back function does not reload the page (I might be wrong).
Why not try to detect the back action by a browser and reload the page when detected instead?
You can try the following code to manually reload the page when the browser back button is clicked.
$(window).bind("pageshow", function() {
// Run reload code here.
});
Also out of curiosity, why do you need so many different stores?
In App.js
useEffect(() => {
window.onpageshow = function(event) {
if (event.persisted) {
window.location.reload();
}
};
}, []);

Next.js React component getInitialProps doesn't bind props

In Next.js, you can use in your React components a lifecycle method bound to the server side rendering on first load.
I tried to use this function as presented in the ZEIT tutorial (alternatively on their Github), but this.props doesn't seem to get JSON returned by the function.
When I click on the component, console print an empty object.
import React from 'react'
import 'isomorphic-fetch'
export default class extends React.Component {
static async getInitialProps () {
const res = await fetch('https://en.wikipedia.org/w/api.php?action=query&titles=Main%20Page&prop=revisions&rvprop=content&format=json')
const data = await res.json()
return { data }
}
print() {
console.log(this.props);
}
render() {
return <div onClick={this.print.bind(this)}>print props</div>
}
}
I had this issue due to a problem in my _app.js (i.e. NextJS Custom App) which I caused while adding in Redux (not a Redux issue). As soon as I started using getInitialProps in a page my props were empty at render though data was there in the static call. The cause was the incorrect propagation of pageProps in my _app.js.
So in my case the fix was changing, in custom _app.js getInitialProps, this:
return {
...pageProps,
initialReduxState: reduxStore.getState()
}
to:
return {
pageProps: {...pageProps},
initialReduxState: reduxStore.getState()
}
.. so the render method as seen in the NextJS doc link could wire them through.
you can make your call in getInitialProps method like this with little modification in your code
import React from 'react'
import 'isomorphic-fetch'
export default class extends React.Component {
static async getInitialProps () {
const res = await fetch('https://en.wikipedia.org/w/api.php?action=query&titles=Main%20Page&prop=revisions&rvprop=content&format=json')
const data = await res.json()
return { jsonData: data }
}
print() {
console.log(this.props.jsonData);
}
render() {
return <div onClick={()=> this.print()}>print props</div>
}
}
I tried your code and it seems to work fine, console shows this.props:

How to show loading UI when calling getComponent in react-router?

I'm really new to React and I can't figure out how to render a "loading..." screen when a route is being loaded with getComponent. The getComponent call works fine and displays the component, but there's no indication on the UI that anything is happening between the request and the response. That's what I'm trying to figure out.
import Main from './pages/Main.jsx';
import Test from './pages/Test.jsx';
import Home from './pages/Home.jsx';
var Routes = {
path: "/",
component: Main,
indexRoute: {
component: Home
},
childRoutes: [
{
path: "test",
component: Test
},
{
path: "about",
getComponent: function(path, cb) {
require.ensure([], (require) => {
cb(null, require("./pages/about/About.jsx"));
});
}
}
]
};
export default Routes;
After trying to unsuccessfully force a "loading" component to display using onEnter or within the getComponent function, I thought maybe I should try using Redux to set a loading state to true/false and getting my main view component to display a loading screen:
import React from 'react';
import {connect} from 'react-redux';
import NavBar from '../components/Navigation/NavBar.jsx';
import Footer from '../components/Footer.jsx';
import Loading from './Loading.jsx';
import navItems from '../config/navItems.jsx';
import setLoading from '../actions/Loading.jsx';
var Main = React.createClass({
renderPage: function() {
if (this.props.loading) {
return (
<Loading/>
);
} else {
return this.props.children;
}
},
render: function() {
return (
<div>
<header id="main-header">
<NavBar navigation={navItems}/>
</header>
<section id="main-section">
{this.renderPage()}
</section>
<Footer id="main-footer" />
</div>
);
}
});
function mapStateToProps(state) {
return {
loading: state.loading
}
}
export default connect(mapStateToProps)(Main);
This seems to work if I manually set the loading state using an action, which is what I was looking to do. But (and I feel this is going to be a real noob question) I can't figure out how to access the store/dispatcher from within the router.
I'm not sure if I'm using the wrong search terms or whatever, but I'm completely out of ideas and every react-router/redux tutorial seems to skip over what I feel like has to be a common problem.
Can anyone point me in the right direction (and also let me know if what I'm doing is best practice?)?
EDIT: I'll try and clarify this a bit more. In the first code block, you can see that if I click a <Link to="/about"> element then the getComponent function will fire, which will lazy-load the About.jsx component. The problem I am having is I can't figure out how to show some sort of loading indicator/spinner that would appear immediately after clicking the link and then have it get replaced once the component loads.
MORE EDITING: I've tried creating a wrapper component for loading async routes and it seems to work, however it feels really hacky and I'm sure it isn't the right way to go about doing this. Routes code now looks like this:
import Main from './pages/Main.jsx';
import Test from './pages/Test.jsx';
import Home from './pages/Home.jsx';
import AsyncRoute from './pages/AsyncRoute.jsx';
var Routes = {
path: "/",
component: Main,
indexRoute: {
component: Home
},
childRoutes: [
{
path: "test",
component: Test
},
{
path: "about",
component: AsyncRoute("about")
}
]
};
export default Routes;
The AsyncRoute.jsx page looks like this:
import React from 'react';
function getRoute(route, component) {
switch(route) {
// add each route in here
case "about":
require.ensure([], (require) => {
component.Page = require("./about/About.jsx");
component.setState({loading: false});
});
break;
}
}
var AsyncRoute = function(route) {
return React.createClass({
getInitialState: function() {
return {
loading: true
}
},
componentWillMount: function() {
getRoute(route, this);
},
render: function() {
if (this.state.loading) {
return (
<div>Loading...</div>
);
} else {
return (
<this.Page/>
);
}
}
});
};
export default AsyncRoute;
If anyone has a better idea, please let me know.
I think I have this figured out. It may or may not be the correct way to go about things, but it seems to work. Also I don't know why I didn't think of this earlier.
First up, move my createStore code to its own file (store.jsx) so I can import it into the main entry point as well as into my Routes.jsx file:
import {createStore} from 'redux';
import rootReducer from '../reducers/Root.jsx';
var store = createStore(rootReducer);
export default store;
Root.jsx looks like this (it's an ugly mess, but I'm just trying to get something that works on a basic level and then I'll clean it up):
import {combineReducers} from 'redux';
import user from './User.jsx';
import test from './Test.jsx';
var loading = function(state = false, action) {
switch (action.type) {
case "load":
return true;
case "stop":
return false;
default:
return state;
}
};
export default combineReducers({
user,
test,
loading
});
I've made a basic component that shows Loading/Loaded depending on the Redux store's value of "loading":
import React from 'react';
import {connect} from 'react-redux';
var Loading = React.createClass({
render: function() {
if (this.props.loading) {
return (
<h1>Loading</h1>
);
} else {
return (
<h1>Loaded</h1>
);
}
}
});
export default connect(state => state)(Loading);
And now my Routes.jsx file looks like this (note I've imported the Redux store):
import Main from './pages/Main.jsx';
import Test from './pages/Test.jsx';
import Home from './pages/Home.jsx';
import store from './config/store.jsx';
var Routes = {
path: "/",
component: Main,
indexRoute: {
component: Home
},
childRoutes: [
{
path: "test",
component: Test
},
{
path: "about",
getComponent: function(path, cb) {
store.dispatch({type: "load"})
require.ensure([], (require) => {
store.dispatch({type: "stop"});
cb(null, require("./pages/about/About.jsx"));
});
}
}
]
};
export default Routes;
This seems to work. As soon as a <Link/> is clicked to go to the /about route, an action is dispatched to set the "loading" state to true in the main store. That causes the <Loading/> component to update itself (I envision it would eventually render a spinner in the corner of the window or something like that). That weird require.ensure([]) function is run to get webpack to do its fancy code splitting, and once the component is loaded then another action is dispatched to set the loading state to false, and the component is rendered.
I'm still really new to React and while this seems to work, I'm not sure if it's the right way to do it. If anyone has a better way, please chime in!
Following the same approach as #David M I implemented a loading reducer and a function to wrap the dispatches.
Excluding the store creation and manage, they are basically as follows:
loadingReducer:
// ------------------------------------
// Constants
// ------------------------------------
export const LOADING = 'LOADING'
// ------------------------------------
// Actions
// ------------------------------------
const loadQueue = []
export const loading = loading => {
if (loading) {
loadQueue.push(true)
} else {
loadQueue.pop()
}
return {
type: LOADING,
payload: loadQueue.length > 0
}
}
export const actions = {
loading
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[LOADING]: (state, action) => (action.payload)
}
// ------------------------------------
// Reducer
// ------------------------------------
const initialState = false
export default function reducer (state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}
Notice how loadingQueue keeps the loading message active while there are remaining modules to fetch, for nested routes.
withLoader function:
import { loading } from 'loadingReducer'
const withLoader = (fn, store) => {
return (nextState, cb) => {
store.dispatch(loading(true))
fn(nextState, (err, cmp) => {
store.dispatch(loading(false))
cb(err, cmp)
})
}
}
export default withLoader
Now when defining new routes we can dispatch the loading action implicitly using withLoader:
someRoute:
import withLoader from 'withLoader'
import store from 'store'
const route = {
path: 'mypath',
getComponent: withLoader((nextState, cb) => {
require.ensure([], require => {
cb(null, require('something').default)
}, 'NamedBundle')
}, store)
}
export default route
OK, let's see if I can shed some light on this here:
I can't figure out how to access the store/dispatcher from within the router
There is no need to do that AFAIK. You can specify all routes, listing the components that should answer each route (like you did above), and then connect each of the components to the redux store. For connecting, your mapStateToProps function can be written in a much simpler fashion, like this:
export default connect(state => state)(Main);
Regarding the loading state: I think it is a step in the wrong direction to have a slow-loading component and to display a waiting indicator while it is loading. I would rather have a fast-loading component that loads all of its data asynchronously from the backend, and while the data is not yet available, the component renders a waiting indicator. Once the data is available, it can be displayed. That is basically what you sketched in your second edit.
It would be even better if you could drive this off of your actual data, i.e. no data present -> show the loading screen / data present -> show the real screen. This way, you avoid issues in case your loading flag gets out of sync. (More technically speaking: Avoid redundancy.)
So, instead of making the wrapper generic, I would rather create a standalone component for the loading screen and display that whenever each individual component feels the need for it. (These needs are different, so it seems to be difficult to handle this in a generic way.) Something like this:
var Page = function(route) {
return React.createClass({
getInitialState: function() {
// kick off async loading here
},
render: function() {
if (!this.props.myRequiredData) {
return (
<Loading />
);
} else {
return (
// display this.props.myRequiredData
);
}
}
});
};
dynamic load async routers are using require.ensure, which use jsonp to download scripts from network.
because of slow networking, sometime, UI blocks, the screen is still showing the previews react component.
#Nicole , the really slow is not the data loading inside component, but is the component self, because of jsonp

Categories

Resources