🤷♂️ formik은 추후에 알아보자...
💻 useForm.js
import { useState } from "react";
const useForm = ({ initialValues, onSubmit, validate }) => {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [isLoading, setIsLoading] = useState(false);
const handleChange = (e) => {
const {name, value} = e.target;
setValues({...values, [name]: value});
};
const handleSubmit = async (e) => {
setIsLoading(true);
e.preventDefault();
const newErrors = validate ? validate(values) : {};
if(Object.keys(newErrors).length===0) {
await onSubmit(values);
}
setErrors(newErrors);
setIsLoading(false);
};
return {
values,
errors,
isLoading,
handleChange,
handleSubmit,
};
};
export default useForm;
💻 useForm.stories.js
import useForm from "../../hooks/useForm";
export default {
title: 'Hook/useForm',
};
const sleep = () => {
return new Promise((resolve) => {
setTimeout(() => resolve(), 1000)
});
};
export const Default = () => {
const { isLoading, errors, handleChange, handleSubmit } = useForm({
initialValues: {
email: '',
password: ''
},
onSubmit: async (values) => {
await sleep();
alert(JSON.stringify(values))
},
validate: ({ email, password }) => {
const errors = {};
if(!email) errors.email = "이메일을 입력해주세요.";
if(!password) errors.password = "비밀번호를 입력해주세요.";
if(!/^.+@.+\..+$/.test(email)) errors.email = "올바른 이메일을 입력해주세요.";
return errors;
}
});
return (
<form onSubmit={handleSubmit}>
<h1>Sign In</h1>
<div>
<input name="email" type="email" placeholder="Email" onChange={handleChange}/>
{errors.email}
</div>
<div>
<input name="password" type="password" placeholder="Password" onChange={handleChange}/>
{errors.password}
</div>
<button type="submit" disabled={isLoading}>{isLoading ? 'Loading...' : 'Submit'}</button>
</form>
)
}
🖨 완성된 훅 시연
💻 useTimeOutFn.js
import { useCallback, useEffect, useRef } from "react";
// 함수 호출을 통한 useTimeOut
const useTimeOutFn = (fn, ms) => {
const timeoutId = useRef();
const callback = useRef(fn);
useEffect(() => {
callback.current = fn;
}, [fn]);
const run = useCallback(() => {
timeoutId.current && clearTimeout(timeoutId.current);
timeoutId.current = setTimeout(() => {
callback.current()
}, ms)
}, [ms])
const clear = useCallback(() => {
timeoutId.current && clearTimeout(timeoutId.current);
}, [])
return [run, clear];
};
export default useTimeOutFn;
💻 useTimeOutFn.stories.js
import useTimeOutFn from "../../hooks/useTimeOutFn";
export default {
title: 'Hook/useTimeoutFn',
}
export const Default = () => {
const [run, clear] = useTimeOutFn(() => {
alert('launch');
}, 3000);
return (
<>
<div>useTimeOutFn 테스트</div>
<button onClick={run}>3초 뒤 실행</button>
<button onClick={clear}>정지</button>
</>
)
}
🖨 완성된 훅 시연
💻 useTimeout.js
import { useEffect } from "react";
import useTimeOutFn from "./useTimeOutFn"
// useTimeOutFn 컴포넌트를 이용한 useTimeout
const useTimeout = (fn, ms) => {
const [run, clear] = useTimeOutFn(fn, ms);
useEffect(() => {
run();
return clear;
}, [run, clear])
return clear;
};
export default useTimeout;
💻 useTimeout.stories.js
import useTimeout from "../../hooks/useTimeout";
export default {
title: 'Hook/useTimeout',
}
export const Default = () => {
const clear = useTimeout(() => {
alert('launch');
}, 3000);
return (
<>
<div>useTimeOut 테스트</div>
<button onClick={clear}>정지</button>
</>
)
}
🖨 완성된 훅 시연
💻 useIntervalFn.js
import { useCallback, useEffect, useRef } from "react"
const useIntervalFn = (fn, ms) => {
const intervalId = useRef();
const callback = useRef(fn);
useEffect(() => {
callback.current = fn;
}, [fn]);
const run = useCallback(() => {
intervalId.current && clearInterval(intervalId.current);
intervalId.current = setInterval(() => {
callback.current();
}, ms)
}, [ms])
const clear = useCallback(() => {
intervalId.current && clearInterval(intervalId.current);
}, []);
useEffect(() => clear, [clear]);
return [run, clear];
}
export default useIntervalFn;
💻 useIntervalFn.stories.js
import { useState } from "react";
import useIntervalFn from "../../hooks/useIntervalFn";
export default {
title: 'Hook/useIntervalFn',
}
export const Default = () => {
const [array, setArray] = useState([]);
const [run, clear] = useIntervalFn(() => {
setArray([...array, '추가됨!']);
}, 1000);
return (
<>
<div>useIntervalFn 테스트</div>
<div>{array}</div>
<button onClick={run}>1초 마다 추가</button>
<button onClick={clear}>정지</button>
</>
)
}
🖨 완성된 훅 시연
💻 useInterval.js
import { useEffect } from "react";
import useIntervalFn from "./useIntervalFn";
const useInterval = (fn, ms) => {
const [run, clear] = useIntervalFn(fn, ms);
useEffect(() => {
run();
return clear;
}, [run, clear]);
return clear;
};
export default useInterval;
💻 useInterval.stories.js
import { useState } from "react";
import useInterval from "../../hooks/useInterval";
export default {
title: 'Hook/useInterval',
}
export const Default = () => {
const [array, setArray] = useState([]);
const clear = useInterval(() => {
setArray([...array, '추가됨!']);
}, 1000);
return (
<>
<div>useInterval 테스트</div>
<div>{array}</div>
<button onClick={clear}>정지</button>
</>
)
}
🖨 완성된 훅 시연
💻 useDebounce.js
import { useEffect } from "react";
import useTimeOutFn from "./useTimeOutFn";
const useDebounce = (fn, ms, deps) => {
const [run, clear] = useTimeOutFn(fn, ms);
// eslint-disable-next-line
useEffect(run, deps);
return clear;
};
export default useDebounce;
💻 useDebounce.stories.js
import { Fragment, useState } from "react";
import useDebounce from "../../hooks/useDebounce";
export default {
title: 'Hook/useDebounce',
}
const companies = [
'kakao',
'Naver',
'Daangn',
'Coupang',
'Line'
]
export const Default = () => {
const [value, setValue] =useState('');
const [result, setResult] = useState([]);
useDebounce(() => {
if(value === '') setResult([]);
else {
setResult(
companies.filter((company) =>
company.toLowerCase().includes(value.toLowerCase())
)
);
}
}, 300, [value]);
return (
<div>
<h1>대기업 검색 해보슈</h1>
<input type="text" value={value} onChange={(e) => setValue(e.target.value)} />
<div>
{result.map((item) => (
<Fragment key={item}>
{item}
<br />
</Fragment>
))}
</div>
</div>
)
};
🖨 완성된 훅 시연
💻 useAsyncFn.js
import { useCallback, useRef, useState } from "react";
const useAsyncFn = (fn, deps) => {
const lastCallId = useRef(0)
const [state, setState] = useState({ isLoading: false });
const callback = useCallback((...args) => {
const callId = ++lastCallId.current;
if(!state.isLoading) {
setState({...state, isLoading: true});
}
return fn(...args).then((value) => {
callId === lastCallId.current && setState({ value, isLoading: false });
return value;
}, (error) => {
callId === lastCallId.current && setState({ error, isLoading: false });
return error;
})
}, deps)
return [state, callback];
};
export default useAsyncFn;
💻 useAsyncFn.stories.js
import useAsyncFn from "../../hooks/useAsyncFn";
export default {
title: 'Hook/useAsyncFn',
}
const asyncReturnValue = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Success');
}, 1000);
})
};
const asyncReturnError = () => {
return new Promise((_, reject) => {
setTimeout(() => {
reject('Error');
}, 1000);
})
};
export const Success = () => {
const [state, callback] = useAsyncFn(async () => {
return await asyncReturnValue();
}, []);
return (
<div>
<div>useAsyncFn 테스트</div>
<div>{JSON.stringify(state)}</div>
<button onClick={callback} disabled={state.isLoading}>비동기 호출</button>
</div>
)
}
export const Error = () => {
const [state, callback] = useAsyncFn(async () => {
return await asyncReturnError();
}, []);
return (
<div>
<div>useAsyncFn 테스트</div>
<div>{JSON.stringify(state)}</div>
<button onClick={callback} disabled={state.isLoading}>비동기 호출</button>
</div>
)
}
🖨 완성된 훅 시연
💻 useAsync.js
import { useEffect } from "react";
import useAsyncFn from "./useAsyncFn";
const useAsync = (fn, deps) => {
const [state, callback] = useAsyncFn(fn, deps);
useEffect(() => {
callback();
}, [callback])
return state;
}
export default useAsync;
💻 useAsync.stories.js
import useAsync from "../../hooks/useAsync";
export default {
title: 'Hook/useAsync',
}
const asyncReturnValue = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Success');
}, 1000);
})
};
const asyncReturnError = () => {
return new Promise((_, reject) => {
setTimeout(() => {
reject('Error');
}, 1000);
})
};
export const Success = () => {
const state = useAsync(async () => {
return await asyncReturnValue();
}, []);
return (
<div>
<div>useAsync 테스트</div>
<div>{JSON.stringify(state)}</div>
</div>
)
}
export const Error = () => {
const state = useAsync(async () => {
return await asyncReturnError();
}, []);
return (
<div>
<div>useAsync 테스트</div>
<div>{JSON.stringify(state)}</div>
</div>
)
}
🖨 완성된 훅 시연
💻 useHotKey.js
import { useCallback, useEffect, useMemo } from "react";
const ModifierBitMasks = {
alt: 1,
ctrl: 2,
meta: 4,
shift: 8
}
const ShiftKeys = {
'~': '`',
'!': '1',
'@': '2',
'#': '3',
'$': '4',
'%': '5',
'^': '6',
'&': '7',
'*': '8',
'(': '9',
')': '0',
'_': '-',
'+': '=',
'|': '\\',
'{': '[',
'}': ']',
':': ';',
'"': '\'',
'?': '/',
'<': ',',
'>': '.'
}
const Aliases= {
win: 'meta',
window: 'meta',
cmd: 'meta',
command: 'meta',
esc: 'escape',
opt: 'alt',
option: 'alt'
}
const getKeyCombo = (e) => {
const key = e.key !== ' ' ? e.key.toLowerCase() : 'space';
console.log(key);
let modifiers = 0;
if(e.altKey) modifiers += ModifierBitMasks.alt;
if(e.ctrlKey) modifiers += ModifierBitMasks.ctrl;
if(e.metaKey) modifiers += ModifierBitMasks.meta;
if(e.shiftKey) modifiers += ModifierBitMasks.shift;
return { modifiers, key };
}
const parseKeyCombo = (combo) => {
const pieces = combo.replace(/\s/g, '').toLowerCase().split("+");
let modifiers = 0;
let key;
for (const piece of pieces) {
if(ModifierBitMasks[piece]) {
modifiers+= ModifierBitMasks[piece];
} else if(ShiftKeys[piece]) {
modifiers += ModifierBitMasks.shift;
key = ShiftKeys[piece];
} else if(Aliases[piece]) {
key = Aliases[piece]
} else {
key = piece;
}
}
return { modifiers, key };
};
const comboMatches = (a, b) => {
return a.modifiers === b.modifiers && a.key === b.key;
}
const useHotKey = (hotkeys) => {
const localKeys = useMemo(() => hotkeys.filter(k => !k.global), [hotkeys]);
const globalKeys = useMemo(() => hotkeys.filter(k => k.global), [hotkeys]);
const invokeCallback = useCallback((global, combo, callbackName, e) => {
for(const hotkey of global ? globalKeys : localKeys) {
// TODO: 단축키 처리를 한다.
// callbackName: onKeyDown, onKeyUp
if(comboMatches(parseKeyCombo(hotkey.combo), combo)) {
hotkey[callbackName] && hotkey[callbackName](e);
}
}
}, [localKeys, globalKeys])
const handleGlobalKeyDown = useCallback((e) => {
invokeCallback(true, getKeyCombo(e), 'onKeyDown', e);
}, [invokeCallback]);
const handleGlobalKeyUp = useCallback((e) => {
invokeCallback(true, getKeyCombo(e), 'onKeyUp', e);
}, [invokeCallback]);
const handleLocalKeyDown = useCallback((e) => {
invokeCallback(false, getKeyCombo(e.nativeEvent), 'onKeyDown', e.nativeEvent);
}, [invokeCallback]);
const handleLocalKeyUp = useCallback((e) => {
invokeCallback(false, getKeyCombo(e.nativeEvent), 'onKeyUp', e.nativeEvent);
}, [invokeCallback]);
useEffect(() => {
document.addEventListener('keydown', handleGlobalKeyDown);
document.addEventListener('keyup', handleGlobalKeyUp);
return () => {
document.removeEventListener('keydown', handleGlobalKeyDown);
document.removeEventListener('keyup', handleGlobalKeyUp);
}
}, [handleGlobalKeyDown, handleGlobalKeyUp])
return { handleKeyDown: handleLocalKeyDown, handleKeyUp: handleLocalKeyUp }
}
export default useHotKey;
💻 useHotKey.stories.js
import { useState } from "react";
import useHotKey from "../../hooks/useHotKey";
export default {
title: 'Hook/useHotKey',
}
export const Default = () => {
const [value, setValue] = useState('');
const hotkeys = [
{
global: true,
combo: 'meta',
onKeyDown: (e) => {
alert('meta')
}
},
{
global: true,
combo: 'alt+w',
onKeyDown: (e) => {
alert('alt+w')
}
},
{
global: true,
combo: 'ctrl+shift+k',
onKeyDown: (e) => {
alert('ctrl+shift+k')
}
},
{
combo: 'esc',
onKeyDown: (e) => {
setValue("");
}
},
];
const { handleKeyDown } = useHotKey(hotkeys);
return (
<>
<h1>useHotKey 테스트</h1>
<h2>1. meta</h2>
<h2>2. alt + w</h2>
<h2>3. ctrl + shift + k</h2>
<h1>로컬 테스트</h1>
<h2>esc를 눌러 입력창 지우기</h2>
<input onKeyDown={handleKeyDown} value={value} onChange={(e) => setValue(e.target.value)} />
</>
)
}
🖨 완성된 훅 시연
이것으로 일단 커스텀 훅 제작을 마무리했다.
추후에 더 만들만한 훅이 있으면 조금씩 더 추가할 예정이다.