I am new to SPA development. I use React and React Router and made simple app. It has two pages: public and protected. User can see protected page only when he was signed in. I use firebase for managing users.
The problem is when I go to protected page, log in and can see its content, I then resresh the page and redirected to "default" state which is public page.
index.js
ReactDOM.render(<App />, document.getElementById('root'));
App.js
const ProtectedPage = () => {
return (
<div>
<h1> Protected page. </h1>
<Link to="/">Public resources</Link>
</div>
);
}
const PublicPage = () => {
return (
<div>
<h1> Public page. </h1>
<p> Login to see protected resources </p>
<Link to="/protected">Protected resources</Link>
</div>
);
}
const PrivateRoute = ({ component: Component, isAuthenticated, ...rest }) => (
<Route {...rest} render={props => (
isAuthenticated ? (
<Component {...props}/>
) : (
<Redirect to={{
pathname: '/',
state: { from: props.location }
}}/>
)
)}/>
)
class App extends React.Component {
constructor(props) {
super(props);
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
this.state = {
username: '',
uuid: ''
}
}
login() {
auth.signInWithEmailAndPassword(email, password).then((user) => {
console.log('Sign in ', user.displayName);
this.setState({
username: user.displayName,
uuid: user.uuid
});
});
}
logout() {
auth.signOut().then(() => {
console.log('Sign out ');
this.setState({
username: '',
uuid: ''
});
})
}
componentDidMount() {
this.releaseFirebaseAuthHandler = auth.onAuthStateChanged((user) => {
if(user) {
this.setState({
username: user.displayName,
uuid: user.uuid
});
}
});
}
componentWillUnmount() {
this.releaseFirebaseAuthHandler();
}
render() {
return (
<div>
<div className="navbar">
<nav className="navbar-nav">
{ this.state.username ?
<button onClick={this.logout}>Logout</button> :
<button onClick={this.login}>Login</button> }
</nav>
</div>
<div>
<div>
<Route exact path="/" component={PublicPage} />
<PrivateRoute path="/protected" component={ProtectedPage} isAuthenticated={this.state.uuid !== ''} />
</div>
</div>
</div>
);
}
}
export default App;
Is it possible to preserve localhost:3000/protected page after refreshing it without involving the server?
In your code you redirect to pathname: '/' if user is not authenticated:
const PrivateRoute = (...) => (
<Route {...rest} render={props => (
isAuthenticated
? <Component {...props}/>
: <Redirect to={{
pathname: '/',
state: { from: props.location }
}}/>
)}/>
)
If you would like to keep /protected in url one way to do it is to replace redirect with something like <div>Please login</div>
The state is not preserved across multiple sessions, for this purpose you can use the localStorage.
Related
I have a reactjs+redux app in which app.js, the first component to be mounted is given below:
//all imports here
class App extends React.Component {
constructor(){
super();
this.state = {
loginState : false,
}
}
componentDidMount() {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
console.log(user);
if(user.displayName&&user.photoURL&&user.email)
this.props.dispatch(login(user.displayName, user.photoURL, user.email));
else
this.props.dispatch(login(user.email.split("#")[0], "", ""));
this.setState({
loginState: true
});
}
else{
this.props.dispatch(changeLoading());
}
});
}
logout = () => {
firebase
.auth()
.signOut()
.then(() => {
this.setState({
loginState : false,
});
this.props.dispatch(logout());
this.props.history.push('/login');
})
.catch((err) => {
console.log(err);
});
};
render() {
return (
<>
<Switch>
{this.props.userName?(<Route
exact
path="/"
component={() => <Homepage userName={this.props.userName} />}
/>):(<Route exact path="/" component={Loading} />)}
<Route exact path="/login" component={Login} />
<Redirect to="/" />
</Switch>
</>
);
}
}
const mapStateToProps = (state) => {
return {
isLoggedIn: state.userState.isLoggedIn,
userName: state.userState.userName,
email: state.userState.email,
photoURL: state.userState.photoURL
};
};
export default withRouter(connect(mapStateToProps)(App));
Below is Homepage.js:
class Homepage extends React.Component{
componentDidMount(){
console.log("component mounted");
this.props.dispatch(fetchPosts());
}
render(){
console.log("Homepage reached");
if(this.props.userName==='') return <Redirect to="/login" />
return(
<div className="container-fluid m-0" style={{paddingTop: '100px',paddingLeft: '0',paddingRight: '0'}}>
<div className="row m-0">
<div class="col-md-1"></div>
<Main userName={this.props.userName} />
<Leftside userName={this.props.userName} />
<div class="col-md-1"></div>
</div>
</div>
);
}
}
const mapStateToProps=(state)=>({})
export default connect(mapStateToProps)(Homepage);
And below is the reducer:
export const userState=(state={isLoading: true,isLoggedIn: false,userName: '', photoURL: ''},action)=>{
switch(action.type){
case 'LOGIN': {console.log("reached");return {...state,isLoading: false,isLoggedIn: true, userName: action.payload.userName, photoURL: action.payload.photoURL, email: action.payload.email}}
case 'LOGOUT': return {...state,isLoading: false,isLoggedIn:false,userName: '',photoUrl: ''}
case 'CHANGE': return {...state,isLoading: false}
default: return {...state}
}
}
Basically what is happening is that initially when the app opens, this.props.userName is empty and hence Loading component is loaded. Once the firebase returns the user details, they are dispatched to redux reducer. When this happens, state.userState.userName becomes available and Homepage is mounted. As expected, its componentDidMount method runs and posts are fetched and dispatched to the redux store( posts are stored in redux store). But then suddenly Homepage unmounts and mounted again and consequently, componntDidMount runs again. So, in total, there are two fetchPost requests.
I do not understand this behaviour. I have read that componentDidMount runs only a single time.
Please help me to remove this bug.
Thank You!
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.
Is there a way to stop the user from directly accessing a URL on my application? For example, we have a page that is accessed as localhost:3000/scheduling but I want to re-route back to the homepage. I couldn't find many helpful articles that could achieve this. I am using React by the way.
Thanks!
You can do it in many ways, this is just an example :
const location = useLocation();
let history = useHistory();
if(location.state == undefined || location.state == null || location.state == ''){
history.push("/");
}
'/' is by default your home page.
You can check this example:
import React from 'react'
import {
BrowserRouter as Router,
Route,
Link,
Redirect,
withRouter
} from 'react-router-dom'
const fakeAuth = {
isAuthenticated: false,
authenticate(cb) {
this.isAuthenticated = true
setTimeout(cb, 100)
},
signout(cb) {
this.isAuthenticated = false
setTimeout(cb, 100)
}
}
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 === true) {
return <Redirect to={from} />
}
return (
<div>
<p>You must log in to view the page</p>
<button onClick={this.login}>Log in</button>
</div>
)
}
}
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
fakeAuth.isAuthenticated === true
? <Component {...props} />
: <Redirect to={{
pathname: '/login',
state: { from: props.location }
}} />
)} />
)
export default function AuthExample () {
return (
<Router>
<div>
<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>
)
}
Source
We can use Conditional rendering tracing the history.
You can also add conditions using this.props.history.location.key or this.props.history.action
Key exists and action is 'PUSH' when we redirect user using this.props.history.push
Key property doesn't exist and action is 'POP' when a user tries to access the URL directly
return this.props.history.location.key ? (<div></div>) : null
When clicking on the back button , the url gets changed, but the component that should be rendered for this url, is not rendered. Rerendering seems to be blocked. I am using connected-react-router. Before implementing connected-react-router, the back button worked fine. I tried a suggested solution , wrapping my connect function with the redux store with withRouter, but still see no result.
index.js
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>,
document.getElementById('root'),
);
App.js
class App extends Component {
componentDidMount() {
const { dispatch } = this.props;
dispatch(getInitialBookData());
}
render() {
return (
<div>
<Navigation />
</div>
);
}
}
export default connect()(App);
Navigation.js
const Navigation = () => {
return (
<Fragment>
<Navbar color="light" light expand="md">
<Collapse className="navbar-collapse">
<Nav className="ml-sm-auto navbar-nav">
<NavItem className="p-2">
<NavLink activeClassName="active" to="/">Dashboard</NavLink>
</NavItem>
<NavItem className="p-2">
<NavLink activeClassName="active" to="/addbook">Add Book</NavLink>
</NavItem>
</Nav>
</Collapse>
</Navbar>
<Container className="content mt-8">
<Switch>
<Route exact path="/" component={Dashboard} />
<Route exact path="/editbook/:id" component={CreateEditBookContainer} />
<Route exact path="/addbook" component={CreateEditBookContainer} />
<Route component={Page404} />
</Switch>
</Container>
</Fragment>
);
};
Dashboard.js
const Dashboard = ({ loading, error, success }) => {
return (
<Fragment>
{ error.status === true
? <ErrorAlert message={error.message} />
: null }
{ success.status === true
? <SuccessAlert message={success.message} />
: null }
<Row className="mt-3">
<h2 className="mb-5">Welcome to Dashboard</h2>
{ loading === true
? null
: <BookListContainer /> }
</Row>
</Fragment>
);
};
const mapStateToProps = ({fetching, errors, success}) => {
return {
loading: fetching.status,
error: errors,
success: success,
};
}
Dashboard.propTypes = {
loading: PropTypes.bool.isRequired,
error: PropTypes.object.isRequired,
success: PropTypes.object.isRequired,
};
export default withRouter(connect(mapStateToProps)(Dashboard));
BookListContainer.js
class BookListContainer extends Component{
state = {
navigateToEditBook: false,
};
idBookToEdit = null;
goToEditComponent = (id) => {
this.idBookToEdit = id;
this.setState({ navigateToEditBook: true });
}
render() {
let idBookToEdit = this.idBookToEdit;
if (this.state.navigateToEditBook === true) {
return <Redirect push to={{
pathname:`/editBook/${idBookToEdit}`,
state: { referrer: '/', bookId: idBookToEdit }
}} />
}
const { books } = this.props;
return(
<Row>
{ books.map((book) => {
return (
<Book
key={book._id}
book={book}
handleEditBook={this.goToEditComponent}
/>
)
}) }
</Row>
)};
};
const mapStateToProps = (props) => {
return {
books: props.books.books,
location: props.router.location,
};
}
BookListContainer.propTypes = {
books: PropTypes.array.isRequired,
};
export default withRouter((connect(mapStateToProps)(BookListContainer)));
CreateEditBookContainer.js
class CreateEditBookContainer extends Component {
render() {
const bookId = this.props.location.state
? this.props.location.state.bookId
:null
const { books, error, success } = this.props;
let book = null;
if(bookId){
book = books.filter(book => book._id === bookId)
}
return(
<Col sm="12" md="6" className="m-auto pt-5">
<CreateEditBookForm
{ ...book ? book = { ...book[0] } : null }
/>
{ error.status === true
? <ErrorAlert message={error.message} />
: null }
{ success.status === true
? <SuccessAlert message={success.message} />
: null }
</Col>
)}
}
const mapStateToProps = ({ books, errors, success }) => {
return {
books: books.books,
error: errors,
success: success,
}
}
CreateEditBookContainer.propTypes = {
books: PropTypes.array.isRequired,
error: PropTypes.object.isRequired,
success: PropTypes.object.isRequired,
};
export default withRouter(connect(mapStateToProps)(CreateEditBookContainer));
When clicking on EditBook Button, CreateEditBookContainer.js gets correctly rendered, but when clicking on the back button, this components remains, while the Dashboard component should actually get rendered since it corresponds to the path '/', that is correctly set in the url. When clicking on the Route links in specified in Navigation, all components are rendered correctly. It ONLY fails on the back button. Thank you for any suggestions.
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;