Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik12 min czytania

Remix, React Router i osobny projekt Remix 3

Remix wtopił się w React Router, a Remix 3 to osobny projekt na forku Preacta. Co wybrać dziś, jak migrować z dwójki i czego nie mylić ze sobą.

Remix, React Router i osobny projekt Remix 3

Remix był frameworkiem do budowania aplikacji w Reakcie, zbudowanym wokół standardów przeglądarki: formularzy, żądań i odpowiedzi. Ostatnie wydanie tej linii nosi numer 2.17.5 i pochodzi z czerwca 2026 roku, a rozwój przeniósł się gdzie indziej.

Sytuacja wokół tej nazwy jest dziś na tyle zagmatwana, że warto rozstrzygnąć ją na początku, bo mylenie trzech różnych rzeczy prowadzi do decyzji podjętych na złych przesłankach.

Trzy rzeczy pod jedną nazwą

Pierwsza to Remix w wersji drugiej, czyli framework, o którym mówi większość materiałów w sieci. Działa, jest utrzymywany w sensie poprawek, a nowe możliwości do niego nie trafiają.

Druga to React Router w trybie frameworka. Zespół ogłosił, że kolejna duża wersja Remiksa zostanie wydana jako React Router w wersji siódmej, i tak się stało. To nie jest inspiracja ani następca duchowy, tylko ta sama rzecz pod inną nazwą, wchłonięta do biblioteki routingu używanej w milionach aplikacji. Ta linia rozwija się dalej i jest dziś w wersji ósmej.

Trzecia to Remix w wersji trzeciej i tu zaczyna się nieporozumienie. To jest osobny projekt, który zaczął od forku Preacta zamiast Reacta, jest wydawany na razie jako wersja beta i nie ma ścieżki migracji z wersji drugiej. Wspólna jest nazwa i zespół, nie kod.

Praktyczny wniosek dla osoby wybierającej dziś technologię brzmi: jeśli chcesz tego, co dawał Remix, sięgasz po React Router w trybie frameworka. Nazwa Remix w kontekście wersji trzeciej dotyczy czegoś innego i wybór jej z powodu przywiązania do dawnego frameworka jest pomyłką.

Co dawał ten framework

Warto to opisać, bo koncepcje przeszły w całości do następcy i to one stanowią o wartości.

Ładowanie danych działało po stronie trasy, a nie komponentu. Trasa deklarowała funkcję pobierającą dane, framework wywoływał ją przed renderowaniem, a komponent dostawał gotowy wynik. Dzięki temu nie było kaskady zapytań uruchamianych po zamontowaniu kolejnych komponentów.

Zapis danych działał przez zwykły formularz. Wysyłasz go, trasa obsługuje żądanie po stronie serwera, a po zakończeniu dane na stronie odświeżają się same. Bez pisania obsługi stanu ładowania i bez ręcznego unieważniania buforów.

Trasy zagnieżdżone pozwalały opisać, że część interfejsu jest wspólna dla grupy podstron, a każda z nich dokłada swój fragment i swoje dane. Przy panelach z bocznym menu i wieloma widokami usuwało to sporo powielania.

Nadrzędną zasadą było działanie bez JavaScriptu. Formularz wysłany przy wyłączonych skryptach nadal działał, bo pod spodem był zwykłym formularzem HTML. To jest cecha, która w praktyce znaczy więcej, niż brzmi: aplikacja działa też wtedy, gdy skrypt nie zdążył się załadować albo padł.

Co zostało po połączeniu

Wszystkie te rzeczy istnieją dalej, tylko pod inną nazwą i w innym pakiecie.

React Router ma dziś trzy tryby pracy i to rozróżnienie warto znać, bo materiały opisują je zamiennie. Tryb podstawowy to routing po stronie przeglądarki, znany z lat wcześniejszych. Tryb z danymi dokłada ładowanie i zapis powiązane z trasą. Tryb frameworka to pełny zestaw z renderowaniem serwerowym, budowaniem i wszystkim, co dawał Remix.

Konsekwencja praktyczna jest wygodna: nie trzeba wybierać na start. Aplikacja może zacząć od routingu w przeglądarce i przejść do trybu frameworka bez zmiany biblioteki, gdy pojawi się potrzeba renderowania na serwerze.

Migracja z Remiksa w wersji drugiej jest z założenia płytka. Pojęcia zostały te same, zmieniły się nazwy pakietów i część ścieżek importu, a zespół przygotował narzędzia automatyzujące. To nie jest przepisanie aplikacji, tylko przejście przez importy i konfigurację.

