2

I am trying to learn React and React Hooks. I have created custom hook that lives in another file: CustomHook.ts. I am using it in my ContactForm.tsx. The problem I am having is inside each value={inputs.property} in the <input /> tags. Typescript is unable to resolve the types of each inputs._propertyName.

I have defined an interface IContact that defines the types I would like to use. I am NOT currently using this interface as I don't know where to put it.

Any help would be greatly appreciated!

Error:

ERROR in /jefthimi/src/components/Contact/ContactForm.tsx(35,31)
      TS2339: Property 'subject' does not exist on type '{}'.
ERROR in /jefthimi/src/components/Contact/ContactForm.tsx(46,31)
      TS2339: Property 'email' does not exist on type '{}'.
ERROR in /jefthimi/src/components/Contact/ContactForm.tsx(57,31)
      TS2339: Property 'name' does not exist on type '{}'.
ERROR in /jefthimi/src/components/Contact/ContactForm.tsx(68,31)
      TS2339: Property 'comments' does not exist on type '{}'.

ContactForm.tsx

import React from 'react';
import './ContactForm.scss';
import useContactForm from './CustomHook';

interface IContact {
  subject: string;
  email: string;
  name: string;
  comments: string;
}

const message = (inputs: any) => {
  alert(`Message Sent!
  Subject: ${inputs.subject}
  Sender: ${inputs.email}
  Name: ${inputs.name}
  Comments: ${inputs.comments}`);
};

const { inputs, handleInputChange, handleSubmit } = useContactForm(message);

export default class ContactForm extends React.Component {
  render() {
    return (
      <div className="contactForm_container">
        <div className="contactForm_inner">
          <form onSubmit={handleSubmit}>
            <div className="input-group">
              <label htmlFor="subject">Subject</label>
              <input
                id="subject"
                name="subject"
                type="text"
                onChange={handleInputChange}
                value={inputs.subject}
                required
              />
            </div>
            <div className="input-group">
              <label htmlFor="email">Your Email</label>
              <input
                id="email"
                name="email"
                type="text"
                onChange={handleInputChange}
                value={inputs.email}
                required
              />
            </div>
            <div className="input-group">
              <label htmlFor="name">Your Name</label>
              <input
                id="name"
                name="name"
                type="text"
                onChange={handleInputChange}
                value={inputs.name}
                required
              />
            </div>
            <div className="input-group">
              <label htmlFor="comments">Comments</label>
              <textarea
                name="comments"
                id="comments"
                rows={10}
                onChange={handleInputChange}
                value={inputs.comments}
                required
              />
            </div>
            <div className="controls">
              <button type="submit">Send Message</button>
            </div>
          </form>
        </div>
      </div>
    );
  }
}

CustomHook.ts

import React, { useState } from 'react';

/*
This is a Custom React Hook that handles our form submission
*/

const useContactForm = (callback) => {
  const [inputs, setInputs] = useState({});

  const handleSubmit = (event) => {
    if (event) {
      event.preventDefault();
    }
    callback();
  };
  const handleInputChange = (event) => {
    event.persist();
    setInputs((inputs) => ({
      ...inputs,
      [event.target.name]: event.target.value
    }));
  };
  return {
    handleSubmit,
    handleInputChange,
    inputs
  };
};

export default useContactForm;

2 Answers 2

7

The issue is that the initial state for inputs in CustomHook.ts is {}. Then you are trying to render inputs.subject, inputs.email, inputs.name and inputs.comments. These properties do not exist on the empty object {}, which is what the error messages are telling you.

Let's start with some basics. You have IContact, but you don't know what to do with it. You should be using it to type the data anywhere you expect that signature. To start with, the message callback.

const message = (inputs: IContact) => {
    alert(`Message Sent!
    Subject: ${inputs.subject}
    Sender: ${inputs.email}
    Name: ${inputs.name}
    Comments: ${inputs.comments}`);
};

How about the useContactForm hook? Well, you could, but I don't think I would recommend it. When I look at that hook, I don't see anything inside of it that references IContact. In this case, the hook is more generic.

If only there was some way of adding typing that was more generic...

Well, there is. What we want to do is be able to pass in a type to use as the typing for other objects.

const useContactForm = <T>(callback: (state: T) => void) => {
    //...code
}

Here, I added <T> to the front of the arrow function parameters. I also typed the callback as (value: T) => void to indicate the callback should accept a type of T as a parameter and not return anything.

Now, we need to type the useState function.

