简体   繁体   中英

Submit a form with data using a custom React hook

I'm having trouble figuring this out. I want to create a hook that's called to submit a form using fetch.

This is what I have right now. The component holding the form:

const MyForm = (): ReactElement => {
    const [status, data] = useSubmitForm('https://myurl-me/', someData);
    
    return <>
        <div className='Feedback-form'>
            <div className='body'>
                <form>
                    <input type='text' name='username' placeholder='name' required />
                    <input type='email' name='email' placeholder='email' required />
                    <button className='submit-feedback-button' type='button'>Send feedback</button>
                </form>
            </div>
        </div>
    </>
}

The custom hook:

import { useState, useEffect } from 'react';

const useSubmitForm = (url: string, data: URLSearchParams): [string, []] => {

    const [status, setStatus] = useState<string>('idle');
    const [responseData, setData] = useState<[]>([]);

    useEffect(() => {
        if (!url) return;

        const fetchData = async () => {
            setStatus('fetching');

            const response = await fetch(url, {      
                method: 'POST',
                headers: {
                    'Accept': 'text/html',
                    'Content-Type': 'application/x-www-form-urlencoded'
                },
                body: data
            });

            const data = await response.json();

            setData(data);
            setStatus('fetched');
        };

        fetchData();
    }, [url]);

    return [status, responseData];
};

export default useSubmitForm;

My problem is that I think this hook is being called right away. How do I make this hook and call it in such a way that it's only called when the form is submitted and all the data I need to send in the request body is there to be included?

You are correct, the effect runs once when the component mounts and since url is truthy, it skips the early return and invokes fetchData .

How do I make this hook and call it in such a way that it's only called when the form is submitted and all the data I need to send in the request body is there to be included?

You need to also return a function for the component to invoke and pass along the form field values. I think you've a couple basic options.

  1. Convert the form fields to be controlled inputs and store the field state in the component and invoke a "fetch" function returned from the useSubmitForm hook.
  2. Return an onSubmit handler from the useSubmitForm to attach to your form element. The onSubmit handler would need to know what fields to access from the onSubmit event though, so passing an array of field names to the hook (ie a "config") makes sense.

Solution 1 - Use controlled inputs and returned fetch function

Unwrap the fetchData function from the useEffect hook and add a form field data parameter to it. Since fetch and response.json() can both throw errors/rejections you should surround this block in a try/catch. Return the custom fetchData function for the form to invoke.

useSubmitForm

const useSubmitForm = (
  url: string,
  data: URLSearchParams
): [function, string, []] => {
  const [status, setStatus] = useState<string>("idle");
  const [responseData, setData] = useState<[]>([]);

  const fetchData = async (formData) => {
    setStatus("fetching");

    try {
      const response = await fetch(url, {
        method: "POST",
        headers: {
          Accept: "text/html",
          "Content-Type": "application/x-www-form-urlencoded"
        },
        body: JSON.stringify(formData)
      });

      const data = await response.json();

      setData(data);
      setStatus("fetched");
    } catch (err) {
      setData(err);
      setStatus("failed");
    }
  };

  return [fetchData, status, responseData];
};

MyForm

const MyForm = (): ReactElement => {
  const [fields, setFields] = useState({ // <-- create field state
    email: '',
    username: '',
  });
  const [fetchData, status, data] = useSubmitForm(
    "https://myurl-me/",
    someData
  );

  useEffect(() => {
    // handle successful/failed fetch status and data/error
  }, [status, data]);

  const changeHandler = (e) => {
    const { name, value } = e.target;
    setFields((fields) => ({
      ...fields,
      [name]: value
    }));
  };

  const submitHandler = (e) => {
    e.preventDefault();
    fetchData(fields); // <-- invoke hook fetchData function
  };

  return (
    <div className="Feedback-form">
      <div className="body">
        <form onSubmit={submitHandler}> // <-- attach submit handler
          <input
            type="text"
            name="username"
            placeholder="name"
            onChange={changeHandler} // <-- attach change handler
            value={fields.username}  // <-- pass state
          />
          <input
            type="email"
            name="email"
            placeholder="email"
            onChange={changeHandler}  // <-- attach change handler
            value={fields.email}  // <-- attach state
          />
          <button className="submit-feedback-button" type="submit">
            Send feedback
          </button>
        </form>
      </div>
    </div>
  );
};

Solution 2 - Return an onSubmit handler and pass an array of fields to the useSubmitForm

useSubmitForm

const useSubmitForm = (
  url: string,
  data: URLSearchParams,
  fields: string[],
): [function, string, []] => {
  const [status, setStatus] = useState<string>("idle");
  const [responseData, setData] = useState<[]>([]);

  const fetchData = async (formData) => {
    setStatus("fetching");

    try {
      const response = await fetch(url, {
        method: "POST",
        headers: {
          Accept: "text/html",
          "Content-Type": "application/x-www-form-urlencoded"
        },
        body: JSON.stringify(formData)
      });

      const data = await response.json();

      setData(data);
      setStatus("fetched");
    } catch (err) {
      setData(err);
      setStatus("failed");
    }
  };

  const onSubmit = e => {
    e.preventDefault();

    const formData = fields.reduce((formData, field) => ({
      ...formData,
      [field]: e.target[field].value,
    }), {});
    fetchData(formData);
  }

  return [onSubmit, status, responseData];
};

MyForm

const MyForm = (): ReactElement => {
  const [onSubmit, status, data] = useSubmitForm(
    "https://myurl-me/",
    someData,
    ['email', 'username'] // <-- pass field array
  );

  useEffect(() => {
    // handle successful/failed fetch status and data/error
  }, [status, data]);

  return (
    <div className="Feedback-form">
      <div className="body">
        <form onSubmit={onSubmit}> // <-- attach submit handler
          <input
            type="text"
            name="username"
            placeholder="name"
          />
          <input
            type="email"
            name="email"
            placeholder="email"
          />
          <button className="submit-feedback-button" type="submit">
            Send feedback
          </button>
        </form>
      </div>
    </div>
  );
};

编辑 submit-a-form-with-data-using-a-custom-react-hook

In my opinion the second solution is the cleaner solution and requires less on consuming components to use.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM