[DAY-54] React (7)
๐ ํท๊ฐ๋ ธ๋ & ๋ชฐ๋๋ ๋ถ๋ถ๋ค๋ง ์ ๋ฆฌํ๋ ๋๋ง์ TIL
๐ฏ ๋ชจ๋ ๊ฐ์ ๋ด์ฉ์ ์ ์ง ์์์!
์ค๋์ ์๊ฐ์?
์ค๋๋ ์ด์ฌํ ์ฌ์ฉ์ ์ ์ ํ
๋ค์ ๋ํด ์ค์ตํ๋ค.
useInterval๊ณผ useTimeout์ Fn(ํจ์) ํ์ผ๋ค์ ๋๋์ด ์์ฑํ๋ค.
์ฐจ์ด๋ ๊ฐ๋จํ๋ค. ์์์ ๋๋ฌ์ฃผ๋๋ ์ปดํฌ๋ํธ๊ฐ ๋ก๋๋์๋ง์ ์คํ๋๋๋.
useDebounce๋ ํฅํ ์ ์ํ ํ๋ก์ ํธ์์๋ ๊ต์ฅํ ์ ์ฉํ๊ฒ ์ฌ์ฉ๋ ๊ฒ ๊ฐ๋ค.
useInterval์ ์ฌ์ฉํ๋ฉด ์ฐ๋กํ๋ง ๋ฐฉ์๋ Hook์ผ๋ก ๊ตฌํํ ์ ์์ง ์์๊น.
[1] ์ปดํฌ๋ํธ ์ค์ต
useResize
// useResize.js
import { useEffect, useRef } from "react";
const useResize = (handler) => {
const savedHandler = useRef(handler);
const ref = useRef(null);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new ResizeObserver((entries) => {
savedHandler.current(entries[0].contentRect);
});
observer.observe(element);
return () => {
observer.disconnect();
};
}, [ref]);
return ref;
};
export default useResize;
// useResize.stories.js
import styled from "@emotion/styled";
import { useState } from "react";
import Image from "../../components/_BasicComponent/Image";
import useResize from "../../hooks/useResize";
export default {
title: "Hook/useResize",
};
const Background = styled.div`
width: 100%;
height: 400px;
background-color: blue;
`;
export const Default = () => {
const [imageSize, setImageSize] = useState({ width: 0, height: 0 });
const ref = useResize((rect) => {
setImageSize({ width: rect.width, height: rect.height });
});
return (
<Background ref={ref}>
<Image
width={imageSize.width}
height={imageSize.height}
src="https://picsum.photos/1000"
mode="contain"
/>
</Background>
);
};
useLocalStorage, useSessionStorage
LocalStorage
์ SessionStorage
์ด์ฉ ๋ฐฉ๋ฒ์ ๋์ผํ๊ธฐ์, ์ ์ฅ์ ์ด๋ฆ๋ง ๋ฐ๊ฟ์ฃผ๋ฉด ๋๋ค.
๋ฐ๋ผ์ ์ฝ๋๋ useLocalStorage
๋ง ์ฒจ๋ถํ๋ค.
// useLocalStorage.js
import { useState } from "react";
const useLocalStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore =
typeof value === "function" ? value(storedValue) : value;
setStoredValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
};
export default useLocalStorage;
// useLocalStorage.stories.js
import useLocalStorage from "../../hooks/useLocalStorage";
export default {
title: "Hook/useLocalStoreage",
};
export const Default = () => {
const [status, setStatus] = useLocalStorage("status", "404 NOT FOUND");
return (
<div>
<button onClick={() => setStatus("200 OK")}>Resend</button>
{status}
</div>
);
};
useForm
์ ๊ท ํํ์ ์ด๋ ต๋คโฆ.
๋ ๋ณต์กํ ์ ๊ท ํํ์์ ์ฌ์ฉํ๋ค๋๋ฐ, ์์๋ณด๊ณ ํ๋ก์ ํธ์์ ์ ์ฉํด๋ด์ผ๊ฒ ๋ค.
// 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(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>
);
};
useTimeout
useTimeout
์ useTimeoutFn
ํ
์ ์ฌ์ฉํ๋ค.
// useTimeoutFn.js
import { useRef, useEffect, useCallback } from "react";
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);
}, []);
useEffect(() => clear, [clear]);
return [run, clear];
};
export default useTimeoutFn;
// useTimeout.js
import { useEffect } from "react";
import useTimeoutFn from "./useTimeoutFn";
const useTimeout = (fn, ms) => {
const [run, clear] = useTimeoutFn(fn, ms);
useEffect(() => {
run();
return clear;
}, [run, clear]);
return clear;
};
export default useTimeout;
// useTimeoutFn.stories.js
import useTimeoutFn from "../../hooks/useTimeoutFn";
export default {
title: "Hook/useTimeoutFn",
};
export const Default = () => {
const [run, clear] = useTimeoutFn(() => {
alert("์คํ");
}, 3000);
return (
<>
<div>useTimeoutFn Test</div>
<button onClick={run}>3์ด ๋ค ์คํ</button>
<button onClick={clear}>Stop</button>
</>
);
};
// useTimeout.stories.js
import useTimeout from "../../hooks/useTimeout";
export default {
title: "Hook/useTimeout",
};
export const Default = () => {
const clear = useTimeout(() => {
alert("์คํ");
}, 3000);
return (
<>
<div>useTimeout Test</div>
<button onClick={clear}>Stop</button>
</>
);
};
useInterval
useInterval
์ useIntervalFn
ํ
์ ์ฌ์ฉํ๋ค.
// useIntervalFn.js
import { useRef, useEffect, useCallback } 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;
// 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;
// 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, "Plus"]);
}, 1000);
return (
<>
<div>useIntervalFn Test</div>
<div>{array}</div>
<button onClick={run}>1์ด๋ง๋ค ์ถ๊ฐ!</button>
<button onClick={clear}>Clear</button>
</>
);
};
// 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, "Plus"]);
}, 1000);
return (
<>
<div>useInterval Test</div>
<div>{array}</div>
<button onClick={clear}>Clear</button>
</>
);
};
useDebounce
eslint๊ฒฝ๊ณ ๊ฐ ๋ ์ disactive ์ฃผ์์ ๋ฃ์ด๋ฒ๋ ธ๋ค.
// 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 react-hooks/exhaustive-deps
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", "Coupang", "Line", "Woowahan"];
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>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<div>
{result.map((item) => (
<Fragment key={item}>
{item}
<br />
</Fragment>
))}
</div>
</div>
);
};
๋๊ธ๋จ๊ธฐ๊ธฐ