const [inputs, setInputs] = useState<T>({});

Uh-oh. {} doesn't match T. We need an initial state of type T. Since T is being passed in, our initial state will need to be as well.

const useContactForm = <T>(callback: (state: T) => void, initialState: T) => {
    const [inputs, setInputs] = useState<T>(initialState);
    // ...code
}

And pass it in.

    const {inputs, handleInputChange, handleSubmit} = useContactForm(message, {
        subject: '',
        email: '',
        name: '',
        comments: '',
    });

Okay. And that's basically how that needs to get handled. However, there are several other issues with the code and use of both TypeScript and Hooks.

  1. You need more typings. The event parameters in the event handlers should have types. e.g. ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  2. You are passing a function into setInputs instead of new state.
  3. You call the callback in handleSubmit with no arguments, but the message callback is clearly looking for an IContact.
  4. You are trying to use hooks in a class component instead of a functional one. You need to change the class component to a function and put the call to your custom hook inside of the function.

Here is some working code.

ContactForm.tsx

import React from 'react';
import './ContactForm.scss';
import useContactForm from './CustomHook';

interface IContact {
    subject: string;
    email: string;
    name: string;
    comments: string;
}

const message = (inputs: IContact) => {
    alert(`Message Sent!
  Subject: ${inputs.subject}
  Sender: ${inputs.email}
  Name: ${inputs.name}
  Comments: ${inputs.comments}`);
};

export default () => {
    const {inputs, handleInputChange, handleSubmit} = useContactForm(message, {
        subject: '',
        email: '',
        name: '',
        comments: '',
    });

    return (
        <div className="contactForm_container">
            <div className="contactForm_inner">
                <form onSubmit={handleSubmit}>
                    <div className="input-group">
                        <label htmlFor="subject">Subject</label>
                        <input
                            id="subject"
                            name="subject"
                            type="text"
                            onChange={handleInputChange}
                            value={inputs.subject}
                            required
                        />
                    </div>
                    <div className="input-group">
                        <label htmlFor="email">Your Email</label>
                        <input
                            id="email"
                            name="email"
                            type="text"
                            onChange={handleInputChange}
                            value={inputs.email}
                            required
                        />
                    </div>
                    <div className="input-group">
                        <label htmlFor="name">Your Name</label>
                        <input
                            id="name"
                            name="name"
                            type="text"
                            onChange={handleInputChange}
                            value={inputs.name}
                            required
                        />
                    </div>
                    <div className="input-group">
                        <label htmlFor="comments">Comments</label>
                        <textarea
                            name="comments"
                            id="comments"
                            rows={10}
                            onChange={handleInputChange}
                            value={inputs.comments}
                            required
                        />
                    </div>
                    <div className="controls">
                        <button type="submit">Send Message</button>
                    </div>
                </form>
            </div>
        </div>
    );
};

CustomHook.ts

import React, {useState, FormEvent, ChangeEvent} from 'react';

/*
This is a Custom React Hook that handles our form submission
*/

const useContactForm = <T>(callback: (state: T) => void, initialState: T) => {
    const [inputs, setInputs] = useState<T>(initialState);

    const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
        if (event) {
            event.preventDefault();
        }
        callback(inputs);
    };
    const handleInputChange = (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
        event.persist();
        setInputs({
            ...inputs,
            [event.target.name]: event.target.value,
        });
    };
    return {
        handleSubmit,
        handleInputChange,
        inputs,
    };
};

export default useContactForm;
Sign up to request clarification or add additional context in comments.

1 Comment

This is the best solution so far. It makes our Hook generic so that we can use it for different forms. I feel that I understand generics in Typescript a little bit more. Thank you for your input!
1

The proposed solution is to define an interface that contains our Message properties, then create a default Message object that has initialized fields.

interface IMessage {
  subject: string;
  email: string;
  name: string;
  comments: string;
}

const message: IMessage = {
  subject: '',
  email: '',
  name: '',
  comments: ''
};

We then setState with this initialized object.

const [inputs, setInputs] = useState(message);

4 Comments

If you define [key: string]: string; it won't warn you if you try to access fields that doesn't exist on the interface
@AsafAviv Good point. This will ruin the point of using Typescript in the first place.
You will just need to use the original interface with const [inputs, setInputs] = useState<IContact>({ Subject: 'me', Sender: 'you', ...rest }) and you will be good to go
@AsafAviv updated the solution. The code works and we have our types within our .tsx render method. Thank you for your input!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.