React-Router: Update route's component on new props - reactjs

App.js
import React, { Component } from 'react';
import { Router, Route, browserHistory } from 'react-router';
import Login from './Login';
import Courses from './Courses';
class App extends Component {
constructor() {
super();
this.state = {
username: '',
password: ''
};
}
setCredentials = ({ username, password }) => {
this.setState({
username,
password
});
}
render() {
return (
<Router history={browserHistory}>
<Route
path='/'
component={Login}
setCredentials={this.setCredentials}
/>
<Route
path='/courses'
component={Courses}
credentials={this.state}
/>
</Router>
);
}
}
The Login component takes in credentials from user, verifies them via an API call, updates App's state with them, and then redirects to the Courses component using this.props.router.push('/courses').
The Courses component should take in the updated credentials (i.e. App's updated state) as props, and afterwards perform an API call to fetch that user's courses.
How can I detect the updates in App's state inside the Courses component? Am I doing it wrong altogether?

Pass props to route's component correctly:
<Route
path='/'
component={(props) => <Login setCredentials={this.setCredentials} {...props} />}
/>
and NOT:
<Route
path='/'
component={Login}
setCredentials={this.setCredentials}
/>
The same for the Courses component, you pass extra-props the same way:
<Route
path='/courses'
component={(props) => <Courses credentials={this.state} {...props} />}
/>
And NOT:
<Route
path='/courses'
component={Courses}
credentials={this.state}
/>
Which answers the question:
How can I detect the updates in App's state inside the Courses component?
since crendentials becomes a prop of Courses and its value state of App.

As you said with the courses component reading from props you could have a lifecycle hook like so in the courses component:
componentWillReceiveProps(nextProps) {
if(nextProps.isAuthenticated && this.props.credentials !== nextProps.credentials) {
// perform fetch for courses here
}
}

Related

How to get params from route in REACTJS

I want to get params in my react class
I got file index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Button } from 'reactstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import { App, addPost, Child } from './components/App';
import { Switch , Route, BrowserRouter } from 'react-router-dom'
ReactDOM.render((
<BrowserRouter>
<Switch>
<Route exact path="/" component={App} />
<Route path="/dodajpost" component={addPost} />
<Route exact path="/:id" component={App} /> // this one
</Switch>
</BrowserRouter>
), document.getElementById('root'));
and now in my class i want to get the param ":id" and send it to server, but i don't know how to get it
export default class Post extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
};
}
componentDidMount () {
const { handle } = this.props.match.params
console.log(handle) // undefined
}
How can I get in componentDidMount the params from route?
I tried many things and none working
As your Route has param name as id,
<Route exact path="/:id" component={App} />
Instead of this,
const { handle } = this.props.match.params
you should do this,
const { id } = this.props.match.params
console.log(id)
Note: You have 2 Route's for App component. So when you are already in App component and try to navigate to path="/:id" you will not get id value, for that you may need componentDidUpdate method.
componentDidUpdate () {
const { id } = this.props.match.params
console.log(id)
}
Or you may have a typo here, <Route exact path="/:id" component={App} />, instead of App as component you might have Child (Post) component <Route exact path="/:id" component={Child} />
Demo

React router dom redirect problem. Changes url, does not render component

Problem: When I use history.push(), I can see that browser changes url, but it does not render my component listening on the path. It only renders if I refresh a page.
App.js file:
import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
import { Provider } from "react-redux";
import PropTypes from "prop-types";
//Components
import LoginForm from "../LoginForm/LoginForm";
import PrivateRoute from "../PrivateRoute/PrivateRoute";
import ServerList from "../ServerList/ServerList";
const App = ({ store }) => {
const isLoggedIn = localStorage.getItem("userToken");
return (
<Router>
<Provider store={store}>
<div className="App">
{isLoggedIn !== true && (
<Route exact path="/login" component={LoginForm} />
)}
<PrivateRoute
isLoggedIn={!!isLoggedIn}
path="/"
component={ServerList}
/>
</div>
</Provider>
</Router>
);
};
App.propTypes = {
store: PropTypes.object.isRequired
};
export default App;
Inside my LoginForm, I am making a request to an API, and after doing my procedures, I use .then() to redirect my user:
.then(() => {
props.history.push("/");
})
What happens: Browser changes url from /login to /, but component listening on / route is not rendered, unless I reload page.
Inside my / component, I use useEffect() hook to make another request to API, which fetches data and prints it inside return(). If I console.log inside useEffect() it happens twice, I assume initial one, and when I store data from an API inside component's state using useState() hook.
EDIT: adding PrivateRoute component as requested:
import React from "react";
import { Route, Redirect } from "react-router-dom";
const PrivateRoute = ({ component: Component, isLoggedIn, ...rest }) => {
return (
<Route
{...rest}
render={props =>
isLoggedIn === true ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: "/login" }} />
)
}
/>
);
};
export default PrivateRoute;
What I tried already:
1) Wrapping my default export with withRouter():
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(LoginForm));
2) Creating custom history and passing it as prop to Router.
react-router-dom version is ^5.0.1. react-router is the same, 5.0.1
You have at two mistakes in your code.
You are not using <switch> component to wrap routes. So all routes are processed at every render and all components from each <route> are rendered.
You are using local store to exchange information between components. But change in local store is invisible to react, so it does not fire component re-rendering. To correct this you should use local state in App component (by converting it to class or using hooks).
So corrected code will look like
const App = ({ store }) => {
const [userToken, setUserToken] = useState(localStorage.getItem("userToken")); // You can read user token from local store. So on after token is received, user is not asked for login
return (
<Router>
<Provider store={store}>
<div className="App">
<Switch>
{!!userToken !== true && (
<Route exact path="/login"
render={props => <LoginForm {...props} setUserToken={setUserToken} />}
/>
)}
<PrivateRoute
isLoggedIn={!!userToken}
path="/"
component={ServerList}
/>
</Switch>
</div>
</Provider>
</Router>
);
};
And LoginForm should use setUserToken to change user token in App component. It also may store user token in local store so on page refresh user is not asked for login, but stored token is used.
Also be sure not to put anything between <Switch> and </Switch> except <Route>. Otherwise routing will not work.
Here is working sample

