Refactoring Our React UI Into Composable Hooks

undraw svg category

Welcome to Part 23 of Up and Running with FastAPI. If you missed part 22, you can find it here .

This series is focused on building a full-stack application with the FastAPI framework. The app allows users to post requests to have their residence cleaned, and other users can select a cleaning project for a given hourly rate.

Up And Running With FastAPI

Last time we built out a CleaningActivityFeed component along with a custom useCleaningFeed hook that implemented all of the redux and async logic needed to fetch paginated lists of cleaning events and display them as a feed for authenticated users.

In this post, we’ll briefly pause any new feature development and focus on refactoring our ever expanding UI. The intent is to centralize much of our frontend logic into composable hooks that will help make our components smaller and more manageable.

Adding a UI Redux Slice

We’re going to be making quite a few updates in this post, so before we get into our first refactor, let’s get a few quick wins in the books. At the moment, we have some disparate logic related to protected routes and authentication that should probably be centralized.

If we look at our ProtectedRoute component, we see that it’s checking for an authenticated user and rendering LoginPage and EuiGlobalToastList components with a custom toast (stored in React local state) if no user is currently logged in. Then in our App component, we’re using the ProtectedRoute component a couple times to ensure that only logged in users can access certain pages.

This doesn’t smell right. The word “global” in EuiGlobalToastList most likely indicates that it should only exist once in our application. And using local state for toasts doesn’t feel right either. What if we want to create success toasts? Or info toasts? We’ll need to rework this one.

On top of that, we’re passing auth information into our ProtectedRoute component through redux’s higher order component, connect. We do that all over our application. Maybe it’s time to think about a better way to handle that as well.

Let’s do this in small chunks, refactoring where we need to. Many things will be left in place until we have a full rework ready for them.

First up, we’ll create a ui redux slice to handle all global UI state.

Add a new section to our redux/initialState.js file:

redux/initialState.js

And then create a new ui.js file in the redux directory.

All we’ll add to this file to begin with will be some logic to handle adding and removing toasts from redux.

redux/ui.js

We create two new actions for adding and removing toasts from redux, a new reducer for managing the toastList array we’re tracking, and two action creator functions responsible for dispatching our actions. In the addToast action creator, we also dedupe any toasts - in case we accidentally added multiple versions of the same toast.

That wasn’t so bad!

Make sure to add the new slice to our root reducer:

redux/rootReducer.js

Now let’s create a custom hook to manage our toasts logic. Since we’re going to be adding quite a few hooks this post, we’ll want to organize our directory more effectively. Create a ui directory inside the hooks directory, and add a useToasts.js file to that new directory. Let’s also move the useCarousel hook into that directory, since it falls under the ui category.

Now that breaks our LandingPage component’s imports, so we’ll go fix that and migrate to absolute imports while we’re at it.

LandingPage.js

That’ll do.

In the useToasts.js file, add the following:

hooks/ui/useToasts.js

Ok. Here we have a custom hook file that’s barely longer than 10 lines. It grabs a reference to the toastList stored in redux, and creates two wrapper functions that dispatch the associated action creator functions. Our hook then returns all three.

With that taken care of, let’s add the EuiGlobalToastList component to the Layout component, so that it will actually be a global component in our application. In this file - as in all others we touch in this post - we’ll also migrate to absolute imports.

Layout.js

No need to import redux, no need to add additional props. We destruture toasts and removeToast from the object returned by our useToasts hook, and then pass the needed values to the EuiGlobalToastList component.

Lovely.

We can now head into our ProtectedRoute component and make similar updates.

ProtectedRoute.js

Some improvements here. We’ve extracted the toasts logic out and that’s nice. But there’s still quite a bit of auth logic that could be centralized as well. Let’s do that next.

The useAuthenticatedUser hook

We’re injecting authenticated user state into multiple places across our app. Standardizing that process is going to make our lives easier, so that’s the first item on our agenda. Afterwards, we’ll use that hook to create a useProtectedRoute hook to handle most of what’s going on in our ProtectedRoute component.

Create a new directory called auth in the hooks directory, and add a useAuthenticatedUser.js file to it.

Now go ahead and add the following code to our new file.

useAuthenticatedUser.js

Looking good. We access any auth errors, a few status booleans, and the authenticated user - if it exists. We also add the logUserOut function for simplicity. Finally, we return an object with everything we’ve just added, and we’re good to go.

So, where can we use this new hook? Well, in a lot of places. Start with the Navbar and make the following changes:

Navbar.js

That was easy enough. We remove any references to redux, no longer need to connect the component, and can simply default export the Navbar function. We’ve also removed any props the component takes in, so everything is defined directly in the component. Additionally, we’ve moved over to using the UserAvatar component we defined in the last post to help standardize our use of avatars.

Moving on the ProfilePage component, let’s do the same thing.

ProfilePage.js

A lot of the same going on here. All that repetition dried up! Excellent.

This feels pretty good, but we’re only just getting to the cool part.

We’re going to make use of our new hooks, inside of another hook! Hook-ception.

Create another file in the hooks/auth directory called useProtectedRoute.js.

Inside that new file, add the following:

useProtectedRoute.js

Woah woah woah. This useProtectedRoute hook takes advantage of not one, but two external hooks to implement the appropriate logic. And it makes sense too. But it’ll make more sense when we actually see what the new ProtectedRoute component looks like.

Let’s update that now.

useProtectedRoute.js

And poof! Most of this component has disappeared. Or more appropriately, been abstracted into our useProtectedRoute hook. It gets the authenticated user, makes sure we’ve attempted to authenticate them already, and if no user is logged in, adds an “Access Denied” toast to our global toasts array. We then pass along the isAuthenticated and userLoaded flags which can be used in our ProtectedRoute component to render the proper display for authed and unauthed users.

Here’s a codesandbox with all of the associated code we’ve written up to this point. Check to make sure things match.

Check it out on Code Sandbox

phresh-frontend-part-10-refactoring-our-UI-with-custom-hooks

Our code is starting to feel quite a bit cleaner and easier to reason about. There’s one more set of changes that will offers marked improvements we should make before wrapping up for the day.

We’re going to create a useLoginAndRegistrationForm hook to abstract away much of the UI work that’s happening in our LoginForm and RegistrationForm components. There’s a lot of repetition between those two files as well, so it’ll feel nice if we can write a custom hook that’ll work for both.

the useLoginAndRegistrationform hook

Since the headline gives away our new hook’s name, there’s no need to introduce it.

Now, there are probably a few readers who might be thinking, “The name of that hook is way too long”. And they would be right. Maybe it should be called useAuthenticationForm or something. However, this isn’t a post about naming variables and hooks, so we’ll leave it what it is. At least the name explicitly describes its intended behavior.

So, what goes in it?

First, create the new file.

We’ll be extracting all of the core logic shared by our LoginForm and RegistrationForm components - minus the submission part - into this new hook. Anything submission-related, we’ll let the components handle.

Just note, we’re going to throw down a bunch of code here, then break apart what we’re accomplishing. First, it would probably make sense to go ahead and take a look at both the LoginForm.js and RegistrationForm.js components.

Notice the overlap between the two? That’s mainly what we’ll be looking to remedy. At the same time, we’re going to remove most of the non-markup-related code, as our hook will be handling that part for us.

This refactor will make our form components smaller and less obtuse. It will also set the stage for how we hope to handle forms in our application going forward. As the number of form hooks starts to grow, it may even make sense to colocate them with the components they server. For now, our current system will do nicely.

Ready?

Here’s what goes in our new custom hook:

useLoginAndRegistrationForm.js

Now this may seem like a lot, but we’ve actually consolidated the logic held in both form components into a single hook. We’ll use a single parameter to differentiate how the hook should handle the LoginForm component and how it should handle the RegistrationForm component.

The only real differences are the form object we return and the error messages displayed. Since the login form doesn’t need a username or passwordConfirm field, we exclude those from the return value for that form. We also coalesce all login auth errors into a single Invalid credentials. Please try again. message. The signup form will still display authentication error messages sent from our FastAPI server like:

As mentioned previously, the one thing this hook doesn’t manage is how to submit the form. Those implementation details are left up to the component. Let’s see what our LoginForm component looks like now:

LoginForm.js

Everything is already baked into our custom hook, so we simply destructure the needed values from the hook’s return value and use them in our component.

Our RegistrationForm component should look very similar. Let’s see what we have now:

RegistrationForm.js

Nothing too different here.

Again, we call the useLoginAndRegistrationForm form at the top of the component, destructure out the values we need, and then use them to render the form and handle the form submission process.

Give them both a whirl to make sure things are working as they should. Create a new dummy user via the registration process and then navigate around the app. Then, logout and attempt to sign in with the credentials for the recently created user.

Works like a charm, doesn’t it?

Here’s the final codesandbox showing the current state of our app with all the ‘fixins.

Check it out on Code Sandbox

phresh-frontend-part-10-implementing-the-useLoginAndRegistrationForm-hook

We’ve made quite a few updates in this post. More than enough to justify wrapping it up for the evening.

Wrapping Up and Resources

Our application is now leveraging multiple custom hooks to handle displaying toasts, injecting the authenticated user into components, protecting routes from unauthenticated users, and handling form interactions for our authentication process. This pattern should pave the road for all architectural designs going forward.

Next time we convene, we’ll be taking a look at populating cleaning jobs with their offers so we can start compiling statistics for the currently authenticated user.

Tags:

Previous Post undraw svg category

Designing A Feed Page For A FastAPI App

Next Post undraw svg category

Refactoring Our UI Into Hooks - Part II