커스텀 훅

jodmsoluth·2020년 7월 14일
0

react

목록 보기
2/5

Component는 사실항 User Interface로 View만을 담당한다.
하지만 우리는 반복되는 로직을 custom hook으로 리팩토링 할 수 있다.(사실상 비지니스 로직으로 볼 수 있다.)

여기에 몇가지 유즈케이스가 있다.

1. 이벤트등록 커스텀훅

App.js

import React, { useRef } from "react";
import { useClickOutside } from "./useClickOutside";

function App() {
  const menuRef = useRef();
  const onClickOutside = () => {
    alert("outside");
  };
  useClickOutside(menuRef, onClickOutside);
  return (
    <>
      <div ref={menuRef}>클릭해봐 멍청아</div>
    </>
  );
}

export default App;

useClickOutside

import React, { useEffect } from "react";

// 결국 클릭 이벤트의 등록
export function useClickOutside(elRef, callback) {
  useEffect(() => {
    const handleClickOutside = (e) => {
      // 클릭한 el을 elRef가 제대로 가르키고 있고와 callback이 있으면
      if (elRef?.current?.contains(e.target) && callback) {
        callback(e);
      }
    };
    document.addEventListener("click", handleClickOutside, true);
    return () => {
      document.removeEventListener("click", handleClickOutside, true);
    };
  }, [callback, elRef]);
}

위의 코드의 문제점은 callback으로 넘긴 props가 객체이기 떄문에 useClickOutside를 호출할 때마다 새로 이벤트를 생성하고 지우고를 한다. 따라서 static의 효과를 넣기위해 ref를 활용해서 최적화 한다.

최적화한 useClickOutside

import { useEffect, useRef } from "react";

// 결국 클릭 이벤트의 등록
export function useClickOutside(elRef, callback) {
  // 아래 코드를 쓰지 않으면 부모 컴포넌트에서 이 함수가 호출될 때마다 callback을 새로 만들어서 메모리 손실을 준다.
  // 하지만 새로 ref로 받으면 callback 함수를 useCallback의 효과를 내줄 수 있다..
  const callbackRef = useRef();
  callbackRef.current = callback;

  useEffect(() => {
    const handleClickOutside = (e) => {
      // 클릭한 el을 elRef가 제대로 가르키고 있고와 callback이 있으면
      if (elRef?.current?.contains(e.target) && callback) {
        callbackRef.current(e);
      }
    };
    document.addEventListener("click", handleClickOutside, true);
    return () => {
      document.removeEventListener("click", handleClickOutside, true);
    };
  }, [callbackRef, elRef]);
}

2. useContext + useReducer 커스텀 훅

App.js

import React from "react";
import Todos from "./Todos";
import { StoreProvider } from "./useStore";

const initialState = {
  todos: [{ name: "New Todo", id: 1 }],
};

// reducer 추가
const reducer = (state, action) => {
  switch (action.type) {
    case "addTodo":
      return {
        ...state,
        todos: [...state.todos, action.todo],
      };
    default:
      throw new Error("Unkown action", action);
  }
};

function App() {
  return (
    <>
      <StoreProvider reducer={reducer} initialState={initialState}>
        <Todos />
      </StoreProvider>
    </>
  );
}

export default App;

useStore.js

import React, {
  createContext,
  useState,
  useMemo,
  useContext,
  useReducer,
} from "react";

// context 생성
const context = createContext();

// context를 뿌려줄 provider 생성 (HOC)
export const StoreProvider = ({ children, reducer, initialState = {} }) => {
  const [store, dispatch] = useReducer(reducer, initialState);

  // value에 객체로 들어가므로 useMemo로 리랜더링 방지
  const contextValue = useMemo(() => [store, dispatch], [store, dispatch]);

  return <context.Provider value={contextValue}>{children}</context.Provider>;
};

export default function useStore() {
  return useContext(context);
  // [store = initialState, dispatch]를 반환한다.
}

Todos.js

import React from "react";
import Todo from "./Todo";
import useStore from "./useStore";

function Todos() {
  const [{ todos }, dispatch] = useStore();
  const addTodo = () => {
    dispatch({
      type: "addTodo",
      todo: {
        name: "New Todo",
      },
    });
  };
  return (
    <div>
      {todos.map((todo) => (
        <>
          <Todo key={todo.id} todo={todo} />;
        </>
      ))}
    </div>
  );
}

export default Todos;

Todo.js

import React from "react";
import useStore from "./useStore";

function Todo({ todo }) {
  const [, dispatch] = useStore();
  const handleClick = () => {
    dispatch({
      type: "toggleTodo",
      todoId: todo.id,
    });
  };
  return (
    <div>
      <div onClick={handleClick}>{todo.name}</div>
    </div>
  );
}

export default Todo;

3. 활용) 멀티 스토어 훅

multi-store-hooks.js

import React, { createContext, useContext, useReducer } from "react";

// context 생성
const storeContext = createContext();
const dispatchContext = createContext();

// context를 뿌려줄 provider 생성 (HOC)
export const StoreProvider = ({ children, reducer, initialState = {} }) => {
  const [store, dispatch] = useReducer(reducer, initialState);

  return (
    <dispatchContext.Provider value={dispatch}>
      <storeContext.Provider value={store}>{children}</storeContext.Provider>;
    </dispatchContext.Provider>
  );
};

// 저장 담당과 수정 담당을 분리해서 hooks를 사용할 수 있다.

export function useStore() {
  return useContext(storeContext);
  // [store = initialState, dispatch]를 반환한다.
}

export function useDispatch() {
  return useContext(dispatchContext);
}

// 사용할 때는
// const dispatch = useDispatch()
// const { todos } = useStore()
// 처럼 기존 리덕스와 비슷하게 사용 가능하다.

4. 활용 ) 팩토리 패턴

makeStore.js

import React, { createContext, useContext, useReducer } from "react";

// 팩토리 패턴으로 수정
export default function makeStore(reducer, initialState) {
  // context 생성
  const storeContext = createContext();
  const dispatchContext = createContext();

  // context를 뿌려줄 provider 생성 (HOC)
  const StoreProvider = ({ children }) => {
    const [store, dispatch] = useReducer(reducer, initialState);

    return (
      <dispatchContext.Provider value={dispatch}>
        <storeContext.Provider value={store}>{children}</storeContext.Provider>;
      </dispatchContext.Provider>
    );
  };

  // 저장 담당과 수정 담당을 분리해서 hooks를 사용할 수 있다.

  function useStore() {
    return useContext(storeContext);
    // [store = initialState, dispatch]를 반환한다.
  }

  function useDispatch() {
    return useContext(dispatchContext);
  }

  return [StoreProvider, useDispatch, useStore];
}

TodoStore.js

import makeStore from "./makeStore";

const todosReducer = (state, action) => {};

const [TodosProvider, useTodos, useTodosDispatch] = makeStore(todosReducer, []);

export { TodosProvider, useTodos, useTodosDispatch };

App.js

import React from "react";
import Todos from "./Todos";
import { TodosProvider } from "./TodosStore";

function App() {
  return (
    <>
      <TodosProvider>
        <Todos />
      </TodosProvider>
    </>
  );
}

export default App;
profile
풀스택이 되고 싶은 주니어 웹 개발자입니다.

0개의 댓글