I am trying to use React Shepherd to create a walkthrough for my application.
I can't seem to find anything that explains how to switch routes within the tour. window.location.replace = "/someurl" refreshes the page and kills the tour completely. I am trying to achieve something along the lines of this
History.js
import { createBrowserHistory } from "history";
const history = createBrowserHistory();
export default history;
steps.js
import hist from "./History";
const Steps = [
{
//...
when: {
hide: () => {
hist.push("/someurl");
},
},
},
//...
]
export default Steps;
App.js
import React from "react";
import { Router } from "react-router";
import { Route } from "react-router-dom";
//...
import Steps from "./Steps";
import hist from "./History";
import "shepherd.js/dist/css/shepherd.css";
const tourOptions = {
defaultStepOptions: {
cancelIcon: {
enabled: true,
},
classes: "shepherd-theme-custom",
},
useModalOverlay: true,
};
const App = () => {
return (
<Router history={hist}>
<Route exact path="/signin" component={SignIn} />
<ShepherdTour steps={Steps} tourOptions={tourOptions}>
<PrivateRoute exact path="/*" component={Main} />
</ShepherdTour>
</Router>
);
};
export default App;
When the steps' hide function is called, the url path is switched but the page is not rendered. I am wondering if I am using react-router wrong or is there a different way to go about this?
So this actually had nothing to do with React Shepherd at all, this was purely a React Router issue. I mistakenly nested two BrowserRouter's since it was also accidentally included in my Main component. Once removed the application navigates with custom history file just fine.
Related
I've created the following very simple React app to simulate the behavior I'm seeing in my production app.
Root component:
import {
BrowserRouter as Router,
Route,
RouteComponentProps,
Switch,
} from "react-router-dom";
import { Home } from "./home";
import { SubRoute } from "./sub-route";
export default function Root(props) {
return (
<Router>
<Switch>
<Route path="/" exact>
<Home {...props} />
</Route>
<Route path="/sub-route" exact>
<SubRoute />
</Route>
</Switch>
</Router>
);
}
Home component:
import { LocationDescriptor } from "history";
import * as React from "react";
import { useHistory } from "react-router-dom";
import { TestLocationState } from "./sub-route";
export const Home = () => {
const history = useHistory();
return (
<>
<h1>Home</h1>
<button
onClick={() => {
const location: LocationDescriptor<TestLocationState> = {
pathname: "/sub-route",
state: {
name: "STATE PAYLOAD",
},
};
history.push(location);
}}
>
Pass state to sub route
</button>
</>
);
};
SubRoute component:
import * as React from "react";
import { useLocation } from "react-router-dom";
export type TestLocationState = {
name: string;
};
export const SubRoute = () => {
const { state } = useLocation<TestLocationState | undefined>();
return (
<>
<h1>Sub Route</h1>
<div>state: {JSON.stringify(state)}</div>
</>
);
};
In the dummy app, when I click the button in the Home component which calls history.push(), passing a location object with a state property, the state is not only successfully passed to the SubRoute component, but if I refresh or hard refresh, the state value is still available.
In my production app (a completely separate app that includes the above in addition to, of course a lot of other stuff), state is successfully passed to the SubRoute component, but it is not retained upon refresh. It is undefined.
I'm very confused as to what could be causing different behavior in my production app versus my test app. Is it a React Router thing? Thoughts?
It turns out that another developer on the team had added this line of code in part of the application that was wiping out the state:
https://github.com/preactjs/preact-router#redirects
I have a website made with Docusaurus v2 that currently contains documentation. However, I would like to add a page of a list of workflows where if a workflow in the list is clicked, the user would be shown a page of additional details of that workflow. For now it seems docusaurus.config seems to be handling most of the routing, but is there a way I can add a dynamic route like /workflows/:id? I made a separate standalone app which had a Router object and it worked if my App.js looks like this:
// App.js
import Navigation from './Navigation'
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom';
function App() {
return (
<Router>
<Navigation />
<Switch>
<Route path="/" exact component={Home}></Route>
<Route path="/workflows" exact component={Workflows}></Route>
<Route path="/workflows/:id" component={WorkflowItem}></Route>
</Switch>
</Router>
)
}
Is it possible to add the Router somewhere in Docusaurus?
Thanks!
I solved this by creating a simple plugin to add my own custom routes. Documentation here.
Let's call the plugin plugin-dynamic-routes.
// {SITE_ROOT_DIR}/plugin-dynamic-routes/index.js
module.exports = function (context, options) {
return {
name: 'plugin-dynamic-routes',
async contentLoaded({ content, actions }) {
const { routes } = options
const { addRoute } = actions
routes.map(route => addRoute(route))
}
}
}
// docusaurus.config.js
const path = require('path')
module.exports = {
// ...
plugins: [
[
path.resolve(__dirname, 'plugin-dynamic-routes'),
{ // this is the options object passed to the plugin
routes: [
{ // using Route schema from react-router
path: '/workflows',
exact: false, // this is needed for sub-routes to match!
component: '#site/path/to/component/App'
}
]
}
],
],
}
You may be able to use the above method to configure sub-routes as well but I haven't tried it. For the custom page, all you need is the Switch component (you are technically using nested routes at this point). The Layout component is there to integrate the page into the rest of the Docusaurus site.
// App.js
import React from 'react'
import Layout from '#theme/Layout'
import { Switch, Route, useRouteMatch } from '#docusaurus/router'
function App() {
let match = useRouteMatch()
return (
<Layout title="Page Title">
<Switch>
<Route path={`${match.path}/:id`} component={WorkflowItem} />
<Route path={match.path} component={Workflows} />
</Switch>
</Layout>
)
}
Okay, I've spent three hours driving myself crazy trying to figure this out. I'm sure I'm doing something wrong, but this is my first foray into React and I'm not sure exactly what the problem is.
My main app file looks like this:
import React, { PropTypes } from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import createHistory from 'history/createBrowserHistory'
import routes from './routes.jsx';
import map from 'lodash/map';
import MainLayout from './components/layouts/main-layout.jsx';
const history = createHistory();
const App = ({store}) => (
<Provider store={store}>
<Router history={history} basename="/admin-panel">
<Route component={MainLayout}>
<Switch>
{map(routes, (route, name) => (
<Route key={name} path={route.path} exact={route.exact} component={route.component} />
))}
</Switch>
</Route>
</Router>
</Provider>
);
App.propTypes = {
store: PropTypes.object.isRequired,
};
export default App;
The route is simply a JSON object that contains route info. Example:
import React from 'react';
import DashboardContainer from './components/containers/dashboard-container.jsx';
import AdminUserContainer from './components/containers/admin-users-container.jsx';
export default {
dashboard: {
order: 1,
path: '/',
exact: true,
name: 'Dashboard',
icon: 'tachometer',
component: DashboardContainer,
},
users: {
order: 2,
path: '/admin-users',
name: 'Admin Users',
icon: 'users',
component: AdminUserContainer,
}
};
(There's irrelevant stuff in the object; that's for the sidebar rendering, which also uses this object to render itself)
dashboard-component.jsx looks like this:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import Dashboard from '../views/dashboard.jsx';
class DashboardContainer extends Component {
render () {
return (<Dashboard />);
}
}
const mapStateToProps = function (store) {
return store;
};
export default connect(mapStateToProps)(DashboardContainer);
And dashboard.jsx looks like this:
import React, { Component } from 'react';
class Dashboard extends Component {
render () {
return (
<p>This is a test.</p>
);
}
}
export default Dashboard;
But for some reason, no matter what, the returned component is UnknownComponent. Apparently, nothing is matching on /admin-panel, and I'm not sure why. I've tried moving components around, merging the Routing stuff with MainLayout (which just contains the stuff that won't change every request, like the sidebar and the header), even tried the HashRouter instead of the BrowserRouter to see if that would do anything. No dice.
Can someone familiar with React tell me just what it is I'm not doing right here?
I am try to make inventory list app with react and redux , but I have a small problem with need some understanding about it .
I know we have few way to create react components and try to get as much as possible all information , but still not sure why this issue is happen .
I have app.js which is my all React-Router set there
console.log('The application has been start...');
import React from 'react'
import { render } from 'react-dom'
import { IndexPage } from './modules/IndexPage'
import { AddItemForm } from './modules/ui/AddItemForm'
import { PageNotFound } from './modules/PageNotFound'
import { Router, Route, hashHistory } from 'react-router'
import routes from './routes'
window.React = React
render(
<Router history={hashHistory}>
<Route path='/' component={IndexPage}/>
<Route path='/add' component={AddItemForm}/>
<Route path='*' component={PageNotFound}/>
</Router>,
document.getElementById('react-container')
)
for IndexPage and PageNotFound the router display and render correctly , But for add , is display blank page with no error .
import { PropTypes } from 'react'
const AddItemForm = ({ onNewItem=f=>f, router}) => {
//const AddItemForm = () => {
let _itemName
const submit = e => {
e.preventDefault()
onNewItem({
itemName: _itemName.value,
itemCount: 1
})
router.push('/')
_itemName.value = ''
}
return (
<form onSubmit={submit} >something
<label htmlFor="item-name"> Name Item</label>
<input id="item-name" type="text" ref={(input) => _itemName = input } />
<button>Add Item</button>
</form>
)
}
AddItemForm.propTypes = {
onNewItem: PropTypes.func,
router: PropTypes.object
}
export default AddItemForm
in order to make sure there is something wrong with React-Router or the components which I made I change the AddItemForm with below code
export const AddItemForm = () =>
<div>
<h1>Oops ! - The page is working!</h1>
</div>
which start working normally which shows something wrong with my components but I am not able to understand what is the main issue. Please give me hit or point where is the issue or what is the different ?
Here is link of github for full source of application so far
https://github.com/msc/Inventory-List
I tried to reproduce this issue. What I figured out is that you also need to import React for your component along with PropTypes.
So try importing React
import React, { PropTypes } from 'react'
in your AddItemForm component. Then worked for me.
I want 2 pages in my Chrome extension. For example: first(default) page with list of users and second with actions for this user.
I want to display second page by clicking on user(ClickableListItem in my case). I use React and React Router. Here the component in which I have:
class Resents extends React.Component {
constructor(props) {
super(props);
this.handleOnClick = this.handleOnClick.bind(this);
}
handleOnClick() {
console.log('navigate to next page');
const path = '/description-view';
browserHistory.push(path);
}
render() {
const ClickableListItem = clickableEnhance(ListItem);
return (
<div>
<List>
<ClickableListItem
primaryText="Donald Trump"
leftAvatar={<Avatar src="img/some-guy.jpg" />}
rightIcon={<ImageNavigateNext />}
onClick={this.handleOnClick}
/>
// some code missed for simplicity
</List>
</div>
);
}
}
I also tried to wrap ClickableListItem into Link component(from react-router) but it does nothing.
Maybe the thing is that Chrome Extensions haven`t their browserHistory... But I don`t see any errors in console...
What can I do for routing with React?
I know this post is old. Nevertheless, I'll leave my answer here just in case somebody still looking for it and want a quick answer to fix their existing router.
In my case, I get away with just switching from BrowserRouter to MemoryRouter. It works like charm without a need of additional memory package!
import { MemoryRouter as Router } from 'react-router-dom';
ReactDOM.render(
<React.StrictMode>
<Router>
<OptionsComponent />
</Router>
</React.StrictMode>,
document.querySelector('#root')
);
You can try other methods, that suits for you in the ReactRouter Documentation
While you wouldn't want to use the browser (or hash) history for your extension, you could use a memory history. A memory history replicates the browser history, but maintains its own history stack.
import { createMemoryHistory } from 'history'
const history = createMemoryHistory()
For an extension with only two pages, using React Router is overkill. It would be simpler to maintain a value in state describing which "page" to render and use a switch or if/else statements to only render the correct page component.
render() {
let page = null
switch (this.state.page) {
case 'home':
page = <Home />
break
case 'user':
page = <User />
break
}
return page
}
I solved this problem by using single routes instead of nested. The problem was in another place...
Also, I created an issue: https://github.com/ReactTraining/react-router/issues/4309
This is a very lightweight solution I just found. I just tried it - simple and performant: react-chrome-extension-router
I just had to use createMemoryHistory instead of createBrowserHistory:
import React from "react";
import ReactDOM from "react-dom";
import { Router, Switch, Route, Link } from "react-router-dom";
import { createMemoryHistory } from "history";
import Page1 from "./Page1";
import Page2 from "./Page2";
const history = createMemoryHistory();
const App: React.FC<{}> = () => {
return (
<Router history={history}>
<Switch>
<Route exact path="/">
<Page1 />
</Route>
<Route path="/page2">
<Page2 />
</Route>
</Switch>
</Router>
);
};
const root = document.createElement("div");
document.body.appendChild(root);
ReactDOM.render(<App />, root);
import React from "react";
import { useHistory } from "react-router-dom";
const Page1 = () => {
const history = useHistory();
return (
<button onClick={() => history.push("/page2")}>Navigate to Page 2</button>
);
};
export default Page1;
A modern lightweight option has presented itself with the package wouter.
You can create a custom hook to change route based on the hash.
see wouter docs.
import { useState, useEffect } from "react";
import { Router, Route } from "wouter";
// returns the current hash location in a normalized form
// (excluding the leading '#' symbol)
const currentLocation = () => {
return window.location.hash.replace(/^#/, "") || "/";
};
const navigate = (to) => (window.location.hash = to);
const useHashLocation = () => {
const [loc, setLoc] = useState(currentLocation());
useEffect(() => {
// this function is called whenever the hash changes
const handler = () => setLoc(currentLocation());
// subscribe to hash changes
window.addEventListener("hashchange", handler);
return () => window.removeEventListener("hashchange", handler);
}, []);
return [loc, navigate];
};
const App = () => (
<Router hook={useHashLocation}>
<Route path="/about" component={About} />
...
</Router>
);