My First React App » The Login Feature

My First React App » The Login Feature


The Login Feature

One classic functionality of any modern Web App is to authenticate people in.

It's truly simple really: If you are authenticated you see "Screen A", else you see "Screen B". Basically, a Login is just a glorified conditional.

Let's build something like that together. The high level requirements are:

  • It should hook into the $REACT_ROOT_COMPONENT provided by react-root Service in order to block the rendering of the main App until authenticated
  • If NOT authenticated, it should render a LoginView component providing the UI/UX for a user to sign in
  • If authenticated, it should simply render the main App as defined by our existing "custom-root" feature
  • The login status should be handled by a React Context that wraps the App
  • 👉 The login itself, is just a dummy in-memory boolean variable, but we want to simulate some asynchronous loading activity for sake of realism

The React Components

Let's start from the simple definition of a few React components that will handle the UI/UX of this feature:

LoginView

This dumb component should handle the NOT Authenticated state, and provide a mean to enter the credentials and sign in.

/src/login/LoginView.js

const LoginView = ({ login }) => {
  const onSubmit = (evt) => {
    evt.preventDefault();
    evt.stopPropagation();
    login(evt.target.user.value);
  };

  return (
    <form onSubmit={onSubmit}>
      <input name="user" />
      <button type="submit">login</button>
    </form>
  );
};

Edit 040-login-feature-s1

LoginRoot

This component is the one that should conditionally render the LoginView any time login is needed.

This is a React Container as it will have to be state-aware.

/src/login/LoginRoot.js

const LoginRoot = ({ rootComponent }) => {
  // Get state info from a custom hook
  // (more about it later on)
  const { needLogin, ...loginAPI } = useLogin();

  // Block the main App and render the login screen:
  if (needLogin) {
    return <LoginView {...loginAPI} />;
  }

  // Render the main App and inject the login API in it:
  return cloneElement(rootComponent, loginAPI);
};

Edit 040-login-feature-s1

Fake it First

We are now going to take a small shortcut and write a very simple and totally fake implementation of the useLogin() hook.

Why do we do so?

We fake it so that we can progressively run our solution and make sure that the components we wrote so far work properly. Afer all, at the moment we only need a needLogin boolean and a login() function to be passed down into the LoginView component.

Let's do that in /src/login/use-login.js:

const useLogin = () => ({
  needLogin: true,
  login: () => {},
});

This is a perfectly viable fake hook. By manually changing the value of needLogin we can control which direction our LoginRoot will take.

A slightly more complicate fake hook, that could dynamically change the state of the App, hence better simulate the expected UX would be:

const useLogin = () => {
  const [user, setUser] = useState(null);
  return {
    needLogin: !user,
    login: setUser,
  };
};

In this implementation we use the useState hook to store the login state, and we pass the modifier to the login() method. With this small piece of code, our App will behave as expected, even if it is not yet complete.

The Feature Manifest - Part 1

We are ready to move into connect-the-dots time. We need to add out login Feature into the App's manifest, and make sure that it runs last, so that we can catch the App's root component as defined by our custom-root feature.

/src/login/index.js

export default {
  priority: -1, // Make sure it runs LAST
  target: '$REACT_ROOT_COMPONENT',
  handler: (App) => ({ component: () => <LoginRoot rootComponent={App} /> }),
};

Edit 040-login-feature-s1

There are two very important things to notice in the piece of code above.

The Priority

We set a negative priority so to make sure that our action fires last, no matter the Feature's order in the App's manifest.

The action's priority regulates the execution order of the handler, from higher priority to the lower.

The Waterfall

We really want our action to go last because we want to receive the App's root component as it was set by the custom-root Feature.

This is possible because the $REACT_ROOT_COMPONENT hook fires in waterfall mode, each action will receive the output of the one before.

Our handler receives the App's root component App and passes it down to our LoginLogic that works as a React HOC: it renders it only when the user is authenticated.

