Creating a Custom Button Component with the Props Spread Process
I'm really excited about this guide and I want to warn you this one is going to be a little bit longer but it is for a very good reason.
Guide Tasks
  • Read Tutorial
  • Watch Guide Video
Video locked
This video is viewable to users with a Bottega Bootcamp license

What we're going to work through and what we're going to build out in this guide alone might be one of the most important things you're going to learn in this entire course, and this isn't even just related to React Native, it's something related straight to React and to JavaScript as a whole. And if you learn the principles that I'm going to show you in this guide, your JavaScript code and specifically the React code that you write, whether it's for Native or for web applications, is going to be exponentially better. It's something that's a little bit more advanced which is why I don't teach it in the earlier courses, but if you learn this, the components you write are going to be perfectly written, you're going to have very refined code, it's going to be more streamlined. Other developers are going to enjoy working with your code and you're going to be able to make all of your components much more customizable.

And so what we're going to use as a case study is we're going to use the styles and we're going to create our own custom button component and along the way, we're going to see how we can dynamically pass in props to our custom components.

So let's get started. So right now we have a pretty ugly-looking button right here, it's nothing except some text. So I want to create a nice-looking button and so the first thing I'm going to do is I'm going to create a component and I'm going to simply put it up here at the top. We're eventually going to move it into its own file, but just to make things easy for you to see, I'm going to put it here. I'm going to start off like normal, I'm going to create an interface and I'm just going to call this IButtonProps and inside of here, I'm going to start listing off the props I want it to accept. So we're not going to go into the more advanced content yet. We're just going to start by creating a dead simple type of component. So I want this tab text and I want that text to be of a type string. I also want that to be required, so we're not going to add a question mark or anything like that. We're going to do that for a few components later because we're never going to want a button that doesn't have text, at least in this course. Maybe for some of your own projects in the future, maybe you want to create a button that only has an icon or something like that or can accept the component. Then that's fine, that's up to you. But for this one, we just want text on our button.

Next, we want to have a onPress handler. So we can say something like, we could call it an action and I'm warning you, we're going to change this later because I'm going to show you kind of the basic way, for lack of a better term, the rookie way to write a component and then we're going to see how we can make this even so much better. So I'm going to call this an action. This is going to be the function that we pass in and I'm just going to say this is of any type. And for right now, that's all we need to do for our interface.

AuthScreen.tsx

interface IButtonProps {
  text: string;
  action: any;
}

Now let's create the button component. So I'm just going to say const Button and then I'm going to say that this is going to work with props and it's going to accept the IButtonProps. This is going to be the shape of our button's properties and we'll make it a fat arrow function. And inside of here, I'm just going to return a TouchableOpacity component. So we can even come down here to the very bottom and we can copy everything that we have for this button. So we want the one for handleSubmit, so let's take that. Just going to cut all of that out and let's place that right inside of our button. Now we know a few things. Our headerText, we don't want to call that function because we're not going to have access to it. Imagine a scenario where we're calling this button and we are going to do this later in the course. We're going to call this button component throughout the entire course and from different parts of the application. So this headerText needs to be replaced with our props.text. So this will say props.text and we don't need a conditional or anything like that right now or ever because we know text is always going to be passed in. For onPress for this, we're going to replace this with props.action. So this should give us what we need.

AuthScreen.tsx

const Button = (props: IButtonProps) => {
    return (
        <TouchableOpacity onPress={props.action}>
            <Text  style={{ color: "white" }}>{props.text}</Text>
        </TouchableOpacity>
    );
};

Now let's just test this out, so I'm going to hit Save, Prettier formats everything for me. And then down here where we had our button before, let's go and let's add that. So I'm just going to say Button and then for text and then let's just put in that same headerText function and then we have that action prop and then let's pass in handleSubmit. And let's just see if we're back to where we were before.

AuthScreen.tsx

<Button text={headerText()} onPress={handleSubmit} />

large

And so yes, we have our login component but it's now being able to, it's working with that button component or new one. And if I press Login, we get our same alert. Everything is working nicely. Now let's start adding some styles. So if I scroll all the way up here, we can start adding a few of the styles that we know we're going to need. I'm going to put these on a few different lines and let's say that we have our styles, the first one we're going to have is a background color. So the background color here is going to be one that we added to our color guide, so let's open up our colors and I have this highlight here, so let's copy that. And then make that a string, put the hash symbol there. And now we have this background color. So that's going to work and if you hit Save, you'll see that our button now has that color.

