I have two problems. I've read some SO posts and browsed through various docs, but I run into different explanations.
This is what I've got:
var Router = window.ReactRouter.Router;
var useRouterHistory = window.ReactRouter.useRouterHistory;
var Route = window.ReactRouter.Route;
var Link = window.ReactRouter.Link;
const createHistory = window.History.createHistory;
const appHistory = useRouterHistory(createHistory)({ queryKey: false });
var MyTable = React.createClass({
getInitialState: function() {
...
history.push({
pathname: window.location.href.split('?')[0],
search: querystring,
state: { 'data': initial_data }
});
...
},
...
});
ReactDOM.render((
<Router history={appHistory}>
<Route path={window.page_url} component={MyTable}>
</Route>
</Router>
), document.getElementById('my-table-wrapper'));
Problem number 1 is: history.push is not a function.
Problem number 2 is: if I use, for example, window.history.pushState, I do get nice URLs that change on browser's back and forward buttons and I saw that I can perform actions onpopstate. What is the equivalent of that for the history that I'm trying to use above?
Thanks.
Related
I'm trying to build a dynamic router with React. The idea is that the Routes are created from the data (object) received from the backend:
Menu Object:
items: [
{
name: "dashboard",
icon: "dashboard",
placeholder: "Dashboard",
path: "/",
page: "Dashboard",
exact: true,
},
{
name: "suppliers",
icon: "suppliers",
placeholder: "Suppliers",
path: "/suppliers",
page: "Suppliers",
exact: true,
}
]
The routes hook :
export const RouteHook = () => {
// Dispatch to fetch the menu object from the backend
const dispatch = useDispatch();
dispatch(setMenu());
// Select the menu and items from Redux
const { items } = useSelector(getMainMenu);
// useState to set the routes inside
const [menuItems, setMenuItems] = useState([]);
const { pathname } = useLocation();
useEffect(() => {
// Loop trough the menu Items
for (const item of items) {
const { page, path, name, exact } = item;
// Import dynamically the route components
import(`../pages/${name}/${page}`).then((result) => {
// Set the routes inside the useState
setMenuItems(
<Route exact={exact} path={path} component={result[page]} />
);
});
}
// Check if pathname has changed to update the useEffect
}, [pathname]);
return (
<Switch>
{/* Set the routes inside the switch */}
{menuItems}
</Switch>
);
};
Now here's the problem. Not all the components load. Usually the last component loads and when clicking on a diffrent route the component won't change. Except if you got to the page and refresh (F5).
What am I missing here? Is it possible to create full dynamic routes & components in react?
I'm not sure 100% what's going on here, but here's a problem I see:
const [menuItems, setMenuItems] = useState([]);
You're saying that menuItems is an array of something. But then:
import(`../pages/${name}/${page}`).then((result) => {
// Set the routes inside the useState
setMenuItems(
<Route exact={exact} path={path} component={result[page]} />
);
});
On every iteration you are setting the menu items to be a singular Route component. Probably what you're thinking youre doing is
const routes = items.map(item => {
const { page, path, name, exact } = item;
return import(`../pages/${name}/${page}`).then((result) => {
<Route exact={exact} path={path} component={result[page]} />
});
})
setMenuItems(routes)
But this makes no sense, because your map statement is returning a Promise.then function. I'm not entirely sure why you're dynamically importing the components here. You're better off doing a simple route mapping:
const routes = items.map(item => {
const { page, path, name, exact } = item;
return <Route exact={exact} path={path} component={components[page]} />
})
setMenuItems(routes)
Where components is an object whose keys are the values of page and whose values are actual components, i.e.:
const components = {
Suppliers: RenderSuppliers,
Dashboard: RenderDashboard
}
If you want these components lazy-loaded, use react suspense:
const Suppliers = React.lazy(() => import("./Suppliers"))
const Dashboard = React.lazy(() => import("./Dashboard"))
const components = {
Suppliers,
Dashboard,
}
const routes = items.map(item => {
const { page, path, name, exact } = item;
return (
<Suspense fallback={<SomeFallbackComponent />}>
<Route
exact={exact}
path={path}
component={components[page]}
/>
</Suspense>
)
})
setMenuItems(routes)
This is just a quick review of what may be going wrong with your code, without a reproducible example, its hard to say exactly.
Seth has some great suggestions, but here's how you can clean this up while still using dynamic imports.
Hopefully you can see that you are calling setMenuItems with a single Route component instead of all of them. Each time that you setMenuItems you are overriding the previous result and that's why only the last Route actually works -- it's the only one that exists!
Your useEffect depends on the pathname which seems like you are trying to do the routing yourself. Since you are using react-router-dom you would include all of the Route components in your Switch and let the router handle the routing.
So you don't actually need any state here.
You can use the React.lazy component import helper inside of the Route. You need a Suspense provider around the whole block in order to use lazy imports.
I don't like that you use two variables in the path for a component ../pages/${name}/${page}. Why not export the component from the ./index.js of the folder?
export const Routes = () => {
// Dispatch to fetch the menu object from the backend
const dispatch = useDispatch();
dispatch(setMenu());
// Select the menu and items from Redux
const items = useSelector((state) => state.routes.items);
return (
<Suspense fallback={() => <div>Loading...</div>}>
<Switch>
{items.map(({ exact, path, name }) => (
<Route
key={name}
exact={exact}
path={path}
component={React.lazy(() => import(`../pages/${name}`))}
/>
))}
</Switch>
</Suspense>
);
};
It works!
Code Sandbox Link
I have a view that is a big paginated table.
I want to include the page index in the URL query params to be able to share a specific page of the table by sharing the URL.
My problem is that the page isn't the only query param, so what I need to do is, when a page change:
Retrieve the current query params
Create the new query params from the old ones and the new page
Update the query params.
However, updating the query params is a problem, because the query params are a dependency of the function that retrieves the old ones and I am stuck in this loop.
I first tried to do something like this:
import query from 'qs'
import { useLocation, useHistory } from 'react-router-dom'
function Comp() {
const [pageIndex, setPageIndex] = useState(0)
// Updates the query params using the provided ones.
const updateQueryParams = useCallback(
(pageIndex) => {
const oldSearch = query.parse(location.search, { ignoreQueryPrefix: true })
const newSearch = { ...oldSearch, pageIndex }
history.push({ pathname: location.pathname, search: query.stringify(newSearch) })
},
[location.search, location.pathname]
)
// Updates the query params when pageIndex changes.
useEffect(() => updateQueryParams(pageIndex), [updateQueryParams, pageIndex])
}
But updating the query params updates location.search which is a dependency of the updateQueryParams callback and it triggers an infinite loop.
I tried to make use of useRef to store the search and the pathname, updating them manually, but I've been unable to come up to a working solution.
My last try was to extract the logic in a custom hook to cut the problem in smaller pieces (and I will have to reuse this piece of logic elsewhere).
The current state of this hook is:
import { useLocation, useHistory } from 'react-router-dom'
import { useRef, useCallback } from 'react'
import query from 'qs'
export default function useQueryParams<QueryParams extends query.ParsedQs>(): [
QueryParams,
(newSearch: QueryParams) => void
] {
const location = useLocation()
const history = useHistory()
const search = useRef(query.parse(location.search, { ignoreQueryPrefix: true }) as QueryParams)
const pathname = useRef(location.pathname)
const setSearch = useCallback(
(newSearch: QueryParams) => {
history.push({ pathname: pathname.current, search: query.stringify(newSearch) })
search.current = newSearch
},
[history, pathname]
)
return [search.current, setSearch]
}
But it doesn't solve my problem.
Trying to access the route triggers an infinite loop and I get a Warning: Maximum update depth exceeded error in the console.
The problem was caused by how my routes was declared.
The solution has been to update all my routes from this pattern:
<Route path="/login" component={Login} />
to this one:
<Route path="/login">
<Login />
</Route>
I'm using the package Reacter Router to create and manage the routes of my application.
I am using an OAuth authentication system, which returns the following URL to my application:
http://localhost/login-meli?code=1234567890
I need that each time this route with the query "code" is triggered to execute a certain function, which as an example, can be: console.log('It works!')
I tried the code below and it worked, but I didn't find in the documentation how can I specify a specific query. I also noticed that when I add other lines of code an error is returned.
<Route exact path="/login-meli" render={() => (
console.log('works!!!')
)}/>
You can use the useLocation hook from react-router-dom.
import { Route, useLocation } from "react-router-dom";
Then in your route:
const MyComponent = () => {
const location = useLocation();
const params = new URLSearchParams(location.search);
const code = params.get('code');
if (code === '123456789') {
return (<div>It Works!!!</div>);
} else {
return (<div>404 (you can redirect to an error page</div>);
}
}
Then, for your Router:
<Route exact path="/login-meli"><MyComponent /></Route>
I have the following URL:
http://my.site/?code=74e30ef2-109c-4b75-b8d6-89bdce1aa860
And I want to redirect to:
http://my.site#/homepage
I do that with:
import { push } from 'react-router-redux'
...
dispatch(push('/homepage'))
But react takes me to:
http://my.site/?code=74e30ef2-109c-4b75-b8d6-89bdce1aa860#/homepage
How can I tell React to drop the query param in the browser's address bar without reloading the application?
With Regular Expression maybe this solution is easier for you:
var oldURL = 'http://my.site/?code=74e30ef2-109c-4b75-b8d6-89bdce1aa860'
var newURL = /(http(s?).+)(\?.+)/.exec(oldDomain)[1] + 'homepage'
You can use newURL in your project.
Try this one (hardcode way)
import { push } from 'react-router-redux'
...
const url = `${window.location.origin}/homepage`;
dispatch(push(url));
Or this one
history.location.pathname =`${window.location.origin}/homepage`;
Those methods are not a good practice at all but those work (the second one for sure)
Read more
Per the following links, I git cloned this and ran npm install in both the main directory and the example basic directory, and ran npm start to get a working example.
https://github.com/reactjs/react-router-redux
https://github.com/reactjs/react-router-redux/tree/master/examples/basic
https://github.com/reactjs/react-router-redux#pushlocation-replacelocation-gonumber-goback-goforward
Code to remove query params (close to the bottom) and validate that they're removed:
import { createStore, combineReducers, applyMiddleware } from 'redux'
import { browserHistory } from 'react-router'
import { syncHistoryWithStore, routerReducer, routerMiddleware, push } from 'react-router-redux'
import * as reducers from './reducers'
const reducer = combineReducers({
...reducers,
routing: routerReducer
})
const middleware = routerMiddleware(browserHistory)
const store = createStore(
reducer,
applyMiddleware(middleware)
)
const history = syncHistoryWithStore(browserHistory, store)
// Dispatch from anywhere like normal.
store.dispatch(push("/?test=ok"));
// store2.dispatch(push(history.createHref("/")));
var count = 0;
history.listen(location => {
console.log(location);
if (count > 1) { return; }
count++;
store.dispatch(push({ pathname:'/', search: '' })); // Drops the params
});
Check console and you'll see the query params (search) field is empty
See my router section
<div>
<MainHeader />
<Switch>
<Route exact path='/:pickupLocationName/:pickupDate/:pickupTime/:returnDate/:returnTime' render={(props) => <Quote {...props} />} />
<Route path='/BookerInitial/:user/:id' component={BookerInitial} />
<Route path='/VehicleList' component={VehicleList} />
<Route path='/AdditionalCoverages' component={AdditionalCoverages} />
<Route path='/BookingDetails' component={BookingDetails} />
<Route path='/RenterInfo' component={RenterInfo} />
</Switch>
<Footer/>
</div>
There is next buton in BookerInitial page
<button className="submit btn-skew-r btn-effect" id="nextBtn" style={{ backgroundColor: "#da1c36" }} onClick={this.handleSubmit} >Next</button>
Submit button method
handleSubmit = () => {
this.props.history.push('./VehicleList');
}
I also faced this problem. finally i found the solution
i changed as this.props.history.push('/VehicleList');
from this.props.history.push('./VehicleList');
above you can see the dot(.) is the issue with my code. so no need to remove parameter in URL by code.
thank you
I've followed a couple of examples in an attempt to get access to a parameter from a Route in the React component that handles it. However the result of console.log on this.props from inside the render or componentDidMount is always {} when I'd expect it to contain the gameId from the gamesView route.
client.js which starts the Router:
// HTML5 History API fix for local
if (config.environment === 'dev') {
var router = Router.create({ routes: routes });
} else {
var router = Router.create({ routes: routes, location: Router.HistoryLocation });
}
router.run(function(Handler) {
React.render(
<Handler />,
document.getElementById('app')
);
});
routes.js with some routes removed for simplicity:
var routes = (
<Route name='home' path='/' handler={app}>
<DefaultRoute handler={home} location="/" />
<Route name='gamesView' path='/games/:gameId' handler={gamesView} />
</Route>
);
module.exports = routes;
...and app.js which wraps the other routes, I've tried it both with and without {...this.props} in the RouteHandler. If I console.log(this.props) from inside the render function here is also returns {}:
var App = React.createClass({
render: function() {
return (
<div className='container'>
<div className='row'>
<RouteHandler {...this.props} />
</div>
</div>
);
}
});
module.exports = App;
Finally the gamesView React component that I expect to see the props object. Here this.props is also {} and the following results in the error TypeError: $__0 is undefined var $__0= this.props.params,gameId=$__0.gameId;:
var GamesView = React.createClass({
render: function() {
var { gameId } = this.props.params;
return (
<div>
<h1>Game Name</h1>
<p>{gameId}</p>
</div>
);
}
});
module.exports = GamesView;
Anybody have any ideas?
You won't see those params until you are at the component defined in your router. App won't know anything about them. If you put the console.log(this.props.params) in your gamesView component, however, you should see them.
After discussing on the React Router (RR) Github it turned out this was due to me using an older version of RR (v0.11.6).
Looking at the example in the docs for that release it showed that I needed to use a Router.State mixin and then get the expected param via var gameId = this.getParams().gameId;.
Without upgrading RR the working version of my original example for GamesView becomes:
var React = require('react');
var Router = require('react-router');
var { Route, RouteHandler, Link } = Router;
var GamesView = React.createClass({
mixins: [ Router.State ],
render: function() {
var gameId = this.getParams().gameId;
return (
<div>
<h1>Game Name</h1>
<p>{gameId}</p>
</div>
);
}
});
module.exports = GamesView;