Biblioteki do formularzy w React #3 – formik

Ten post jest trzecią częścią serii o obsłudze formularzy w React:

Spis Treści

  1. Wprowadzenie
  2. Prosty przykład
    1. Baza do dalszych prac
      1. Wersja klasyczna
      2. Wersja z hookami
    2. Walidacja
      1. Prosta walidacja
      2. Asynchroniczna walidacja
      3. Walidacja z pomocą biblioteki Yup i tworzenie schematów
      4. Pełny kod walidacji
    3. Customowe komponenty z hookiem useField
  3. Przykład zaawansowany
    1. Brak obsługi plików w Formik
    2. Komponent FieldsArray
    3. Wyświetlanie za pomocą CSS GRID
    4. Pełny kod
  4. Podsumowanie

Wprowadzenie

Formik to jedna z najbardziej uznanych bibliotek do zarządzania formularzami w React. Jej autor, Jared Palmer, jest autorem wielu projektów których my jako programiści używamy na codzień.

Formik upraszcza codzienną pracę z formularzami, ułatwiając zarządzanie wartościami w formularzu, walidacją i jego stanem wewnętrznym. Dzięki bibliotece możesz w prosty sposób:

  • podpiąć customowe walidacje które podepniemy tak prosto jak walidacje natywne,
  • podpiąć walidacje asynchroniczne
  • sygnalizować użytkownikowi, które pola zostały już przez niego wypełnione
  • obsługiwać tablice i obiekty jako pola formularza

Oraz wiele innych, jednocześnie używając prostego i czytelnego API.
Formik wspiera integrację z TypeScript. Obsługuje również React Native, dzięki czemu raz zdobyta wiedza może być użyta w aplikacji internetowej i aplikacji natywnej.
Wiedza jaką zdobędziesz czytając ten wpis pozwoli Ci używać biblioteki Formik w codziennej pracy, oszczędzając Ci wielu nieprzyjemności ręcznej obsługi formularzy.
Zahaczymy również o CSS GRID – jeśli chcesz dowiedzieć się o nim czegoś więcej, możesz obejrzeć prezentację jaką miałem podczas webinaru dla lokalnej grupy Tarnów Devs:

W opisie filmu znajdziesz link do slajdów na Google Drive oraz link do repo na Githubie do playgroundu którego w prezentacji używałem.

Prosty przykład

Formika używać można klasycznie jak i za pomocą hooków.

Zainstalujmy bibliotekę poleceniem

npm install formik

Baza do dalszych prac

Wersja klasyczna

Stwórzmy bazę do dalszych prac zawierającą jedno pole, najpierw w wersji klasycznej:

import React from 'react';
import { Formik } from 'formik';

function App() {
return (
<Formik
initialValues={{userName: ''}}
onSubmit={values => alert(JSON.stringify(values))}
>
{
({
values,
handleChange,
handleSubmit,
}) => (
<form onSubmit={handleSubmit}>
<input
onChange={handleChange}
name="userName"
value={values.userName}
/>
<button type="submit">
Submit
</button>
</form>
)
}
</Formik>
);
}

export default App;

Po zaimportowaniu głównego komponentu używamy go, przekazując parametr initialValues jako wartości startowe dla pól oraz funkcję onSubmit która jako parametr dostanie dane wysłane w formularzu.

Do wyświetlenia formularza używany jest wzorzec FACC – Function As Child Components, który umożliwia przekazywanie danych od komponentu biblioteki do naszego kodu w prosty sposób, jako parametry funkcji.

W kolejnej wersji formularza użyjemy większej ilości komponentów Formik więc kodu będzie mniej – natomiast na ten moment, aby dane mogły być zbierane, pole musi posiadać nazwę w atrybucie name oraz podpiętą w parametrze onChange funkcję handleChange, którą dostajemy od biblioteki jako parametr.

Dzięki komponentowi Field kod wygląda prościej:

<Field
  name="userName"
/>