Warto natomiast sprawdzić zależności. Biblioteki napisane pod poprzednią nazwę bywają nieaktualizowane, a to jest zwykle większa część pracy niż sama zamiana importów.

Remix 3, czyli osobna historia

Ten projekt zasługuje na wyjaśnienie, bo budzi najwięcej nieporozumień.

Nie jest kolejną wersją tego, co opisano wyżej. Jest próbą zbudowania czegoś na innym fundamencie: zaczął od forku Preacta zamiast Reacta, a opublikowane paczki mają dziś własny model komponentów bez zależności od zewnętrznej biblioteki widoku, zgodnie z zasadą zespołu, że docelowo zależności ma nie być wcale. Nie ma ścieżki migracji, bo nie ma czego migrować.

Warto też znać stan projektu, bo to zmienia rachunek ryzyka. Paczka trafia do rejestru jako wersja beta pod osobnym znacznikiem, więc mówimy o czymś przed pierwszym stabilnym wydaniem, a nie o dojrzałej alternatywie.

Dla osoby mającej działającą aplikację w wersji drugiej ta informacja jest jednoznaczna: to nie jest kierunek, w który zmierzasz. Twoją drogą jest React Router w trybie frameworka.

Dla osoby zaczynającej nowy projekt sprawa jest bardziej otwarta i warto podejść do niej ostrożnie. Projekt oparty na forku popularnej biblioteki wiąże Cię z decyzjami jednego zespołu w warstwie, którą zwykle traktuje się jako stabilny fundament. To bywa uzasadnione i wymaga świadomej zgody na inne ryzyko niż przy Reakcie.

Praktyczna rada: przy projekcie komercyjnym z długim horyzontem sprawdź, jak wygląda ekosystem bibliotek wokół, bo to jest zwykle czynnik rozstrzygający, a nie sama jakość frameworka.

Loadery i akcje w praktyce

Warto pokazać, jak to wygląda w kodzie, bo te dwa pojęcia są sednem całego podejścia i przeszły do następcy bez zmian.

Code
TypeScript
export async function loader({ params }: LoaderFunctionArgs) {
  const zamowienie = await baza.znajdzZamowienie(params.id)
  if (!zamowienie) throw new Response('Nie znaleziono', { status: 404 })
  return { zamowienie }
}

export async function action({ request }: ActionFunctionArgs) {
  const dane = await request.formData()
  await baza.anulujZamowienie(String(dane.get('id')))
  return redirect('/zamowienia')
}

Trzy rzeczy w tym kodzie warto zauważyć.

Funkcja ładująca zwraca dane, a nie odpowiedź HTTP, i to jest wygodne. Może natomiast rzucić odpowiedzią, i wtedy framework pokaże właściwą stronę błędu. Ten mechanizm zastępuje sprawdzanie warunków w komponencie i renderowanie warunkowe stanu pustego.

Funkcja zapisująca przyjmuje żądanie i czyta z niego dane formularza tak, jak zrobiłby to zwykły serwer. Nie ma tu ciała żądania w formacie tekstowym ani serializacji, bo formularz jest formularzem.

Code
TypeScript
export default function Zamowienie() {
  const { zamowienie } = useLoaderData<typeof loader>()
  const nawigacja = useNavigation()
  const wysylanie = nawigacja.state === 'submitting'

  return (
    <Form method="post">
      <input type="hidden" name="id" value={zamowienie.id} />
      <button type="submit" disabled={wysylanie}>
        {wysylanie ? 'Anulowanie...' : 'Anuluj zamowienie'}
      </button>
    </Form>
  )
}

Komponent formularza dostarczony przez framework przechwytuje wysłanie i wykonuje je bez przeładowania strony, a przy wyłączonym JavaScripcie zachowuje się jak zwykły formularz HTML. To jest ta właściwość, dla której cały ten wzorzec ma sens: strona działa, zanim skrypty się wczytają.

Przekierowanie po zapisie jest tu zwykłą odpowiedzią, a nie wywołaniem funkcji nawigującej. To jest ten sam wzorzec, który w aplikacjach serwerowych obowiązuje od lat i który zapobiega ponownemu wysłaniu formularza po odświeżeniu strony.

Warto też wiedzieć, że dane z funkcji ładującej odświeżają się po zakończeniu zapisu automatycznie. Nie ma unieważniania buforów ani ręcznego wywoływania ponownego pobrania, bo framework wie, które trasy mogły się zmienić.

