Rewriting code with useParams, deconstructuring not going well - react-router

Trying to upgrade react router to version 5.1.
I have a problem with the useParams, the exampleId is not correct. I'm definatelly missing something?
My Route file with the broken exampleId:
export default function MyExample() {
const { url } = useRouteMatch()
const history = useHistory()
const exampleCreateQuery = useExampleCreate()
return (
<>
<Header url={url} />
<Switch>
<Route path={url} exact>
</Route>
<Route path={`${url}/:id`} children={exampleId !== undefined ? <ExampleThing exampleId={exampleId} /> : null} />
</Switch>
</>
)
}
The route where the id comes from
export default function ExampleThing() {
const { id: exampleId } = useParams<{ id: string }>()
const exampleQuery = useExample(exampleId, { useErrorBoundary: false })
return (
<>
</>
)
}

Related

CartItems routing error using React-Router-Dom v6

I am trying to introduce 'cartItems' functionality to my react-redux app and store the added data in the browser's local storage.
Indeed the problem raises when I try to show cart items by clicking on the cart link at the navbar section. The error message is 'GET http://localhost:3000/products/undefined 500 (Internal Server Error)' and 'Uncaught (in promise)'. and I don't know how to fix the issue.
Note: the same component 'CartScreen.js' would display the cart items in both cases, when adding new items to the cart & when also clicking on the cart link at the navbar.
Please follow the code snippets
Thanks & Regards
App.js
import Header from './components/Header';
import { Container } from 'react-bootstrap';
import HomeScreen from './screens/HomeScreen';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import ProductScreen from './screens/ProductScreen';
import CartScreen from './screens/CartScreen';
function App() {
return (
<Router>
<Header />
<main className="py-3">
<Container>
<Routes>
<Route path="/" element={<HomeScreen />} exact />
<Route path="/product/">
<Route path=":id" element={<ProductScreen />} />
<Route index element={<ProductScreen />} />
</Route>
<Route path="/cart" >
<Route index element={<CartScreen />} />
<Route path=":productid" element={<CartScreen />} />
</Route>
</Routes>
</Container>
</main>
<Footer />
</Router>
);
}
export default App;
ProductScreen.js
import { useParams, Link, useNavigate } from 'react-router-dom';
import {Row,Col,Image,ListGroup,Button,Card,Form} from 'react-bootstrap';
import Rating from '../components/Rating';
import { listProductDetails } from '../actions/productActions';
import { useDispatch, useSelector } from 'react-redux';
import Loader from '../components/Loader';
import Message from '../components/Message';
function ProductScreen() {
const { id } = useParams();
const navigate = useNavigate();
const [qty, setQty] = useState(1);
const dispatch = useDispatch();
const productListDetail = useSelector((state) => state.productDetail);
const { loading, error, product } = productListDetail;
useEffect(() => {
dispatch(listProductDetails(id));
}, [dispatch, id]);
const addToCartHandler = () => {
navigate(`/cart/${id}?qty=${qty}`);
};
return (
<div> <Link to={-1} className="btn btn-primary my-3">Go Back</Link>
{loading ? (<Loader />): error ? (<Message variant="danger">{error}</Message>) : (
<Row>
<Col md={6}>
<Image src={product.image} alt={product.name} fluid />
</Col>
<Col md={3}>
<ListGroup variant="flush">
<ListGroup.Item>
<h3> {product.name}</h3>
</ListGroup.Item>
<ListGroup.Item>
<Rating value={product.rating} text={`${product.numReviews} reviews`}
color={'#fae500'}/>
</ListGroup.Item>
<ListGroup.Item>Price: ${product.price}</ListGroup.Item>
<ListGroup.Item>
Description: {product.description}
</ListGroup.Item>
</ListGroup>
</Col>
<Col md={3}>
<Card>
<ListGroup variant="flush">
<ListGroup.Item>
<Row>
<Col> Price: </Col>
<Col>
<strong>${product.price} </strong>
</Col>
</Row>
</ListGroup.Item>
<ListGroup.Item>
<Row>
<Col> Status: </Col>
<Col>
<strong>
{product.countInStock > 0 ? 'In Stock' : 'Out of Stock'}
</strong>
</Col>
</Row>
</ListGroup.Item>
{product.countInStock > 0 && (
<ListGroup.Item>
<Row>
<Col> Qty </Col>
<Col xs="auto" className="my-1">
<Form.Control as="select" value={qty}
onChange={(e) => setQty(e.target.value)}>
{[...Array(product.countInStock).keys()].map((x) => (
<option key={x + 1} value={x + 1}>{x + 1}</option>))}
</Form.Control>
</Col>
</Row>
</ListGroup.Item>)}
<ListGroup.Item>
<Button onClick={addToCartHandler}
className="btn btn-primary container-fluid"
disabled={product.countInStock === 0}
type="button">
Add to Cart
</Button>
</ListGroup.Item>
</ListGroup>
</Card>
</Col>
</Row>
)}
</div>
);
}
export default ProductScreen;
CartScreen.js
import React, { useEffect } from 'react';
import { Col, ListGroup,Row,} from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation, useNavigate, useParams, Link, Outlet } from 'react-router-dom';
import { addToCart } from '../actions/cartAction';
import Message from '../components/Message';
const CartScreen = () => {
const { search } = useLocation();
const { productid } = useParams();
const qty = search ? Number(search.split('=')[1]) : 1;
const dispatch = useDispatch();
const cart = useSelector((state) => state.cart);
const { cartItems } = cart;
useEffect(() => {
dispatch(addToCart(productid, qty));
},[dispatch, productid, qty]);
return (
<Row>
<Col md={8}> {cartItems.length === 0 ? (<Message variant="info">
Go Back To Home Page <Link to="/"></Link> </Message> ) : (
<ListGroup> {cartItems.map((x) => (
<ListGroup.Item key={x.product}>
{x.name} , {x.qty}
</ListGroup.Item> ))}
</ListGroup>)}
</Col>
<Col md={4}></Col>
</Row>
);
};
export default CartScreen;
cartReducers.js
import { CART_ADD_ITEM } from '../constants/cartConstants';
export const cartReducer = (state = { cartItems: [] }, action) => {
switch (action.type) {
case CART_ADD_ITEM:
const item = action.payload;
const existItem = state.cartItems.find((x) => x.product === item.product);
if (existItem) {
return {
...state, cartItems: state.cartItems.map((x) =>
x.product === existItem.product ? item : x),};}
else {
return {
...state, cartItems: [...state.cartItems, item],};}
default:
return state;
}
};
cartAction.js
import axios from 'axios';
import { CART_ADD_ITEM } from '../constants/cartConstants';
export const addToCart = (productid, qty) => async (dispatch, getState) => {
const { data } = await axios.get(`/products/${productid}`);
dispatch({
type: CART_ADD_ITEM,
payload: {
product: data._id,
name: data.name,
image: data.image,
price: data.price,
countInStock: data.countInStock,
qty,
},
});
localStorage.setItem('cartItems', JSON.stringify(getState().cart.cartItems));
};
store.js
import { legacy_createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from '#redux-devtools/extension';
import {
productDetailsReducer,
productListReducer,
} from './reducers/productReducers';
import { cartReducer } from './reducers/cartReducers';
const reducer = combineReducers({
productList: productListReducer,
productDetail: productDetailsReducer,
cart: cartReducer,
});
const cartItemsFromStorage = localStorage.getItem('cartItems')
? JSON.parse(localStorage.getItem('cartItems'))
: [];
const initialState = { cart: { cartItems: cartItemsFromStorage } };
const middleware = [thunk];
const store = legacy_createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
export default store;
It seems the issue here might be resolved by using the same logic in my answer here to your other question regarding route matching. It wasn't explicitly called out as an issue or something the needed to be addressed/fixed (in other words, I thought you'd used the code and had an issue elsewhere), so adding an answer here for the specific axios issue and resolution.
It looks like navigating to "/cart" will result in both productid and qty being undefined/invalid values, and the useEffect hook is unconditionally dispatching the action to add the item & quantity. productid is undefined at axios.get(`/products/${productid}`) in the action creator.
You should only dispatch the addToCart action if there is a valid product id and a quantity to add.
const CartScreen = () => {
const { search } = useLocation();
const { productid } = useParams();
const qty = search ? Number(search.split('=')[1]) : 1;
const dispatch = useDispatch();
const cart = useSelector((state) => state.cart);
const { cartItems } = cart;
useEffect(() => {
if (productid && qty > 0) {
dispatch(addToCart(productid, qty)); // <-- only dispatch if valid
}
}, [dispatch, productid, qty]);
return (
<Row>
<Col md={8}>
{!cartItems.length
? (
<Message variant="info">
<Link to="/">
Go Back To Home Page
</Link>
</Message>
) : (
<ListGroup>
{cartItems.map((x) => (
<ListGroup.Item key={x.product}>
{x.name} , {x.qty}
</ListGroup.Item>
))}
</ListGroup>
)
}
</Col>
<Col md={4}></Col>
</Row>
);
};

