I am using this approach to make some routes in my application only accessible after log in.
So in App.js I have some ProtectedRoutes.
<ProtectedRoute path='/projects' auth={this.props} component={Projects} />
<ProtectedRoute path='/search' auth={this.props} component={Search} />
<ProtectedRoute path='/admin' auth={this.props} component={Admin} />
And my ProtectedRoute function is like this:
const ProtectedRoute = ({ component: Comp, auth, path, ...rest }) => {
const isAuthenticated = auth.isAuthenticated;
const sendprops = { ...auth, ...rest };
return (
<Route
path={path}
{...rest}
render={props => {
return isAuthenticated ? (
<Comp {...sendprops} />
) : (
<Redirect to='/' />
);
}}
/>
);
};
export default ProtectedRoute;
But having done that, I've totally broken my Search component, which relies on being able to see and push to the history property.
this.props.history.push({
pathname: this.props.history.location.pathname,
search: `?query=${ this.state.searchQuery}&search_type=${this.state.searchType}&page=${this.state.searchPage}`
});
When I try to console.log out the properties in the Search component, I can see location still, but I've lost history. So in what feels like a somewhat ham fisted, ignorant fashion, I've wrapped my Search export in withRouter.
export default withRouter(Search);
And now I can see the history again. But I have no idea why I lost it in the first place? Insights would be greatly appreciated.
As requested, here is the Search component.
import React, { Component } from 'react';
import ResultList from './SearchResultList';
import { withRouter } from 'react-router'
class Search extends Component {
constructor(props) {
super(props);
console.log(props);
let url_query = '';
let url_search_type = '';
let url_search_page = 1;
let loading = false;
if(this.props.location.search) {
const params = new URLSearchParams(this.props.location.search);
url_query = params.get('query');
url_search_type = params.get('search_type');
url_search_page = params.get('page') || 1;
loading = true;
}
this.state = {
searchQuery: url_query,
searchType: url_search_type,
searchPage: url_search_page,
searchResult: {
results: [],
pages_left: '',
pages_right: '',
page: '',
type: ''
},
isLoading: loading
}
}
componentDidMount() {
if(this.state.isLoading){
this.doSearch();
}
}
handleChange = (e) => {
const search_name = e.target.name;
const search_value = e.target.value.trim();
this.setState({
[search_name]: search_value,
})
}
handleSubmit = (e) => {
this.props.history.push({
pathname: this.props.history.location.pathname,
search: `?query=${ this.state.searchQuery }&search_type=${this.state.searchType}&page=${this.state.searchPage}`
});
this.setState({
isLoading: true,
searchPage: 1
}, this.doSearch);
}
changePage = (e) => {
this.props.history.push({
pathname: this.props.history.location.pathname,
search: `?query=${ this.state.searchQuery }&search_type=${this.state.searchType}&page=${this.state.searchPage}`
});
this.setState({
searchPage: e,
isLoading: true
}, this.doSearch);
}
doSearch = () => {
//console.log(this.state);
const endpoint = 'http://urlurlurllll/search?query=' + this.state.searchQuery + '&search_type=' + this.state.searchType + '&page=' + this.state.searchPage;
//console.log(endpoint);
fetch(endpoint)
.then(data => data.json())
.then(jdata => {
this.setState({
searchResult: {
results: jdata.results,
pages_left: jdata.pages_left,
pages_right: jdata.pages_right,
page: jdata.page,
type: this.state.searchType
},
isLoading: false
})
})
.catch(error => console.log(error));
}
render (){
const isLoading = this.state.isLoading;
const searchResult = this.state.searchResult;
return (
<React.Fragment>
<div>
<h1>Search</h1>
<form>
<label>Search Term:</label>
<input type="text" id="input-search" name="searchQuery" value={this.state.searchQuery} onChange={ event => this.handleChange(event) } />
<label>Search Type:</label>
<select id="select-type" name="searchType" value={this.state.searchType} onChange={ event => this.handleChange(event) }>
<option value=''>Select Type</option>
<option value='hashtag'>hashtag</option>
<option value='handle'>handle</option>
<option value='keyword'>keyword</option>
</select>
<button type="button" id="btn-search" onClick={event => this.handleSubmit(event)}>Search</button>
</form>
</div>
<div>
{ isLoading ? (
<div>
<p>Loading...</p>
</div>
) : (
<React.Fragment>
<h2>Search Results</h2>
{searchResult.results.length ? (
<ResultList searchResult={searchResult} changePage={this.changePage} />
) : (
<p>Use the little form up there to start searching!</p>
)}
</React.Fragment>
)}
</div>
</React.Fragment>
);
}
}
export default withRouter(Search);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.3.1/umd/react-dom.production.min.js"></script>
Issue
I think the issue is the route props from render aren't passed along to the component. (Though it is unclear to me how the location route prop was still working)
Solution
Spread the route props (match, location, and history) provided from render into the component being rendered.
const ProtectedRoute = ({ component: Comp, auth, path, ...rest }) => {
const isAuthenticated = auth.isAuthenticated;
const sendprops = { ...auth, ...rest };
return (
<Route
path={path}
{...rest}
render={(props) => // <-- route props need to be
isAuthenticated ? (
<Comp {...sendprops} {...props} /> // <-- passed to component
) : (
<Redirect to="/" />
)
}
/>
);
};
I just checked that tutorial/lesson you linked to see what they were doing and they pass the route props through as well.
Related
Just trying to simply redirect once authenticated. figured i would convert my login screen to class component because i had a condition to redirect that was causing an endless loop. however once i converted my login screen, the redirect wasnt working at all. Im using react-router-dom v5.2...i read someone say that redirect isnt a part of v5+ and it changed to 'Navigate'...but it definitely was working at one point. and plus when trying to use 'Navigate' it says its not an exported package.
here the login screen:
class LoginScreen extends React.Component {
constructor(props) {
super(props)
this.state = {
username: '',
password: '',
password2: '',
role: '',
email: ''
}
}
componentDidUpdate() {
if (this.props.isAuthenticated) {
console.log("Logged in")
return (<Redirect to={'/dashboard'} />)
}
}
render() {
const { email, role, password, password2, username } = this.state
return (
<$.Container>
<$.Wrapper>
<$.Logo />
<$.Form>
<$.Input
type="email"
value={email}
placeholder="Email"
onChange={e => this.setState({ email: e.target.value })}
/>
<$.Input
type="username"
value={username}
placeholder="Username"
onChange={e => this.setState({ username: e.target.value })}
/>
<$.Input
type="password"
value={password}
placeholder="Password"
onChange={e => this.setState({ password: e.target.value })}
/>
<$.Input
type="password"
value={password2}
placeholder="Password Confirm"
onChange={e => this.setState({ password2: e.target.value })}
/>
<$.Input
type="role"
value={role}
placeholder="Role"
onChange={e => this.setState({ role: e.target.value })}
/>
</$.Form>
<$.Button onClick={() => this.props.login(email, password)}>Sign In</$.Button>
<$.Button onClick={() => this.props.register({ email, password, password2, role, username })}>Register</$.Button>
{this.props.message != null ?
this.props.error ?
<$.Error>{this.props.message}</$.Error>
: <$.Info>{this.props.message}</$.Info>
: null}
</$.Wrapper>
</$.Container >
)
}
}
const mapStateToProps = ({ auth }) => {
const { isAuthenticated, message, error } = auth
return { isAuthenticated, message, error }
}
const mapDispatchToProps = { login, register }
export default connect(mapStateToProps, mapDispatchToProps)(LoginScreen)
the condtion passes, as the log statement is output. but the redirect isnt redirecting.
heres app.js render:
<Provider store={store}>
<Router>
<div className="App">
<Switch>
{routes.map((route, index) => (
route.private ?
<PrivateRoute exact={route.exact} path={route.path} component={route.main} key={index} />
:
<Route exact={route.exact} path={route.path} component={route.main} key={index} />))}
</Switch>
</div>
</Router>
</Provider>
page redirected to:
import React from 'react'
function DashboardScreen(props) {
return (
<div>
<h1>dashboard screen</h1>
</div>
)
}
export default DashboardScreen
privateRoute component:
import React from 'react'
import { connect } from 'react-redux'
import { Route, Redirect } from 'react-router-dom'
import Sidebar from '../components/navigation/Sidebar'
function PrivateRoute({ component: Component, ...rest }) {
return (
<Route {...rest} render={(props) => props.isAuthenticated ? (
<div style={{ display: 'grid', gridTemplateColumns: '200px auto' }}>
<Sidebar role={'super-admin'} />
<Component role={'test-role'} {...props} />
</div>
) : (
<Redirect to={{ pathname: '/', state: { referrer: props.location } }} />
)}
/>
)
}
const mapStateToProps = ({ auth }) => {
const { isAuthenticated } = auth
return { isAuthenticated }
}
export default connect(mapStateToProps)(PrivateRoute)
how can i get this redirect working?
You need to put the Redirect in JSX that is going to be rendered. componentDidUpdate is not going to render that returned value. So instead, remove componentDidUpdate and change the top of your render to:
render() {
const { isAuthenticated } = this.props;
const { email, role, password, password2, username } = this.state;
if (isAuthenticated) {
return (<Redirect to={'/dashboard'} />);
}
return (
...
I'm trying to complete my app, have learned react, redux, react router all in one, now I'm just confused a bit when it comes to putting it all together.
Say I have a Nav component that's included in a header that's included globally on all pages and it calls a redux action which then runs a reducer and returns some search results.
When one searches from the navigation bar, how do I get it to redirect a search page that then returns the search results?
Nav component
class Nav extends React.Component {
render() {
const { search } = this.props;
return (
<header>
<SearchBox search={search} />
</header>
)
}
}
that includes a search component
class SearchBox extends React.Component {
constructor() {
super();
this.state = {
name: ''
}
}
handleChange = event => {
this.setState({
[event.target.id]: event.target.value
});
}
handleSubmit = event => {
event.preventDefault();
this.props.search(JSON.stringify({name: this.state.name}))
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input type="text" id="name" onChange={this.handleChange} placeholder="Search" />
<button type="submit">Search</button>
</form>
)
}
}
my layouts are like
index.js
const Index = () => {
return (
<Nav />
... Home Content
)
}
profile.js
const Profile = () => {
return (
<Nav />
... Profile Content
)
}
search.js
const Users = (props) => {
let list = null;
list = props.users.map((user, index)=> {
const { name } = user.profile;
const { username } = user;
return (
<li key={index}>
<h3><a href={'/'+username}>{name}</a></h3>
</li>
)
});
return <section id="search"><ul>{list}</ul></section>;
}
class Search extends React.Component {
render() {
const { searchResults } = this.props;
return (
<Nav />
<div>
{
/* show 'No results when no results found' */
searchResults !== ''
? seachResults.length == 0
? 'No results found'
: <Users users={searchResults} />
: null
}
</div>
)
}
}
const mapStateToProps = state => ({
searchResults: state.store.searchResults,
});
the user action is
export const search = (name) => dispatch => {
...
dispatch({
type: SEARCH_USER,
payload: res
})
the reducer is
const initialState = {
searchResults: ''
};
case SEARCH_USER:
return {
...state,
searchResults: action.payload.search
}
}
index.js
class App extends React.Component {
render() {
return (
<Router>
<Switch>
<Route path="/" exact={true} component={Index} />
<Route path="/profile" component={Profile} />
<Route path="/search" component={Search} />
</Switch>
</Router>
)
}
}
I want to build a membership-based web app using React. The users would sign-up and pay before they can access exclusive content. If they have already registered or signed up they can just login and access the content.
How would I go about making such a web app with React? What would I need to implement?
You can use react-router, found in npm packages : https://www.npmjs.com/package/react-router
See this example to private routes https://reacttraining.com/react-router/web/example/auth-workflow
import React from "react";
import {
BrowserRouter as Router,
Route,
Link,
Redirect,
withRouter
} from "react-router-dom";
////////////////////////////////////////////////////////////
// 1. Click the public page
// 2. Click the protected page
// 3. Log in
// 4. Click the back button, note the URL each time
const AuthExample = () => (
<Router>
<div>
<AuthButton />
<ul>
<li>
<Link to="/public">Public Page</Link>
</li>
<li>
<Link to="/protected">Protected Page</Link>
</li>
</ul>
<Route path="/public" component={Public} />
<Route path="/login" component={Login} />
<PrivateRoute path="/protected" component={Protected} />
</div>
</Router>
);
const fakeAuth = {
isAuthenticated: false,
authenticate(cb) {
this.isAuthenticated = true;
setTimeout(cb, 100); // fake async
},
signout(cb) {
this.isAuthenticated = false;
setTimeout(cb, 100);
}
};
const AuthButton = withRouter(
({ history }) =>
fakeAuth.isAuthenticated ? (
<p>
Welcome!{" "}
<button
onClick={() => {
fakeAuth.signout(() => history.push("/"));
}}
>
Sign out
</button>
</p>
) : (
<p>You are not logged in.</p>
)
);
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
fakeAuth.isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
);
const Public = () => <h3>Public</h3>;
const Protected = () => <h3>Protected</h3>;
class Login extends React.Component {
state = {
redirectToReferrer: false
};
login = () => {
fakeAuth.authenticate(() => {
this.setState({ redirectToReferrer: true });
});
};
render() {
const { from } = this.props.location.state || { from: { pathname: "/" } };
const { redirectToReferrer } = this.state;
if (redirectToReferrer) {
return <Redirect to={from} />;
}
return (
<div>
<p>You must log in to view the page at {from.pathname}</p>
<button onClick={this.login}>Log in</button>
</div>
);
}
}
export default AuthExample;
I'm using react router 4 and I'm having trouble accessing the id from a url using params. I've followed the react router 4 documentation however when I console.log match.params.id it returns Cannot read property 'params' of undefined. The URL contains the id so I'm lost. You can find the console.log in Path: Container
What am I doing wrong?
Path: App
const App = appProps => (
<Router>
<div className="bgColor">
<NavBar {...appProps} />
<Grid className="main-page-container">
<Switch>
<Admin exact path="/admin/candidate_profile/:id" component={AdminCandidateProfileContainer} {...appProps} />
</Switch>
</Grid>
</div>
</Router>
);
App.propTypes = {
loggingIn: PropTypes.bool,
authenticatedCandidate: PropTypes.bool,
authenticatedAdmin: PropTypes.bool
};
export default createContainer(() => {
const loggingIn = Meteor.loggingIn();
return {
loggingIn,
authenticatedCandidate: !loggingIn && !!Meteor.userId() && !!Roles.userIsInRole(Meteor.userId(), 'Candidate'),
authenticatedAdmin: !loggingIn && !!Meteor.userId() && !!Roles.userIsInRole(Meteor.userId(), 'Admin')
};
}, App);
Path: AdminRoute
const Admin = ({ loggingIn, authenticatedAdmin, component: Component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if (loggingIn) return <div />;
return authenticatedAdmin ?
(<Component loggingIn={loggingIn} authenticatedAdmin={authenticatedAdmin} {...rest} />) :
(<Redirect to="/login" />);
}}
/>
);
Admin.propTypes = {
loggingIn: PropTypes.bool,
authenticatedAdmin: PropTypes.bool,
component: PropTypes.func
};
export default Admin;
Path: Container.js
export default CandidateProfileContainer = createContainer(({ match }) => {
console.log('match', match.params.id);
const profileCandidateCollectionHandle = Meteor.subscribe('admin.candidateProfile');
const loading = !profileCandidateCollectionHandle.ready();
const profileCandidateCollection = ProfileCandidate.findOne({ userId: Meteor.userId() });
const profileCandidateCollectionExist = !loading && !!profileCandidateCollection;
return {
loading,
profileCandidateCollection,
profileCandidateCollectionExist,
profileCandidate: profileCandidateCollectionExist ? profileCandidateCollection : {}
};
}, CandidateProfilePage);
You're not passing props from render
const Admin = ({ loggingIn, authenticatedAdmin, component: Component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if (loggingIn) return <div />;
return authenticatedAdmin ?
(<Component
loggingIn={loggingIn}
authenticatedAdmin={authenticatedAdmin}
{...rest}
{...props} <--- match, location are here
/>) :
(<Redirect to="/login" />);
}}
/>
);
I've created a login page that takes a user from public to authenticated routes which works well. If there is an error with login (eg. email not found) I get the error in console Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the LoginForm component..
I think it might be related to how I use createContainer in the App.
I believe the problem is related to the way meteor passes Meteor.loggingIn(); equals true before it has heard back from the server. If there is an error the true quickly changes to false, which I think leads to the page reloading.
I would like to be able to use this.setState({ loginError: error.reason }); so I can tell the user what went wrong.
Any suggestions.
Path: App.jsx
const App = appProps => (
<Router>
<Grid className="main-page-container">
<Switch>
<Authenticated exact path="/" component={Home} {...appProps} />
<Public exact path="/login" component={Login} {...appProps} />
</Switch>
</Grid>
</Router>
);
App.propTypes = {
loggingIn: PropTypes.bool,
authenticated: PropTypes.bool
};
export default createContainer(() => {
const loggingIn = Meteor.loggingIn();
return {
loggingIn,
authenticated: !loggingIn && !!Meteor.userId()
};
}, App);
Path: Public.jsx
const Public = ({ loggingIn, authenticated, component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if (loggingIn) return <div />;
return !authenticated ?
(React.createElement(component, { ...props, loggingIn, authenticated })) :
(<Redirect to="/" />);
}}
/>
);
Public.propTypes = {
loggingIn: PropTypes.bool,
authenticated: PropTypes.bool,
component: PropTypes.func
};
export default Public;
Path: LoginForm.jsx
export default class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
email: '',
errors: {},
password: '',
loginError: ''
};
this.handleInputChange = this.handleInputChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox'
? target.checked
: target.value;
const name = target.name;
this.setState({[name]: value});
}
handleSubmit(event) {
event.preventDefault();
this.setState({
errors: {}
}, function() {
var data = {
email: this.state.email,
password: this.state.password
};
var email = this.state.email;
var password = this.state.password;
const errors = loginUserValidation(data);
if (errors) {
this.setState({errors: errors});
} else {
Meteor.loginWithPassword(email, password, (error) => {
if (error) {
this.setState({ loginError: error.reason });
}
});
}
});
}
render() {
return (
<div className="registration-form-container">
<Row>
<Col sm={8} smOffset={2} md={6} mdOffset={3}>
<div className="paper">
<Form onSubmit={this.handleSubmit}>
<section className="form-title">
<h3 className="text-center">Login</h3>
</section>
<hr />
<section className="form-content-login-or-registration">
{this.state.loginError &&
<div className="alert alert-danger">
<p>{this.state.loginError}</p>
</div>
}
<SingleInput
name={'email'}
inputType={'email'}
controlFunc={this.handleInputChange}
content={this.state.email}
placeholder={'Email'}
bsSize={null}
error={this.state.errors && this.state.errors.email}
/>
<SingleInput
name={'password'}
inputType={'password'}
controlFunc={this.handleInputChange}
content={this.state.password}
placeholder={'Password'}
bsSize={null}
error={this.state.errors && this.state.errors.password}
/>
</section>
<section className="form-buttons">
<Button type="submit" className="btn btn-primary" block>Login</Button>
</section>
</Form>
</div>
</Col>
</Row>
</div>
)
}
}
When Meteor.loginWithPassword changes your logginIn value to true, your <Public /> component unmounts your wrapped <LoginForm />
const Public = ({ loggingIn, authenticated, component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if (loggingIn) return <div />; // <--- this right here
return !authenticated ?
(React.createElement(component, { ...props, loggingIn, authenticated })) :
(<Redirect to="/" />);
}}
/>
);
So loggingIn values changes causing <Public /> to re-render. Now that loggingIn is true, you render a div instead of the component that was, unmounting it and making setStateunavailable when the errors callback tries to invoke it.
EDIT: In response to your comment...
To prevent this you can handle the error display inside the <LoginForm />
Remove if (loggingIn) return <div />; from your <Route /> in your <Public /> component.
Inside your <LoginForm /> you can handle the error condition. Do this by making a component that displays your errors and include it in your <LoginForm /> component where you want it displayed. Make the <ErrorDisplay /> component return nothing if there are no errors.
Example <ErrorDisplay /> component
const ErrorDisplay = ({ errors }) => {
errors && <div className="error-container">{ errors }</div>
};
That is obviously barebones, but I hope it helps you understand!