Now let's make it a little bit taller just so we have, just so it's not so tiny there. So now let's just give it a height and for the height we can play around with a few different things. I'm going to go with 40. And that looks good. And if you notice, we have the text up here in the top left-hand side. But thankfully, if you remember, we get to use flexbox for all of our styles and we don't even have to import it manually, so we can just say justifyContent, that's going to be center and then alignItems which is also going to be center. Hit Save and that's looking good.

Now let's come down here to our text component and let's make the button a little bit bigger and let's also add the font so it's a little bit thicker as well. So let's go fontSize and for this one let's give it a font size of 20. And then let's also add a font weight and for that we'll go font weight of 700. Hit Save and that's looking good. Feel free to play with those, change 'em up, whatever you want to do so that that works for the type of styles that you're wanting.

AuthScreen.tsx

const Button = (props: IButtonProps) => {
  return (
    <TouchableOpacity
      style={{
        backgroundColor: "#eb523c",
        height: 40,
        justifyContent: "center",
        alignItems: "center"
      }}
      onPress={props.action}
    >
      <Text style={{ color: "white",  fontSize: 20, fontWeight: "700" }}>
        {props.text}
      </Text>
    </TouchableOpacity>
  );
};

large

So now if this, if we come to it, you'll see everything is still working. Our button is fantastic. But now I want you to imagine a scenario where all of a sudden, you might want to change some functionality with this button and if you're working for a company and you're working on, say it's a big company, and you're tasked with building a button component and you build it and you do everything you want, everything's perfect and it works great for the design they gave you, it would work for that one design. But if that's really what you were going to do, then there's not even a huge reason to create a dedicated component. Components need to be flexible, they need to work in all different kinds of scenarios. And so, now I'm going to show you what we can do to streamline this entire process.

And so, right here, let's imagine, because we are going to do this in this guide, let's imagine that you want to add a disabled state to the button. So in other words, right now I can technically press this a bunch of times and look, I get a bunch of error messages. That could create a problem. And then also, when we click this, we need to give some feedback to the user that a process is occurring, in this case, we're calling the API. So essentially, we want to disable the button so it can't be pressed multiple times, and then we also want to update the text so that we're letting the user know something is happening. And we might even want to change a few of the styles or something like that. Well, let's imagine because if you went through the documentation for TouchableOpacity, you can go see that it has what's called a disabled prop.

So let's go and let's still take kind of our rookie approach with this. So I'm going to say disabled, this is going to be optional, and then here this is going to be a boolean. So now we can pass this in.

AuthScreen.tsx

interface IButtonProps {
  text: string;
  action: any;
  disabled?: boolean;
}

And so let's come down here now and so if you have this, you have to make sure that you have this disabled state available to you. So you have a couple of options. I'll show you a horrible option, this is the one you definitely would never do. You could say something like this, you could say if props.disabled, then I want you to return and then you have to copy all of this and then create an else condition. And then wrap all of it in that. So I'm going to hit Save and this is, I'm going to show you this technically works. But notice that everything's pretty much identical and we're going to say if props is disabled, we're only going to make one change. We're just going to say disabled equals, we could say true or we could just say props.disabled. And so now let's also add one thing, let's add like a different color. Let's change the background color. So now let's change this, if you open up your color style guide, change this to be light gray. And then we can also change the text and you might even, yeah so we can just change the text or the text color or something like that. So you could change this maybe to be black or whatever you want to do, it's up to you. We're not going to keep this implementation, so just feel free to play around with it.

AuthScreen.tsx

const Button = (props: IButtonProps) => {
  if (props.disabled) {
      return (
        <TouchableOpacity
          style={{
            backgroundColor: "#EAEAEA",
            height: 40,
            justifyContent: "center",
            alignItems: "center"
          }}
          onPress={props.action}
          disabled={props.disabled}
        >
          <Text style={{ color: "white",  fontSize: 20, fontWeight: "700" }}>
            {props.text}
          </Text>
        </TouchableOpacity>
      );
    } else {
      return (
        <TouchableOpacity
          style={{
            backgroundColor: "#eb523c",
            height: 40,
            justifyContent: "center",
            alignItems: "center"
          }}
          onPress={props.action}
        >
          <Text style={{ color: "white",  fontSize: 20, fontWeight: "700" }}>
            {props.text}
          </Text>
        </TouchableOpacity>
      );
    };
};

