Skip to content
This repository was archived by the owner on Jun 11, 2024. It is now read-only.

Commit 7ffdd13

Browse files
committed
폼 상태 관리 패키지 변경
1 parent f59b4fe commit 7ffdd13

File tree

10 files changed

+205
-95
lines changed

10 files changed

+205
-95
lines changed

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
![electron-vite-template-github-card](https://user-images.githubusercontent.com/43225384/196135599-585afdc5-9905-4400-bb02-ab0e7720da50.png)
22

3-
43
# Electron + Vite + React + TypeScript Template
54

65
A template for using electron quickly.<br/>
@@ -9,6 +8,7 @@ Please understand that the code and explanation are mainly written in Korean.
98
<br />
109

1110
## 특징들 둘러보기
11+
1212
- electron & vite를 사용해 빠른 개발, 빌드가 가능한 TypeScript 환경
1313
- 앱에 필수적인 요소 자동 업데이트, 저장소, 로그 등 사전구성
1414
- 파일 시스템 라우팅 기능 (Next.js에서 사용하던 방식)
@@ -32,7 +32,6 @@ Please understand that the code and explanation are mainly written in Korean.
3232
- CSS: [`styled-components`](https://styled-components.com/)
3333
- State management library: [`recoil`](https://hookstate.js.org/)
3434
- Date: [`dayjs`](https://day.js.org/)
35-
- Form value handle: [`formik`](https://formik.org/)
3635

3736
<br />
3837

@@ -61,7 +60,7 @@ yarn build:all
6160
<br />
6261

6362
## 스크린샷들
63+
6464
<img width="1718" alt="image" src="https://user-images.githubusercontent.com/43225384/196127143-2fd2fb65-5858-4bda-87a8-97c6e0487d8f.png">
6565
<img width="1718" alt="image" src="https://user-images.githubusercontent.com/43225384/196126603-388acf2c-760b-45f2-8738-5c1d2a4b4892.png">
6666
<img width="1718" alt="image" src="https://user-images.githubusercontent.com/43225384/196126770-08f75a7c-653d-4264-8c38-eb147c55193d.png">
67-

app/stores/config.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import Store from 'electron-store';
22

33
export interface ConfigStoreValues {
44
general: {
5-
theme: 'light' | 'dark';
65
developerMode: boolean;
76
};
87
}
@@ -12,7 +11,6 @@ export const configStore = new Store<ConfigStoreValues>({
1211
accessPropertiesByDotNotation: false,
1312
defaults: {
1413
general: {
15-
theme: 'dark',
1614
developerMode: false,
1715
},
1816
},

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@
2424
"electron-store": "^8.1.0",
2525
"electron-updater": "^5.3.0",
2626
"fast-deep-equal": "^3.1.3",
27-
"formik": "^2.2.9",
2827
"framer-motion": "^9.1.6",
2928
"glob": "^8.1.0",
3029
"lodash": "^4.17.21",
3130
"path-to-regexp": "^6.2.1",
3231
"polished": "^4.2.2",
3332
"react": "^18.2.0",
3433
"react-dom": "^18.2.0",
34+
"react-hook-form": "^7.43.2",
3535
"react-router-dom": "^6.6.2",
3636
"react-virtualized": "git+https://[email protected]/remorses/react-virtualized-fixed-import.git#9.22.3",
3737
"recoil": "^0.7.6",

src/components/SaveButton/SaveButton.tsx

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
import { ReactNode, useState } from 'react';
1+
import { ReactNode, useEffect, useState } from 'react';
2+
import { useWatch } from 'react-hook-form';
23

34
import { Button, Popconfirm, Space } from 'antd';
45
import clsx from 'clsx';
56
import deepEqual from 'fast-deep-equal';
67
import { AnimatePresence } from 'framer-motion';
78

9+
import { UseCustomUseFormReturn } from '~/hooks/useCustomForm';
10+
811
import { SaveButtonStyled } from './styled';
912

1013
export interface SaveButtonProps {
11-
formik: any;
14+
form: UseCustomUseFormReturn<any, any>;
1215
defaultValues: any;
1316
className?: string;
1417
confirmText?: ReactNode;
1518
useConfirm?: boolean;
16-
reset?: boolean;
1719
}
1820

1921
const animation = {
@@ -41,37 +43,59 @@ const animation = {
4143
},
4244
};
4345

46+
let timeoutHandle: NodeJS.Timeout;
47+
4448
const SaveButton = ({
49+
form,
4550
className,
4651
defaultValues,
47-
formik,
4852
confirmText,
4953
useConfirm = false,
50-
reset,
5154
}: SaveButtonProps) => {
55+
const [invalid, setInvalid] = useState(false);
5256
const [confirmVisible, setConfirmVisible] = useState(false);
5357
const [loading, setLoading] = useState(false);
5458

55-
const isEqual = deepEqual(defaultValues, formik.values);
59+
const values = useWatch({
60+
control: form.control,
61+
});
62+
63+
const isEqual = deepEqual(defaultValues, values);
5664

5765
const handleSave = async () => {
5866
setLoading(true);
59-
await formik.submitForm();
67+
68+
const valid = await form.submit();
69+
70+
if (!valid) {
71+
setInvalid(true);
72+
}
73+
6074
setLoading(false);
6175
};
6276

77+
useEffect(() => {
78+
clearTimeout(timeoutHandle);
79+
80+
if (invalid) {
81+
timeoutHandle = setTimeout(() => {
82+
setInvalid(false);
83+
}, 1000);
84+
}
85+
}, [invalid]);
86+
6387
return (
6488
<AnimatePresence>
6589
{!isEqual && (
66-
<SaveButtonStyled className={clsx('SaveButton', className)} key="SaveButton" {...animation}>
90+
<SaveButtonStyled
91+
className={clsx('SaveButton', className, { invalid })}
92+
key="SaveButton"
93+
{...animation}
94+
>
6795
<span>저장하지 않은 변경 사항이 있어요!</span>
6896

6997
<Space>
70-
<Button
71-
className="cancel"
72-
onClick={() => (reset ? formik.handleReset() : formik.setValues(defaultValues))}
73-
disabled={loading}
74-
>
98+
<Button className="cancel" disabled={loading} onClick={() => form.reset(defaultValues)}>
7599
되돌리기
76100
</Button>
77101

@@ -80,8 +104,8 @@ const SaveButton = ({
80104
okText="저장"
81105
cancelText="취소"
82106
placement="topRight"
83-
visible={confirmVisible}
84-
onVisibleChange={(visible: boolean) => {
107+
open={confirmVisible}
108+
onOpenChange={(visible: boolean) => {
85109
if (useConfirm) {
86110
setConfirmVisible(visible);
87111
}
@@ -95,7 +119,7 @@ const SaveButton = ({
95119
className="save"
96120
type="primary"
97121
loading={loading}
98-
onClick={useConfirm ? () => {} : handleSave}
122+
onClick={useConfirm ? undefined : handleSave}
99123
>
100124
변경사항 저장하기
101125
</Button>

src/components/SaveButton/styled.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ export const SaveButtonStyled = styled(motion.div)`
1515
margin-top: 2rem;
1616
border-radius: 8px;
1717
z-index: 100;
18+
border: 2px solid transparent;
19+
20+
&.invalid {
21+
animation: invalidAnimation 500ms;
22+
}
1823
1924
> span {
2025
font-size: 1rem;
@@ -40,4 +45,48 @@ export const SaveButtonStyled = styled(motion.div)`
4045
background-color: ${props => darken(0.1, props.theme.colors.success)};
4146
}
4247
}
48+
49+
@keyframes invalidAnimation {
50+
0% {
51+
transform: translate(1px, 1px);
52+
border-color: ${props => props.theme.colors.error};
53+
}
54+
10% {
55+
transform: translate(-1px, -2px);
56+
border-color: ${props => props.theme.colors.error};
57+
}
58+
20% {
59+
transform: translate(-3px, 0px);
60+
border-color: ${props => props.theme.colors.error};
61+
}
62+
30% {
63+
transform: translate(3px, 2px);
64+
border-color: ${props => props.theme.colors.error};
65+
}
66+
40% {
67+
transform: translate(1px, -1px);
68+
border-color: ${props => props.theme.colors.error};
69+
}
70+
50% {
71+
transform: translate(-1px, 2px);
72+
border-color: ${props => props.theme.colors.error};
73+
}
74+
60% {
75+
transform: translate(-3px, 1px);
76+
border-color: ${props => props.theme.colors.error};
77+
}
78+
70% {
79+
transform: translate(3px, 1px);
80+
border-color: ${props => props.theme.colors.error};
81+
}
82+
80% {
83+
transform: translate(-1px, -1px);
84+
}
85+
90% {
86+
transform: translate(1px, 2px);
87+
}
88+
100% {
89+
transform: translate(1px, -2px);
90+
}
91+
}
4392
`;

src/hooks/useCustomForm.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { FieldValues, UseFormProps, UseFormReturn, useForm } from 'react-hook-form';
2+
3+
import deepEqual from 'fast-deep-equal';
4+
5+
import { useDidUpdateEffect } from './useDidUpdateEffect';
6+
7+
export interface UseCustomUseFormProps<
8+
TFieldValues extends FieldValues = FieldValues,
9+
TContext = any,
10+
> extends UseFormProps<TFieldValues, TContext> {
11+
onSubmit: (data: TFieldValues) => void;
12+
syncDefaultValues?: boolean;
13+
}
14+
15+
export interface UseCustomUseFormReturn<
16+
TFieldValues extends FieldValues = FieldValues,
17+
TContext = any,
18+
> extends Omit<UseFormReturn<TFieldValues, TContext>, 'handleSubmit'> {
19+
handleSubmit: (e?: React.BaseSyntheticEvent) => void;
20+
submit: () => Promise<boolean>;
21+
}
22+
23+
export const useCustomForm = <TFieldValues extends FieldValues = FieldValues, TContext = any>(
24+
props: UseCustomUseFormProps<TFieldValues, TContext>,
25+
): UseCustomUseFormReturn<TFieldValues, TContext> => {
26+
const { onSubmit, syncDefaultValues = false, ...rest } = props;
27+
28+
const form = useForm(rest);
29+
30+
const handleDefaultValuesChange = async () => {
31+
if (syncDefaultValues) {
32+
const defaultValues =
33+
rest.defaultValues instanceof Function ? await rest.defaultValues() : rest.defaultValues;
34+
35+
if (!deepEqual(rest.defaultValues, form.getValues())) {
36+
form.reset(defaultValues);
37+
}
38+
}
39+
};
40+
41+
useDidUpdateEffect(() => {
42+
handleDefaultValuesChange();
43+
}, [rest.defaultValues]);
44+
45+
return {
46+
...form,
47+
handleSubmit: e => {
48+
form.handleSubmit(onSubmit)(e);
49+
},
50+
submit: async () => {
51+
if (await form.trigger()) {
52+
await onSubmit(form.getValues());
53+
return true;
54+
}
55+
56+
return false;
57+
},
58+
};
59+
};

src/hooks/useDidUpdateEffect.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { DependencyList, EffectCallback, useEffect, useRef } from 'react';
2+
3+
export const useDidUpdateEffect = (effect: EffectCallback, deps: DependencyList) => {
4+
const didMountRef = useRef(false);
5+
6+
useEffect(() => {
7+
if (didMountRef.current) return effect();
8+
didMountRef.current = true;
9+
}, deps);
10+
};

0 commit comments

Comments
 (0)