React router dom v5 Nested routes never matching

I have two main pages: LoginPage and HomePage. HomePage is main page with navbar and content. I'm trying to implement nested routing.
loginPage
HomePage
SchoolsPage
UserDetails
I prepared routing for them:
<Switch>
<ProtectedRoute
key="loginPage"
path={`${ROOT_PATHS.login}`}
component={AuthPageContainer}
shouldBeRedirected={authState === 'AUTHENTICATED'}
authenticationPath={`${ROOT_PATHS.version}`}
/>
<ProtectedRoute
key="homePage"
path={ROOT_PATHS.version}
component={HomePageContainer}
shouldBeRedirected={authState === 'NOT_AUTHENTICATED'}
authenticationPath={`${ROOT_PATHS.login}`}
/>
</Switch>
Home page:
export const HomePage: React.FC<HomePageProps> = ({ fetchUserInfo, user, isLoading, userLogout }): JSX.Element => {
useEffect(() => {
fetchUserInfo()
}, [fetchUserInfo])
return (
<LoadingContent isLoading={isLoading}>
<Navbars user={user} userLogout={userLogout} />
<Pages />
</LoadingContent>
)
}
An routings in Pages:
export const Pages: React.FC = (): JSX.Element => {
const { url, path } = useRouteMatch()
return (
<Switch>
<Route path={`${url}${ROOT_PATHS.userDetails}`} exact component={UserDetailsContainer} />
<Route path={`${url}${ROOT_PATHS.schools}`} exact component={SchoolListContainer} />
<Route path={`${path}`} exact>
{console.log('redirected')}
<Redirect to={`${url}${ROOT_PATHS.schools}`} />
</Route>
</Switch>
)
}
And paths:
export const ROOT_PATHS = {
login: '/login',
version: '/v1',
schools: '/schools',
userDetails: '/userDetails'
}
My index.tsx:
// axios should be configured before saga because saga makes requests on startup
configureAxios()
const store = configureStore()
export const DOMStructure: React.FC = (): JSX.Element => (
<Suspense fallback={<Spinner animation="border" role="status" />}>
<Provider store={store}>
<ConnectedRouter history={history}>
<CookiesProvider>
<AppContainer />
</CookiesProvider>
</ConnectedRouter>
</Provider>
</Suspense>
)
ReactDOM.render(<DOMStructure />, document.getElementById('root'))
It all works fine when navigating via links in navbar.
But when I refresh page, it never matches to any child-routes and is redirected to v1/schools (I checked that with console.log('redirected').
So I cannot render page under specific ur. When I put 'localhost:3000/v1/userDetails' in browser, it's always redirected to 'localhost:3000/v1/schools'.
Why is that? What i'm missing?

React router throws and error when getting async component

I'm using react, redux and react router amoung others to build and example app.
I'm trying to load asynchronously different parts of my application. I've divided my app in ducks and I'm following this example https://github.com/insin/react-examples/tree/master/code-splitting-redux-reducers
But I'm getting this error:
Uncaught Invariant Violation: The root route must render a single element
When trying to get async component with getComponent method of react router.
I'm using:
react-router 2.0.1
My routes:
export default function configureRoutes(reducerRegistry) {
return(
<Route>
<Route component={Landing}>
<Route path='/login' component={Login}/>
<Route path='/register' component={Register}/>
</Route>
<Route path="admin" getComponent={(location, cb) => {
require.ensure([], require => {
cb(null, require('./containers/admin'))
})
}}/>
<Route component={App}>
<Route path='/' component={Home} />
</Route>
</Route>
)}
My component
class Admin extends Component {
componentDidMount() {
this.props.load()
}
render() {
const { message, isFetching } = this.props
return (
<div>
<p>{message}</p>
<p>This module was loaded via chunk </p>
{loading && <p>Doing some fake loading ...</p>}
</div>
)
}
}
Admin.propTypes = {
message: PropTypes.string.isRequired,
isFetching: PropTypes.bool.isRequired,
load: PropTypes.string.isRequired
}
const mapStateToProps = state => state.admin
function mapDispatchToProps(dispatch) {
return bindActionCreators({ load }, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(Admin)
Does anyone have the same error? any ideas? Anyone have something similar working?
Thanks community!
Update: Added index.js for clarity
import configureRoutes from './routes'
import configureStore from './store/configureStore'
import coreReducers from './modules/core'
import ReducerRegistry from './reducer-registry'
var reducerRegistry = new ReducerRegistry(coreReducers)
// Configure hot module replacement for core reducers
if (process.env.NODE_ENV !== 'production') {
if (module.hot) {
module.hot.accept('./modules/core', () => {
var nextCoreReducers = require('./modules/core')
reducerRegistry.register(nextCoreReducers)
})
}
}
const routes = configureRoutes(reducerRegistry)
const store = configureStore(reducerRegistry)
render(
<I18nextProvider i18n={i18n}>
<Provider store={store}>
<Router history={browserHistory} routes={routes} />
</Provider>
</I18nextProvider>,
document.getElementById('root')
)
I think your root <Route> is missing the component field.
You need to specify either component or getComponent for every parent route, as this will be the component that the current child route’s component gets passed to as this.props.children.
Rather than
export default function configureRoutes(reducerRegistry) {
return (
<Route>
you want something like
export default function configureRoutes(reducerRegistry) {
return (
<Route component={App}>
In this case, you probably won’t need another App route below.

Routing is not work with react-router-redux actions

When use the Link, router works as expected, though i get a warning [history] pushState is deprecated; use push instead.
Using routeActions from react-router-redux does't work, url was change (http://localhost:3002/addCity), but view still the same (Home) or show error if i go to page by url for example: localhost:3002/addCity.
git: https://github.com/kirsanv43/Weather.git
reducers:
export default combineReducers({
cities,
routing: routeReducer
});
store config: https://github.com/kirsanv43/Weather/blob/master/app/redux/config/store.js
import rootReducer from '../reducers'
export default function configureStore(initialState) {
const history = createHistory();
const middleware = syncHistory(history)
const finalCreateStore = compose(
applyMiddleware(middleware)
)(createStore)
const store = finalCreateStore(rootReducer)
middleware.listenForReplays(store);
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => {
const nextRootReducer = require('../reducers').default
store.replaceReducer(nextRootReducer)
})
}
return store
}
Router:
const store = configureStore()
ReactDOM.render(
<Provider store={store}>
<Router history={browserHistory}>
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="addCity" component={AddCity}/>
</Route>
</Router>
</Provider>
,
document.getElementById('root')
);
Component:
class CitiesList extends React.Component {
onAddCity = () => {
this.props.route.push('/addCity')
};
render() {
return <div className="weatherContainer">
<ul>
<li className="listItem addBtn"><a onClick={this.onAddCity}><span>ADD CITY</span></a></li>
</ul>
</div>
}
}
function mapDispatchToProps(dispatch) {
return {
route:bindActionCreators(routeActions, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(CitiesList)
insted createHistory need use createHashHistory from 'history'
that working for me
Please try to change "react-router-redux" version to "^5.0.0-alpha.8". It solved my problem

react router - Redirection after login

Could you please help me in understanding the redirection mechanism I could use with latest version of react router ( v1.1.0 ) . I would like to redirect to a url depending on the success or failure of user login .
I have tried to do the following
First created a history using.
let history = createBrowserHistory();
then tried to push the state using
history.pushState(null, 'abc')
Nothing is happening. Could you please let me know the correct way to do transitions .From the docs I understood that transitionTo() API is not present in the latest versions.
It will be great If you could point to a simple working example.
Wanted to update this thread because I spent a good amount of time digging around on this. In React Router 2.0.x, replaceState is deprecated in favor of replace. See here for details: https://github.com/ReactTraining/react-router/blob/v2.0.0/upgrade-guides/v2.0.0.md#link-to-onenter-and-isactive-use-location-descriptors
The correct way to do this would be something like this:
function requireAuth(nextState, replace) {
if (!userExists()) {
replace({
pathname: '/signin',
state: { nextPathname: nextState.location.pathname }
})
}
}
export const renderRoutes = () => (
<Router history={browserHistory}>
<Route path="protectedRoute" component={Protected} onEnter={requireAuth} />
<Route path="signin" component={SignIn} />
</Route>
</Router>
);
Then, in the SignIn component, you can redirect after a successful sign in like this:
signInFunction({params}, (err, res) => {
// Now in the sign in callback
if (err)
alert("Please try again")
else {
const location = this.props.location
if (location.state && location.state.nextPathname) {
browserHistory.push(location.state.nextPathname)
} else {
browserHistory.push('/')
}
}
})
You can register "hooks" on your routes that get triggered when you enter and leave the routes. Check out the documentation for onEnter and onLeave hooks.
There is also an example of requiring auth on a route and redirecting to a different path if the user is not logged in.
Here's a snippet taken from the require auth example within app.js:
function requireAuth(nextState, replaceState) {
if (!auth.loggedIn())
replaceState({ nextPathname: nextState.location.pathname }, '/login')
}
// And from the route configuration, use the requireAuth function in onEnter...
<Router history={history}>
<Route path="/" component={App}>
<Route path="login" component={Login} />
<Route path="logout" component={Logout} />
<Route path="about" component={About} />
<Route path="dashboard" component={Dashboard} onEnter={requireAuth} />
</Route>
</Router>
The nextState and replaceState arguments are objects from rackt/history and get injected into the method you pass into onEnter.
React Router v4.2
I am using React-16.2 & React-router-4.2
and I get solution by this
this.props.history.push("/");
My working code:
.then(response => response.json())
.then(data => {
if(data.status == 200){
this.props.history.push("/");
console.log('Successfully Login');
}
})
I was following this document redirect-on-login-and-logout
#terranmoccasin 's answer is correct. However there is a common need very few examples address.
Let's say you need to secure several routes (dashboard1, dashboard2, ...). How do you redirect back to the original page once you log in successfully? In other words, what do you do with {nextPathname: nextState.location.pathname}?
Here's what I do in ./containers/LoginContainer.js:
import { push } from 'react-router-redux';
const mapStateToProps = (state) => ({
nextPathname: state.routing.locationBeforeTransitions.state.nextPathname,
});
const mapDispatchToProps = (dispatch) => ({
changeLocationOnSignIn: (nextPathname) => {
dispatch(push(nextPathname));
},
});
and in ./components/Login.js
componentWillReceiveProps(nextProps) {
// user signed in or signed up, assuming redux. you may use this elsewhere.
if (nextProps.user.status === 'authenticated' && nextProps.user.user &&
!nextProps.user.error) {
this.props.changeLocationOnSignIn(this.props.nextPathname);
}
React-router 2.4.0 (April 2016) introduced withRouter which creates a HOC. However it wraps React.createClass, not a JS class. I haven't been able to get it working with redux-form, etc. Besides I think the above code is easier to comprehend.
i want just share the actual answer at 2020 year.
The main way for storing previous location in state is the same. But onEnter was removed from library. Now we can use AuthRoute as in the documentation:
<AuthRoute exact path="/food">
<Food />
</AuthRoute>
<Route exact path="/login">
<Login />
</Route>
const AuthRoute = ({ children, isAuthorized, ...rest }) => {
const loginLink = usePrepareLink({
to: "/login",
isRelativePath: true
});
return (
<Route {...rest} render={({ location }) =>
isAuthorized ? (
children
) : (
<Redirect to={{
...loginLink,
state: { from: location }
}} />
)
} />
);
};
and we can use the state to restore previouse URL after login
const onSignIn = useCallback(() => {
setIsAuthorized(value);
const link = (state && state.from) || "/restore-prevented-route";
history.replace(link);
}, [setIsAuthorized, value, history, state]);
The details you can find here (or RU)
This helps me.
Redirect to Login After Logout
import { useHistory } from "react-router-dom";
const history = useHistory();
history.push("/login");
onEnter no longer exists on react-router-4, You can make use of <Route render={ ... } /> to achieve the same functionality.
Here is an example of the same.
<React.Fragment>
<Switch>
<Route path="/dashboard" render={() => (isAuth() ? <Redirect to="/login" /> : <DashboardRoutes />)} />
<Route path="/login" component={Login} />
</Switch>
</React.Fragment>
isAuth() in my case is a function that basically check whether we have the auth token or not and returns true/false based on that.
function isLoggedIn() {
if (!localStorage.getItem('token')) {
return true;
}
return false;
}
As #JohnSz mentions I too had issues with using withRouter. Instead I did it as instructed here:
https://github.com/reactjs/react-router/blob/master/upgrade-guides/v2.0.0.md#programmatic-navigation
const RouteComponent = React.createClass({
contextTypes: {
router: React.PropTypes.object.isRequired
},
someHandler() {
this.context.router.push(...)
}
})
Basically:
Define contextType
Use this.context.router.push(...)
Cheers.
You can set a condition for the success and failure. Then use the useNavigate hook.
import { useNavigate } from "react-router-dom";
function useLogoutTimer() {
const userIsInactive = useFakeInactiveUser();
const navigate = useNavigate();
useEffect(() => {
if (userIsInactive) {
fake.logout();
navigate("/url");
}
}, [userIsInactive]);
}
Read more: https://reactrouter.com/en/main/hooks/use-navigate