Nedioプロジェクトをするときの悩み(1)

47246 ワード

悩みの内容を整理する
モジュール化による再利用
最初に作成したパターンは,パターンを使いたい場所でパターンの状態を伝える仕組みである.
  const [modalState, setModalState] = useState(false);

  return (
    <>
      <AuthButton tapMenu="로그인" handleClick={onOpen} />
      <Modal ref={modal} modalState={modalState} onClose={onClose}>
        {/* TODO: modal 내부 구현 */}
        <h3>Nedio에 오신 것을 환영합니다!</h3>
        <p>모달 내부</p>
      </SignInModal>
    </>
  );
このようにすれば、モードを使用する必要がある場所でそのコンポーネントを管理するために必要な状態に加えて、モードを状態管理する必要があるため、効率が低下する可能性がある.だから、モード素子でモードの状態管理をする方法はないかと思います.
Modelを使用したい場所にModelを開いたり閉じたりできる関数しか存在しない場合、Modelの状態管理はModelでも良いが、子供の状態を管理できる関数を親に渡す方法は考えられない.
useImperativeHandle
上記の悩みは、hookの種類でuseImperativeHandleを使って解決できます.
  const modalRef = useRef<React.ElementRef<typeof Modal>>(null);
                                        
  return (
    <>
      <AuthButton
        tapMenu="로그인"
        handleClick={() => modalRef.current?.show()}
      />
      <Modal ref={modalRef}>
        {/* TODO: modal 내부 구현 */}
        <h3>Nedio에 오신 것을 환영합니다!</h3>
        <p>모달 내부</p>
      </Modal>
    </>
 );             
const Modal = React.forwardRef<ModalHandle, Props>((props, ref) => {
  const [modalState, setModalState] = useState(false);

  useImperativeHandle(ref, () => ({
    show() {
      setModalState(true);
    },
  }));
  
  // 생략...
});
userImperativeHandleをforwardRefとともに使用する場合は、refを使用する親コンポーネントでカスタム関数を使用できます.これにより、Modelのステータス管理はModel構成部品で管理でき、Modelを使用する親構成部品は自由に使用できます.
Carousel
メリーゴーランド
スライダを移動するときは、curIdxを使用して現在のスライダの位置を表し、transform:translateX()プロパティを使用してスライダを移動します.(margin-leftプロパティなどに移動することもできます)
const [curIdx, setCurIdx] = useState(0);


const CarouselRow = styled.section<{ moveX: number; transitionEffect: string }>`
  position: relative;
  transform: ${({ moveX }) => `translateX(calc(${moveX}%))`};
`;
carousel中央位置
前に実装したcarouselとは異なり、現在のスライダは画面の真ん中にあり、左右に次のスライダが表示されます.
これらの条件を満たすために、まずスライダ行left:50%を与え、画面を中央から開始します.
const CarouselRow = styled.section<{ moveX: number; transitionEffect: string }>`
  display: flex;
  left: 50%;
`;
左:50%;変換:TranslationX(-50%)と同じ方法でホイールを移動します.

これを実現するには、上で使用した現在のスライダ位置curIdxの値を使用して、%を現在のスライダ位置に移動し、スライダの半分を中央に移動すると、次のスライダを移動して現在のスライダを中央に移動できます.
<CarouselRow
  moveX={(-100 / paddedItems.length) * (curIdx + 0.5)}
  >
  // 생략...
</CarouselRow>


const CarouselRow = styled.section<{ moveX: number; transitionEffect: string }>`
  position: relative;
  display: flex;
  left: 50%;
  width: fit-content;
  transform: ${({ moveX }) => `translateX(calc(${moveX}%))`};
`;
infinite carousel
最初の使い方は以下の通りです.

しかし、一つ問題があります.今回のホイールは左右に次のスライダが見えるので、最後の要素が移行すると、一時的に作成したレプリカスライダの横に空の現象が発生します.

この問題は、最後の要素からコピースライダに移動した後、隣にスライダがなくなったため、各エンドポイント要素にスライダをもう1つ作成することで解決できます.

クエリーの管理
キーワード入力→検索→このキーワードを使用してapiを要求する構造
refreshではキーワード初期化URLがそのままになるという問題が発生した.
キーワードではなくURLをグループ化してAPIを要求する
検索ウィンドウにキーワードを入力し、検索時に検索するオプションを使用して検索ページのロールにルーティングします.
navigation(`${PATH.GALLERY_SEARCH}?${selectedOption}=${keyword}`);
実際のAPIリクエストは、検索結果ページに該当するURLを区切って検索します
useEffect(() => {
    const queryParam = decodeURIComponent(query.toString());
    const [key, value] = queryParam.split('=');
    let queryStr = `/galleries/filtering?page=${page}&perPage=${perPage}&`;

    switch (key) {
      case 'title': {
        queryStr += `${key}=${value}&nickname=&category=`;
        break;
      }

      case 'nickname': {
        queryStr += `title=&${key}=${value}&category=`;
        break;
      }

      case 'category': {
        queryStr += `title=&nickname=&${key}=${value}`;
        break;
      }

      default:
        queryStr += `title=&nickname=&category=`;
    }

    axiosInstance
      .get(queryStr)
      .then((res) => setCards(res.data.data))
      .catch((e) => {
        // console.log(e);
      })
      .finally(() => {
        dispatch(setKeyword(value));
      });
  }, [page, perPage, query, dispatch]);
