2

In my component A, I want to get the isAuthenticated prop from Redux state. It’s always null, which is the initial value, whereas in all other components it’s always the latest value.

When user authenticates I send their credentials to the server. If it’s a success I store the token in Redux state. But my component A doesn’t see that token.

I tried to connect the Redux store directly to the component A and extract that isAuthenticated, but again it’s always null when I console.log(this.props.isAuthenticated)

The full code is in the GitHub repo

Component A:

import React from "react";
import { connect } from "react-redux";
import { auth } from "../../store/actions/index";
import { Redirect } from "react-router-dom";
import Input from "../../components/UI/input/input";
import Button from "../../components/UI/button/button";

class Auth extends React.Component {
  state = {
    controls: {
      email: {
        elType: "input",
        elConfig: {
          type: "email",
          name: "email",
          placeholder: "Email",
          label: "Email"
        },
        value: "",
        validation: {
          required: true,
          isEmail: true
        },
        valid: false,
        touched: false
      },
      password: {
        elType: "input",
        elConfig: {
          type: "password",
          name: "password",
          label: "Password",
          placeholder: "Password"
        },
        value: "",
        validation: {
          required: true,
          minLength: 9
        },
        valid: false,
        touched: false,
        redirect: null
      }
    }
  };

  checkValidity = (value, rules) => {
    let valid = true;
    if (!rules) return true;

    if (rules.required) valid = value.trim() !== "" && valid;
    if (rules.minLength) valid = value.length >= rules.minLength && valid;
    if (rules.maxLength) valid = value.length <= rules.maxLength && valid;

    if (rules.isEmail) {
      const pattern = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
      valid = pattern.test(value) && valid;
    }
    return valid;
  };

  inputChangedHandler = (e, elId) => {
    const updatedControls = {
      ...this.state.controls,
      [elId]: {
        ...this.state.controls[elId],
        value: e.target.value,
        valid: this.checkValidity(
          e.target.value,
          this.state.controls[elId].validation
        ),
        touched: true
      }
    };
    this.setState({
      controls: updatedControls
    });
  };

  signUpRedirect = () => {
    this.setState({
      redirect: "/sign-up"
    });
  };

  signInRedirect = () => {
    this.setState({
      redirect: "/sign-in"
    });
  };

  submitHandler = e => {
    e.preventDefault();
    this.props.onAuth(
      this.state.controls.email.value,
      this.state.controls.password.value,
      this.props.signUp
    );
  };

  render() {
    console.log(this.props.isAuthenticated);
    console.log(this.props);

    const formElementsArray = [];
    const { controls } = this.state;

    for (let key in controls) {
      formElementsArray.push({
        id: key,
        config: controls[key]
      });
    }

    const form = formElementsArray.map(el => (
      <Input
        key={el.id}
        elType={el.config.elType}
        elConfig={el.config.elConfig}
        value={el.config.value}
        shouldValidate={el.config.validation}
        touched={el.config.touched}
        valid={el.config.valid}
        name={el.config.elConfig.name}
        label={el.config.elConfig.label}
        placeholder={el.config.elConfig.placeholder}
        changed={e => this.inputChangedHandler(e, el.id)}
      />
    ));

    return (
      <section className="section-auth">
        {this.state.redirect !== null && this.state.redirect !== undefined && (
          <Redirect to={this.state.redirect} />
        )}
        <form onSubmit={this.submitHandler} className="section-auth__form">
          <h2 className="section-auth__title">
            {this.props.signUp ? "Sign up" : "Sign in"}
          </h2>
          {form}
          <Button
            type="submit"
            className="button--secondary"
            style={{
              marginTop: "2rem"
            }}
          >
            SUBMIT
          </Button>

          {this.props.signUp ? (
            <div className="section-auth__btn-box">
              <p className="section-auth__text">Already have an account?</p>
              <Button
                clicked={this.signInRedirect}
                type="button"
                className="button--primary section-auth__btn"
              >
                Sign in
              </Button>
            </div>
          ) : (
            <div className="section-auth__btn-box">
              <p className="section-auth__text">Don't have an account?</p>
              <Button
                clicked={this.signUpRedirect}
                type="button"
                className="button--primary section-auth__btn"
              >
                Sign up
              </Button>
            </div>
          )}
        </form>
      </section>
    );
  }
}

const mapStateToProps = state => ({
  isAuthenticated: state.auth.token
});

const mapDispatchToProps = dispatch => ({
  onAuth: (email, password, signUp) => dispatch(auth(email, password, signUp))
});

export default connect(mapStateToProps, mapDispatchToProps)(Auth);

Action creators:

import * as firebase from "firebase";
import {
  AUTH_START,
  AUTH_SUCCESS,
  AUTH_FAIL,
  AUTH_SIGNOUT
} from "./actionTypes";

const tokenExpTimeInMs = 3600000;

const saveToLocalStorage = (token, expDate, userId) => {
  localStorage.setItem("token", token);
  localStorage.setItem("expirationDate", expDate);
  localStorage.setItem("userId", userId);
};

