DEV Community 👩‍💻👨‍💻

Brett Fisher
Brett Fisher

Posted on • Originally published at brettfisher.dev

How to Create Your Own React Text Field

On the home page of my blog, there is a search field that lets you search for posts by title or tags. I originally tried using Material UI's Text Field component, which seemed to be working just fine. However, once I built my site and ran it in production mode, the styling on the Text Field disappeared. I don't know if this is a problem with Material UI or Gatsby (or even something else), but I wasn't about to publish that to my site. All I wanted was a simple input element with some nice styling. I thought, "Hey, I'm a web developer. I'll just make the component myself"! I'll show you how I did it.

I'll be using React with TypeScript. If you want to follow along, I suggest either creating a new Gatsby site, which comes with TypeScript support out of the box, or adding TypeScript to a Create React App project. I'll be using Tailwind CSS to style my components, so you'll also want to install that.

Create a Basic input Wrapper

My goal was to just create a simple search field, or a generic text field that I could use in other places on my site. HTML's input field works just fine, but we'll need to style it a bit so that it looks nice. First, create a TextField.tsx file that just wraps a basic input element:

Code Sandbox

import React from 'react';

const TextField = () => {
  return <input />;
};

export default TextField;

Test it out in your App.tsx and it should just render a normal input field. Ok, it works, but it's not very useful. Let's add props for value and onChange so we can observe and manipulate the Text Field's state.

Code Sandbox

import React from 'react';

interface TextFieldProps {
  value: string;
  onChange: (val: string) => void;
}

const TextField = ({ value, onChange }: TextFieldProps) => {
  return (
    <input
      value={value}
      onChange={({ target: { value } }) => onChange(value)}
    />
  );
};

export default TextField;

Notice that input's default onChange event accepts a callback where the first argument is the input event. I'm not too interested in that, so I destructured that event and just pass in the value to the onChange callback. It just simplifies things a bit. Great, now we have a basic input wrapper! Let's work on styling it.

Styling our Text Field

If you haven't used Tailwind before, then it basically just provides a set of utility classes that easily let you style your components. I highly recommend checking it out.

Add the following className to your input component:

Code Sandbox

<input className="rounded-md w-full border border-gray-400 p-3 mb-5" ... />

These are Tailwind classes that round the corners on the input, give it a light gray border, add some padding and bottom margin, and makes the input the full width of its parent. These are just my personal preferences for a generic Text Field component - feel free to style yours however you want!

Adding More Useful Props

Our Text Field is looking great. But it would be nice to be able to modify other important values on the underlying input element, such as placeholder, autoFocus, name, and type. Adding those in as props is pretty easy:

Code Sandbox

import React from 'react';

interface TextFieldProps {
  value: string;
  onChange: (val: string) => void;
  placeholder?: string;
  autoFocus?: boolean;
  name?: string;
  type?: 'email' | 'password' | 'text';
}

const TextField = ({ onChange, ...rest }: TextFieldProps) => {
  return (
    <input
      className="rounded-md w-full border border-gray-400 p-3 mb-5"
      onChange={({ target: { value } }) => onChange(value)}
      {...rest}
    />
  );
};

export default TextField;

Notice that I decided to only destructure onChange because I use it a little differently than the way input does. All the other props are stored in rest because then they can be directly passed to input with the spread operator.

Doubling our Text Field as a textarea

I'll add in one more prop called textarea. If it's true, it will make our Text Field render a textarea instead of an input. This is simpler to do than creating a custom Textarea component because all the props we are passing to input can also be passed to textarea.

Code Sandbox

import React from 'react';

type InputElement = HTMLInputElement | HTMLTextAreaElement;
type InputChangeEvent = React.ChangeEvent<InputElement>;

interface TextFieldProps {
  value: string;
  onChange: (val: string) => void;
  placeholder?: string;
  autoFocus?: boolean;
  name?: string;
  type?: 'email' | 'password' | 'text';
  textarea?: boolean;
}

const TextField = ({ onChange, textarea = false, ...rest }: TextFieldProps) => {
  const InputElement = textarea ? 'textarea' : 'input';
  return (
    <InputElement
      className={`rounded-md w-full border border-gray-400 p-3 mb-5 ${
        textarea ? 'h-32' : ''
      }`}
      onChange={({ target: { value } }: InputChangeEvent) => onChange(value)}
      {...rest}
    />
  );
};

export default TextField;

There are a few important changes here. First take a look at the variable called InputElement. If the textarea is true, then our component will render a textarea. Otherwise, it will render a normal input.

Next, take a look at the className property. I wasn't satisfied with the height of the default textarea, so I conditionally added an h-32 class if the textarea prop is true. That just makes its height a little bigger.

Finally, take note of the onChange prop. Since our component can render either a textarea or an input, TypeScript got confused about the type of the value variable in the onChange callback. I created these two types at the top of the file:

type InputElement = HTMLInputElement | HTMLTextAreaElement;
type InputChangeEvent = React.ChangeEvent<InputElement>;

I just figured these out with my editor's intellisense. Now when I add the InputChangeEvent as the type annotation for the parameter in our onChange callback, TypeScript stops complaining.

Passing a Ref to our Text Field

So far, our custom Text Field is working out pretty well. The last feature I'd like is to be able to pass a ref to the underlying input element. This would be useful if we wanted to programatically focus the input, for example. Luckily this is pretty easy to do with React's ref forwarding feature. Pretty much all we have to do is wrap our functional component in a call to React.forwardRef.

Code Sandbox

import React from 'react';

type InputElement = HTMLInputElement | HTMLTextAreaElement;
type InputChangeEvent = React.ChangeEvent<InputElement>;

interface TextFieldProps {
  value: string;
  onChange: (val: string) => void;
  placeholder?: string;
  autoFocus?: boolean;
  name?: string;
  type?: 'email' | 'password' | 'text';
  textarea?: boolean;
}

const TextField = React.forwardRef<InputElement, TextFieldProps>(
  ({ onChange, textarea = false, ...rest }, ref) => {
    const InputElement = textarea ? 'textarea' : 'input';
    return (
      <InputElement
        ref={ref as any}
        className={`rounded-md w-full border border-gray-400 p-3 mb-5 ${
          textarea ? 'h-32' : ''
        }`}
        onChange={({ target: { value } }: InputChangeEvent) => onChange(value)}
        {...rest}
      />
    );
  }
);

export default TextField;

Now if a ref is given to Text Field, it will apply directly to the input or textarea component. The only way I was able to get TypeScript to stop complaining was to put ref={ref as any}, which isn't ideal but I wasn't too concered about it since I'm the only one who will be using this component. If you know how to give it a proper type, please let me know!

Conclusion

That's about all there is to creating a custom Text Field. While I love Material UI, it's a fun exercise to see if I can create components on my own. Plus, I still haven't figured out why Material UI has problems with its Text Field on my built site...anyways, happy coding!

Top comments (0)

🌚 Browsing with dark mode makes you a better developer.

It's a scientific fact.