参考コーチはparams Serializerの方法を使って、再包装します
useEffect(() => {
    const queryParam = decodeURIComponent(query.toString());
    const [queryKey, queryValue] = queryParam.split('=');
    const customParams = {
      page,
      perPage,
      title: '',
      nickname: '',
      category: '',
    };

    axiosInstance
      .get<string, AxiosRequestConfig>('/galleries/filtering', {
        params: customParams,
        paramsSerializer: (params) =>
          combineQuery(params, queryKey, queryValue),
      })
      .then((res) => setCards(res.data.data))
      .catch((e) => {
        // console.log(e);
      })
      .finally(() => {
        dispatch(setKeyword(value));
      });
  }, [page, perPage, query, dispatch]);
ブラウザが予期せぬ終了とリフレッシュを行った場合でも、ユーザーが入力したキーワードを保持したいと考えています.
検索ページでは、URLをグループ化してkeywordを再メンテナンスすることができますが、すべてのページで共有されるナビゲーションであるため、他のページではkeywordのメンテナンスが困難です.
クエリーの保持(localstorageを使用)
一般的なWebサイトでは、localStorageでsearchHistoryを使用してユーザーの検索結果を管理するサービス検索に似た構造があります.
これを参考に,ユーザは検索結果をlocalStorageに保存する考えを持ち,reduxを用い,redux−persistを用いて状態をlocalStorageに保存することで,所望の結果を得ることができる.

検索ページのほかに、検索項目の初期化を保持
ナビゲーションは、ログインページと展覧ページ以外のすべてのページで表示されるため、localstorageを使用して検索語を保持すると、サービスページに再接続すると、検索ページのほかにキーワードが保持されます.

クライアントがグローバルに管理するキーワードステータスに加えて、検索コンポーネント内で管理できるステータスが追加され、置換キーワード値を使用することで、現在のパスに基づいてサブコンポーネントに降格する問題が解決されます.
useEffect(() => {
    if (pathname !== PATH.GALLERY_SEARCH) {
      setSideKeyword('');
    }
}, [pathname]);

return (
    <>
      <SearchBar
        keyword={pathname === PATH.GALLERY_SEARCH ? keyword : sideKeyword}
        handleKeyword={handleKeyword}
        resetKeyword={initKeyword}
        handleSubmit={handleSubmit}
      />
      <SearchSelect
        option={selectedOption}
        handleSelectOption={handleSelectOption}
      />
    </>
);
無限スクロール
まず、各スケジュールスクロールでは、受信データfetchのコードは、コンポーネントに初めてアクセスしたときと同じであるため、上記で使用したコードの一部を以下のように分離する.
const loadCards = useCallback(async () => {
    const customParams = {
      page: page.current,
      perPage: perPage.current,
      title: '',
      nickname: '',
      category: '',
    };

    if (!isValidQuery(queryKey, queryValue)) {
      navigation('/NotFound');
      return;
    }

    try {
      setLoading(true);

      const res = await axiosInstance.get<string, AxiosRequestConfig>(
        '/galleries/filtering',
        {
          params: customParams,
          paramsSerializer: (params) =>
            combineQuery(params, queryKey, queryValue),
        },
      );

      const list = listOfDisplayGalleries(res.data.data);

      if (res.data.data.length === 0) lastCard.current = true;

      return list;
    } catch (e) {
      // console.log(e);
    } finally {
      setLoading(false);
      dispatch(setKeyword(queryalue));
      dispatch(setOption(queryKey));
    }
  }, [dispatch, navigation, queryKey, queryValue]);
では、上記の関数を検出して呼び出して追加のデータをロードする方法は難題です.
いろいろな方法がありますが、私が使っている方法は  これはIntersection Observer APIを利用する方法です.Window Scrollを使用しない理由は、スクロールする特定のポイントを観察するために、短時間で大量の呼び出しが発生するため、パフォーマンスの面で効率が悪く、最も重要なのはそれを使用したいからです.クロスビューポイントで特定の要素を検出し、上記の関数を呼び出すようにデータのロードを継続し、ロード、lastCard状態で不要な呼び出しを防止する.
const loadMoreCards = useCallback(async () => {
    if (cards && cards.length > 0) {
      page.current += 1;
      const newCards = await loadCards();
      if (newCards !== undefined) setCards([...cards, ...newCards]);
    }
}, [cards, loadCards]);

useIntersectionObserver({
    target: targetRef.current,
    onIntersect: ([{ isIntersecting }]: any) => {
      if (isIntersecting && !loading && !lastCard.current) {
        loadMoreCards();
      }
    },
});
要素を感知し続け、内部コードを実行し、ロード可能なデータがない場合は、観察者を解除するように実現したほうがいいです.
Reference
  • https://ko.reactjs.org/docs/hooks-reference.html