I have been working on a complex application using REACT.js for the past few months and have run into a problem that I have been unable to solve despite extensive research and experimentation.
The issue is that I am trying to implement dynamic routing in my application, where the routes are determined by data that is fetched from an API. I have been using the react-router-dom library to handle my routing, and have successfully implemented static routes. However, when I try to use dynamic routes based on the data fetched from the API, the application throws an error.
I have tried several different approaches to resolving this issue, including using the useEffect hook to fetch the data and update the routes, and also using a library such as react-dynamic-route to handle the dynamic routing. However, I have not been able to get any of these approaches to work.
Here is an example of the code I am currently using to fetch the data and update the routes:
import { useEffect, useState } from 'react';
import { fetchData } from './api';
import {
BrowserRouter as Router,
Route,
Switch,
useLocation
} from 'react-router-dom';
const App = () => {
const [data, setData] = useState([]);
const location = useLocation();
useEffect(() => {
const fetchAPI = async () => {
setData(await fetchData());
};
fetchAPI();
}, [location]);
return (
<Router>
<Switch>
{data.map((item) => (
<Route key={item.id} path={`/${item.id}`} component={DetailPage} />
))}
<Route component={NotFound} />
</Switch>
</Router>
);
};
export default App;
I would greatly appreciate any insight or advice on how to properly implement dynamic routing in a REACT.js application using data fetched from an API. Any help would be greatly appreciated. Thank you.
There are several things in your code that I believe are incorrect. Firstly Switch is outdated, if you are using an older version of react-router-dom you may be able yo use it, instead use Routes. The component attribute was deprecated before v5.1 I believe and cannot be used in newer versions of react, instead use the element attribute. Also it is important to note that this attribute need to have the name in tags.
<Route
key={item.id}
path={`/${item.id}`}
element={<DetailPage />}
/>
Also useLocation may only be used in the context of a router, meaning that it can be used in one of the element that you are routing to but unless you have a route for the App component it's not going to work.
I've made a small example to show what your code could look like to make it work.
import { useEffect, useState } from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
const App = () => {
const [data, setData] = useState([]);
useEffect(() => {
fetch('http://localhost:3000/testItems')
.then((res) => res.json())
.then((res) => setData(res));
}, []);
return (
<Router>
<Routes>
{data.map((item: { id: number; idk: string }) => (
<Route
key={item.id}
path={`/${item.id}`}
element={<Comp />}
/>
))}
<Route element={<p>This seems wrong</p>} />
</Routes>
</Router>
);
};
export default App;
function Comp() {
return <div>Hello</div>;
}
I've tested the code with a local server for the fetch. The server has the following data.
{
"testItems": [
{
"id": 1,
"idk": "other stuff"
},
{
"id": 2,
"idk": "not good at naming stuff"
}
]
}
Related
<Route path="*">
<Redirect to="/" />
</Route>
The code above is what I am using to handle all not found routes. I have all routes in switch and for the most part this is working fine <Switch>. I was wondering how I would go about sending a windows alert saying something along the lines of "this page doesn't exist" when the unknown route is called. I tried using render and calling a function, but that didn't seem to work. Any thoughts?
edit: to make it clear, I am using React framework, javascript, react-router.
Your current implementation simply redirects back to (/) when the route is not found.
However, to display a window.alert() you should consider making a new NotFound component and handle the display of the alert on that page.
Afterward, use the useHistory hook to redirect back to '/'
See example code below
NotFound Component
import { useEffect } from "react";
import { useHistory } from "react-router-dom";
const NotFound = () => {
let history = useHistory();
useEffect(() => {
window.alert('Page Not found');
history.push('/');
}, []);
return (
<div>404</div>
)
}
export default NotFound;
Existing Routes
<Route path="*">
<NotFound />
</Route>
I have managed to set up Google Analytics for my React application with the ReactGA library so it will send the pageview to analytics when the user navigates around.
The problem
The problem I'm facing is that I'm not sending any analytics to google on the initial page load, since the history.listen method only fires when the location changes.
My setup
In the root of my project, I initialize the connection:
const history = require("history").createBrowserHistory;
import { Router } from "react-router-dom"
ReactGA.initialize(envConstants.ANALYTICS_TRACKING_ID);
const MyApp = () => (
<Router history={history}>
<MyRoutes />
</Router>
)
Since I only want to see which routes the users are on I have this in my router:
const MyRoutes = props => {
props.history.listen(location => {
// won't see this on initial load, but navigating to another route will give me this
console.log("LISTENING")
})
return (...)
}
So I wonder how I can come around this and send the first/ initial pageview when a user comes to my site. I believe I cannot achieve this with the history.listen method. So, I guess we have to add some other functionality that I'm not too sure of.
I appreciate all the help I can get with this. And if there's something unclear, please let me know.
Thanks for reading and have a nice day!
The issue is that history listen is not called on initial page load since it's only called when the location changes. Try something like the following
import { Router } from 'react-router-dom';
import createHistory from 'history/createBrowserHistory';
import ReactGA from 'react-ga';
const trackPageView = location => {
ReactGA.set({ page: location.pathname });
ReactGA.pageview(location.pathname);
};
const initGa = history => {
ReactGA.initialize('UA-XXXXXX-X', {
debug: true
});
trackPageView(history.location);
history.listen(trackPageView);
};
const history = createHistory();
initGa(history);
ReactDOM.render((
<Router history={history}>
<Layout />
</Router>
), document.getElementById('root'));
I've been working on trying to modularize my React.js app (that will be delivered as a Desktop app with Electron) in a way that if I make a new module in the future, I can just add a new folder and modify a couple of files and it should integrate fine.
I got originally inspired by this article: https://www.nylas.com/blog/react-plugins/
After that point, I started doing as much research as I could and ended up creating a JSON file that would live in the server with a manifest of the plugins that are registered for that specific client.
Something like this:
{
"plugins": [
{
"name": "Test Plugin",
"version": "0.0.1",
"path": "testplugin",
"file": "test",
"component":"TestPlugin"
},
{
"name": "Another Plugin",
"version": "0.0.1",
"path": "anothertest",
"file": "othertest",
"component":"TestPluginDeux"
}
]
}
After that, I made a couple folders that match the path value and that contain a component that matches the name in the manifest (e.g. testplugin/test.jsx that exports the TestPlugin component as a default). I also made a pluginStore file that reads the manifest and mounts the plugins in the this.state.
Then, did a ton of research on Google and here and found this answer: React - Dynamically Import Components
With that function, I was able to iterate through the manifest, find the folders in the directory, and mount the plugins in the this.state by running the mountPlugins() function I had created in the pluginStore, inside a componentDidMount() method in my homepage.
So far so good. I'm using React-Router and I was able to mount the plugins dynamically in the State and able to load them in my Home Route by just calling them like this: <TestPlugin />.
The issue that I have now, is that I wanted to dynamically create Routes that would load these components from the state, either by using the component or the render method, but I had no luck. I would always get the same result... Apparently I was passing an object instead of a String.
This was my last iteration at this attempt:
{this.state.modules.registered.map((item) =>
<Route exact path={`/${item.path}`} render={function() {
return <item.component />
}}></Route>
)}
After that, I made a Route that calls a PluginShell component that is called by a Navlink that sends the name of the plugin to inject and load it dynamically.
<Route exact path='/ex/:component' component={PluginShell}></Route>
But I ended having the same exact issue. I'm passing an object and the createElement function expected a string.
I searched all over StackOverflow and found many similar questions with answers. I tried applying all the possible solutions with no luck.
EDIT:
I have put together a GitHub repo that has the minimal set of files to reproduce the issue.
Here's the link:
https://codesandbox.io/embed/aged-moon-nrrjc
Okey pokey. There are a lot of moving parts here that can be vastly simplified.
I'd recommend moving toward a more developer-friendly, opinionated state store (like Redux). I've personally never used Flux, so I can only recommend what I have experience with. As such, you can avoid using plain classes for state management.
You should only import the modules ONCE during the initial application load, then you can dispatch an action to store them to (Redux) state, then share the state as needed with the components (only required if the state is to be shared with many components that are spread across your DOM tree, otherwise, not needed at all).
Module imports are asynchronous, so they can't be loaded immediately. You'll have to set up a condition to wait for the modules to be loaded before mapping them to a Route (in your case, you were trying to map the module's registered string name to the route, instead of the imported module function).
Module imports ideally should be contained to the registered modules within state. In other words, when you import the module, it should just overwrite the module Component string with a Component function. That way, all of the relevant information is placed within one object.
No need to mix and match template literals with string concatenation. Use one or the other.
Use the setState callback to spread any previousState before overwriting it. Much simpler and cleaner looking.
Wrap your import statement within a try/catch block, otherwise, if the module doesn't exist, it may break your application.
Working example (I'm just using React state for this simple example, I also didn't touch any of the other files, which can be simplified as well):
App.js
import React from "react";
import Navigation from "./components/MainNavigation";
import Routes from "./routes";
import { plugins } from "./modules/manifest.json";
import "./assets/css/App.css";
class App extends React.Component {
state = {
importedModules: []
};
componentDidMount = () => {
this.importPlugins();
};
importPlugins = () => {
if (plugins) {
try {
const importedModules = [];
const importPromises = plugins.map(plugin =>
import(`./modules/${plugin.path}/${plugin.file}`).then(module => {
importedModules.push({ ...plugin, Component: module.default });
})
);
Promise.all(importPromises).then(() =>
this.setState(prevState => ({
...prevState,
importedModules
}))
);
} catch (err) {
console.error(err.toString());
}
}
};
render = () => (
<div className="App">
<Navigation />
<Routes {...this.state} />
</div>
);
}
export default App;
routes/index.js
import React from "react";
import React from "react";
import isEmpty from "lodash/isEmpty";
import { Switch, Route } from "react-router-dom";
import ProjectForm from "../modules/core/forms/new-project-form";
import NewPostForm from "../modules/core/forms/new-post-form";
import ProjectLoop from "../modules/core/loops/project-loop";
import Home from "../home";
const Routes = ({ importedModules }) => (
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/projectlist/:filter" component={ProjectLoop} />
<Route exact path="/newproject/:type/:id" component={ProjectForm} />
<Route exact path="/newpost/:type" component={NewPostForm} />
{!isEmpty(importedModules) &&
importedModules.map(({ path, Component }) => (
<Route key={path} exact path={`/${path}`} component={Component} />
))}
</Switch>
);
export default Routes;
You've got the right idea, if anything I guess your syntax is slightly off. I didn't have to tweak much from your example to get dynamic routing work.
Here's a working example of what I think you want to do:
const modules = [{
path: '/',
name: 'Home',
component: Hello
},{
path: '/yo',
name: 'Yo',
component: Yo
}];
function DynamicRoutes() {
return (
<BrowserRouter>
{ modules.map(item => <Route exact path={item.path} component={item.component}/>) }
</BrowserRouter>
);
}
https://stackblitz.com/edit/react-zrdmcq
I think the problem is the way you are trying to render <item.component /> but not sure, did you get the same error whit this?
Try:
<Route exact path={`/${item.path}`} render={function() {
return React.createElement(item.component, props)
}}></Route>
)}
I am currently unable to find a solution to this problem.
I have a React Component that is connected to React Router 4, Redux store and is wrapped by two HOCs. Its pretty crazy, but this is how it was coded.
Here is the export to give you an idea:
export default withFetch(Component)(fetchData, mapStateToProps)
I am trying to run some basic tests on it:
it('should render self and subcomponents', () => {
const wrapper = shallow(<Component {...props} />)
expect(toJson(wrapper)).toMatchSnapshot()
})
Which outputs a console.log/snapshot of:
<Route render={[Function: render]} />
Things tried but no succeed:
I've tried wrapping my component in the Memory Router
Supply a redux store to the component
Used .dive() and .chilndren() to try and see the children
Tried mount and render with no success.
Still keeps rendering the <Route render={[Function: render]} />
Trying out :
<MemoryRouter>
<Component {...props} />
</MemoryRouter>
Still produces the same result.
Note that I've also tried importing my component as
import { Component } from './components/'
But it returns undefined.
Any help is deeply appreciated. Thank you! 😊🙏
I assume that by <Router> you are referring to BrowserRouter.
The best way is to isolate the wrapped component and test it with testing alternatives.
For example assume that you want to test that:
// App.jsx
export const App = () =>
<Router>
<ReduxProvider>
<AppInner>
</ReduxProvider>
</Router>
My suggestion is to test AppInner with testing env of Router & ReduxProvider.
In tests:
// AppInner.test.jsx
import {mount} from 'enzyme';
import {MemoryRouter} from 'react-router';
describe('AppInner', () => {
it('should do something', () => {
const TestingComponent = () =>
<MemoryRouter>
<ReduxProvider>
<AppInner />
<ReduxProvider>
<MemoryRouter>;
const component = mount(TestingComponent);
});
})
Pay attention that I've wrapped the AppInner with MemoryRouter, it allows your mimic router but without the dependency of the browser.
For more info you can read the testing section of react-router;
I can see in this file (https://github.com/ReactTraining/react-router/blob/v0.13.3/modules/createRouter.js) that there is a refresh function but I have no idea how to call it. I'm fairly new to react-router, I've only used it to move between some pages a couple times using hashHistory.
Right now I am trying to use it so that when an install fails, the user is given the option to 'retry' which I plan to execute by refreshing the page where the install happens (the page the user would be currently on). Any help would be appreciated.
This is a node app that runs on electron, not a web app.
firstly, add react-router as a dependency
yarn add react-router or npm install react-router
Then (for react-router v5)
import { useHistory } from 'react-router'
const history = useHistory()
// then add this to the function that is called for re-rendering
history.go(0)
This causes your page to re-render automatically
For react-router v6 use the useNavigate hook instead:
import { useNavigate } from 'react-router'
const navigate = useNavigate()
// refresh
navigate(0)
If you're using react-router v6
import { useNavigate } from "react-router-dom";
const navigate = useNavigate();
const refreshPage = () => {
navigate(0);
}
You can use this to refresh Current route:
import createHistory from 'history/createBrowserHistory'
const history = createHistory();
history.go(0)
You don't really need react-router for this. You can just use location.reload:
location.reload();
Also that version of react-router you linked to is very old, I think it's linking to v1 when it's currently on v4.
I guess that you're using react-router.
I'll copy my answer from another post.
So you have few possibilities to do that, currently my favorite way to do that is using anonymous function in component prop:
<Switch>
<Route exact path="/" component={()=><HomeContainer/>} />
<Route exact path="/file/:itemPath/:refHash" component={()=><File/>} />
<Route exact path="/:folderName" component ={()=><Folder/>}/>
</Switch>
Or if you want to refresh with current url params, you'll need extra route (reload), and play a little with router stack:
reload = ()=>{
const current = props.location.pathname;
this.props.history.replace(`/reload`);
setTimeout(() => {
this.props.history.replace(current);
});
}
<Switch>
<Route path="/reload" component={null} key="reload" />
<Route exact path="/" component={HomeContainer} />
<Route exact path="/file/:itemPath/:refHash" component={File} />
<Route exact path="/:folderName" component ={Folder}/>
</Switch>
<div onClick={this.reload}>Reload</div>
React
window.location.reload();
working
if you want to re-fetch the data just do the below:
import { useLocation } from 'react-router'
const location = useLocation()
useEffect(() => {
fetchData()
}, [location.key])
I know that this is old, but I found a simple solution according to the documentation of react-router.
Just put that attribute on your Router, and whenever you are on a new Path it will force the page to reload itself.
<Router forceRefresh={true}>
Source:
https://reactrouter.com/web/api/BrowserRouter/forcerefresh-bool
This solution won't cause the undesired full page reload but requires you to make this modification to each page that needs refreshing:
export const Page = () => {
const location = useLocation();
return <PageImpl key={location.key} />
}
So the idea is: create a wrapper around your page and make React re-create the actual page every time the location key changes.
Now it's enough to call history.push(/this-page-route) again and the page refreshes.
If you want to use <Link/> to reload some route, or simply have single history push, you can setup <Redirect/> route under <Switch/> like this:
<Switch>
<Route exact path="/some-route" component={SomeRoute} />
<Redirect exact from="/some-route/reload" to="/some-route" />
</Switch>
And then <Link to="/some-route/reload" /> or push("/some-route/reload")
If you don't want to reload all scripts again you can replace the current path with a fake/empty path and replace it again with the current path like this
// ...
let currentPath = window.location.pathname;
history.replace('/your-empty-route');
setTimeout(() => {
history.replace(currentPath)
}, 0)
// ...
Update:
If the changing of the address bar bothering, you can add a patterned route like this:
<Route path="/*/reload" component={null}/>
and add /replace to the end of currentPath to replace the router with null component. like this:
// ...
let currentPath = window.location.pathname;
history.replace(`${currentPath}/replace`);
setTimeout(() => {
history.replace(currentPath)
}, 0)
// ...
In this way, the reload keyword will add to the end of your current path and I think it's more user friendly.
Notice: If you already have a route that ends with replace It will cause conflict. To solve that you should change the path of the patterned route to something else.
You could try this workaround:
// I just wanted to reload a /messages page
history.pushState(null, '/');
history.pushState(null, '/messages');
You can use this function.
function reloadPage(){
window.location.reload();
}
<input type="button" onClick={ reloadPage } value="reload"/>
May be you are trying to push in history object, then bind your component with withrouter or use window.location.href = url to redirect ..
With React Router 6 you can simply write :
import {useNavigate} from "react-router-dom";
const navigate = useNavigate()
const goToPageOnClick = () =>{
navigate(target_url)
navigate(0)
}
You can achieve that with React Router v6.
import React from 'react';
import { useNavigation, useLocation } from 'react-router-dom';
const Component = () => {
const history = useNavigation();
const location = useLocation();
const reload = () => {
navigate(location.pathname);
};
return (
...
);
};
and then put your reload function inside a useEffect hook.
PS: but this is a weird question, since react-router reloads the page automatically.
If you are needing an asynchronous reload, use history.go(0) (it wraps the History.go() method).
If you need to reload the page synchronously, use history.push(location.pathname) (it wraps the History.pushState() method).
Since there are already examples here using history.go(0), here's an example using history.push(location.pathname):
import React from 'react';
import { useHistory, useLocation } from 'react-router-dom';
const Component = () => {
const history = useHistory();
const location = useLocation();
const reload = () => {
history.push(location.pathname);
};
return (
...
);
};
update webpacker.yml
devServer: {
historyApiFallback: true,
}
Well, the easiest way is to first identify a route for reload and thereafter call the window.location.reload() function on the route like so:
<Switch>
<Route exact exact path="/" component={SomeComponent} />
<Route path="/reload" render= {(props)=>window.location.reload()} />
</Switch>
I recently had the same problem and created this(https://github.com/skt-t1-byungi/react-router-refreshable).
<Refreshable>
<Switch>
<Route path="/home">
<HomePage />
</Route>
<Route path="/post">
<PostPage />
</Route>
{/* ... */}
</Switch>
</Refreshable>