I want my child to rerender when my parent updates its state. Seems simple but I haven't gotten it to work.
In App.js i make the API call in componentWillMount. I then want "PrivateRoute" to update the state "auth" but it seems like componentDidUpdate never runs and I always get rediercted.
The purpose of the code is that the API call checks if the user has a valid authentication token and then sets the "isLoggedIn" state to true or false. This should make the child "PrivateRoute" either redirect (if isLoggedIn is false) or render another page (if isLoggedIn is true).
I have checked out a lot of other similar questions but no solution has worked this far.
My app.js:
import React, { Component } from "react";
import {
BrowserRouter as Router,
Route,
Switch,
Link,
Redirect,
} from "react-router-dom";
import axios from "axios";
import PrivateRoute from "./components/PrivateRoute";
// Pages
import IndexPage from "./pages/index";
import HomePage from "./pages/home";
class App extends Component {
constructor() {
super();
console.log("Constructor");
this.state = {
loggedInStatus: false,
test: "TEST",
};
}
// Checks if user is logged in
checkAuth() {
let token = localStorage.getItem("token");
let isLoggedIn = false;
if (token === null) token = "bad";
console.log("Making API call");
// API call
axios
.get("http://localhost:8000/authentication/checkAuth", {
headers: { Authorization: "token " + token },
})
.then((res) => {
console.log("Updateing state");
this.setState({ loggedInStatus: true });
})
.catch((error) => {
console.log("Updating state");
this.setState({ loggedInStatus: false });
});
return isLoggedIn;
}
componentWillMount() {
this.checkAuth();
}
render() {
//console.log("Render");
// console.log("isLoggedIn: ", this.state.loggedInStatus);
return (
<Router>
<Switch>
<PrivateRoute
exact
path="/home"
component={HomePage}
auth={this.state.loggedInStatus}
/>
<Route exact path="/" component={IndexPage} />
</Switch>
</Router>
);
}
}
export default App;
PrivateRoute.jsx:
import { Redirect } from "react-router-dom";
import React, { Component } from "react";
class PrivateRoute extends Component {
state = {};
constructor(props) {
super(props);
this.state.auth = false;
}
// Update child if parent state is updated
componentDidUpdate(prevProps) {
console.log("Component did update");
if (this.props.auth !== prevProps.auth) {
console.log("Child component update");
this.setState({ auth: this.props.auth ? true : false });
}
}
render() {
console.log("Props: ", this.props);
console.log("State: ", this.state);
//alert("this.props.auth: ", this.props.auth);
//alert("TEST: ", this.props.test);
if (this.props.auth) {
return <h1>Success!</h1>;
//return <Component {...this.props} />;
} else {
return (
<Redirect
to={{ pathname: "/", state: { from: this.props.location } }}
/>
);
}
}
}
export default PrivateRoute;
checkAuth should be sync, if you want auth status before first-ever render in the component.
Your checkAuth will return instantly, making the auth status always false.
async checkAuth() {
try {
let token = localStorage.getItem("token");
let isLoggedIn = false;
if (token === null) token = "bad";
const res = await axios
.get("http://localhost:8000/authentication/checkAuth", {
headers: {Authorization: "token " + token},
})
console.log("Updateing state");
this.setState({loggedInStatus: true});
} catch (e) {
// for non 2XX axios will throw error
console.log("Updating state");
this.setState({loggedInStatus: false});
}
}
async componentWillMount() {
await this.checkAuth();
}
In the child component, you have to set state from props.
constructor(props) {
super(props);
this.state.auth = props.auth;
}
Related
I am trying to verify that the user is authenticated with passport before allowing them to access a certain route. To do so, I need to access my API which will return the authentication status of my user. However, I wish to make the call before the route has rendered so that the route remains protected. Here is my current attempt:
import React, { Component } from 'react';
import { Route, Redirect } from 'react-router-dom';
class ProtectedRoute extends Component {
constructor(){
super()
this.fetchAuthState().then(result => console.log('result',result))
this.state = {
isAuth: false,
error: null
}
}
async fetchAuthState() {
try {
const response = await fetch('http://localhost:3001/logincheck', {
headers: {
'content-type': 'application/json',
accept: 'application/json',
},
});
return await response.json();
} catch (error) {
console.error(error);
this.setState({ error });
}
};
render() {
console.log('client check', this.state.isAuth)
const { component: Component, ...props } = this.props
return (
<Route
{...props}
render={props => (
this.state.isAuth ?
<Component {...props} /> :
<Redirect to='/login' />
)}
/>
)
}
}
export default ProtectedRoute;
Here is the server side code:
app.get('/logincheck', (req, res) =>{
res.send(req.isAuthenticated())
})
I figured that because of the react lifecycle, the fetch should be called in the constructor, and then stored in state. However, when I try to store the result, either directly or in a temporary variable, the fetch result shows undefined. I have checked the passport docs for any client side form of isAuthenticated(), but it appears that it only works server side. I was considering implementing a jwt token system to see if that would be a better way to maintain authentication status but thought that having an api route to check would be easier. I am new to web development so any advice/criticism would be much appreciated!
You can accomplish this by adding an extra state variable - something like loading - and then toggling it when the API returns a response. But I would refactor your code slightly to make it look like so:
import React, { Component } from 'react';
import { Route, Redirect } from 'react-router-dom';
class ProtectedRoute extends Component {
constructor(){
super()
this.state = {
loading: true,
isAuth: false,
error: null
}
}
componentDidMount(){
fetch('http://localhost:3001/logincheck', {
headers: {
'content-type': 'application/json',
accept: 'application/json',
},
})
.then(res => res.json())
.then(json => {
this.setState({isAuth: json.isAuth, loading: false})
})
.catch(e => console.log(e))
}
render() {
console.log('client check', this.state.isAuth)
const { component: Component, ...props } = this.props;
const {loading, isAuth} = this.state;
return (
<Route
{...props}
render={() => {
return loading ?
<div>Loading...</div>
:
isAuth ?
this.props.children
:
<Redirect to='/login' />
}} />
)
}
}
export default ProtectedRoute;
You could add some loading screen until authentication response arrived.
Initiate loading state and do the fetch on component mount
class ProtectedRoute extends Component {
constructor(){
super()
this.state = {
isAuth: false,
error: null,
loading:true // default loading will be true
}
}
componentDidMount(){
this.fetchAuthState().then(result => this.setState({loading:false})) // after fetch end set loading false
}
....
render() {
console.log('client check', this.state.isAuth)
const { component: Component, ...props } = this.props
return (
{this.state.loading ? <LoadingComponent/> :<Route
{...props}
render={props => (
this.state.isAuth ?
<Component {...props} /> :
<Redirect to='/login' />
)}
/>}
)
}
}
i have an application in react and I want to protect all pages with a password
i have header navbar and footer
and when I do this with protected route class if the mail and password are good it redirect me to the component without NavBar and header, only component
how can I protect display all page
<ProtectedRoute exact path="/" component={analyticsDashboard} />
with
import React from 'react'
import { Redirect } from 'react-router-dom'
import { is } from '#babel/types';
import AppRouter from '../Router';
class ProtectedRoute extends React.Component {
render() {
const Component = this.props.component;
const isAuthenticated = localStorage.getItem('token');
if(isAuthenticated==='0')
{
return <Redirect to={{ pathname: '/pages/login' }} />
}
return <Component/>
}
}
export default ProtectedRoute;
and thanks to tell me which method can I use on production instead of local storage
you can create a HOC as following
first call this function when you want to set the token value to local storage
// call this function in the root component when you want to set the token
function setLocalStorage (key, value) {
const originalSetItem = localStorage.setItem;
localStorage.setItem = function(key, value) {
const event = new Event('itemInserted');
event.value = value;
event.key = key;
document.dispatchEvent(event);
originalSetItem.apply(this, arguments);
};
}
Second, create a hoc
import { Redirect } from 'react-router-dom'
export default function(ComposedComponent) {
class Authentication extends Component {
state = {
isAuthenticated: !!localStorage.getItem('token')
};
localStorageSetHandler({ key, value }) {
if (key !== 'token') return;
this.setState(({
isAuthenticated: !!value,
}))
}
componentDidMount() {
document.addEventListener("itemInserted", this.localStorageSetHandler, false);
}
render() {
const { isAuthenticated } = this.state;
if (!isAuthenticated) {
return <Redirect to={{ pathname: '/pages/login' }} />;
}
return <ComposedComponent />;
}
}
}
then use the hoc you've created to create protected routes like so
<Route
path={'/path'}
component={RequireAuth(lazy(() => import('./component path')))} // or you can use component={RequireAuth(YourComponent)}
/>
I'm new to React and Firebase and I'm trying get the user object that's populated after sign in/sign up to a new component so I can access it and create a database for each user, however I can't seem to access the this.state.user from another component. Here's my code:
import React, { Component } from 'react';
import fire from './config/Fire';
import Login from './Login';
import Home from './Home';
class App extends Component {
constructor(props){
super(props);
this.state = {
user: {},
}
}
// After this component renders, we will call on auth lister which will begin auth listener process
componentDidMount(){
this.authListener();
}
authListener() {
fire.auth().onAuthStateChanged((user) => {
console.log(user);
if (user) {
this.setState({ user });
} else {
this.setState({ user: null });
}
});
}
render() {
return (
<div className="App">
{this.state.user ? (<Home user={this.state.user}/>) : (<Login/>)}
</div>
);
}
}
export default App;
and in my new component i have:
componentDidMount(){
fire.auth().onAuthStateChanged((user) => {
if (this.props.user){
console.log(this.props.user);
// const userId = user.uid;
// fire.database().ref(`users/${userId}`).set({
// username: '',
// age: ''
// });
} else {
console.log('No user');
}
});
}
You are listening for the user in componentDidMount of the App component, then you are passing that data into the home component in <Home user={this.state.user}/>
This means that you can refrence the user object inside the Home component as this.props.user.
You don't need to call the onAuthStateChanged handler again inside Home, you already did that in the App Component.
Hope that helps.
The function below is meant to check if user is authorized before rendering a route with react-router. I got the basic code from somewhere I forgot.
This was working fine when I had a simple synchronous check to local storage, but when I introduced an Axios call to the server things got messy.
I read a lot of the SO's questions on like issues, ie, promises not returning value and I seem to have modified my code to conform to the regular pitfalls.
I particularly used the last answer to this question to fix my setup, but the issue remains.
On the code below, the console.logs output the parameters correctly, meaning that the failure with related to the return statement.
The specific error is:
Error: AuthRoute(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.
import React from 'react';
import PropTypes from 'prop-types';
import { Redirect, Route } from 'react-router-dom';
import axios from 'axios';
const PRIVATE_ROOT = '/account';
const PUBLIC_ROOT = '/';
const RenderRoute = ({response, isPrivate, Route, Redirect, component, ...props}) => {
console.log('is_logged', response.data.is_logged); // boolean
console.log('isPrivate', isPrivate); // boolean
console.log('response', response); // axios object
console.log('route', Route); // function route
console.log('component', component); // home component
console.log('props', {...props}); // route props
if (response.data.is_logged) {
// set last activity on local storage
let now = new Date();
now = parseInt(now.getTime() / 1000);
localStorage.setItem('last_active', now );
return isPrivate
? <Route { ...props } component={ component } />
: <Route { ...props } component={ component } />;
} else {
return isPrivate
? <Redirect to={ PUBLIC_ROOT } />
: <Route { ...props } component={ component } />;
}
}
const AuthRoute = ({component, ...props}) => {
const { isPrivate } = component;
let last_active_client = localStorage.getItem('last_active') ? localStorage.getItem('last_active') : 0;
let data = {
last_active_client: last_active_client
}
let getApiSession = new Promise((resolve) => {
resolve(axios.post('/api-session', data));
});
getApiSession.then(response => RenderRoute({response, isPrivate,Route, Redirect, component, ...props})
).catch((error) => {
console.log(error);
});
}
export default AuthRoute;
This is what worked for me after comments above. It was necessary to create a separate component class. It's not tremendously elegant, but works.
The code needs to be placed into componentWillReceiveProps() for it to update at each new props. I know componentWillReceiveProps() is being deprecated. I will handle that separately.
/auth/auth.js
import React from 'react';
import RenderRoute from './components/render_route';
const AuthRoute = ({component, ...props}) => {
let propsAll = {...props}
return (
<RenderRoute info={component} propsAll={propsAll} />
)
}
export default AuthRoute;
/auth/components/render_route.js
import React from 'react';
import PropTypes from 'prop-types';
import { Redirect, Route } from 'react-router-dom';
import axios from 'axios';
const PRIVATE_ROOT = '/account';
const PUBLIC_ROOT = '/';
class RenderRoute extends React.Component {
constructor (props) {
super(props);
this.state = {
route: ''
}
}
componentWillReceiveProps(nextProps, prevState){
const { isPrivate } = this.props.info;
let last_active_client = localStorage.getItem('last_active') ? localStorage.getItem('last_active') : 0;
let data = {
last_active_client: last_active_client
}
axios.post('/api-session', data).then(response => {
let isLogged = response.data.is_logged;
if(response.data.is_logged) {
// set last activity on local storage
let now = new Date();
now = parseInt(now.getTime() / 1000);
localStorage.setItem('last_active', now );
this.setState({ route: isPrivate
? <Route { ...this.props.propsAll } component={ this.props.info } />
: <Route { ...this.props.propsAll } component={ this.props.info } />
})
} else {
this.setState({ route: isPrivate
? <Redirect to={ PUBLIC_ROOT } />
: <Route { ...this.props.propsAll } component={ this.props.info } />
})
}
}).catch((error) => {
console.log(error);
});
}
render() {
return (
<div>
{this.state.route}
</div>
)
}
}
export default RenderRoute;
Background:
I am practicing the idea of React/Redux. I would want to follow the flow of data.
axios dispatches action -> reducer setState to props -> Component render()
The problem may be more than 1 point. Because I am new to Frontend world.
Please feel free to re-design my app(if needed)
Problem:
company does not render out because this.props.companies is blank. But axios does fetch the array from backend.
action/index.js
//First experiment action returns promise instance
export function fetchCompanies(token) {
const jwtReady = 'JWT '.concat(token);
const headers = {
'Content-Type': 'application/json',
'Authorization': jwtReady
};
const instance = axios({
method: 'GET',
url: `${ROOT_URL}/api/companies/`,
headers: headers
});
return {
type: FETCH_COMPANIES,
payload: instance
}
}
export function getCompanies(token){
const jwtReady = 'JWT '.concat(token);
const headers = {
'Content-Type': 'application/json',
'Authorization': jwtReady
};
const instance = axios({
method: 'GET',
url: `${ROOT_URL}/api/companies/`,
headers: headers
});
return instance
.then(data=> store.dispatch('GET_COMPANIES_SUCCESS', data));
}
company_reducers.js
import {FETCH_COMPANIES, GET_COMPANIES_ERROR, GET_COMPANIES_SUCCESS} from "../actions/const";
export default function (state = {}, action) {
switch (action.type) {
case GET_COMPANIES_SUCCESS:
return {
...state,
companies: action.payload
};
case GET_COMPANIES_ERROR:
return {
...state,
err_msg: action.payload.text
};
default:
return state;
}
}
reducers/index.js
import {combineReducers} from 'redux';
import {reducer as formReducer} from 'redux-form';
import LoginReducer from './login_reducers';
import CompanyReducer from './company_reducers';
const rootReducer = combineReducers({
login: LoginReducer,
companies: CompanyReducer,
form: formReducer
});
export default rootReducer;
component/select_teams.js
import _ from 'lodash';
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {fetchCompanies, getCompanies} from "../actions";
import {Link} from 'react-router-dom';
class SelectTeam extends Component {
constructor(props) {
super(props);
const token = localStorage.getItem('token');
this.state = {
token,
companies: null,
err_msg: null
}
}
componentWillMount() {
const tmp = this.props.getCompanies(this.state.token);
tmp.then(res => {
console.log(res)
})
.catch(err => {
console.log(err);
})
};
renderErrors() {
return (
<div>{this.state.err_msg}</div>
);
}
renderCompanies() {
return _.map(this.props.companies, company => {
return (
<li className="list-group-item" key={company.id}>
<Link to={`/${company.id}`}>
{company.name}
</Link>
</li>
)
});
}
render() {
if (this.props.companies === null) {
return (
<div>Loading...</div>
);
}
console.log(this.props);
return (
<div>
<h3>❤ Select Team ❤</h3>
{this.renderErrors()}
{this.renderCompanies()}
</div>
);
}
}
function mapStateToProps(state){
return {companies: state.companies}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
fetchCompanies: fetchCompanies,
getCompanies: getCompanies
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(SelectTeam);
App.js
import React, {Component} from 'react';
import './App.css';
import SelectTeam from "./components/select_teams";
import reducers from './reducers/index';
import {Provider} from 'react-redux';
import promise from "redux-promise";
import {applyMiddleware, createStore} from 'redux';
import {BrowserRouter, Route, Switch, Redirect} from 'react-router-dom';
import LoginPage from './components/loginPage';
const createStoreWithMiddleware = applyMiddleware(promise)(createStore);
const PrivateRoute = ({component: Component, isAuthorized, ...otherProps}) => (
<Route
{...otherProps}
render={props => (
isAuthorized() ? (<Component {...props} />) :
(
<Redirect to={
{
pathname: '/login',
state: {from: props.location},
}
}
/>
)
)}
/>
);
function PageNotFound() {
return (
<div>404 Page Not Found</div>
);
}
// TODO: I will add RESTful validation with backend later
function hasToken() {
const token = localStorage.getItem('token');
const isAuthenticated = !((token === undefined) | (token === null));
return isAuthenticated;
}
export const store = createStoreWithMiddleware(reducers);
class App extends Component {
//I will add security logic with last known location later.
//Get the features done first
render() {
return (
<Provider store={store}>
<BrowserRouter>
<div>
<Switch>
<PrivateRoute exact path="/select-teams" isAuthorized={hasToken} component={SelectTeam}/>
<Route path="/login" component={LoginPage}/>
<Route component={PageNotFound}/>
</Switch>
</div>
</BrowserRouter>
</Provider>
);
}
}
export default App;
You should dispatch an action with the data fetched from the server.
Actions are pure functions that return an object (the object has at minimum a TYPE field).
If you have any async operations, you may use Redux-Thunk, which is an action creator that returns a function, and call the api fetch within it.
Here is the actions snippet:
// imports..
export const fetchCompaniesSuccess = (data) => {
retyrn {
type: FETCH_COMPANIES_SUCCESS,
data
}
}
export const fetchCompanies = (token) => dispatch => {
// ...
axios(...).then(dispatch(data => fetchCompaniesSuccess(data)))
}
In your company_reducers.js,
// Company Reducer Function, State here represents only the companies part of the store
case FETCH_COMPANIES_SUCCESS: // should match the the type returned by the action
return [
...state,
...action.data
]
// other cases & default
MAKE SURE to add redux-thunk as a middleware in your createStore, read Redux-Thunk doc for instructions.
then in you component:
componentDidMount(){
this.props.fetchCompanies(this.state.token);
}
Once companies data is added to the redux store, your component will rerender and the companies array will be available in props
You don't need to have a duplicate companies array in the component state.
You may want to Watch Dan Abramov introduction to redux, it is a free course.
Seems like your dispatch syntax is wrong. The parameter should be an object with type and payload.
return instance
.then(data=> store.dispatch({
type: 'GET_COMPANIES_SUCCESS',
payload: data
}));