//Powyższy kod jest równoważny do:
<input
  onChange={handleChange}
  name="userName"
  value={values.userName}
/>

Niestety komponent Field nie działa z prezentowanym poniżej Hookiem useFormik.

Wersja z hookami

Użyjmy hooka useFormik:

import React from 'react';
import {useFormik} from "formik";

function App() {
  // dzięki destrykturyzacji API jest czytelniejsze
  const {
    values,
    handleChange,
    handleSubmit,
  } = useFormik({
    initialValues: {
      userName: '',
    },
    onSubmit: values => alert(JSON.stringify(values)),
  })

  return (
    <form onSubmit={handleSubmit}>
      <input
        onChange={handleChange}
        name="userName"
        value={values.userName}
      />
      <button type="submit">
        Submit
      </button>
    </form>
  );
}

export default App;

W tej wersji wszystkie potrzebne nam elementy / funkcje zwracane nam są z hooka useFormik.
Kod wygląda nieco czytelniej i jest zdecydowanie mniej zagnieżdżony – choć musimy pamiętać, że sami autorzy biblioteki przestrzegają przed nadużywaniem tego hooka. Nie wszystkie komponenty będą z nim działać.

Walidacja

Prosta walidacja

Oznaczenie pola jako wymagane możemy zaimplementować przekazując do komponentu Formik bądź hooka useFormik funkcję validate:

validate={values => {
  const errors = {};
  if (!values.userName) {
    errors.userName = true;
  }
  return errors;
}}

Następnie obiekt zwracany przez tą funkcję dostępny będzie jako parametr errors, używamy również parametru touched by nie wyświetlać błędów dla pól, których użytkownik jeszcze nie zmodyfikował:

{touched.userName && errors.userName && (
  <div>Provide user name</div>
)}

Biblioteka Formik dostarcza komponent ErrorMessage który upraszcza wyświetlanie błędów. By kod pozostał prosty, musimy nieco zmienić funkcję walidującą, tak by ustawiać nie boolean a string z wiadomością walidacji:

validate={values => {
  const errors = {};
  if (!values.userName) {
    errors.userName = 'Provide user name';
  }
  return errors;
}}

Teraz używamy komponentu ErrorMessage, który automatycznie sprawdzi czy pole o danym name zostało zmodyfikowane i czy jest błąd z nim zwiazany:

<ErrorMessage name="userName" component="div" />

//Równoważne do:
{touched.userName && errors.userName && (
  <div>{errors.userName}</div>
)}

Asynchroniczna walidacja

Walidacja może być asynchroniczna. Dodajmy, jak w poprzednim wpisie, pole captcha:

<Field
  name="captcha"
  validate={validateCaptcha}
/>
<ErrorMessage name="captcha" component="div" />

Skorzystaliśmy tutaj z faktu, ze walidację podpinać możemy nie tylko pod cały formularz, ale również pod konkretne pole. Funkcja validateCaptcha może wyglądać w następujący sposób:

// Symulacja zapytania
const sendRequest = value => new Promise((resolve, reject) => {
  setTimeout(() => {
    const valid = value === 'test';
    resolve(valid)
  }, 5000)
})

const validateCaptcha = async value => {
  const valid = await sendRequest(value);
  return valid? true : 'Please provide valid captcha';
}

Walidacja asynchroniczna pola opóźni również walidację formularza – jeżeli nie podasz ani name ani captcha , wykona się zapytanie i dopiero wtedy zobaczysz błędy dotyczące obu pól.

Walidacja z pomocą biblioteki Yup i tworzenie schematów

Biblioteka Yup pozwala w czytelny sposób walidować nawet zaawansowane obiekty. Zainstalujmy ją

npm install yup

Następnie naszą funkcję walidującą:

validate={values => {
const errors = {};
if (!values.userName) {
errors.userName = 'Provide user name';
}
return errors;
}}

Zastąpmy validationSchema używającym biblioteki Yup:

validationSchema={yup.object().shape({
userName: yup.string().required('Provide user name')
})}

