React Router(3)[React]

SnowCat·2023년 2월 6일
0

React Router

목록 보기
3/3
post-thumbnail

URL Search Params and GET Submissions

  • html의 form 태그를 사용하면 데이터 변경없이 url을 변경 가능
  • search form을 사용하면 http://127.0.0.1:5173/?q=ryan과 같이 쿼리가 들어간 URLSearchParams을 얻을 수 있음

root.jsx

<form id="search-form" role="search">
  <input
    id="q"
    aria-label="Search contacts"
    placeholder="Search"
    type="search"
    name="q"
  />
  <div id="search-spinner" aria-hidden hidden={true} />
  <div className="sr-only" aria-live="polite"></div>
</form>

GET Submissions with Client Side Routing

  • form 태그를 Form 태그로 바꾸고, URLSearchParams을 통해 쿼리에 맞는 연락처를 가져오게 수정함
    post 메서드가 아니기 떄문에 action이 아닌 loader에서 url을 처리해야 함

root.jsx

export async function loader({ request }) {
  const url = new URL(request.url);
  const q = url.searchParams.get("q");
  const contacts = await getContacts(q);
  return { contacts };
}

<Form id="search-form" role="search">
  <input
    id="q"
    aria-label="Search contacts"
    placeholder="Search"
    type="search"
    name="q"
  />
  <div id="search-spinner" aria-hidden hidden={true} />
  <div className="sr-only" aria-live="polite"></div>
</Form>

Synchronizing URLs to Form State

  • 검색기능은 작동하지만, 검색 이후 연락처 상세보기를 하거나 새로고침을 하면 검색 필터가 초기화되고,

  • 쿼리 데이터를 반환하게 함으로써 새로고침을 해도 form의 검색 결과가 남아있게 할 수 있음

root.jsx

// existing code

export async function loader({ request }) {
  const url = new URL(request.url);
  const q = url.searchParams.get("q");
  const contacts = await getContacts(q);
  return { contacts, q };
}

export default function Root() {
  const { contacts, q } = useLoaderData();
  const navigation = useNavigation();

  return (
    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          <Form id="search-form" role="search">
            <input
              id="q"
              aria-label="Search contacts"
              placeholder="Search"
              type="search"
              name="q"
              defaultValue={q}
            />
            {/* existing code */}
          </Form>
          {/* existing code */}
        </div>
        {/* existing code */}
      </div>
      {/* existing code */}
    </>
  );
}
  • hook을 사용해 상세보기를 하더라도 검색창에 데이터가 남아있게 설정 가능
import { useEffect, useState } from "react";
// existing code

export default function Root() {
  const { contacts, q } = useLoaderData();
  const [query, setQuery] = useState(q);
  const navigation = useNavigation();

  useEffect(() => {
    setQuery(q);
  }, [q]);

  return (
    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          <Form id="search-form" role="search">
            <input
              id="q"
              aria-label="Search contacts"
              placeholder="Search"
              type="search"
              name="q"
              value={query}
              onChange={(e) => {
                setQuery(e.target.value);
              }}
            />
            {/* existing code */}
          </Form>
          {/* existing code */}
        </div>
        {/* existing code */}
      </div>
    </>
  );
}

Submitting Forms "onChange"

  • onChange를 사용해 키보드 입력시마다 form 입력을 받아낼 수 있음
  • 또한 useSubmit을 사용해 자동으로 form을 제출하게 할 수 있음

root.jsx

// existing code
import {
  // existing code
  useSubmit,
} from "react-router-dom";

export default function Root() {
  const { contacts, q } = useLoaderData();
  const navigation = useNavigation();
  const submit = useSubmit();

  return (
    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          <Form id="search-form" role="search">
            <input
              id="q"
              aria-label="Search contacts"
              placeholder="Search"
              type="search"
              name="q"
              defaultValue={q}
              onChange={(event) => {
                submit(event.currentTarget.form);
              }}
            />
            {/* existing code */}
          </Form>
          {/* existing code */}
        </div>
        {/* existing code */}
      </div>
      {/* existing code */}
    </>
  );
}

Adding Search Spinner

  • navigation.location을 통해 새로운 url을 navigating하고 데이터를 로딩하는지 여부를 확인할 수 있음
  • 이를 활용해 검색창에 검색이 진행중이라는 표현을 보여줄 수 있음

root.jsx

// existing code

export default function Root() {
  const { contacts, q } = useLoaderData();
  const navigation = useNavigation();
  const submit = useSubmit();

  const searching =
    navigation.location &&
    new URLSearchParams(navigation.location.search).has(
      "q"
    );

  useEffect(() => {
    document.getElementById("q").value = q;
  }, [q]);

  return (
    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          <Form id="search-form" role="search">
            <input
              id="q"
              className={searching ? "loading" : ""}
              // existing code
            />
            <div
              id="search-spinner"
              aria-hidden
              hidden={!searching}
            />
            {/* existing code */}
          </Form>
          {/* existing code */}
        </div>
        {/* existing code */}
      </div>
      {/* existing code */}
    </>
  );
}

Managing the History Stack

  • submit을 할 때 replace 속성을 부여하면 브라우저에 히스토리가 남는 것을 방지할 수 있음
  • 이를 사용해 처음 검색 시 브라우저 스택에 히스토리가 남는 것을 방지해줄 수 있음

root.jsx

// existing code

