Simple Authentication in React, Redux, Redux-Saga App

Simple Authentication in React, Redux, Redux-Saga App

·

0 min read

Let's write some Components and Hooks for Authentication in our App.

The Setup

setuo.png

  • routes/login: we'll write our container/router here.
  • auth.duck.js: this is where we write our reducer/selector/action creator/saga following Duck Pattern
  • hooks/commonHook.js: we'll implement a hook that tell us whether user has logged in or not.

Let get started

First, we will write the login container with a login form.

Capture.PNG

const _LoginRoute = ({ loginAction }) => {
  const isAuth = useAuthStatus(false);
  const handleLogin = values => {
    loginAction(values);
  };
  return (
    <React.Fragment>
      {isAuth ? <Redirect to="/user" /> : null}
      <div
        style={{
          display: "flex",
          alignItems: "center",
          height: "100%",
          justifyContent: "center"
        }}
      >
        <EnhancedLoginForm handleLogin={handleLogin} />
      </div>
    </React.Fragment>
  );
};

export const LoginRoute = connect(
  () => {
    return {};
  },
  {
    loginAction: loginAction
  }
)(_LoginRoute);

There are things that need explanation here:

  • LoginAction: action creator from auth reducer that we will write later
  • useAuthStatus: hook that tells us user login status, if user is already login, the page will be redirected to user page.
  • handleLogin/EnhancedLoginForm: we will not go in detail on this, the point of the EnhancedLoginForm component is to have a login form that compose and return user input in this case is values and call the handleLogin function that passed by parent component.

Ok, let write some ducks

First, we need some action creators

// action creator
export const loginAction = loginValues => {
  return { type: LOGIN_REQUEST, payload: loginValues };
};
export const logoutAction = () => {
  return { type: LOGOUT_REQUEST };
};

Then, we need Redux-Saga to watch our action:

// side effect
function* watchLogin(action) {
  const { username, password } = action.payload;
  //do some async validation with username and password
  //fake authentication
  let userData;
  if (username === "admin" && password === "admin") {
    userData = {
      username: "admin",
      name: "admin",
      roles: ["admin", "user", "VIP"]
    };
    const authValues = {
      apiToken:
        "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InRoYW5nIGxlIiwiaWF0IjoxNTE2MjM5MDIyfQ.RwCuXkEg2S1027iFOO3k59f8LFPXNdPrKjvAlLSzIo4",
      ...userData
    };
    localStorage.setItem("auth", JSON.stringify(authValues));
  }

  yield put({
    type: LOGIN_FINISH,
    payload: userData
  });
}

function* watchLogout(action) {
  localStorage.removeItem("auth");
  yield put({
    type: LOGOUT_FINISH
  });
}

export function* initUserData() {
  const userData = JSON.parse(localStorage.getItem("auth"));
  if (userData) {
    yield put({
      type: INIT_USERDATA,
      payload: userData
    });
  }
}

And we bind the Action with the saga like this:

export const AuthSaga = [
  takeLatest(LOGIN_REQUEST, watchLogin),
  takeLatest(LOGOUT_REQUEST, watchLogout)
];

Now, Redux-Saga will listen if there are any LOGIN_REQUEST, LOGOUT_REQUEST action dispatched, it will execute the corresponding functions.

In a usecase where we need to check if user has logged in when page first load or manually refreshed, that's when we need the initUserData, simply by getting data from the localStorage then trigger an action for the reducer to save it to the redux store.

And here is our reducer

export const AuthReducer = (state = initialState, action) => {
  switch (action.type) {
    case LOGIN_FINISH: {
      return {
        ...state,
        ...action.payload
      };
    }
    case LOGOUT_FINISH: {
      return {
        ...state,
        username: null,
        name: null,
        roles: []
      };
    }
    case INIT_USERDATA: {
      const { username, name, roles } = action.payload;
      return {
        ...state,
        username,
        name,
        roles
      };
    }
    default: {
      return state;
    }
  }
};

Last, we going for the hook

export const selectUsername = state => state.AuthReducer.username;
export const useAuthStatus = authStatus => {
  const [isAuth, setIsAuth] = useState(authStatus);
  const username = useSelector(selectUsername);
  const userData = localStorage.getItem("auth");
  const isLoggedIn = userData === null ? false : true;

  useEffect(() => {
    isLoggedIn ? setIsAuth(true) : setIsAuth(false);
  }, [username]);
  return isAuth;
};

Simple, right?, we just need to check if there are any data in localStorage, and by set username inside the 2nd parameter, the effect will re-run, check and set the auth status everytime username changed.

Conclusion

That's a Simple implementation of Authentication with React, Redux, Redux-Saga, there are more problems that i don't include to this article like:

  • JWT expire time where it failed to validate on server and we need to clear localStorage data and user need to re-login
  • Error handling where user/password is wrong, server is unavailable.
  • Loading state, show spinner and more.

Any suggestions are always welcome. See you in the next article.