useFormikを使う方法が一番簡単だと思います。このフックはフォームを操作する為の様々なハンドラーを返します。以下のように使います。
import {useFormik} from 'formik';
const AComponent = () => {
  const formik = useFormik(options);
  // ...
};
useFormikはいくつらオプションを受け取ります。ここではまだ全て書きませんが、initialValuesとonSubmitは必須、validateはオプショナルですがほぼ置いたほうが良いので取り上げます。
initialValuesはそのフォームの初期値でオブジェクトで指定します。例えば連絡フォームであれば、
const initialValues = {
  name: '',
  email: '',
};
のような感じです。既存のデータのものを復元する際は''ではなく、'佐藤 太郎'のような値を置いてあげます。
onSubmitは投稿した際に発火するハンドラーです。ハンドラーは引数としてその時のフォーム値を受け取るので、API に POST したり、Reducer にアクションを流したり様々なことができます。このハンドラーは下記のバリデーションチェックで返すオブジェクトが空(エラーが無い)場合のみ発火されます。
validateはフォーム値のバリデーションチェックを行います。この関数はフォーム内のformik.handleChangeが呼ばれる度実行されます。その関数にはその時のvaluesは引数として渡されるので、値が正しいかどうかを確認できます。この関数で返したオブジェクトはformik.errorsの中身として渡されるので、formik.errorsの対象のプロパティがあるかどうかや、それにエラーメッセージがあるかどうかでエラーメッセージの出し分けができます。
例です。
import { useFormik } from "formik";
const AComponent = () => {
  const formik = useFormik({
    initialValues: {
      foo: ""
    },
    validate(values) {
      if (values.foo !== "foo") {
        return { foo: "Please enter `foo`" };
      }
      return {};
    },
    onSubmit: values => {
      console.log(values);
    }
  });
  return (
    
  );
};
ここで使ってるformikプロパティはhandleSubmitとhandleChange、errorsの3つです。errorsは上記のバリデーションチェックの辺りに書きました。
formik.handleSubmitを対象の<form>タグに必ず渡します。よしなに Fromik がサイクルを回してonSubmitを発火してくれます。
<input>や<textarea>タグなど入力を期待する要素にはformik.handleChangeを設定します。このハンドラーを設定するタグにはnameかid属性が必ず必要です。Formik はその名前を見て、その名前のvaluesのプロパティ値を更新する為です。
上記のフォームではinput[name=foo]がありますが、nameという値にはバリデーションで'foo'しか渡せないようになってます。なのでaaやhogeなどと入力するとformik.errors.fooにエラーメッセージが渡る為、その時だけ<span style={{ color: "red" }}>{formik.errors.foo}</span>のような要素を表示するといったことができます。