export default function Root() {
  // existing code

  return (
    <>
      <div id="sidebar">
        <h1>React Router Contacts</h1>
        <div>
          <Form id="search-form" role="search">
            <input
              id="q"
              // existing code
              onChange={(event) => {
                const isFirstSearch = q == null;
                submit(event.currentTarget.form, {
                  replace: !isFirstSearch,
                });
              }}
            />
            {/* existing code */}
          </Form>
          {/* existing code */}
        </div>
        {/* existing code */}
      </div>
      {/* existing code */}
    </>
  );
}

Mutations Without Navigation

  • navigate에서는 히스토리 스택을 생성하게 되는데, 히스토리 스택 생성을 원하지 않는 경우 useFetcher 훅을 사용함으로써 이를 방지할 수 있음

contact.jsx

import {
  useLoaderData,
  Form,
  useFetcher,
} from "react-router-dom";

// existing code

function Favorite({ contact }) {
  const fetcher = useFetcher();
  let favorite = contact.favorite;

  return (
    <fetcher.Form method="post">
      <button
        name="favorite"
        value={favorite ? "false" : "true"}
        aria-label={
          favorite
            ? "Remove from favorites"
            : "Add to favorites"
        }
      >
        {favorite ? "★" : "☆"}
      </button>
    </fetcher.Form>
  );
}
  • 기존의 Form에서의 동작과 거의 일치하지만, 히스토리를 생성시키지 않는 form을 생성가능
  • Form은 action prop을 호출하는데, action이 존재하지 않는 경우 렌더링되는 route에 데이터를 post하게 됨
  • 이를 방지하기 위해 action function을 추가해야 함

contact.jsx

import { getContact, updateContact } from "../contacts";

export async function action({ request, params }) {
  let formData = await request.formData();
  return updateContact(params.contactId, {
    favorite: formData.get("favorite") === "true",
  });
}

export default function Contact() {
  // existing code
}

main.jsx

// existing code
import Contact, {
  loader as contactLoader,
  action as contactAction,
} from "./routes/contact";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <ErrorPage />,
    loader: rootLoader,
    action: rootAction,
    children: [
      { index: true, element: <Index /> },
      {
        path: "contacts/:contactId",
        element: <Contact />,
        loader: contactLoader,
        action: contactAction,
      },
      /* existing code */
    ],
  },
]);

결과:

Optimistic UI

  • favorite 설정시 딜레이를 방지하기 위해 navigation.state와 유사한 fetcher.state를 사용해 로딩중임을 표현할 수 있음
  • 또는 fetcher.formData에 제출 완료 전까지 데이터가 남아있다는 사실을 활용할 수 있음
  • form에 미리 favorite을 반영하고, 데이터 post가 완료되면 실제 데이터로 바꿔주는 과정을 통해 network delay를 느끼지 못하게 해줄 수 있음

contact.jsx

// existing code

function Favorite({ contact }) {
  const fetcher = useFetcher();

  let favorite = contact.favorite;
  if (fetcher.formData) {
    favorite = fetcher.formData.get("favorite") === "true";
  }

  return (
    <fetcher.Form method="post">
      <button
        name="favorite"
        value={favorite ? "false" : "true"}
        aria-label={
          favorite
            ? "Remove from favorites"
            : "Add to favorites"
        }
      >
        {favorite ? "★" : "☆"}
      </button>
    </fetcher.Form>
  );
}

Not Found Data

  • 컴포넌트 내부에서 조건에 따라 특정한 Response를 반환할 수 있음
  • 404 에러를 출력 시 main컴포넌트에서 errorElement가 오류를 감지해 오류 페이지를 출력함

contact.jsx

export async function loader({ params }) {
  const contact = await getContact(params.contactId);
  if (!contact) {
    throw new Response("", {
      status: 404,
      statusText: "Not Found",
    });
  }
  return contact;
}

없는 연락처를 찾으려 했을 때 결과:

Pathless Routes

  • 전체화면이 아닌 상세 페이지 내부에서만 오류 메시지를 출력하려면 child 내부에 errorElement를 생성 후, 이를 공유하는 route들을 다시 child로 두면 됨

main.jsx

createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    loader: rootLoader,
    action: rootAction,
    errorElement: <ErrorPage />,
    children: [
      {
        errorElement: <ErrorPage />,
        children: [
          { index: true, element: <Index /> },
          {
            path: "contacts/:contactId",
            element: <Contact />,
            loader: contactLoader,
            action: contactAction,
          },
          /* the rest of the routes */
        ],
      },
    ],
  },
]);

결과:

JSX Routes

  • jsx문법을 사용해서도 react router 사용 가능

main.jsx

import {
  createRoutesFromElements,
  createBrowserRouter,
} from "react-router-dom";

const router = createBrowserRouter(
  createRoutesFromElements(
    <Route
      path="/"
      element={<Root />}
      loader={rootLoader}
      action={rootAction}
      errorElement={<ErrorPage />}
    >
      <Route errorElement={<ErrorPage />}>
        <Route index element={<Index />} />
        <Route
          path="contacts/:contactId"
          element={<Contact />}
          loader={contactLoader}
          action={contactAction}
        />
        <Route
          path="contacts/:contactId/edit"
          element={<EditContact />}
          loader={contactLoader}
          action={editAction}
        />
        <Route
          path="contacts/:contactId/destroy"
          action={destroyAction}
        />
      </Route>
    </Route>
  )
);

출처:
https://reactrouter.com/en/6.8.0/start/tutorial

profile
냐아아아아아아아아앙

0개의 댓글