But let's go down now to our component and what we're going to do is we're going to create some state. We're going to create some dynamic state. So actually we're going to get this whole thing working and then we're going to see how much better we can make our component. So let's add some state whenever this button or the app is in a loading state which is when everything gets submitted. So I'm going to add a new piece of state here, I'm back up top, I'm not in the Button prop, I'm in the AuthScreen prop. And I'm just going to create a new piece of state and we're just going to call it isSubmitting because we want this to work for both the registration component and the login component. So I'll say setIsSubmitting and we want this to have the default of false. So we want it to be false. We only want this to be triggered when we are contacting the server.

AuthScreen.tsx

const [isSubmitting, setIsSubmitting] = useState(false);

So the way we can do that is we can copy this setter method here and then come down to handleSubmit and say when you are working, so when you're about to call the API, we want to setIsSubmitting to true. And then you can copy that. As soon as we get a response back from the server, whether it is that the user is active or they're not, we want to make sure this goes to false because say the user types in the wrong email address, we want them to be able to type in the right one. So if we disabled our button, that wouldn't work. So we have to add setIsSubmitting false when it's successful, but we also want to do it inside of our error block. So we're going to say this and I'm going to put it even above the error. So let's hit Save here.

AuthScreen.tsx

  const handleSubmit = () => {
    setIsSubmitting(true);

    const params = {
      auth: {
        email: email,
        password: password
      }
    };
    API.post("memipedia_user_token", params)
      .then(response => {
        if (response.data.jwt) {
          props.navigation.navigate("Feed");
        } else {
          alert(
            "It looks like you typed in the wrong email or password, please try again"
          );
        }

        setIsSubmitting(false);
      })
      .catch(error => {
        setIsSubmitting(false);
        alert(
          "It looks like you typed in the wrong email or password, please try again"
        );
      });
  };

So this is all wired up, now let's come down to our button and I'm going to create two versions of this. So I'm going to create a ternary operator and don't worry if this looks a little bit messy when you're looking at it, the ternary operators in your React code are going to be something the more you do 'em, the more comfortable you're going to get writing them. It just takes practice. I remember the first time I saw a ternary operator, it was just really confusing and now I'll just do them without even really thinking about them. They just become second nature. So I'm going to create a ternary operator here, so make sure you add your blocks here. And I'm going to say if isSubmitting, so if ifSubmitting is true, then I want to have our disabled button or our submitted button. So I'm going to come up here and the Button text for the header, I want to change this. So I want this to be just a regular string and it can say Submitting. Something like that. And then I want to also put here that disabled is true. That's the case on when it's submitting.

Now if not, then we want to just return our normal button. So let's do that. So I'm going to add that in and let's hit Save, make sure we don't have any syntax errors or anything like that. And that all looks good. So now let's come up here. If I hit Login, you'll see that worked. We now have a isSubmitting button and it changed its state and it showed us our alert and then it turned off. Everything is working properly.

large

But I'm going to show you how we can actually take care of refactor this so that the code is going to be so much better and it's going to work not just in this scenario but in all kinds of different scenarios. If we play our cards right and write this out the way it should be, then we are not going to have to create any more button props no matter how big this application gets. So let's see how we can do that.

AuthScreen.tsx

      {isSubmitting ? (
        <Button text={"Submitting..."} action={handleSubmit} disabled={true} />
      ) : (
        <Button text={headerText()} action={handleSubmit} />
      )}

So I'm going to come up here and I'm going to actually get rid of a little bit of our interface. So I'm going to get rid of the action and I'm going to get rid of disabled. And I'm going to come down here and thank goodness, we're going to be able to get rid of half of this code. So I'm going to get rid of the entire conditional all the way from if all the way down to else and then make sure you get this trailing curly bracket here at the end so we don't have any syntax errors. And now what I'm going to be able to do is I'm going to get rid of this onPress handler and instead I'm going to do this. So at the very end, this is in TouchableOpacity, I'm going to say curly bracket dot dot dot which is our JavaScript spread operator props. This is really cool, this is one of my favorite parts of how you can write React code.

