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 byreact-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>
);
};
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);
};
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} /> }),
};
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} />
);
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 },
});
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
orreact-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,
};
};
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>
);
};
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: