Our authentication system is pretty solid and works well, but if we try to start using it anywhere other than the one spot we’ve used it so far, we’re going to be copying and pasting the same request over and over within the application. On top of that just being a pain to manage, we’ll also be making duplicate, unnecessary requests that just waste time and bandwidth.
React gives us a few options for how we can mitigate this. Particularly, we’ll be looking at the Context API, which will allow us to easily access state in components that are defined in the same tree as whatever component we wrap with that Context, such as the root of the app, meaning all components will be able to access that Context.
In this lesson, we’ll walk through all the bits of how to make this work, which includes creating a new hook where we define that Context and the Provider which will wrap our application. We’ll then define and set up Types for the state that we’ll pass through our context, migrate our auth-related requests to this new hook, and dynamically update our UI to make use of our new hook including our login flow, hiding the Add Event for guests, and even redirect user’s away from pages that require authentication.
What You’ll Learn
Instructor: [0:00] So far, we've set up a pretty good foundation for being able to have different ways to interact with our authentication service. Looking specifically inside of lib auth.ts, we have our login, we have our session verification once we trigger that login path. We also have the ability to get our current session and a way to delete that session.
[0:18] The problem we're facing right now is in order to get that session and do anything with it, we need to do it per component or per page, which creates multiple requests and is just very inefficient for how we can interact with that service. What we need is a way to access that authenticated session globally.
[0:33] Whether it's inside the navigation or within an individual page, it's all accessing that same state. To do this, we're going to create a global instance of state using the context API, where we'll wrap our entire application so that state can be accessed anywhere within that tree.
[0:48] To start, we're going to package this all up all inside of a React hook file. I'm going to create a new directory. Since this is going to be our first hook. I'm going to call that Hooks. Inside, I'm going to create a new file called useAuth.ts.
[1:01] Inside, we're going to do four things. We're going to set up some context. We're going to set up a provider, which is going to be a component that wraps the entire application. We're going to create an internal hook, which we're going to call useAuthState, which is just going to allow us to work with the logic inside of this hook.
[1:17] Finally, our useAuth hook, which will be exported and pretty simple. It'll just use an instance of the auth state. Ultimately, that useAuth() hook will allow us to access that state anywhere inside of the tree that's wrapped by this provider. Let's get started by creating that context.
[1:32] First off, we're going to import the createContext() function from React. With that, I'm going to export a new const called AuthContext and I'm going to set that equal to CreateContext. Inside, we're going to need to explicitly set our default value. For now, let's just go with undefined. Ultimately, we need to define the type of the context that we're going to create.
[1:52] Let's go ahead and type that out. Let's call this LiveBeatAuthContext. That's probably a little long. You could probably figure out a different name, but ultimately we want to avoid a conflict with AuthContext.
[2:03] Let's start actually defining out this interface. We're going to define that LiveBeatAuthContext. This type is going to include anything that we include in this context that we want to pass down through the entire application.
[2:14] Let's start off with the core thing that we need. That's ultimately going to be our session. If we remember from earlier, our session is actually going to be the type that we get from Appwrite. I'm going to first import models from Appwrite.
[2:26] To start, we know that the session might not exist yet if they're currently unauthenticated. Let's make that optional, but then let's define that as models.session. As the last piece of AuthContext, if we look at the very end, we see that undefined is currently not an accepted value.
[2:41] What we want to do is we want to make sure that we scope that out inside of the type and also add undefined. Our basic authContext is ready to go. At this point, we need to use that authContext, where we're going to create what's called a provider, which is basically going to be a component that we wrap the entire application in, or wherever we want to include that context.
[2:59] Next, I'm going to export a new constant called authProvider. We're going to set this up to look like a component. That means I want to define my children so I can pass that right through, but first, we need to type that out. I'm going to create a new interface called AuthProvider Props, where we can define our children, which is potentially optional.
[3:18] We're going to define those as a React node. We can import that React node directly from the React package. Just as a quick note for the children type here, you can probably find different ways of typing this out, depending on what Stack Overflow you find. React node coming directly from the React package is a good way to handle this.
[3:34] Now let's use those props and actually define them coming through. Here, I'm ultimately going to return my component code. At this point, I can start to use my authContext. I'm going to define a new component as authContext, where the property of .provider is going to allow us to use that provider to wrap our component tree.
[3:52] Before we wrap up this component, we have a little bit of an issue. We can even see TypeScript isn't happy. We're currently working inside of a useAuth.ts file. That means we're not going to be able to actually define React components from within that. What we also need to do is we need to rename this file to a .tsx file, which is going to allow us to actually export that authProvider.
[4:13] Then we can finish wrapping up that provider. We can pass through the children. Finally, on this provider, we're going to want to define a value prop. This value prop is going to be where we define and pass our state all the way down the tree. We don't quite have that state set up yet.
[4:28] For now, just so that we can start testing this, let's add session is undefined, so that we know that we have something inside of there.
[4:35] AuthProvider isn't quite finished yet. We're ultimately going to want to grab our state and pass that down, but we don't have that set up yet. Before we do that, let's jump to our externally-exported hook that we're going to create so that we can start testing this and making sure that it's working in the first place.
[4:50] I'm going to export a new function called useAuth(). Inside, the first thing we're going to want to do is grab that context. Right now, we have createContext imported, but we also need to import useContext. In our function, let's say constant auth is equal to useContext. We'll pass right along in our authContext, which is what we defined earlier with createContext within this file.
[5:13] From here, we're going to want to pass this right through. We're not going to really do too much inside of this useAuth hook. When we're defining a hook like this that uses Context like this, we want to also make sure that we safeguard against the possibility that somebody uses this hook where they shouldn't. That location would be anywhere that's not wrapped by the provider.
[5:32] All we need to do in order to do that is let's just say, if auth doesn't exist, let's throw a new error. You can't make this work outside of that provider. We can just say, useAuth cannot be used outside of auth Context. Our useAuth hook is ready to go. We can start testing this out in our application. We can start to see things come through.
[5:54] To start, let's actually wrap our application with this auth provider. Inside of main.tsx, let's first import our auth provider from @hooks/useAuth. If we then scroll it down, we're going to want to wrap everything with this auth provider. We can probably do it inside of the router.
[6:11] I'm going to go ahead and just do it at the root of the tree. Let's say, auth provider is going to wrap that router. Again, we don't necessarily need to wrap our entire component tree with a provider in order to make Context work. In our useCase, we want our authstate to be global. We do want to wrap everything with it.
[6:29] We can start to test this out. Probably, the most obvious way to do that is within our nav component. We're currently accessing the session in order to show whether we want to log in or log somebody out.
[6:40] At the top of my nav component, I'm going to import useAuth from @hooks/useAuth. At the top of the nav component itself, I'm going to say constant Context is equal to useAuth.
[6:52] Ultimately, we don't want to import this as Context. This is just to test this out. Let's go ahead and console log out our Context. If we head back into the application, we can see that we are logging out that Context, which includes a property of session, which is currently undefined.
[7:07] That's exactly how we defined it when we were passing it down through that value prop. That means so far so good. It's working as expected. Ultimately, we want this value to be dynamic. We want it dependent on React state.
[7:19] To do this, we're going to create an internal state hook, which is going to manage that state and anything else we want to do inside of this whole Context. I'm going to export a new function called useAuthState.
[7:31] Inside, we can start to define our actual state, which is going to be our session where we can set our session. Just like we did inside of the navigation, of course, we need to import our useState hook from React. Let's return a new object, which is going to include that session.
[7:47] One thing to point out and keep in mind is this return statement is ultimately going to be the Context that we define and pass through the application. That means that that's going to be whatever we define inside of this auth Context type. At this point, we can grab our session just like we were doing inside of nav.tsx.
[8:04] I'm going to simply copy all this over. First, we need to make sure we import our getCurrentSession. At the top of useAuth, I'm going to import, getCurrentSession from @lib/auth.
[8:15] I'm going to copy this useEffect hook that's ultimately grabbing that session and setting it in state. I'm going to paste it right inside of my useAuthState hook, of course, we need to make sure we define useEffect as an import inside of React.
[8:28] We can see TypeScript is unhappy much like we did inside of nav.tsx. We need to make sure we define that state as that session. Let's define our state type as model session. Before we start to define anything else inside of this hook, let's wire this up so we can test it out.
[8:44] As I mentioned before, inside of auth provider is where we're going to pass all that through using the value prop. At the top of auth provider, I'm going to say, constant auth is equal to useAuthState. I'm going to simply replace this value with auth.
[8:59] Without doing anything else, let's check out the value of Context. Once the app reloads, we can see at first it's undefined. We can start to see our Context that includes that session object. That means that we can start to clean up some code.
[9:11] Let's get rid of all this session logic that we have inside of that nav component. I'm going to get rid of that useEffect hook. I'm going to get rid of that session state. I'm going to get rid of that console log. We don't need it anymore.
[9:22] I'm going to actually just destructure session from useAuth. Just for a moment, I'm going to comment out this set session just so that we can see this update. If we head back to our application, we can see that even if we reload the page, everything's working just as expected, just like it did before.
[9:37] We're now just getting that session from a global position. This is great progress. Let's keep adding on to this.
[9:43] Next up, how about this handle on log out function? If I uncomment this out, what's happening is we're deleting the current session. We're setting the session as undefined. That's something we could probably do globally as well.
[9:55] Heading back inside of our useAuth hook, the first thing that we want to do is make sure that anything that we create and define for that Context is defined inside of that authcontext type. Let's create a log out function, which is going to be the type of a function.
[10:09] Inside of our useAuthState hook, let's create a new async function called logout, where inside we're ultimately going to perform that same action.
[10:17] To start, let's copy over that code and paste it directly inside of our logout function. Of course, we need to make sure we define this delete import. I'm going to add it to lib/auth. Finally, to make this actually accessible, we now return this inside of that object.
[10:31] We're back inside of our nav file. I can now destructure logOut from useAuth, and instead of running the same code over and over wherever I want to logOut, I can now just say that I want to simply await logOut. To test this out, let's head back to the application. I'm going to click logOut. We can see we were immediately logged out.
[10:48] Now, before we move forward, because it's so much fun, we can also make sure that we're cleaning up all the code that we're no longer using inside of nav.tsx. Now, let's do the same thing for logging in. If I head over to my login page, we can see that we're currently importing login from auth.
[11:03] Now, technically, because of the way login works, where we're just sending an email with login, we're not doing anything to update state, we probably don't technically need to actually use it inside of the useAuth hook.
[11:14] One thing we want to keep in mind is trying to keep all the pieces together that we want to interact with so that we have one logical place to import all these pieces from. At the top of useAuth, I'm going to import login from lib/auth. I want to make sure that I define that on my auth context type.
[11:30] Then, I'm going to just simply return it directly inside of that object. Now, another benefit to this is in the future, if I need to define a custom login function and do something special, I can do that without having to re-import that function from everywhere within my application.
[11:46] For now, all we need to do is trigger that email so we can just pass in the login as is. Now, instead of importing login from auth, we're going to import our useAuth hook from @hooks/useAuth. At the top of our login page component, we're going to destructure login from our useAuth hook.
[12:05] Again, just to test this out, we can go to the login page, I can shoot over my email, and we can see our login link pop in right into our inbox. Before we click this, we might have one more thing to do. Inside of session.tsx, we're currently firing this verify session function, where we take the parameters that come back from that link and we validate that session.
[12:23] Now, after we do that, we navigate over to the home page, and what happens is it's going to trigger a new request which gets that updated session state. What if instead, after we run verify session, we take that session result and we just update our session globally.
[12:36] Back inside of auth.tsx, let's now import that verify session function. We're like login and logout, we want to make sure that we define that within our context.
[12:45] Heading to our useAuthState hook, we don't want to just simply pass this through like we did login, because what we want to do is similar to logout, once we run that function, we want to update our session with the results, which will actually come from that function.
[12:57] I'm going to define a new async function, and I'm going to call that verify session. I'm going to append and save, because we don't want to have a name conflict there. Inside, I'm going to say constantdata=awaitverifysession. We can see that TypeScript is unhappy with me, because if we remember, we need to pass in some options here.
[13:16] If we look at the verify session function, we can see that we have a user ID and a secret to pass through. Now, when setting these up, we do need to pass through these options from verify session save to verify session. When we're doing this, we're going to need to make sure we type that out.
[13:29] Now, there's definitely different ways in order to do this, but I'm thinking that probably the easier way to do this is just to make these arguments as an object. The only place that I'm going to be using this right now is inside of useAuth.tsx, so that migration should be pretty painless at this point.
[13:42] Let's turn these arguments into a single object, where now we need to define these types again. To do that, we're going to define verify session options. What I'm going to do is define that right above my verify session function, where I'm going to pass in my user ID as a string, and my secret, which will also be a string.
[14:02] Now, what I can also do is export this interface, where now inside of useAuth.tsx, I can now import that type, where now for the function itself, this should be pretty easy to set up. I can now say I have my options that I want to pass through, which will be the verify session options.
[14:16] I can pass that right through to the function. Now, looking back quickly at our verify session function, as soon as we run this update magic URL session and it's valid, we're going to return data, which is going to be a session. This new constant is going to be a session, so we can just say set session, which is going to be data.
[14:32] Finally, to make use of this, let's now pass this through the application as verify session, where we don't need to use the same name as this function because this is going to be a wrapper, but we know that we want to use it as verify session. Now, over on our session page, we can now import our useAuth hook from @hooks/useAuth.
[14:52] We can then destructure, verify session, from useAuth. The only other thing that we need to do is update how we pass in these parameters. If we remember, we're now passing in an object, so we just need to wrap that with an object.
[15:05] Now, whenever we go to that page, it'll fire verify session, which is actually coming from our hook, which is triggering verify session on save, which updates it and then passes that session data to our hook. Let's give that a shot where I'm going to copy that link and head back to my page, where I'm going to paste that in.
[15:20] We can see that it's logging us in and then redirecting us to the home page, which we can see we're now in an authenticated session. At this point, the only thing left for us to do is start to use this throughout our application and provide some optimizations for how we use that session data.
[15:32] Like on the login page, for instance, if we already know that we have a session, we can use that session object and say if we do have a session, we can bail out of rendering that page at all. Where the router we're using has a redirect component, which is specifically Wouter, so we can say that we want to redirect to our home page instead.
[15:51] While we can't click it inside of our nav, we can manually go to login and see that we will get redirected back to the home page. Because I know that I only want authenticated users to be able to add an event, heading over to my home page, I can import that useAuth hook from @hooks/useAuth, where I can destructure my session from useAuth.
[16:11] Then, for my link, I can say if I have a session, I'll go ahead and render that entire paragraph tag. We can see if I'm logged in, I get that addevent button. If not, we can see that I can log in but I don't get that button. As we can see, this gives us a great way to be able to manage our session logic from one place by being able to use hooks and context.
[16:31] Now, one thing to also point out and while we're not going to cover it in this course, you can take this a step further and use this with your router in order to provide protected routes. Now, because this can be router specific, we're not really going to touch on this.
[16:44] One thing you could do for instance is wrap all these routes in a protected route. That way, it'll first check for that session and then render that route. Otherwise, it'll just render or redirect to the login page like we did before.
[16:56] Additionally, because this is a client-side application, someone could technically go to this add event page where if I type out, events new, I can go to that page, but the nice thing is because this is session based and it's interfacing with AppRight, AppRight will know if I have permissions to submit that in the first place.
[17:12] If I don't, it'll lock it down, which leads us to our next lesson where now that we have authentication set up and we're sharing the way to interact with it globally, we'll see how we can configure granular permissions so that only the people who have access to perform certain tasks can actually do so.