AuthScreen.tsx

const Button = (props: IButtonProps) => {
  return (
    <TouchableOpacity
      style={{
        backgroundColor: "#eb523c",
        height: 40,
        justifyContent: "center",
        alignItems: "center"
      }}
      {...props}
    >
      <Text style={{ color: "white",  fontSize: 20, fontWeight: "700" }}>
        {props.text}
      </Text>
    </TouchableOpacity>
  );
};

So the way this works if you've never seen this before, it can look really weird, but really pay attention right now because this is going to save you so much time and your code is going to be so much better. When we do this in JavaScript, essentially what we're doing is we're saying send me an object and then I'm going to break it up into pieces and then from there, we're going to be able to use each of those pieces as just normal objects. And I know that's still a little bit fuzzy if you've never done this before. And don't worry, it was for me too. When I initially saw this type of code in documentation, it made no sense to me. So it took some time for me to really work through it. So what we're doing here is we're telling any component that is going to call our Button component that if they pass in any prop that works for TouchableOpacity, we're going to dissect it, we're going to take out each one of the props they give us and they're all going to be passed in.

Now because we're using TypeScript, we do have to add an interface here. The key is this interface now has to match with whatever names TouchableOpacity uses. So we can't use our own made up names like action. Instead, what we're going to do is we're going to use the names of the props that the particular library in this case, the TouchableOpacity library uses.

So here, I'm going to say onPress and for onPress we can still make it required, that's fine, I'm still going to call it any. So this is like what we were doing with our action. Then from there, now let's work with that disabled prop. This is going to be one that is going to be optional and for this one it's still going to be boolean. And then from there, what's going to happen and I'm not going to hit Save quite yet because we're going to have some errors down below that we need to fix. And so what's going to happen here is when the Button prop is called, now assuming that we're passing in some value that is inside of our interface because TypeScript won't allow us to do anything else, then TouchableOpacity is going to be passed to each of those props, it's going to go into that prop list and it's going to apply them exactly like what we're doing right now.

AuthScreen.tsx

interface IButtonProps {
  text: string;
  action: any;
  disabled?: boolean;
}

So let me show you, this is one that makes more sense when you actually see it in action. So I'm going to come down here and to our Button you can see we have some errors and that's fine. Now we're going to change this to onPress just like we would if this Button component was a TouchableOpacity component. So now I can say onPress and let's hit Save here.

AuthScreen.tsx

      {isSubmitting ? (
        <Button text={"Submitting..."} onPress={handleSubmit} disabled={true} />
      ) : (
        <Button text={headerText()} onPress={handleSubmit} />
      )}

So now let's see what happens. So I'm going to come over here and I'm going to do the same thing and you can see it's working, it switched. I didn't have the styles yet so that's the reason why it's not gray but it switched it to the other one and you can also see that we have each of the things that we passed in. So we have the onPress handler. Let's go up and let's kind of tear this apart a little bit and see what's happening.

So even though we never called onPress in our TouchableOpacity component call, because we called props, we were able to extract each of the props that were sent and the reason why this is so powerful is imagine a scenario where you want to be able to create that button. So let's pretend you're working at some big company and you were given the task of building a button that's going to be used throughout the entire application. And this is true for web applications or mobile applications. You do not want to limit how others can interact with your component. So you don't want to be able to do something like say okay, I've created my list of props and that is what you have to use and you are going to have to also memorize these other names that I decided to come up with like the one we used before, like action and some of those. You're going to have to live by the terms my code wrote, not by the terms that the React Native or React uses. That is what makes this so powerful is it's going to make this so easy for yourself and also for any other developers to use your code. They can essentially do anything they want with it now.

So let's play with it a little bit more and let's customize it. So with the backgroundColor. Remember we wanted to change that, so here let's say that if props.disabled is passed in, then instead of the primary background color, let's use that light gray one that we had here before. So I'm just going to copy that and we're going to say once again with another ternary operator that if props is disabled, we want to use this light gray color. And if props is enabled, then we want to use the primary. So let's hit Save and let's just see if this is working.

