Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent submit on route change Formik AutoSave

My app has form with <AutoSave/> component. This component calls submit once form values were changed. Everything works well but when changing the route it changes form values and <AutoSave/> calls submit. How to solve this problem? A possible solution is to mount <AutoSave/> again when changing the route.

Codesandbox

AutoSave:

import React, { useEffect, useCallback } from 'react'
import { useFormikContext } from 'formik'
import debounce from 'lodash.debounce'

const AutoSave = ({ debounceMs }) => {
  const formik = useFormikContext()

  const debouncedSubmit = useCallback(
    debounce(formik.submitForm, debounceMs),
    [formik.submitForm, debounceMs]
  )

  useEffect(() => debouncedSubmit, [debouncedSubmit, formik.values])

  return <>{!!formik.isSubmitting && "saving..."}</>
}

My app:

const App: FC = () => {
  const {books} = getBooks() // [{id: 1, title: 'test', summary: 'test'}, ...]
  const {query} = useRouter()

  const handleSubmit = useCallback(async values => {
    try {
      await API.patch('/books', {id: query.book, ...values})
    } catch (e) {}
  }, [query.book])

  return (
    <>
      <span>Books</span>
      {books.map(({id, title}, key) => (
        <Link key={key} href='/book/[book]' as={`/book/${id}`}>
          <a>{title}</a>
        </Link>
      ))}
      {query.book && (
        <MainForm  
         book={books.find(book => book.id === query.book)}
         handleSubmit={handleSubmit}/>
      )}
    </>
  )
}

MainForm:

type Props = {
  book: BookProps // {id: string, title: string ...},
  handleSubmit: (values) => Promise<void>
}

const MainForm: FC<Props> = ({book, handleSubmit}) => (
  <Formik 
    enableReinitialize 
    initialValues={{title: book.title, summary: book.summary}}
    handleSubmit={values => handleSubmit(values)}>
    {() => (
      <Form>
        //...My fields...
        <AutoSave debounceMs={500}/> // <=== AutoSave with debounce
      </Form>
    )}
  </Formik>
)
like image 525
Arthur Avatar asked Dec 11 '19 11:12

Arthur


2 Answers

Check it out: https://codesandbox.io/s/clever-sun-057vy

# Problem

useEffect(() => debouncedSubmit, [debouncedSubmit, formik.values]);

formik.values will always change even when the component mounts. That is why debouncedSubmit gets called on route change.

So basically, we don't want to run it as component first rendering but when the form is made changes by user.

formik.dirty is the key. Just check for formik.dirty before doing submit.

const AutoSave = ({ debounceMs }) => {
  const formik = useFormikContext();

  const debouncedSubmit = useCallback(
    debounce(formik.submitForm, debounceMs),
    [formik.submitForm, debounceMs]
  );

  useEffect(() => {
    formik.dirty && debouncedSubmit();
  }, [debouncedSubmit, formik.dirty, formik.values]);

  return <>{!!formik.isSubmitting && 'saving...'}</>;
};

Another thing is the Formik instance. This Formik will be used for all the books. So, you will need to reset the form when binding a new book into it, by using enableReinitialize prop.

<Formik
  enableReinitialize
  initialValues={{ title: book.title, summary: book.summary, id: book.id }}
  onSubmit={values => handleSubmit(values)}
>

Or use seperated instances for each book with key={book.id}

<Formik
  key={book.id}
  initialValues={{ title: book.title, summary: book.summary, id: book.id }}
  onSubmit={values => handleSubmit(values)}
>
like image 146
Ninh Pham Avatar answered Nov 15 '22 00:11

Ninh Pham


You need to have something like firstSubmit, where you check if firstSubmit already happened, so it only calls AutoSave on the second submit (where it actually changed).

const AutoSave = ({debounceMs}) => {
    const [firstSubmit, setFirstSubmit] = React.useState(false)
    const formik = useFormikContext();

    const debouncedSubmit = React.useCallback(
      debounce(firstSubmit ? formik.submitForm : () => setFirstSubmit(true), debounceMs),
      [debounceMs, formik.submitForm, firstSubmit, setFirstSubmit]
    );

    React.useEffect(debouncedSubmit , [debouncedSubmit, formik.values]);

    return <>{!!formik.isSubmitting ? 'saving...' : null}</>;
}

I'm not sure if the code works, I haven't tested it yet, because I'm not sure where debounce comes from, but the logic is that.

You should check if it already have submited once, and if so, skip it, only submiting when is the second time.

If you provide a working example, I can test it and make it work if the code above doens't work.

like image 24
Vencovsky Avatar answered Nov 14 '22 22:11

Vencovsky