Component는 사실항 User Interface로 View만을 담당한다.
하지만 우리는 반복되는 로직을 custom hook으로 리팩토링 할 수 있다.(사실상 비지니스 로직으로 볼 수 있다.)
여기에 몇가지 유즈케이스가 있다.
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;
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를 활용해서 최적화 한다.
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]);
}
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;
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]를 반환한다.
}
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;
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;
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()
// 처럼 기존 리덕스와 비슷하게 사용 가능하다.
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];
}
import makeStore from "./makeStore";
const todosReducer = (state, action) => {};
const [TodosProvider, useTodos, useTodosDispatch] = makeStore(todosReducer, []);
export { TodosProvider, useTodos, useTodosDispatch };
import React from "react";
import Todos from "./Todos";
import { TodosProvider } from "./TodosStore";
function App() {
return (
<>
<TodosProvider>
<Todos />
</TodosProvider>
</>
);
}
export default App;