Na tym opiera się aktualizacja wyprzedzająca: interfejs pokazuje zmianę, zanim serwer odpowie, a gdy odpowiedź przyjdzie, prawdziwe dane po prostu ją zastępują.

Code
TypeScript
const wysylane = useFetcher()
const stan = wysylane.formData?.get('status') ?? zamowienie.status

return (
  <wysylane.Form method="post">
    <input type="hidden" name="status" value="anulowane" />
    <span>{stan}</span>
    <button type="submit">Anuluj</button>
  </wysylane.Form>
)

Odczytanie wysyłanych danych z obiektu pobierającego daje wartość docelową bez utrzymywania osobnego stanu. Jeśli zapis się nie powiedzie, wartość znika sama, bo pole z danymi formularza przestaje istnieć, a komponent wraca do wartości z serwera.

Granice tego podejścia

Uczciwie warto wymienić przypadki, w których to nie jest właściwy wybór, bo materiały promocyjne ich nie pokazują.

Aplikacja bez zaplecza. Cała wartość ładowania na trasie i zapisu przez formularz opiera się na tym, że jest serwer. Statyczna strona wizytówka nie potrzebuje tej warstwy, a jej dodanie oznacza budowanie i wdrażanie czegoś, co mogłoby leżeć jako pliki.

Warto natomiast wiedzieć, że framework nie zmusza do czekania na wszystko. Dane wolne można oddać jako obietnicę i pokazać resztę strony od razu.

Code
TypeScript
export async function loader({ params }: LoaderFunctionArgs) {
  const zamowienie = await baza.znajdzZamowienie(params.id)
  const rekomendacje = baza.policzRekomendacje(params.id)

  return { zamowienie, rekomendacje }
}

export default function Strona() {
  const { zamowienie, rekomendacje } = useLoaderData<typeof loader>()

  return (
    <>
      <Podsumowanie zamowienie={zamowienie} />
      <Suspense fallback={<Szkielet />}>
        <Await resolve={rekomendacje}>
          {(lista) => <Rekomendacje lista={lista} />}
        </Await>
      </Suspense>
    </>
  )
}

Brak słowa oczekiwania przy drugim wywołaniu to cała różnica. Zamówienie blokuje odpowiedź, bo bez niego strona nie ma sensu, a rekomendacje dopływają później i do tego czasu widać szkielet.

Aplikacja o bardzo dużej interaktywności. Panel z płótnem, edytorem albo widokiem odświeżanym co sekundę pracuje głównie w przeglądarce, a wzorzec oparty o żądania i odpowiedzi mniej tam pasuje. Biblioteka zapytań plus routing w przeglądarce bywa prostszym rozwiązaniem.

Zespół przyzwyczajony do innego modelu. Ładowanie danych na trasie jest inne od pobierania ich w komponencie, i to jest zmiana nawyku, nie tylko składni. Przy zespole, który dobrze zna jeden sposób, koszt przestawienia bywa większy niż zysk.

Ostatnia granica dotyczy ekosystemu. Biblioteki zakładające konkretny framework bywają dostosowane najpierw do najpopularniejszego, a dopiero potem do reszty. Przy wyborze warto sprawdzić te, których faktycznie będziesz używał, zamiast liczyć na to, że wszystko działa wszędzie.

Remix kontra alternatywy

RozwiązanieStanEkosystemKiedy wybrać
Remix 2Utrzymywany, bez nowościDuży, oparty o ReactMasz to wdrożone, planujesz przejście
React Router, tryb frameworkaRozwijanyDużyChcesz tego, co dawał Remix
Next.jsRozwijanyNajwiększyStrony z treścią, renderowanie serwerowe
Remix 3Osobny projekt, wersja betaNowy, mniejszyŚwiadomie wybierasz inny fundament

Wybór między drugim a trzecim wierszem jest dziś głównym pytaniem przy nowej aplikacji w Reakcie i nie ma na nie jednej odpowiedzi.

React Router w trybie frameworka jest bliżej standardów przeglądarki i mniej narzuca. Kod trasy wygląda jak obsługa żądania, a nie jak konstrukcja specyficzna dla frameworka, więc łatwiej go zrozumieć osobie znającej podstawy sieci.

Next.js daje większy ekosystem, więcej gotowych rozwiązań i głębszą integrację z platformą wdrożeniową swojego autora. Cena to więcej pojęć własnych, których trzeba się nauczyć, i mocniejsze związanie z jednym sposobem robienia rzeczy.

Pierwszy wiersz warto traktować wyłącznie jako opis stanu obecnego. Nowa aplikacja tam nie powstanie, a istniejąca ma jasną drogę wyjścia.