Co ważne, również przy użyciu validationSchema walidacja poczeka na walidację asynchroniczną, która może odbywać się np na którymś z pól.

Pełny kod walidacji

Zobacz pełny kod
import React from 'react';
import {ErrorMessage, Field, Formik} from 'formik';

const sendRequest = value => new Promise((resolve, reject) => {
setTimeout(() => {
const valid = value === 'test';
resolve(valid)
}, 5000)
})

const validateCaptcha = async value => {
const valid = await sendRequest(value);
return valid? true : 'Please provide valid captcha';
}

function App() {
return (
<Formik
initialValues={{userName: '', captcha: ''}}
validate={values => {
const errors = {};
if (!values.userName) {
errors.userName = 'Provide user name';
}
return errors;
}}
onSubmit={values => alert(JSON.stringify(values))}
>
{
({
handleSubmit,
}) => (
<form onSubmit={handleSubmit}>
<Field
name="userName"
/>
<ErrorMessage name="userName" component="div" />
<br />
<Field
name="captcha"
validate={validateCaptcha}
/>
<ErrorMessage name="captcha" component="div" />
<br />
<button type="submit">
Submit
</button>
</form>
)
}
</Formik>
);
}

export default App;

Customowe komponenty z hookiem useField

Hook useField udostępnia API pozwalające podpięcie customowego pola pod Formik:

const [field, meta, helpers] = useField(props);

Stwórzmy więc komponent, który umieścimy w formularzu:

<CustomCheckbox name="checkbox" />

Zaimplementujmy ten komponent jako dwa przyciski gdzie jeden ustawia wartość na true a drugi na false. Dodatkowo, jeżeli użytkownik nie wykonał jeszcze żadnej akcji wyświetla się podpowiedź by kliknął jeden z przycisków, a jeżeli akcję już wykonał – możliwość wyczyszczenia obecnego wyboru.
Element zostanie automatycznie oznaczony jako touched w momencie wysłania formularza, my jednak chcemy dokonywać tego po każdym wyborze.
Komponent może wyglądać następująco:

const CustomCheckbox = props => {
  const [field, meta, helpers] = useField(props);

  return (
    <>
      {!meta.touched && <div>Hint: click one of buttons:</div>}
      {meta.touched && (
        <div>
          <button type="button" onClick={() => {
            helpers.setTouched(false);
            helpers.setValue(false)
          }}>
            Clear field status
          </button>
        </div>
      )}
      <button type="button" onClick={() => {
        helpers.setValue(true);
        helpers.setTouched(true);
      }}>
        Set to true {field.value && '(current)'}
      </button>
      <button type="button" onClick={() => {
        helpers.setValue(false);
        helpers.setTouched(true);
      }}>
        Set to false {!field.value && '(current)'}
      </button>
    </>
  );
};

Przykład zaawansowany

Zaawansowany przykład zawierał będzie, jak w poprzednim poście, galerię zdjęć – dajmy jednak więcej możliwości konfiguracji sposobu wyświetlania i za pomocą CSS GRID poukładamy te zdjęcia nieco ładniej.
Co ważne, Formik na ten moment nie wspiera obsługi plików, stąd nie możemy użyć np <Field type="file" />, musimy napisać nasz własny komponent.

Brak obsługi plików w Formik

Posłużymy się wiedzą z poprzedniego rozdziału artykułu:

const FileUpload = (props) => {
const [field, meta, helpers] = useField(props);

return (
<input
name={props.name}
type="file"
onChange={e => helpers.setValue(URL.createObjectURL(e.target.files[0]))}
/>
);
}

Dzięki hookowi useField możemy ustawiać wartość na polu formularza.

Komponent FieldsArray

Komponent FieldsArray pomoże nam w obsłudze tablicy jako pola formularza:

<FieldArray
name="images"
render={helpers => (
<div>
{
values.images.map((image, index) => (
<div>
<Field
name={`images[${index}].name`}
placeholder={`Image ${index + 1} name`}
/>
<br />
<FileUpload
name={`images[${index}].file`}
/>
</div>
))
}
<button
type="button"
onClick={() => helpers.push({name: ''})}
>
Add image
</button>
</div>
)}
/>

Komponent ten udostępnia metody pomocne w obsłudze pól, np metodą push dodasz kolejny obiekt do tablicy pól. Zauważ, że pola nazwane są w konwencji images[index].field, ponieważ Formik obsługuje zarówno tablice jak i obiekty zagnieżdżone mamy możliwość zapisywania danych od razu do tablicy obiektów, bez konieczności żadnych przekształceń.

Wyświetlanie za pomocą CSS GRID

Dane zapisujemy do lokalnej zmiennej stanu:

const [images, setImages] = useState([])

// W reakcji na wysłanie formularza
onSubmit={(values, {resetForm}) => {
  setImages([
    ...images,
    ...values.images,
  ]);
  resetForm();
}}

Same obrazki wyświetlimy na siatce CSS GRID:

Images:
<div
  style={{
    display: 'grid',
    gridTemplateColumns: 'repeat(2, 20%) 50px 3em 1fr',
  }}
>
  {images.map(image => (
    <div>
      <strong>
        {image.name}
      </strong>
      <br />
      <img style={{maxWidth: '100%'}} alt="" src={image.file} />
    </div>
  ))}
</div>

Stworzyliśmy siatkę 5 wierszy na 5 kolumn, gdzie mamy 2 kolumny zajmujące 20% przestrzeni, jedną zajmującą 50px, jedną zajmującą 3em i ostatnią zajmującą pozostałą przestrzeń.

Jeśli chcesz dowiedzieć się więcej o CSS GRID, polecam obejrzeć moją prezentację z webinaru Tarnów Devs:

W opisie filmu znajdziesz link do prezentacji na Google Drive oraz adres repo Githuba dla playgroundu którego użyłem w prezentacji.

Pełny kod

Pokaż pełny kod
import React, {useState} from 'react';
import {Field, FieldArray, Formik, useField} from 'formik';

const FileUpload = (props) => {
const [field, meta, helpers] = useField(props);

return (
<input
name={props.name}
type="file"
onChange={e => helpers.setValue(URL.createObjectURL(e.target.files[0]))}
/>
);
}

function App() {
const [images, setImages] = useState([])

return (
<>
<Formik
initialValues={{images: []}}
onSubmit={(values, {resetForm}) => {
setImages([
...images,
...values.images,
]);
resetForm();
}}
>
{
({
handleSubmit,
values,
}) => (
<form onSubmit={handleSubmit}>
<FieldArray
name="images"
render={helpers => (
<div>
{
values.images.map((image, index) => (
<div>
<Field
name={`images[${index}].name`}
placeholder={`Image ${index + 1} name`}
/>
<br />
<FileUpload
name={`images[${index}].file`}
/>
</div>
))
}
<button
type="button"
onClick={() => helpers.push({name: ''})}
>
Add image
</button>
</div>
)}
/>
<button type="submit">
Submit
</button>
</form>
)
}
</Formik>
<p>
Images:
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 20%) 50px 3em 1fr',
gridTemplateRows: 'repeat(5, 20%)',
}}
>
{images.map(image => (
<div>
<strong>
{image.name}
</strong>
<br />
<img style={{maxWidth: '100%'}} alt="" src={image.file} />
</div>
))}
</div>
</p>
</>
);
}

export default App;

Podsumowanie

Choć zaskoczył mnie brak obsługi plików przez Formik, brak ten da się dość łatwo załatać – jednocześnie sama biblioteka jest przyjemna w użyciu. Prosta rejestracja customowych komponentów czy helpery do pracy z tablicami i możliwość łatwej pracy z zagnieżdżonymi obiektami czynią ją dobrym wyborem – dzięki przeczytaniu tego posta masz silne podstawy, by użyć biblioteki Formik w kolejnym projekcie.