React

TIL | 간단한 custom hook 만들어보기 1

간단한 custom hook 만들어보기 1

이 글은, 노마드코더의 실전형 강의를 참고하여 공부한 내용을 바탕으로 정리하였습니다.

 

useInput (input을 위한 hook)

useInput.ts

import { useState } from "react";

const useInput = (
  // 초기 상탯값 및 벨리데이터 함수(원하는 조건을 리턴하는 함수) 인자로 받음
  initialValue: string,
  validator: (value: string) => boolean
) => {
  // 인자로 받은 값을 기준으로 useState 지정
  const [value, setValue] = useState<string>(initialValue);

  // validator 타입이 함수가 아닐시, early return
  if (typeof validator !== "function") {
    return;
  }

  // 사용할 onChange 함수 구현
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target;

    let willUpdate = true;

    if (typeof validator === "function") {
      willUpdate = validator(value);
    }

    if (willUpdate) {
      setValue(value);
    }
  };

  // 상탯값과 onChange 함수를 Object shorthand로 전달
  return { value, onChange };
};

export default useInput;

Input.tsx

import React from "react";

import { useInput } from "src/hooks";

const Input = (): JSX.Element => {
  // 원하는 조건의 validator 함수 및 초기 상탯값(여기서는 input의 value값이 된다.) 인자로 전달
  const maxLength = (value: string) => value.length <= 10;
  const name = useInput("Mr.", maxLength);
  return (
    <>
      <h1>Input</h1>
      // {value:value, onChange:onChange} 형태이므로 spread operator로도 전달 가능
      <input placeholder="Name" {...name}></input>
    </>
  );
};

export default Input;

결과


 

useTabs (탭 전환을 위한 hook)

useTabs.ts

import { useState } from "react";

interface ITab {
  tab: string;
  content: string;
}
// index에 따라 탭을 전환하기 위해 인자로 초기 상탯값인 index와 전체 배열을 받음
const useTabs = (initialTab: number, allTabs: ITab[]) => {
  const [currentIndex, setCurrentIndex] = useState<number>(initialTab);
  // 인자로 받은 값(초기 상탯값)을 기준으로 useState 지정
  // 현재 아이템을 보여주는 currentItem 및 setState 함수 리턴
  return { currentItem: allTabs[currentIndex], changeItem: setCurrentIndex };
};

export default useTabs;

Tab.tsx

import React from "react";

import { useTabs } from "src/hooks";

const Tab = () => {
  const {
    currentItem: { content },
    changeItem,
  } = useTabs(1, CONTENTS);

  return (
    <>
      <h1>Tab</h1>
      {CONTENTS.map((element, index) => {
        const { tab } = element;
        console.log(index);
        return (
          <>
            <button
              key={index}
              onClick={() => {
                // 보여주고자 하는 컨텐츠의 index값을 setState함으로써 렌더링
                changeItem(index);
              }}
            >
              {tab}
            </button>
          </>
        );
      })}
      <div>{content}</div>
    </>
  );
};

export default Tab;

const CONTENTS = [
  { tab: "Section1", content: "I'm the content of the Section1" },
  { tab: "Section2", content: "I'm the content of the Section2" },
];

결과


 

useTitle(title tag 변화를 위한 hook)

useTitle.ts

import { useState, useEffect } from "react";

const useTitle = (initialTitle: string) => {
  // title 태그의 text 요소로 쓰기 위해 초기 상탯값 인자로 전달
  const [title, setTitle] = useState<string>(initialTitle);

  const updateTitle = () => {
    const htmlTitle = document.querySelector("title") as HTMLTitleElement;
    htmlTitle.innerText = title;
  };

  // title값이 변함에 따라 updateTitle 함수가 재실행되도록 의존성 배열 넣기
  useEffect(updateTitle, [title]);

  return setTitle;
};

export default useTitle;

Title.tsx

import React from "react";

import useTitle from "src/hooks/useTitle";

const Title = (): JSX.Element => {
  // titleUpdator 함수가 실행이 되기 전까지 'loading' 텍스트를 띄우기 위해 초기 상탯값으로 인자 전달
  const titleUpdator = useTitle("Loading");

  setTimeout(() => {
    // titleUpdator는 setState 함수이므로 5초뒤 기존 상탯값인 "Loading"에서 "로딩이 끝난 뒤"라는 텍스트로 변화시켜줌
    titleUpdator("로딩이 끝난 뒤");
  }, 3000);

  return (
    <>
      <h1>Title</h1>
    </>
  );
};

export default Title;

결과


 

useClick(click event를 위한 hook)

useClick.ts

import { useRef, useEffect } from "react";

// click의 대상을 특정 element로 특정짓고 싶지 않았기 때문에 HTMLElement를 상속하는 제네릭 타입으로 지정
const useClick = <T extends HTMLElement>(
  handleClick: () => void
): React.RefObject<T> => {
  const element = useRef<T>(null);

  useEffect(() => {
    // handleClick 타입이 함수가 아닐시, early return
    if (typeof handleClick !== "function") {
      return;
    }

    if (element.current) {
      element.current.addEventListener("click", handleClick);
    }

    return () => {
      if (element.current) {
        element.current.removeEventListener("click", handleClick);
      }
    };
  }, []);

  // useRef 값을 return
  return element;
};

export default useClick;

Click.tsx

import React from "react";

import { useClick } from "src/hooks";

const Click = (): JSX.Element => {
  const handleClick = (): void => {
    console.log("클릭 이벤트 테스트 함수입니다.");
  };

  // 참조될 element에 따라 제네릭 타입을 다르게 줌으로써, 좀더 자유롭게 useClick 훅을 사용 가능
  const title = useClick<HTMLDivElement>(handleClick);

  return (
    <>
      <div ref={title}>Click</div>
    </>
  );
};

export default Click;

결과