So now I hit Login, you'll see that worked perfectly. Now we can also even change the text. So now you could see it was kinda hard to see that white text, let's do the same thing here with our color. I'm going to say props.disabled if that's true. Then I want you to let's just say we just want to use the primary text. So that's fine, we can just copy this primary text. We're eventually going to switch this so it's using the variable name, so don't worry about that. And then this is our normal ternary operator. We're checking to see our props.disabled, if yes, show this primary color, if not, show the white color. Hit Save once again and test it out. Hit Login, you'll see that is working beautifully.

AuthScreen.tsx

export default (props: IButtonProps) => {
  return (
    <TouchableOpacity
      style={{
        backgroundColor: props.disabled ? #EAEAEA : #eb523c,
        height: 40,
        justifyContent: "center",
        alignItems: "center"
      }}
      {...props}
    >
      <Text
        style={{
          color: props.disabled ? #eb523c : "white",
          fontSize: 20,
          fontWeight: "700"
        }}
      >
        {props.text}
      </Text>
    </TouchableOpacity>
  );
};

So I'm really happy with how this is coming along and before we end this, and I thank you for your patience and really working through this, because I know this is a little bit of a longer guide. But I want to show you something else that's really neat. So say that you're working in a situation where the other developer says you know what? I don't want these button styles. I don't want to have this as my background color and maybe I want everything positioned to the left, what if I want to do with those kinds of things. But I still want to have some of the other functionality that you've built in. And this isn't the best example because for the most part, then they really should just create their own TouchableOpacity or their own customized button if they don't want any of these things. So this is going to, the reason we wrote this component is to make it easy so we can essentially get this or something that looks like this button anywhere in the application whenever we need it.

But let's say that you did want that. You can even override props. So say that you also, and all you had to do was extend the interface. So you can just say style, it's optional, and this is going to be any. You do not have to change any of your code right here. If I come down here and I say okay, for my special super custom button, now I want to pass in styles. And for some reason, I want to do a backgroundColor that's going to be I don't know white, we can use anything like that. And, or not white because then we won't be able to see our text. Let's just say black. Just make it easy. And then you could also say, we'll hit Save and then I'll show you. You can see, it's kinda hard to see the black button but that is our button and we're able to override all of the style props. So we essentially now have a component that you can do anything that you want with. And so, I'm really liking the way that that is structured. So I'm going to remove that though because I do like this.

And just make a few more changes. So I'm going to remove this style prop and then let's add a borderRadius so I'm going to add a borderRadius of just three. So just add a few kinda rounded edges on not much, but as you can see, if you look here, you can see there's just a little bit of rounded edges. Now you can change this up if you want, you could say 30, you could say anything you wanted and you have a button that looks like that. I'm going to leave that up to you on how you want to design your own application.

AuthScreen.tsx

export default (props: IButtonProps) => {
  return (
    <TouchableOpacity
      style={{
        backgroundColor: props.disabled ? #EAEAEA : #eb523c,
        height: 40,
        justifyContent: "center",
        alignItems: "center",
        borderRadius: 3
      }}
      {...props}
    >
      <Text
        style={{
          color: props.disabled ? #eb523c : "white",
          fontSize: 20,
          fontWeight: "700"
        }}
      >
        {props.text}
      </Text>
    </TouchableOpacity>
  );
};

So now with all of that in place, let's actually put this in its own file just because we're good developers and we want to structure our code properly. So we're going to go into our components directory here and we have images, layouts, navigation. If this were a really big application and I knew I needed to have multiple buttons throughout it, then I might create a buttons directory. But in this case, we're only going to use this button. So I'm going to create a directory here inside of components and let's just call it helpers. And inside of helpers, this is going to be our button component. So button.tsx.

And now with that in place, let's go back to our AuthScreen and we're going to take our interface all the way down. Let's cut that out, paste it in, and let's see which components we need to import from React Native. So we know we need TouchableOpacity and we need Text and it appears that's all we need from React Native. So let's come up here, we can just copy this entire link and at the very top of the file, just paste that in. And as you can see, we don't need the View and we don't need the TextInput there. So you can hit Save, we can even refactor this so it doesn't have to say Button, so it can just say export default, hit Save.

