There are different ways that you could set up the login flow for connecting to an Urbit ship through JS. Notice that in this example app we are keeping all the code in one file, this is to serve as a reference more than recommended architecture for a functional app. With that in mind, we'll be using a very simple method to allow a user to login to their ship and store their credentials in localStorage
for ease. It starts with a function we'll call createApi
outside of our main App()
function on line 32.
const createApi = (host: string, code: string) =>
_.memoize(
(): UrbitInterface => {
const urb = new Urbit(host, code);
urb.ship = "zod";
urb.onError = (message) => console.log(message);
urb.connect();
return urb;
}
);
While this tutorial does its best to be self-contained, notice here that we are using the memoize
method courtesy of lodash
which we import at the top of App.tsx
. This essentially caches the result of the function to reduce the amount of expensive computation our app has to do.
Next thing to notice is that we're using two imports from @urbit/http-api
. UrbitInterface
is is a TypeScript interface that lives in @urbit/http-api/dist/types.d.s
Read it's source to see the methods and variables it contains. We are passing it a variable called urb
that will hold an Urbit
object (which lives in @urbit/http-api/dist/Urbit.d.ts
).
The Urbit
object itself accepts a host
url and a ship code
. The host
is the port on localhost
that our Urbit uses to interact with the web, and the code
is the authentication code that we can use to open this connection. Once we pass in the urb
object we can now call methods on it such as .connect()
which establishes our connection to our ship.
Refer to the rest of UrbitInterface
to see other calls we will make in this tutorial, especially poke
thread
scry
and subscribe
, these are the four ways we will interact with our ship.
Take note that after we connect to our urb
we then have the createApi()
function return the urb
object itself.
Refer to the breakout lesson on React Hooks for a more detailed explanation of useState
and useEffect
, for the purposes of logging in we'll just say that these two hooks allow us to check to see whether or not a user has previously logged in, and if so, to keep track of their login status with a state variable that is accessible to our entire app.
The first state variable we define is loggedIn
on line 46:
const [loggedIn, setLoggedIn] = useState<boolean>();
We defined the variable name as loggedIn
and useState
then gives us a function for free to update it. Here we're calling it setLoggedIn
. Notice how these two names are destructured from the useState
hook which we are defining as a boolean
.
Next we define the second state vaiable urb
on line 47:
const [urb, setUrb] = useState<UrbitInterface | undefined>(); // Stores our Urbit connection. Notice we declare the type as UrbitInterface
Same as loggedIn
, urb
is a variable that will store our urb
object and we get a function setUrb
to add it to our state so that anywhere in our app we can call functions directly on our urb
.
Then on line 54 we have a function to setloggedIn
and tell our app whether or not our user has already logged in:
useEffect(() => {
if (localStorage.getItem("host") && localStorage.getItem("code")) {
setLoggedIn(true);
const _urb = createApi(
localStorage.getItem("host")!,
localStorage.getItem("code")!
);
setUrb(_urb);
return () => {};
}
}, []);
The short explanation of useEffect is that it replaces the traditional React lifecycle and allows us to specify actions to perform after the intial render is complete. Here we're checking to see if host
and code
exist in localStorage
. If so then we use our setLoggedIn
function described above to set the loggedIn
state variable to true
. Now we can run our createApi()
function from earlier, passing it the variables from localStorage
.
Remember that createApi()
returns an urb
object. Here we call it from within the variable _urb
which will become our connected urb
once createApi()
finishes. We then run setUrb(_urb)
(our useState
function that stores urb
) and thus our entire app now has access to our ship object. In the future we'll use this object a lot to call functions such as urb.poke()
urb.thread()
etc.
Notice the empty array []
at the end of the function. useEffect
allows us to give it variables or functions to monitor, and if they change the effect will run again with the new data. Since we only want this effect to run after the initial render we just pass an empty array []
so it only runs once.
The previous section covered situations in which a user has already used our app to login. If they have not, then the useEffect()
function will skip over the connect
function. Now let's look at how a new user can log in. The next function we see on line 68 is
const login = (host: string, code: string) => {
localStorage.setItem("host", host);
localStorage.setItem("code", code);
const _urb = createApi(host, code);
setUrb(_urb);
setLoggedIn(true);
return () => {};
};
In a moment we'll see the UI that calls this function. For now note that we take a host
and code
string (rember that an Urbit
object uses these two parameters to log in) and add them to localStorage
. Then using the same format from the last function we looked at, we call our createApi()
function, passing it the host
and code
credentials and then storing the resulting object in our state as urb
.
This time as well we set the loggedIn
state variable to true
. You should now be familiar with the pattern of using the functions that useState
gives us to modify state variables. With this, we are connected to our ship, our whole app now has access to our ship to call functions, and our whole app knows that we are logged in. This will come in handy when we look at how we collect the login credentials from our user below.
This section starts on line 413. The <form>
tag is mostly boiler plate React TypeScript code for collecting and submitting <input>
data. We're just adding a target
object that has a host
and code
key and handles the input from our text fields. We destructure each of those keys into its own variable, and then passing them into the login()
function that we explained above. We will use some variation of this pattern for each of our input fields in this app.
<pre>Login:</pre>
<form
onSubmit={(e: React.SyntheticEvent) => {
e.preventDefault();
const target = e.target as typeof e.target & {
host: { value: string };
code: { value: string };
};
const host = target.host.value;
const code = target.code.value;
login(host, code);
}}
>
Now we will use our loggedIn
state variable with a ternary operator to determine whether we should render UI for a user who has already logged in, or UI asking the user to login. We see this in the placeholder
prop.
loggedIn ? localStorage.getItem("host")! : "Host"
Simply writing loggedIn
actually means "if loggedIn
is true." (You would write !loggedIn
if you wanted "if loggedIn
is false). The ?
is the equivalent of then
and here we're having it render host
from localStorage
. As in, if the user is logged in then use host
from localStorage
as the placeholder in our text field. The !
at the end of this line is to tell TypeScript that we know host
will be a string.
The :
here serves as an else
statement. If we don't already know the user's host then this placeholder serves as a prompt for them to enter it.
Here is the rest of the code snippet. Notice we're doing the same thing for code
. Again this is a very simple application of conditional rendering, you can use ternary operators to completely customize your UI for a user who is logged in versus one who isn't.
{/* We are using ternary operators to get if the use already has login info in localStorage. If so we render that info as a placeholder
for each input form. Otherwise we render 'Host' or 'Code' as the placeholder*/}
<input
type="host"
name="host"
placeholder={
loggedIn ? localStorage.getItem("host")! : "Host"
}
/>
<br />
<input
type="code"
name="code"
placeholder={
loggedIn ? localStorage.getItem("code")! : "Code"
}
/>
<br />
<input type="submit" value="Login" />
</form>