The LoginContext

Remember the time when we wrote a fake useLogin() hook? You do? Well, the time has come to de-fake it.

We should be able to use the useLogin hook all around the application, whenever we want to get details about the authentication state, or whenever we need to trigger an action such a logout.

As long the implementation relies on a useState it won't work as each component will share its own state.

👉 The solution is to use React's Context API to store the Authentication state globally for the whole App, and the useContext hook to operate it.

The LoginContextProvider Component

Let's start by creating the Context Provider for storing the Authentication state:

/src/login/LoginContext.js

const LoginContext = createContext();

const LoginContextProvider = ({ app }) => (
  <LoginContext.Provider value={useState(null)} children={app} />
);

Edit 040-login-feature-s1

Here we create a context and a custom provider component that will instrument our App with it.

In a normal React App you have the "context hell" issue, where you find a list of nested context providers like:

<ReactRouter>
  <MaterialUI>
    <i18nContext.Provider>
      <LoginContext.Provider>
        <MyApp />
      </LoginContext.Provider>
    </i18nContext.Provider>
  </MaterialUI>
</ReactRouter>

If you ask me, this is pretty awful and we get this issue solved for good with ForrestJS.

Our provider component receives the app property and wraps it with whatever is needed only for this specific Feature.

The state itself is just the User's name. It's a demo App so we don't really dig much into the theory and practice of a mutable React context.

Let's just say that the LoginContextProvider it's a regular React component, so it can use any kind of React hooks itself. By storing the result of a useState hook we guarantee a render action any time that such property changes its value.

The Feature Manifest - Part 2

To put in place our LoginContextProvider we are going to register a new action as it is provided by the react-root service:

registerAction({
  target: '$REACT_ROOT_WRAPPER',
  handler: { component: LoginContextProvider },
});

Edit 040-login-feature-s1

This is just another waterfall hook, each Action handler will receive the previous value, and it is supposed to return it as well. Or, in our case, a wrapped version of it.

Services like react-mui or react-router do exactly this.

The Final useLogin()

With the LoginContext in place, we can refactor the useLogin so to benefit from a globally available Authentication state:

import { LoginContext } from './LoginContext';

const useLogin = () => {
  const [user, setUser] = useContext(LoginContext);
  return {
    needLogin: !user,
    login: setUser,
  };
};

Edit 040-login-feature-s1

By all means, we simply moved the useState to be part of the LoginContext instead of the hook's instance.

More than that, we also provide a logout API that can be used all around the application by simply using this hook.

Export the useLogin

The last problem to solve is to make it possible for the use to logout.

Once logged in we render the custom-root Feature's component, and that Feature is supposed to be decoupled from the login.

We can bend this rule a little by saying that cross-feature dependencies must be exported by the Feature's manifest. In the /src/login/index.js you can add:

export { useLogin } from './use-login';

And in /src/custom-root/CustomRoot.js we can now show the authenticated user's name, and logout:

// Use the login state from the login Feature
import { useLogin } from '../login';

// My Component:
export const CustomRoot = () => {
  const { user, logout } = useLogin();

  return (
    <Box>
      <AppBar position="static">
        <Toolbar>
          <StarIcon />
          <Typography>Welcome {user}</Typography>
        </Toolbar>
      </AppBar>
      <Typography>My First React App with MaterialUI</Typography>
      <Link href="https://mui.com" color="primary" variant="body2">
        Open MUI Documentation
      </Link>

      <Button
        fullWidth
        variant="outlined"
        onClick={logout}
        sx={{ marginTop: 2 }}
      >
        Logout
      </Button>
    </Box>
  );
};

Edit 040-login-feature-s1

Working Examples

You can take a look at the basic source code as written following this tutorial:

And you can take a look to a slightly richer version of it, in which we also simulate an asynchronous login action that can fail, and implement error messages:

results matching ""

    No results matching ""