And now let's import that. So let's come down here and we'll say import Button from and then this is going to be in a string dot dot slash and AuthScreen is in the screens directory so we need to jump back one more. Go to components, go to helpers, go to Button. Now let's test all of this out. If I hit Save, oh and it says it can't find the variable React. Yes, that's one other thing. You also need to import React into the Button component. So let's bring in that. We don't need useState. Okay, hit Save and you can see everything there is working.

Button.tsx

import React from "react";
import { Text, TouchableOpacity } from "react-native";

import { highlight, lightGrey } from "../../styles/colors";

interface IButtonProps {
  text: string;
  onPress: any;
  disabled?: boolean;
}

export default (props: IButtonProps) => {
  return (
    <TouchableOpacity
      style={{
        backgroundColor: props.disabled ? #EAEAEA : #eb523c,
        height: 40,
        justifyContent: "center",
        alignItems: "center",
        borderRadius: 3
      }}
      {...props}
    >
      <Text
        style={{
          color: props.disabled ? #eb523c : "white",
          fontSize: 20,
          fontWeight: "700"
        }}
      >
        {props.text}
      </Text>
    </TouchableOpacity>
  );
};

AuthScreen.tsx

import Button from "../../components/helpers/Button";

Now if I hit Login, it's still working, if I type in a valid email address, and hit Login, you can see we've been redirected. If you hit Refresh, it'll take you right back and you can see the button.

The last thing and then I will let you have a coffee break or water break or whatever you need after this long guide is to refactor this button so that it's actually using our style guide and then that's going to be our last thing. So we want to go grab our colors. So now let's go and say import and I'm just going to leave this open for right now and say from dot dot slash dot dot slash styles and colors. And then inside of colors, you can just copy these. Well I know we wanted highlight and light gray. Highlight and let's just type it out, lightGrey.

Button.tsx

import { highlight, lightGrey } from "../../styles/colors";

And part of the reason why I also like building out a style guide the way we did, not only does it make everything easier to change, so say you wanted to change your color so highlight maybe is a slightly different hexadecimal code, something like that, that's a very normal thing to happen, then you can make one change and it populates the rest of the application automatically. But also, it's kinda nice because when you're using named colors, it makes it a lot easier than trying to remember what these hexadecimal codes represent.

So I'm just going to anytime it says this eb one, that is our highlight. So I'm going to change it in the TouchableOpacity and then in the text prop. And then for lightGrey, then I'm going to do this and then we're going to keep the white here, so we don't need to change that one. So let's just hit Save here, verify everything's still working, I hit Login, everything is working beautifully.

Button.tsx

export default (props: IButtonProps) => {
  return (
    <TouchableOpacity
      style={{
        backgroundColor: props.disabled ? lightGrey : highlight,
        height: 40,
        justifyContent: "center",
        alignItems: "center",
        borderRadius: 3
      }}
      {...props}
    >
      <Text
        style={{
          color: props.disabled ? highlight : "white",
          fontSize: 20,
          fontWeight: "700"
        }}
      >
        {props.text}
      </Text>
    </TouchableOpacity>
  );
};

large

So really nice job in going through this. I know this was a lot of content, you may even want to go through this one again just to get a feel for this process. I know it's only one line of code, it's really just a word but this is going to make you such a better more professional React developer, whether it's on web or whether it's on mobile. Because you are going to have so many scenarios, I remember when I first started writing React code and I didn't have this, I had so many times where I would create a component where I would have a prop and then I would check to see if it existed and I'd have one component and then I'd say, if that prop doesn't exist, then do all of these other things. And so your components would get really long and then, you'd have this weird scenario where I would have to have like say it was a big component and it had five or six props, and half of those actually needed to be passed to the component, it would just have this kind of really weird feel where I was creating an interface and all I was doing was listing out each of those props exactly the way that they would if they were working with the direct component.

So that's really not a great way of writing code and it makes it very hard to change and maintain later on. But when you write it like this, then the users as long as they are familiar with working with say the TouchableOpacity component, all they have to do is reference that documentation to see how to work with yours. And the only change you'll ever have to make is to set up your rules in your interface and say these are the props I will allow, these are the ones that are optional, and because of the way TypeScript works, then the other ones wouldn't get allowed. That's how you can control the shape and how others get to use your component.

So great job going through that, that was a very advanced concept. So I hope it made sense to you and in the next guide, we're going to continue building out our application.

Resources