Работа с формами и API
Форма → валидация → отправка → обработка ответа. Полный цикл.
Состояние формы
type FormValues = {
title: string;
body: string;
};
const [values, setValues] = useState<FormValues>({ title: '', body: '' });
const [errors, setErrors] = useState<Partial<Record<keyof FormValues, string>>>({});
const [submitting, setSubmitting] = useState(false);
Валидация
function validate(values: FormValues) {
const errors: Partial<Record<keyof FormValues, string>> = {};
if (!values.title.trim()) errors.title = 'Заголовок обязателен';
if (values.body.length < 10) errors.body = 'Минимум 10 символов';
return errors;
}
Отправка
const mutation = useMutation({
mutationFn: (data: FormValues) => api.post('/posts', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
navigation.goBack();
},
onError: (error) => {
if (axios.isAxiosError(error) && error.response?.status === 422) {
setErrors(error.response.data.errors);
}
},
});
function handleSubmit() {
const validationErrors = validate(values);
setErrors(validationErrors);
if (Object.keys(validationErrors).length > 0) return;
mutation.mutate(values);
}
Форма с полями
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={{ flex: 1 }}>
<ScrollView keyboardShouldPersistTaps="handled" contentContainerStyle={{ padding: 16 }}>
<TextInput
value={values.title}
onChangeText={(v) => setValues((s) => ({ ...s, title: v }))}
placeholder="Заголовок"
style={[styles.input, errors.title && styles.inputError]}
/>
{errors.title ? <Text style={styles.error}>{errors.title}</Text> : null}
<TextInput
value={values.body}
onChangeText={(v) => setValues((s) => ({ ...s, body: v }))}
placeholder="Текст"
multiline
numberOfLines={6}
style={[styles.input, styles.textarea, errors.body && styles.inputError]}
/>
{errors.body ? <Text style={styles.error}>{errors.body}</Text> : null}
<Pressable
onPress={handleSubmit}
disabled={mutation.isPending}
style={[styles.button, mutation.isPending && styles.disabled]}
>
{mutation.isPending ? <ActivityIndicator color="#fff" /> : <Text style={styles.buttonText}>Сохранить</Text>}
</Pressable>
</ScrollView>
</KeyboardAvoidingView>
React Hook Form
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
title: z.string().min(1, 'Заголовок обязателен'),
body: z.string().min(10, 'Минимум 10 символов'),
});
type FormValues = z.infer<typeof schema>;
function Form() {
const { control, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: { title: '', body: '' },
});
const mutation = useMutation({
mutationFn: (data: FormValues) => api.post('/posts', data),
});
const onSubmit = (data: FormValues) => mutation.mutateAsync(data);
return (
<ScrollView>
<Controller
control={control}
name="title"
render={({ field }) => (
<TextInput
value={field.value}
onChangeText={field.onChange}
onBlur={field.onBlur}
placeholder="Заголовок"
/>
)}
/>
{errors.title ? <Text style={styles.error}>{errors.title.message}</Text> : null}
<Pressable onPress={handleSubmit(onSubmit)} disabled={isSubmitting}>
<Text>Сохранить</Text>
</Pressable>
</ScrollView>
);
}
Работа с ошибками сервера
const mutation = useMutation({
mutationFn: (data: FormValues) => api.post('/posts', data),
onError: (error) => {
if (axios.isAxiosError(error) && error.response?.status === 422) {
const serverErrors = error.response.data.errors;
Object.entries(serverErrors).forEach(([field, messages]) => {
setError(field as keyof FormValues, { message: (messages as string[])[0] });
});
}
},
});
Успех и переход
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
Toast.show({ type: 'success', text1: 'Сохранено' });
navigation.goBack();
}
Итоги
- Состояние формы + ошибки
- React Hook Form + Zod
ControllerдляTextInput- Обработка 422 от сервера
mutation.isPendingдля индикатора- Toast и навигация после успеха