react.js redirect to view

i want redirect to "/user". i write but this not work.
how to correctly redirect to the right page
onClick = (e) => {
this.setState({ errorLoad: false});
getPlayerInfo(this.state.id).then(data => {
if(data.success == false) {
this.setState({ errorLoad: true});
return;
}
this.setState({ user: data.player});
console.log(data);
<Redirect to="/user"/>
});
}
My router list. Among them there is a router with the path "/ user"
<Route path="/user" render={(props) => <User {...props} user={this.state.user} />} />
UPADATE
App.js
The button I click on is in the component <SearchForm/>
render() {
let style = {marginLeft: '20px'};
return (
<div>
<Header source='https://www.shareicon.net/data/2017/02/15/878753_media_512x512.png'/>
<SearchForm onClick={this.onClick} style={style} onChange={this.onHandle} placeholder="search"/>
<Centered style={ {marginTop: '50px'} }>
<BrowserRouter>
<Switch>
<Route exact path='/' component={Startup} />
<Route path="/user" render={(props) => <User {...props} user={this.state.user} />} />
</Switch>
</BrowserRouter>
</Centered>
</div>
);
}
There are two ways to programmatically navigate with React Router - <Redirect /> and history.push. Which you use is mostly up to you and your specific use case.
<Redirect /> should be used in user event -> state change -> re-render order.
The downsides to this approach is that you need to create a new property on the component’s state in order to know when to render the Redirect. That’s valid, but again, that’s pretty much the whole point of React - state changes update the UI.
The real work horse of React Router is the History library. Under the hood it’s what’s keeping track of session history for React Router. When a component is rendered by React Router, that component is passed three different props: location, match, and history. This history prop comes from the History library and has a ton of fancy properties on it related to routing. In this case, the one we’re interested is history.push. What it does is it pushes a new entry onto the history stack - aka redirecting the user to another route.
You need to use this.props.history to manually redirect:
onClick = (e) => {
this.setState({ errorLoad: false});
getPlayerInfo(this.state.id).then(data => {
if(data.success == false) {
this.setState({ errorLoad: true});
return;
}
this.setState({ user: data.player});
console.log(data);
this.props.history.push('/user');
});
}
You should be getting history as a prop from your <Router> component.
EDIT:
Okay thank you for the code update. The SearchForm component is not nested under your BrowserRouter, so it is not getting the history prop. Either move that component inside the BrowserRouter or use the withRouter HOC in SearchForm reacttraining.com/react-router/web/api/withRouter
Option 1: Move SearchForm inside the BrowserRouter
render() {
let style = {marginLeft: '20px'};
return (
<div>
<Header source='https://www.shareicon.net/data/2017/02/15/878753_media_512x512.png'/>
<Centered style={ {marginTop: '50px'} }>
<BrowserRouter>
<SearchForm onClick={this.onClick} style={style} onChange={this.onHandle} placeholder="search"/>
<Switch>
<Route exact path='/' component={Startup} />
<Route path="/user" render={(props) => <User {...props} user={this.state.user} />} />
</Switch>
</BrowserRouter>
</Centered>
</div>
);
}
Option 2: use the withRouter HOC to inject the history prop into SearchForm manually:
import { withRouter } from 'react-router-dom';
class SearchForm extends React.Component { ... }
export default withRouter(SearchForm)

Keeping State After Component Unmounts

