Перейти к содержимому

23. Progressive Enhancement

Progressive Enhancement — философия, при которой базовая функциональность работает без JavaScript, а JS лишь улучшает опыт. Remix реализует это через <Form> и стандартные HTML формы.

Без JavaScript:
<Form method="post"> → обычная HTML форма → POST запрос → redirect
С JavaScript:
<Form method="post"> → fetch запрос → обновление данных без перезагрузки
// Эта форма работает в обоих случаях
export async function action({ request }) {
const formData = await request.formData();
const email = formData.get("email");
await subscribeUser(email);
return redirect("/thank-you"); // работает и без JS!
}
export default function Subscribe() {
return (
<Form method="post">
<input type="email" name="email" required />
<button type="submit">Подписаться</button>
</Form>
);
}
export default function Subscribe() {
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return (
<Form method="post">
<input type="email" name="email" required />
<button type="submit" disabled={isSubmitting}>
{/* Без JS: всегда "Подписаться" */}
{/* С JS: меняется на "Подписка..." */}
{isSubmitting ? "Подписка..." : "Подписаться"}
</button>
</Form>
);
}
import { useHydrated } from "remix-utils/use-hydrated";
function EnhancedButton() {
const hydrated = useHydrated();
if (!hydrated) {
// Серверный рендер — простой HTML элемент
return <button type="submit">Сохранить</button>;
}
// После гидратации — богатый UI
return (
<button
type="submit"
onClick={handleClick}
data-tooltip="Сохранить изменения"
>
💾 Сохранить
</button>
);
}

Progressive Enhancement начинается с правильного HTML:

// ❌ Неправильно
<div onClick={handleSubmit}>
<div>Отправить</div>
</div>
// ✅ Правильно
<Form method="post">
<fieldset>
<legend>Контактная форма</legend>
<label htmlFor="name">Имя</label>
<input id="name" name="name" type="text" required />
<button type="submit">Отправить</button>
</fieldset>
</Form>
// Дата-пикер с fallback на input[type="date"]
function DatePicker({ name, value }) {
const [hydrated, setHydrated] = useState(false);
useEffect(() => setHydrated(true), []);
if (!hydrated) {
return <input type="date" name={name} defaultValue={value} />;
}
return <FancyDatePicker name={name} value={value} />;
}