I am using react router 3 to create a generic tab. The tab is working but is not too generic and flexible as for when changing from one tab to another the route is hardcoded in the onClick event handler as I want to route it programmatically. How could I make my route more flexible and generic? Also, one issue is here for the details tab I have to provide the empty value for "to" property otherwise clicking on details tab will redirect me to commissions/1/details/details
Here is the demo
https://codesandbox.io/s/1zk95w1vwj
Here is the code
const Overview = () => <x-text>Overview Section</x-text>;
const Details = props => <x-text>Details Section Here</x-text>;
const Home = () => (
<x-text>
Home Section
<Link to="commissions/1/details">List</Link>
</x-text>
);
const List = () => <Link to="commissions/1/details">List</Link>;
const App = () => (
<Router history={hashHistory}>
<Route path="/" component={Home} />
<Route key="list" path="list" component={List} />
<Route key="commissions" path="commissions">
<IndexRedirect to="list" />
<Route key="details" path=":id/details" component={CommissionsContainer}>
<IndexRoute component={Details} />
<Route path="overview" component={Overview} />
</Route>
</Route>
</Router>
);
const tabItems = [
{ id: 1, name: "Overview", to: "overview" },
{ id: 2, name: "Details", to: "" }, // have to provide empty for routing to details
{ id: 3, name: "Activity", to: "overview" },
{ id: 4, name: "Feed", to: "" }
];
const CommissionsContainer = props => {
return (
<div>
<FunctionalTab items={tabItems} activeItem={1} {...props} />
{props.children}
</div>
);
};
export default CommissionsContainer;
const FunctionalTab = ({ items, handleTabKey, ...props }) => {
console.log("props", props.router);
return (
<x-tab>
<x-tab-item>
{(items || []).map((item, idx) => (
<Link key={idx} onClick={() => handleTabKey(item)}>
{" "}
{item.name} <br />
</Link>
))}
<br />
</x-tab-item>
</x-tab>
);
};
const enhance = compose(
// handler responsible for changing the tab
withHandlers({
handleTabKey: props => route =>
props.router.push(`/commissions/${props.params.id}/details/${route.to}`) // this to be generic
})
);
export default withRouter(enhance(FunctionalTab));
Note: I am using react router 3
Related
When I click on the link in the HoverBooks Component to get to a new page where I can render the book location state in Book component, but when I press on it nothing happens. I think the error is in Route:
function App() {
return (
<div className="App">
<Router>
<Switch>
<Route path="/book:/book.Key">
<Book />
</Route>
<Route path="/signin">
<Signin />
</Route>
<Route path="/">
<Header />
<Home />
</Route>
</Switch>
</Router>
</div>
)
}
export default App
import React from 'react'
import { useLocation } from 'react-router-dom'
const Book = () => {
const {
state: { book },
} = useLocation()
console.log({ book })
return (
<div key={book.key}>
<h1>{book.bookName}</h1>
</div>
)
}
export default Book
const HoverBooks = ({ ...book }) => {
const [inHoverBooks, setInHoverBooks] = React.useState(false)
return (
<>
<Link
to={{
pathName: `/book/${book.key}`,
state: {
book,
},
}}
>
<img
onMouseLeave={() => setInHoverBooks(false)}
onMouseEnter={() => setInHoverBooks(true)}
src={book.image}
key={book.key}
/>
</Link>
{inHoverBooks && (
<div className="hover__containter">
<h3>{book.bookName}</h3>
<h2>{book.by}</h2>
<h2>{book.Narreted}</h2>
<h2>{book.length}</h2>
<h2>{book.rating}</h2>
</div>
)}
</>
)
}
export default HoverBooks
Below is the correct form, e.g. /:someName, to define a route with URL params:
<Route path="/book/:bookKey">
<Book />
</Route>
And here is the right syntax to make a Link for the above route:
<Link
to={{
pathname: `/book/SOME_BOOK_KEY`, // replace SOME_BOOK_KEY with some value
state: {
book, // e.g. const book = { key: 'js', bookName: 'Learn JavaScript'}
},
}}
>
<img src="some_src" alt="something" />
</Link>
And you useParams and useLocation react-hooks to access the "URL params" and "location state" in a component:
const Book = () => {
const {
state: { book },
} = useLocation()
const { bookKey } = useParams();
console.log(book, bookKey)
// prints "book" object (from location state) and "bookKey" (from URL params)
return (
<div key={book.key}>
<h1>{book.bookName}</h1>
</div>
)
}
I would suggest you to add typescript to your ReactJS app. It helps you find errors early by doing "static Type-checking".
With react router you need to pass the component you want to render to the Route like this
const ComponentA = (props) => {...}
<Route path="/component-a" component={ComponentA} />
And here is how to link to component a
<Link to="/component-a" >Go to component A</Link>
I'm building a small app with firebase and react and currently working on implementing the authentication. I've set the onAuthStateChanged in my app component as a side effect and whenever user is logged in it should be redirected to a desired component from ProtectedRoute.
This works correctly but unfortunately when refreshing the page the ProtectedRoute is not rendering correct component and is just firing redirection.
I get what is happening: on refresh user is empty and only after then it change so I would expect to see a screen flicker and a proper redirection.
Could you please look at below code and maybe tell me how to fix this behavior?
App component:
const App = () => {
const [authUser, setAuthUser] = useState<firebase.User | null>(null);
const Firebase = useContext(FirebaseContext);
useEffect(() => {
const authListener = Firebase!.auth.onAuthStateChanged((authUser) => {
authUser ? setAuthUser(authUser) : setAuthUser(null);
});
return () => authListener();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<AuthUserContext.Provider value={authUser}>
<Router>
<div>
<Navigation />
<hr />
<Route exact path={ROUTES.LANDING} component={Landing} />
<Route exact path={ROUTES.SIGN_UP} component={SignUpPage} />
<Route exact path={ROUTES.SIGN_IN} component={SignIn} />
<Route
exact
path={ROUTES.PASSWORD_FORGET}
component={PasswordForget}
/>
<ProtectedRoute exact path={ROUTES.HOME} component={Home} />
<ProtectedRoute exact path={ROUTES.ACCOUNT} component={Account} />
<Route exact path={ROUTES.ACCOUNT} component={Account} />
<Route exact path={ROUTES.ADMIN} component={Admin} />
</div>
</Router>
</AuthUserContext.Provider>
);
};
Protected Route:
interface Props extends RouteProps {
component?: any;
children?: any;
}
const ProtectedRoute: React.FC<Props> = ({
component: Component,
children,
...rest
}) => {
const authUser = useContext(AuthUserContext);
return (
<Route
{...rest}
render={(routeProps) =>
!!authUser ? (
Component ? (
<Component {...routeProps} />
) : (
children
)
) : (
<Redirect
to={{
pathname: ROUTES.SIGN_IN,
state: { from: routeProps.location },
}}
/>
)
}
/>
);
};
Found the fix. Had to add the flag checking for user authentication status (default value of that flag is set to true). Flag needs to be passed to ProtectedRoute as prop and if is True then render some loading component:
App component:
const App = () => {
const [authUser, setAuthUser] = useState(false);
const [authPending, setAuthPending] = useState(true);
const Firebase = useContext(FirebaseContext);
useEffect(() => {
const authListener = Firebase!.auth.onAuthStateChanged((authUser) => {
setAuthUser(!!authUser);
setAuthPending(false);
});
return () => authListener();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<AuthUserContext.Provider value={authUser}>
<Router>
<div>
<Navigation />
<hr />
<Switch>
<Route exact path={ROUTES.LANDING} component={Landing} />
<Route exact path={ROUTES.SIGN_UP} component={SignUpPage} />
<Route exact path={ROUTES.SIGN_IN} component={SignIn} />
<Route
exact
path={ROUTES.PASSWORD_FORGET}
component={PasswordForget}
/>
<ProtectedRoute
pendingAuth={authPending}
exact
path={ROUTES.HOME}
component={Home}
/>
<ProtectedRoute
pendingAuth={authPending}
exact
path={ROUTES.ACCOUNT}
component={Account}
/>
<Route exact path={ROUTES.ACCOUNT} component={Account} />
<Route exact path={ROUTES.ADMIN} component={Admin} />
</Switch>
</div>
</Router>
</AuthUserContext.Provider>
);
};
ProtectedRoute:
interface Props extends RouteProps {
component?: any;
children?: any;
pendingAuth: boolean;
}
const ProtectedRoute: React.FC<Props> = ({
component: Component,
children,
pendingAuth,
...rest
}) => {
const authUser = useContext(AuthUserContext);
if (pendingAuth) {
return <div>Authenticating</div>;
}
return (
<Route
{...rest}
render={(routeProps) =>
!!authUser ? (
Component ? (
<Component {...routeProps} />
) : (
children
)
) : (
<Redirect
to={{
pathname: ROUTES.SIGN_IN,
state: { from: routeProps.location },
}}
/>
)
}
/>
);
};
I am building an ecommerce website which includes a cart for each user. I want to pass the state of cart to a component using props
In App.js -
<Header cart={cart} />
In Header.js -
function Header({ cart }) {
console.log(cart);
}
I have checked console.log(cart) in App.js and it gives me the desired and correct array of objects.
I pass in this state as a prop to Header Component and then I want to display the number of items in the cart.
But, the console.log(cart) in Header.js gives me undefined.
What am I doing wrong here? Thanks in advance.
App.js -->
function App() {
const [{ bag }, dispatch] = useStateValue();
const [cart, setCart] = useState([]);
console.log("CART -", cart);
useEffect(() => {
//will only load when app component loads
auth.onAuthStateChanged((authUser) => {
console.log("USER ->", authUser);
if (authUser) {
dispatch({
type: "SET_USER",
user: authUser,
});
db.collection("users")
.doc(authUser.uid)
.collection("cart")
.onSnapshot((snapshot) =>
setCart(
snapshot.docs.map((doc) => ({
id: doc.data().id,
image: doc.data().image,
productName: doc.data().productName,
productPrice: doc.data().productPrice,
}))
)
);
} else {
dispatch({
type: "SET_USER",
user: null,
});
}
});
}, []);
return (
<Router>
<div className="App">
<Switch>
<Route exact path="/payment">
<Header cart={cart} />
<Elements stripe={promise}>
<Payment />
</Elements>
</Route>
<Route exact path="/login">
<Login />
</Route>
<Route exact path="/checkout">
<Header />
{/* <CategoriesHeader /> */}
<Checkout />
</Route>
<Route exact path="/">
<Header />
<CategoriesHeader />
<Home />
</Route>
</Switch>
</div>
</Router>
);
}
export default App;
I'm using react-router-dom and generate my routes dynamically from an array like so
Routes Data
const routes = [
{
path: '/',
exact: true,
component: () => <MyComponent />
}
}
Route Generation
{routes.map((route, index) => {
return <Route
key={index}
path={route.path}
exact={route.exact}
component={route.component}
/>
})}
Render Prop
I found out about the render() prop I could define but even so how do I do this since the component is inside a variable
const props = this.props;
{routes.map((route, index) => {
return <Route
key={index}
path={route.path}
exact={route.exact}
render={(props) => <??? {...props} />
/>
})}
How can I pass this.props during the route generation?
You could change your array to just include the component instead of a new function component that renders the component and you will get the props sent to it:
const routes = [
{
path: '/',
exact: true,
component: MyComponent
}
}
You can use any capitalized variable as a component, so you could also do this if you prefer:
{routes.map((route, index) => {
const Component = route.component;
return <Route
key={index}
path={route.path}
exact={route.exact}
render={(props) => <Component {...props} />
/>
})}
So this question is more about the approach I should take to solve this problem.
I have a JSON like this
const app = [
{
tab: 'Home',
// content: '<div>This is home</div>',
nav: [
{
tab: 'Dashboard',
content: '<div>This is Dashboard</div>'
},
{
tab: 'Profile',
content: '<div>This is Profile</div>'
},
]
},
{
tab: 'About',
content: '<div>This is About</div>'
},
{
tab: 'Pricing',
content: '<div>This is Pricing</div>'
},
];
Now what I would want is setup the entire routes and pages to render the above JSON.
Below is the pseudo code:
Approach 1...
Loop through and for each nav/subnav add Route.
import {
BrowserRouter,
Route,
NavLink,
} from 'react-router-dom';
const Page = (content) => {
return (
<div>{content}</div>
)
}
<BrowserRouter>
app.map((nav, i) =>
<NavLink key={nav.tab} to={`/${nav.tab}`} activeClassName="active">
{nav.tab}
</NavLink>
<Route
path={`/${nav.tab}`} render={(props) => (
<Page {...props} pageData={nav.content}>
if(nav.subNav) {
nav.subNav.map((subnav, i) =>
<NavLink key={subnav.tab} to={`/${nav.tab}/${subnav.tab}`} activeClassName="active">
{nav.tab}
</NavLink>
<Route
path={`/${nav.tab}/${subnav.tab}`} render={(props) => (
<Page {...props} pageData={subnav.content} />
)}
/>
)
}
</Page>
)}
/>
)
</BrowserRouter>
Approach 2
Would something like this work/be better? Just have 2 routes returning same component, pass the app object to Page and then based on URL render the corresponding data
const Page = (app) => {
return (
// Loop over app
if(mainNav from URL == tab) {
if(subNav from URL == tab) {
<div>{content}</div>
}
}
)
}
<BrowserRouter>
<Route
path={'/:mainNav'} render={(props) => (
<Page {...props} pageData={app} />
)}
/>
<Route
path={'/:mainNav/:subNav'} render={(props) => (
<Page {...props} pageData={app} />
)}
/>
</BrowserRouter>
Thanks for reading through and appreciate your help. If theres another better approach I would love to know!
Try utilizing the exact prop offered in your <Route /> components.
<Route exact path="/foo" component={Foo} />
If you have the routes:
/foo
/foo/bar
/foo/bar/baz
/foo matches in all three cases.