export const authStart = () => ({ type: AUTH_START });

export const authSuccess = (token, userId) => ({
  type: AUTH_SUCCESS,
  idToken: token,
  userId: userId
});

export const authFail = error => ({ type: AUTH_FAIL, error: error });

export const authSignout = () => {
  localStorage.removeItem("token");
  localStorage.removeItem("expirationDate");
  localStorage.removeItem("userId");
  return {
    type: AUTH_SIGNOUT
  };
};

export const checkAuthTimeout = expTime => dispatch => {
  setTimeout(() => {
    dispatch(authSignout());
  }, expTime);
};

export const auth = (email, password, signUp) => dispatch => {
  dispatch(authStart());

  if (signUp) {
    firebase
      .auth()
      .createUserWithEmailAndPassword(email, password)
      .then(response => {
        response.user.getIdToken().then(token => {
          const expirationDate = new Date(
            new Date().getTime() + tokenExpTimeInMs
          );
          saveToLocalStorage(token, expirationDate, response.user.uid);
          dispatch(checkAuthTimeout(tokenExpTimeInMs));
          dispatch(authSuccess(token, response.user.uid));
        });
      })
      .catch(error => {
        console.log(error);
        dispatch(authFail(error));
      });
  } else {
    firebase
      .auth()
      .signInWithEmailAndPassword(email, password)
      .then(response => {
        response.user.getIdToken().then(token => {
          const expirationDate = new Date(
            new Date().getTime() + tokenExpTimeInMs
          );
          saveToLocalStorage(token, expirationDate, response.user.uid);
          dispatch(checkAuthTimeout(tokenExpTimeInMs));
          dispatch(authSuccess(token, response.user.uid));
        });
      })
      .catch(error => {
        console.log(error);
        dispatch(authFail(error));
      });
  }
};

export const authCheckState = () => dispatch => {
  const token = localStorage.getItem("token");

  if (!token) {
    dispatch(authSignout());
  } else {
    const expDate = new Date(localStorage.getItem("expirationDate"));
    if (expDate <= new Date()) {
      dispatch(authSignout());
    } else {
      const userId = localStorage.getItem("userId");
      dispatch(authSuccess(token, userId));
      dispatch(checkAuthTimeout(expDate.getTime() - new Date().getTime()));
    }
  }
};

Reducer:

import {
  AUTH_START,
  AUTH_SUCCESS,
  AUTH_FAIL,
  AUTH_SIGNOUT
} from "../actions/actionTypes";

const updateObject = (oldObject, updatedProperties) => {
  return {
    ...oldObject,
    ...updatedProperties
  };
};

const initialState = {
  token: null,
  userId: null,
  error: null
};

const authStart = (state, action) => updateObject(state, { error: null });

const authSuccess = (state, action) => {
  return updateObject(state, {
    token: action.idToken,
    userId: action.userId,
    error: null
  });
};

const authFail = (state, action) =>
  updateObject(state, {
    error: action.error
  });

const authSignout = (state, action) =>
  updateObject(state, { token: null, userId: null });

export default (state = initialState, action) => {
  switch (action.type) {
    case AUTH_START:
      return authStart(state, action);
    case AUTH_SUCCESS:
      return authSuccess(state, action);
    case AUTH_FAIL:
      return authFail(state, action);
    case AUTH_SIGNOUT:
      return authSignout(state, action);
    default:
      return state;
  }
};

Redux store:

import thunk from "redux-thunk";
import { createStore, combineReducers, applyMiddleware, compose } from "redux";

import authReducer from "./reducers/auth";
import userCdtReducer from "./reducers/userCountdownTimers";
import eventFormReducer from "./reducers/eventForm";

const rootReducer = combineReducers({
  auth: authReducer,
  userCdt: userCdtReducer,
  eventData: eventFormReducer
});

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

export default createStore(
  rootReducer,
  composeEnhancers(applyMiddleware(thunk))
);
  • Have you logged directly in the reducer to verify that the update is happening? I would also log `state` in `mapStateToProps`. – Brian Thompson Mar 20 '20 at 14:31
  • I did `console.log(state)`, but the values there are initial. And in my Redux dev tools it shows me that `AUTH_SUCCES` did change the state and I can see the token – Arnur Kupanov Mar 20 '20 at 14:45

4 Answers4

2

Your code is working but after you sign in this happens. So when App renders the sign-in route disappears and Auth is never re rendered.

Try the following in src/App.js:

if (isAuth !== null) {
  routes = (
    <Switch>
      <Route path="/new-countdown-timer" component={EventForm} />
      <Route path="/my-countdown-timers" component={UserCountdownTimers} />
      <Route path="/signout" component={Signout} />
      <Route path="/" exact component={Home} />
      <Route path="*" exact={true} component={() => <h1>404</h1>} />
    </Switch>
  );
}

Then when you sign in you'll get a nice 404. Maybe do a history.replace after you log in successfully