I have an Auth component which is responsible for both login & sign up. I simply receive a prop (isSignup) and display the appropriate form fields. I also use react-router:
<BrowserRouter>
<Route
path="/signup" exact
render={() => <Auth isSignup />} />
<Route
path="/login" exact
render={() => <Auth />} />
</BrowserRouter>
In the Auth component I have a state which holds the values of the form fields.
I'd like to keep the state of Auth after it unmounts, i.e when react-router no longer renders it, so I can keep the values when the user switches between /signup and /login.
Is there a way to do this without global state management (e.g Redux)?
If you don't want to use Redux for this purpose, but you still want to share this specific state across your app, then there are a few ways to go about it. If you want to stay "Reacty", then you really have 2 options:
Pass state as props
Use a Provider
If you just want something that works but isn't necessarily the React way, you have more options (like setting window.isLoggedIn and toggling it at will... which I don't recommend).
For this answer, we'll focus on the "Reacty" options.
Pass state as props
This is the default approach with React. Track your isLoggedIn variable somewhere at the root of your component tree, then pass it down to your other components via props.
class App extends Component {
state = { isLoggedIn: false }
logIn = () => this.setState({ isLoggedIn: true })
logOut = () => this.setState({ isLoggedIn: false })
render() {
return (
<BrowserRouter>
<Route
path="/signup"
exact
render={() => (
<Auth
isSignup
isLoggedIn={this.state.isLoggedIn}
logIn={this.logIn}
logOut{this.logOut}
/>
)}
/>
<Route
path="/login"
exact
render={() => (
<Auth
isLoggedIn={this.state.isLoggedIn}
logIn={this.logIn}
logOut{this.logOut}
/>
)}
/>
</BrowserRouter>
)
}
}
This works but gets tedious as you need to pass isLoggedIn, logIn, and logOut to almost every component.
Use a Provider
A Provider is a React component that passes data to all its descendants via context.
class AuthProvider extends Component {
state = { isLoggedIn: false }
logIn = () => this.setState({ isLoggedIn: true })
logOut = () => this.setState({ isLoggedIn: false })
getChildContext() {
return {
isLoggedIn: this.state.isLoggedIn,
logIn: this.logIn,
logOut: this.logOut
}
}
static childContextTypes = {
isLoggedIn: PropTypes.bool,
logIn: PropTypes.func,
logOut: PropTypes.func
}
render() {
return (
<>
{this.props.children}
</>
)
}
}
Then, you initialize your app by wrapping the whole thing in AuthProvider:
<AuthProvider>
<BrowserRouter>
// routes...
</BrowserRouter>
</AuthProvider>
And in any component of your app, you will be able to access isLoggedIn, logIn, and logOut via this.context.
You can read more about the Provider pattern and how it works in this excellent article from Robin Wieruch.
notes:
It's common for components to receive context as props using a higher order component, but I left that out of the answer for brevity. Robin's article goes into this further.
You may recognize the Provider pattern as one that react-redux and other state management libraries utilize. It's fairly ubiquitous at this point.
You could create a wrapper which stores the state above the components:
class AuthWrapper extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return ([
<Route
key="signup"
path="/signup" exact
>
<Auth
state={this.state}
setState={(state) => this.setState(state)}
isSignup
/>
</Route>,
<Route
key="login"
path="/login" exact
>
<Auth
state={this.state}
setState={(state) => this.setState(state)}
/>
</Route>
]);
}
}
Then this component can be used, which stores the state over those routes.
<BrowserRouter>
<AuthWrapper />
</BrowserRouter>
You would just need to use the state and setState in the params instead in the Auth component.

React router not rendering inactive route components

I started with create-react-app and am attempting to set up auth-based routing via a Gatekeeper component. Right now the only thing the Gatekeeper component does is render its children. I use redux to get the current user from the state tree and save it as currentUser. I'll use that to validate access to the child routes later.
import React from 'react';
import { connect } from 'react-redux';
import { subscribeToCurrentUser } from '../../reducers/authentication';
class Gatekeeper extends React.Component {
componentDidMount() {
this.props.subscribeToCurrentUser();
}
render() {
return this.props.children
}
}
function mapStateToProps(state) {
return {
currentUser: state.currentUser
}
}
const GatekeeperContainer = connect(mapStateToProps, {subscribeToCurrentUser})(Gatekeeper);
export default GatekeeperContainer;
If I initially load the app on, say /account everything loads as expected. But if I navigate to /templates/123 via a <NavLink> the URL changes but the <Template> component doesn't render. Inspecting with React Dev Tools shows me the children of every route underneath the Gatekeeper component is null. If I refresh the page then the <Template> component renders as expected but navigating back to /account doesn't render the <AccountPage> component.
<Provider store={store}>
<Router history={history}>
<div>
<Route exact path="/" component={LandingPage} />
<Layout>
<Route path="/login" component={Login} />
<Gatekeeper>
<Switch>
<Route path="/home" component={HomePage} />
<Route path="/templates/:templateID" component={Template} />
<Route path="/account" component={AccountPage} />
</Switch>
</Gatekeeper>
</Layout>
</div>
</Router>
</Provider>
What am I doing wrong here?

Resources