То, что принесла нам Astro, — это мир без Javascript по умолчанию. Они используют эту философию, чтобы уменьшить размер пакета и ускорить первую отрисовку браузера, но это также ставит перед разработчиком множество новых задач, таких как переключение кнопки темной темы в блоге.

С Next.js

Я написал ThemeContext для переключения темного режима в своем блоге, созданном поверх Next.js. ThemeContext добавит class=”dark” в корневой HTML-код при монтировании приложения. Таким образом, TailwindCSS может распознать текущую тему.¹

```ts
// The code I am using in my blog built with Next.js
import { useEffect, useState, createContext } from "react";
const defaultState = {
  theme: "light",
  toggleDark: () => {},
};
export const ThemeContext = createContext(defaultState);
export const ThemeProvider = ({ initialTheme, children }) => {
  const [theme, setTheme] = useState("light");
const rawSetTheme = (rawTheme) => {
    const root = window.document.documentElement;
    const isDark = rawTheme === "dark";
    root.classList.remove(isDark ? "light" : "dark");
    root.classList.add(rawTheme);
  };
if (initialTheme) {
    rawSetTheme(initialTheme);
  }
React.useEffect(() => {
    rawSetTheme(theme);
  }, [theme]);
return (
    <ThemeContext.Provider value={[ theme, setTheme ]}>
      {children}
    </ThemeContext.Provider>
  );
};
```

С Astro.js

Но если мы хотим сохранить использование Context API, нам может понадобиться написать что-то вроде приведенного ниже.

// mainPage.astro
---
import ContextWrapperedComponent from "./ContextWrapperedComponent"
---
<ContextWrapperedComponent client:load />
// ContextWrapperedComponent
export const ContextWrapperedComponent = () => {
  // logic for context and components
return (
    <div>
       // bunch of components that rely on context
    </div>
  )
}

Основываясь на идее Astro.js — Partial Hydration², нам нужно пометить весь `ContextWrapperedComponent` как `client:load` или `client:only`, чтобы сделать его гидратированным и внедрить его в HTML как Javascript. Но таким образом появляется множество ненужных скриптов, которые вводятся и увеличивают общий размер пакета, а это не то, что нам нужно.

Поэтому я решил переключить свое хранилище состояния темного режима с Context на localStorage.

  • При первом монтировании я получу доступ к значению ключа (тема блога) в localStorage и определяю тему страницы в соответствии с этим значением.
  • Как только пользователь переключит тему, я сброшу тему страницы и обновлю значение в localStorage.

На первый взгляд, эта реализация выглядит просто, но все же имеет много предостережений.

Предупреждение 1 – мигающие страницы

Процесс рендеринга Astro сначала отправляет HTML и CSS клиенту, монтирует расширенную строку шаблона и прослушивает событие для внедрения скрипта. Такое поведение может привести к некоторым нежелательным результатам.

// This code will update the page's theme upon the finish 
// of the first time painting, but that is not what we want.
useEffect(() => {
  let theme: "light" | "dark";
if (typeof localStorage !== "undefined" && localStorage.getItem("theme")) {
    theme = localStorage.getItem("theme") as "light" | "dark";
  } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
    theme = "dark";
  } else {
    theme = "light";
  }
if (theme === "light") {
    setTheme("light");
  } else {
    setTheme("dark");
  }
}, []);

Например, если вашей темой по умолчанию является темный режим, но в localStorage хранится светлый режим. HTML сначала будет отображать темный режим, как только клиентская сторона смонтирована, событие запускает внедрение гидратированного скрипта, ваша страница будет обновляться как светлый режим, и этот процесс будет продолжать работать каждый раз, когда пользователь меняет страницы. В результате ваши страницы будут продолжать мигать.

Чтобы решить проблему мерцания, мы должны обновить тему перед первой прорисовкой браузера.

К счастью, Astro.js уже позаботились о нас, в их API вы можете сделать скрипт, отправляемый клиенту с HTML и CSS как есть, со специальным атрибутом `is:inline`³. Astro не будет гидратировать этот скрипт и практиковать какую-либо оптимизацию. Если серьезно, это не приветствуется философией Astro, но необходимо для нашего приложения. Кроме того, этот подход также используется в официальном документе Astro.js.⁴

// With this code, we can update the page's theme before the first-time painting.
// You should put the code into <script is:inline>
const html = document.querySelector("html");
const theme = (() => {
  if (
    typeof localStorage !== "undefined" &&
    localStorage.getItem("theme")
  ) {
    return localStorage.getItem("theme");
  }
  if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
    return "dark";
  }
  return "light";
})();
if (theme === "light") {
  html.classList.remove("dark");
} else {
  html.classList.add("dark");
}

Предостережение 2 — переход кнопки переключения

Первоначально моя кнопка переключения темного режима находится в темном режиме, она будет отображать значок солнца и отображать значок луны в светлом режиме. Проблема с этим дизайном близка к предостережению 1, у него будет проблема с мерцанием. На первый взгляд, я думаю, что могу обновить значок так же, как я сделал с приведенным выше решением, но позже я обнаружил, что это не работает, и причина довольно проста.

Поскольку нам нужно, чтобы кнопка-переключатель была интерактивной, она должна иметь префикс «client:load», чтобы указать, что Astro.js гидратирует этот скрипт. Но при первом рисовании гидратированный скрипт еще не внедрен, поэтому скрипт is:inline не может найти целевую кнопку для обновления значка. Кнопка появится только после того, как первая картина будет закончена.

Чтобы решить эту проблему, мне нужно изменить дизайн кнопки-переключателя. Теперь он отображает значок солнца и луны одновременно, но имеет мерцающий переход, указывающий, какая из них является текущей темой.

Заключение

Программирование с помощью Astro.js — это новое путешествие, у него есть много возможностей для открытия, я буду продолжать обновлять то, что я нашел во время исследования.

¹: TailwindCSS — темный режим

²: Астро — частичное увлажнение

³: Астро — это: встроенный

⁴: withAstro — документы — GitHub