HMR
  • 37,593
  • 24
  • 91
  • 160
  • Thank you for the clarification! But what does that * mean in the Route path prop? – Arnur Kupanov Mar 20 '20 at 19:16
  • 1
    @ArnurKupanov The * means everything, so if none of the previous paths pick up the current url then the 404 will take it. – HMR Mar 20 '20 at 19:23
1

Little chance that this is the issue, but I think it's best practice to declare the state in the component constructor. => https://reactjs.org/docs/state-and-lifecycle.html

It's difficult to debug this amount of code just by looking at it. I would create a smaller project with a simplified working version of the code. If it's working, your start from there and check what's causing the issue...

Charles dB
  • 619
  • 5
  • 12
1

Your mini project is working for me, I can see the token.

The only reason you don't see it in your version is because you use componentDidMount that is only called once before you make the API call. If you replace component did mount by:

  componentDidUpdate(prevProps) {
      if (prevProps.state.token !== this.props.state.token) {
          console.log("[APP.JS], TOKEN:", this.props.state.token);
      }
  }

You'll see it,

it's working here >codeSandbox

Charles dB
  • 619
  • 5
  • 12
  • 1
    There is no componentdidmount in [the component](https://github.com/ExP15/timeres/blob/071edd5f3a6d19cfedb4cb64792a4b78c0355fb0/src/containers/auth/Auth.js). I think the OP is confused because OP removes the login route after login, does not have a 404 route and does not push or replace history so after login the login component never re renders. – HMR Mar 20 '20 at 18:05
0

I created a small version and it works. I get the token in my component. I would've provided codesandbox of it, but there are error I can't handle.

User credentials:

email t@t.com password 123456

GitHub repo

I would really use some help, or I think that I'll spend days of trying to fix it

Component App.js

import React from "react";
import { connect } from "react-redux";
import { auth } from "./actions.js";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      emailValue: "",
      passwordValue: ""
    };
  }

  inputChangeHandler = (e, elName) => {
    if (elName === "email") {
      this.setState({
        emailValue: e.target.value
      });
    } else {
      this.setState({
        passwordValue: e.target.value
      });
    }
  };

  formSubmiHandler = e => {
    e.preventDefault();
    this.props.onAuth(this.state.emailValue, this.state.passwordValue);
  };

  componentDidMount() {
    console.log("[APP.JS], TOKEN:", this.props.state.token);
    console.log("[APP.JS], STATE:", this.props.state);
  }

  render() {
    console.log("[APP.JS], TOKEN:", this.props.state.token);
    console.log("[APP.JS], STATE:", this.props.state);
    return (
      <div className="App">
        {this.props.state.token !== null && "You're logged in"}
        <form onSubmit={this.formSubmiHandler}>
          <input
            type="text"
            placeholder="email"
            value={this.state.emailValue}
            onChange={e => this.inputChangeHandler(e, "email")}
          />
          <input
            type="password"
            placeholder="password"
            value={this.state.passwordValue}
            onChange={e => this.inputChangeHandler(e, "password")}
          />
          <button type="submit">submit</button>
        </form>
      </div>
    );
  }
}

const mapDispatchToProps = dispatch => {
  return {
    onAuth: (email, password) => dispatch(auth(email, password))
  };
};

const mapStateToProps = state => {
  return {
    state: state,
    isAuth: state.token
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(App);

index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

import thunk from "redux-thunk";
import { createStore, applyMiddleware, compose } from "redux";
import { Provider } from "react-redux";

import reducer from "./reducer";

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));

const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  rootElement
);

Actions actions.js

import * as firebase from "firebase";

const firebaseConfig = {
};

firebase.initializeApp(firebaseConfig);

export const authStart = () => ({ type: "AUTH_START" });

export const authSuccess = (token, userId) => {
  return {
    type: "AUTH_SUCCESS",
    idToken: token,
    userId: userId
  };
};

export const authFail = error => ({ type: "AUTH_FAIL", error: error });

export const auth = (email, password) => dispatch => {
  dispatch(authStart());

  firebase
    .auth()
    .signInWithEmailAndPassword(email, password)
    .then(response => {
      response.user.getIdToken().then(token => {
        dispatch(authSuccess(token, response.user.uid));
        console.log(response);
      });
    })
    .catch(error => {
      console.log(error);
      dispatch(authFail(error));
    });
};

Reducer reducer.js

const updateObject = (oldObject, updatedProperties) => {
  return {
    ...oldObject,
    ...updatedProperties
  };
};

const initialState = {
  token: null,
  userId: null,
  error: null
};

const authStart = (state, action) => updateObject(state, { error: null });

const authSuccess = (state, action) => {
  return updateObject(state, {
    token: action.idToken,
    userId: action.userId,
    error: null
  });
};

const authFail = (state, action) =>
  updateObject(state, {
    error: action.error
  });

export default (state = initialState, action) => {
  switch (action.type) {
    case "AUTH_START":
      return authStart(state, action);
    case "AUTH_SUCCESS":
      return authSuccess(state, action);
    case "AUTH_FAIL":
      return authFail(state, action);
    default:
      return state;
  }
};