Jak przeprowadzić migrację

Kolejność, która działa przy aplikacji w wersji drugiej.

Zacznij od zależności, nie od kodu. Wypisz biblioteki, które importują pakiety pod dawną nazwą, i sprawdź, czy mają wersje zgodne z nową. To jest miejsce, w którym migracja najczęściej się zatrzymuje, i lepiej dowiedzieć się o tym przed rozpoczęciem.

Potem uruchom narzędzia automatyzujące zamianę importów. Zamiana nazw pakietów i ścieżek jest mechaniczna, więc robienie tego ręcznie w kilkuset plikach jest stratą czasu.

Trzeci krok to konfiguracja budowania. Tu bywa najwięcej ręcznej pracy, bo ustawienia własne z poprzedniej wersji trzeba przenieść świadomie, a nie przepisać jeden do jednego.

Czwarty to przejrzenie typów. Nazwy typów pomocniczych częściowo się zmieniły, a błędy w tym miejscu są wyraźne i szybkie do naprawienia.

Nie migruj przy okazji innych zmian. Migracja, w której zespół przy okazji zmienia strukturę katalogów i przepisuje kilka widoków, kończy się tym, że nikt nie wie, co zepsuło aplikację.

Typowe błędy

Pierwszy to mylenie Remiksa w wersji trzeciej z następcą wersji drugiej. To są osobne projekty na innym fundamencie i migracja między nimi nie istnieje.

Drugi to zaczynanie nowego projektu od Remiksa w wersji drugiej. Nowe możliwości tam nie trafiają, a droga wyjścia jest znana, więc lepiej wejść od razu tam, gdzie się kończy.

Trzeci to uczenie się z materiałów sprzed połączenia bez sprawdzenia daty. Pojęcia zostały, natomiast nazwy pakietów i ścieżki importu są inne.

Czwarty to migracja bez wcześniejszego sprawdzenia zależności. Biblioteka bez wersji zgodnej z nową nazwą zatrzymuje całość, a odkrycie tego w połowie pracy jest kosztowne.

Piąty to łączenie migracji z innymi zmianami. Przy awarii nie da się wtedy stwierdzić, co ją spowodowało.

Szósty to wybór trybu frameworka tam, gdzie wystarczy routing w przeglądarce. Aplikacja bez potrzeby renderowania serwerowego dostaje wtedy warstwę budowania i wdrożenia, której nie potrzebuje.

Siódmy to pobieranie danych w komponencie mimo dostępnej funkcji ładującej na trasie. Wraca wtedy kaskada zapytań uruchamianych po zamontowaniu kolejnych komponentów, czyli dokładnie to, co ten model miał usunąć.

Ósmy to zakładanie, że skoro jest renderowanie serwerowe, sprawdzanie danych po stronie serwera jest zbędne. Funkcja zapisująca przyjmuje żądanie, które da się wysłać z pominięciem interfejsu, więc walidacja musi być w niej, a nie tylko w formularzu.

FAQ

Czy Remix nadal istnieje?

Wersja druga istnieje i jest utrzymywana w sensie poprawek, natomiast rozwój przeniósł się do React Routera, gdzie tryb frameworka jest tym, czym był Remix. Osobno istnieje projekt o nazwie Remix 3, który jest czymś innym.

Czym jest Remix 3?

Osobnym projektem, który zaczął od forku Preacta zamiast Reacta i jest dziś wydawany jako wersja beta, bez ścieżki migracji z wersji drugiej. Wspólna jest nazwa i zespół, a nie kod, więc wybieranie go z przywiązania do dawnego frameworka jest nieporozumieniem.

Na co migrować z Remiksa 2?

Na React Router w trybie frameworka. To jest ta sama rzecz pod inną nazwą, więc pojęcia zostają, a zmieniają się nazwy pakietów i część ścieżek importu, przy wsparciu narzędzi automatyzujących.

Czym różni się od Next.js?

Bliskością do standardów przeglądarki i mniejszą liczbą pojęć własnych. Kod trasy wygląda jak obsługa żądania, a nie jak konstrukcja frameworka. Next.js daje w zamian większy ekosystem i głębszą integrację z platformą wdrożeniową swojego autora.

Czy muszę używać trybu frameworka?

Nie. Biblioteka ma trzy tryby, więc możesz zacząć od routingu w przeglądarce i przejść wyżej wtedy, gdy pojawi się potrzeba renderowania serwerowego, bez zmiany biblioteki.

Opis połączenia stoi na blogu projektu, a dokumentacja bieżącej linii na stronie React Routera.