diff --git a/assets/images/p/generic-component-encapsulate-reusable-component/Untitled-1.png b/assets/images/p/generic-component-encapsulate-reusable-component/Untitled-1.png
new file mode 100644
index 0000000..551f8c4
Binary files /dev/null and b/assets/images/p/generic-component-encapsulate-reusable-component/Untitled-1.png differ
diff --git a/assets/images/p/generic-component-encapsulate-reusable-component/Untitled-2.png b/assets/images/p/generic-component-encapsulate-reusable-component/Untitled-2.png
new file mode 100644
index 0000000..6adde78
Binary files /dev/null and b/assets/images/p/generic-component-encapsulate-reusable-component/Untitled-2.png differ
diff --git a/assets/images/p/generic-component-encapsulate-reusable-component/Untitled-3.png b/assets/images/p/generic-component-encapsulate-reusable-component/Untitled-3.png
new file mode 100644
index 0000000..9914957
Binary files /dev/null and b/assets/images/p/generic-component-encapsulate-reusable-component/Untitled-3.png differ
diff --git a/assets/images/p/generic-component-encapsulate-reusable-component/Untitled-4.png b/assets/images/p/generic-component-encapsulate-reusable-component/Untitled-4.png
new file mode 100644
index 0000000..9de5d25
Binary files /dev/null and b/assets/images/p/generic-component-encapsulate-reusable-component/Untitled-4.png differ
diff --git a/assets/images/p/generic-component-encapsulate-reusable-component/Untitled-5.png b/assets/images/p/generic-component-encapsulate-reusable-component/Untitled-5.png
new file mode 100644
index 0000000..0e515dc
Binary files /dev/null and b/assets/images/p/generic-component-encapsulate-reusable-component/Untitled-5.png differ
diff --git a/assets/images/p/generic-component-encapsulate-reusable-component/Untitled.png b/assets/images/p/generic-component-encapsulate-reusable-component/Untitled.png
new file mode 100644
index 0000000..e84c1a9
Binary files /dev/null and b/assets/images/p/generic-component-encapsulate-reusable-component/Untitled.png differ
diff --git a/components/DarkModeBtn.tsx b/components/DarkModeBtn.tsx
index 42db2cc..852fffb 100644
--- a/components/DarkModeBtn.tsx
+++ b/components/DarkModeBtn.tsx
@@ -1,14 +1,11 @@
import classNames from 'classnames';
+import useMounted from 'lib/hooks/useMounted';
import { useTheme } from 'next-themes';
-import { useEffect, useState } from 'react';
import { FiMoon, FiSun } from 'react-icons/fi';
const DarkModeBtn = () => {
- const [mounted, setMounted] = useState(false);
+ const { mounted } = useMounted();
const { systemTheme, theme, setTheme } = useTheme();
- // When mounted on client, now we can show the UI
- useEffect(() => setMounted(true), []);
-
const currentTheme = theme === 'system' ? systemTheme : theme;
if (!mounted)
diff --git a/components/RUA/RUACodeSandbox.tsx b/components/RUA/RUACodeSandbox.tsx
new file mode 100644
index 0000000..61591fe
--- /dev/null
+++ b/components/RUA/RUACodeSandbox.tsx
@@ -0,0 +1,72 @@
+import classNames from 'classnames';
+import useInView from 'lib/hooks/useInView';
+import { useTheme } from 'next-themes';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import RUALoading from './loading/RUALoading';
+
+const partten =
+ /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
+const commonClass = classNames(
+ 'rounded-lg h-[500px] border-0',
+ 'overflow-hidden w-full'
+);
+
+type Props = {
+ url: string;
+};
+
+const RUACodeSandbox = ({ url }: Props) => {
+ const isUrl = partten.test(url);
+ const { systemTheme, theme } = useTheme();
+ const currentTheme = theme === 'system' ? systemTheme : theme ?? 'light';
+
+ const { ref, inView } = useInView();
+ const sandUrl = new URL(url);
+ const embed = sandUrl.pathname.split('/')[2];
+ const [src, setSrc] = useState('');
+ useEffect(() => {
+ inView &&
+ setSrc(
+ `https://codesandbox.io/embed/${embed}?fontsize=14&hidenavigation=1&theme=${currentTheme}&view=preview`
+ );
+ }, [currentTheme, embed, inView]);
+
+ const [load, setLoad] = useState(false);
+ const handleLoad = useCallback(() => {
+ setLoad(true);
+ }, []);
+
+ if (!isUrl) return null;
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default RUACodeSandbox;
diff --git a/components/post/PostTOC.module.css b/components/post/PostTOC.module.css
deleted file mode 100644
index 36f0d0f..0000000
--- a/components/post/PostTOC.module.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.head:hover:before {
- content: unset !important;
-}
diff --git a/components/post/PostTOC.tsx b/components/post/PostTOC.tsx
deleted file mode 100644
index 00b2176..0000000
--- a/components/post/PostTOC.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { getHeadings } from 'lib/utils';
-import Anchor from 'components/mdx/Anchor';
-import styles from './PostTOC.module.css';
-import classNames from 'classnames';
-import { useCallback, useState } from 'react';
-import { FiChevronDown } from 'react-icons/fi';
-
-interface Props {
- headings: ReturnType;
-}
-
-const PostTOC = ({ headings }: Props) => {
- const [show, setShow] = useState(false);
- const handleClick = useCallback(() => setShow((show) => !show), []);
-
- return (
- <>
-
-
- What's inside?
-
-
-
-
-
- {headings?.map((h) => (
- -
-
- {h.text}
-
-
- ))}
-
-
- >
- );
-};
-
-export default PostTOC;
diff --git a/components/post/SlideToc.tsx b/components/post/SlideToc.tsx
index 5792339..846870f 100644
--- a/components/post/SlideToc.tsx
+++ b/components/post/SlideToc.tsx
@@ -1,9 +1,9 @@
-import { useEffect, useState } from 'react';
+import useMounted from 'lib/hooks/useMounted';
+import { useEffect } from 'react';
import tocbot from 'tocbot';
const SlideToc = () => {
- const [mounted, setMounted] = useState(false);
- useEffect(() => setMounted(true), []);
+ const { mounted } = useMounted();
useEffect(() => {
// Waiting the right time.
diff --git a/lib/hooks/useMounted.tsx b/lib/hooks/useMounted.tsx
new file mode 100644
index 0000000..cc0036f
--- /dev/null
+++ b/lib/hooks/useMounted.tsx
@@ -0,0 +1,12 @@
+import { useState, useEffect } from 'react';
+
+const useMounted = () => {
+ const [mounted, setMounted] = useState(false);
+ useEffect(() => setMounted(true), []);
+
+ return {
+ mounted,
+ };
+};
+
+export default useMounted;
diff --git a/pages/p/generic-component-encapsulate-reusable-component/hook-form-basic/App.ts b/pages/p/generic-component-encapsulate-reusable-component/hook-form-basic/App.ts
new file mode 100644
index 0000000..e6246a8
--- /dev/null
+++ b/pages/p/generic-component-encapsulate-reusable-component/hook-form-basic/App.ts
@@ -0,0 +1,44 @@
+const app = `import { useForm } from 'react-hook-form';
+
+type Pet = 'Cat' | 'Dog';
+type FormData = {
+ firstName: string;
+ lastName: string;
+ favorite: Pet;
+};
+
+export default function App() {
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ } = useForm();
+ const onSubmit = handleSubmit((data) => console.log(data));
+
+ return (
+
+ );
+}`;
+export default app;
diff --git a/pages/p/generic-component-encapsulate-reusable-component/index.mdx b/pages/p/generic-component-encapsulate-reusable-component/index.mdx
new file mode 100644
index 0000000..6a7459f
--- /dev/null
+++ b/pages/p/generic-component-encapsulate-reusable-component/index.mdx
@@ -0,0 +1,338 @@
+---
+title: 组件泛型实例-封装可复用的表单组件
+date: '2022-08-12'
+tags: [TypeScript, React]
+---
+
+export const meta = {
+ title: '组件泛型实例-封装可复用的表单组件',
+ date: '2022-08-12',
+ tags: ['TypeScript', 'React'],
+};
+
+import Layout from 'layouts/MDXLayout';
+import dynamic from 'next/dynamic';
+import Image from 'components/mdx/Image';
+import image0 from 'assets/images/p/generic-component-encapsulate-reusable-component/Untitled.png';
+import image1 from 'assets/images/p/generic-component-encapsulate-reusable-component/Untitled.png';
+import image2 from 'assets/images/p/generic-component-encapsulate-reusable-component/Untitled.png';
+import image3 from 'assets/images/p/generic-component-encapsulate-reusable-component/Untitled.png';
+import image4 from 'assets/images/p/generic-component-encapsulate-reusable-component/Untitled.png';
+import image5 from 'assets/images/p/generic-component-encapsulate-reusable-component/Untitled.png';
+import app from './hook-form-basic/App.ts';
+import app2 from './react-generic/App.ts';
+import child from './react-generic/Child.ts';
+
+export const RUASandpack = dynamic(() => import('components/RUA/RUASandpack'));
+export const RUACodeSandbox = dynamic(() =>
+ import('components/RUA/RUACodeSandbox')
+);
+
+export default ({ children }) => {children};
+
+当前很多 UI 库都会为我们集成可复用的 Form 组件,并且是开箱即用。但有时候我们往往可能需要为自己的组件集成 Form。单纯的手动管理所有的状态可能不是件理想的活,尤其是表单验证。
+
+[React Hook Forms](https://react-hook-form.com/) 为我们提供了完善的状态管理,并且可以集成到任何组件中去。
+
+你可能会问,如今已经有了像是 MUI、Ant Design 等此类优秀的组件库,为什么还需要使用 React Hook Forms 来管理表单。
+
+[MUI: The React component library you always wanted](https://mui.com/zh/)
+
+虽然一些优秀的成熟组件库会为我们提供良好的表单解决方案,但它终究需要与组件库一起使用。而并非只是提供表单的状态管理,并没有完全的与组件库解耦合。
+
+同时,当我们使用诸如 [Daisyui](https://daisyui.com/) 等此类的 CSS 组件时,它们是与状态完全解耦合的。我们需要自己为其维护状态。
+
+## Hook our form
+
+对于一个表单来说,提供的表单项越多,所需要的状态管理就越繁琐。不仅仅是状态管理,后续的表单验证才是一个表单的核心所在。
+
+React Hook Forms 对 TypeScript 支持良好,有了 TypeScript 我们就可以在开发时验证表单类型。而表单的数据类型也是后续封装通用组件较为繁琐的一个地方。
+
+
+
+React Hook Forms 在使用方面,使用了一个 `register` 函数代替了我们为每个表单项管理状态的步骤。从写法上就可以看出,这个函数返回了我们的表单所需要的属性,以及其状态。
+
+```tsx
+
+```
+
+在表单提交方面,`handleSubmit` 方法接受一个回调,其参数就是表单输入后的状态。
+
+```tsx
+const onSubmit = handleSubmit((data) => console.log(data));
+```
+
+表单验证通过后,就可以成功调用这个函数,以实现我们的表单提交。
+
+这是一段最基础的用法,没有表单验证提示,仅仅只是接受任何用户输入的数据。并且同样的组件也没有实现复用。
+
+## Input 组件
+
+封装一个可复用的 `Input` 组件可能是再简单不过的事情了,对于其参数类型,主要部分还是来自于 `HTMLInput` 。我们只需要个别定义的属性,再利用剩余参数将其全部赋值给真正的 `input`
+
+```tsx
+export type FormInputProps = {
+ label?: string | undefined;
+} & DetailedHTMLProps, HTMLInputElement>;
+```
+
+```tsx
+const Input = ({ name, label, ...rest }: FormInputProps) => {
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+};
+```
+
+用起来自然也是和常见的组件一样方便:
+
+```tsx
+
+
+
+
+
+
+
+```
+
+但是如果仅仅只是这样,我们的组件还不能与 React Hook Forms 一起工作。因为其核心部分 `register` 函数还无法传递给我们的 `Input` 组件。也就是说我们的组件现在还是不可控的,这时候再尝试提交就会发现无法获取其状态。
+
+
+
+当然我们不能简单的将 `register` 函数塞给 `Input` 组件,因为它还没有合适的签名。`register` 函数会根据表单的数据签名和不同的表单项来实现自己的签名。
+
+从 `register` 函数的签名中就可以看出,它接受一个泛型,该泛型就是对应的表单项类型。
+
+```tsx
+register: <"firstName">(name: "firstName", options?: ...)
+```
+
+也就是 `FormData` 中的 `firstName` :
+
+```tsx
+type FormData = {
+ firstName: string;
+ lastName: string;
+ favorite: Pet;
+};
+```
+
+没错,要想正确的给组件中的 `register` 函数签名,我们就得给我们的函数式组件上个泛型。
+
+## 泛型
+
+在考虑给组件添加一个泛型之前,需要先简单的了解下泛型是如何工作的。
+
+一个函数的泛型可以非常的简单,它代表了一个任意的类型值(当然也可以对其进行约束)。并根据指定的参数为泛型时,自动推断该类型值。
+
+```ts
+const logAndReturn = (target: T) => {
+ console.log(target);
+ return target;
+};
+
+// const logAndReturn: <42>(target: 42) => 42
+logAndReturn(42);
+// const logAndReturn: <"42">(target: "42") => "42"
+logAndReturn('42');
+```
+
+### 类型别名中的泛型
+
+类型别名中的泛型与函数不同的是,它需要手动传递一个函数的泛型值(或来自其他地方的泛型),并根据该泛型来决定其值。并且如果泛型有约束的话,还需要符合其约束。
+
+例如,我们有一个描述个人的类型别名:
+
+```ts
+type Person = {
+ name: string;
+ age: number;
+ favorite: T;
+};
+```
+
+而我们需要编写一个函数,根据其 `favorite` 来决定打印的值。函数大概长这样:
+
+```tsx
+const sayIt = (p: Person) => {
+ const type = typeof p.favorite;
+ switch (type) {
+ case 'string':
+ console.log(`My favorite word is: ${p.favorite}`);
+ return;
+ case 'number':
+ console.log(`My favorite number is: ${p.favorite}`);
+ return;
+ }
+};
+```
+
+当指定参数为 `p: Person` 时,就需要将函数的泛型传递给类型别名。且类型别名中的泛型约束在了 `` 之间,函数必须保证使其子类型。否则就会提示无法满足其类型。
+
+
+
+和参数类型,泛型也是向下兼容的,只要保证其类型是子类型即可。也就是说这样也是可以的 `const sayIt = (p: Person) => {}` 。数字 42 是 `number` 类型的子类型。
+
+随后在调用函数时,就能发现泛型给我们带来的作用了。
+
+
+
+
+
+### React 中的泛型
+
+我们的 React 函数组件也是一个函数,对于泛型的规则同样适用。
+
+来看一个简单的小组件,该组件可以以一个常见的对象类型 `Record` 来根据指定的 key 访问其值,并展示在 DOM 上。
+
+
+
+例如,这样的一个值:
+
+```tsx
+const testData = {
+ firstName: 'xfy',
+ lastName: 'xfyxfy',
+};
+```
+
+当传递其对应的 key 时,我们的子组件就会展示对应的属性。也就是 `data[key]` ,这是再简单不过的一个属性访问方式了。
+
+```tsx
+
+```
+
+但不仅如此,我们还希望我们的子组件能够根据已经存在的值,推断出我们能够传递的 key。
+
+
+
+这正是泛型的作用。
+
+首先,我们子组件的参数签名必然需要一个泛型。并且我们将泛型约束在为一个常见的对象 `Record`,且不在乎属性值具体是什么类型(unknown)。
+
+```tsx
+type Props> = {
+ name: keyof T;
+ data: T;
+};
+```
+
+这便是我们组件的参数具体的签名。还记得上述类型别名需要将函数的泛型传递给它吗?接下来就是要给函数式组件添加一个泛型,并将其传递给 `Props` 。
+
+我们的组件也是一个标准的函数,所以接下来就简单多了。只需要将泛型正确的约束,并传递给别名即可。
+
+```tsx
+const Child = >({
+ name,
+ data,
+}: Props) => {};
+```
+
+```tsx
+const Child = >({ name, data }: Props) => {
+ const [showName, setShowName] = useState();
+ const valid = () => {
+ console.log(data[name]);
+ setShowName(data[name]);
+ };
+
+ return (
+ <>
+ {name}
+
+
+ {JSON.stringify(showName)}
+ >
+ );
+};
+```
+
+## 带有泛型的 Input 组件
+
+`register` 函数对表单项的验证与上述较为类似,它也会根据表单项的 key 来决定传递对应的 name。为了满足 `register` 函数,可复用的 Input 组件就得需要一个泛型,用来接受不同的表单数据类型。
+
+React hook forms 为我们提前准备好了适用于 `register` 函数的类型别名 `UseFormRegister` ,它会接受一个泛型,该泛型就是我们的表单数据类型。
+
+所以 `register` 函数的签名看起来就像这样 `register?: UseFormRegister;` 这里的 `T` 就是我们的表单类型。但是我们还不知道传入当前组件中的表单类型是什么,所以我们的组件参数签名也需要一个泛型。
+
+所以这里我们的组件参数看起来是这样的:
+
+```tsx
+export type FormInputProps = {
+ name: Path;
+ label?: string | undefined;
+ rules?: RegisterOptions;
+ register?: UseFormRegister;
+} & DetailedHTMLProps, HTMLInputElement>;
+```
+
+值得注意的是,这里给 `input` 使用的 name 属性。因为 `register` 函数注册时使用的名称需要确保为表单类型的中的一个。所以这里需要使用 React hook forms 导出的 `Path` 类型,以配合 `register` 函数。
+
+这里就和上述泛型组件很相似了,接下来要做的就是将组件的泛型传递给参数签名:
+
+```tsx
+const Input = >({
+ name,
+ label,
+ ...rest
+}: FormInputProps) => {};
+```
+
+这里给组件的泛型小小的约束一下,我们希望传递过来的表单类型是一个普通的对象结构 `>` 。
+
+不仅如此,还不能忘了 `register` 函数还需要注册在 DOM 上。
+
+```tsx
+
+```
+
+得益于泛型的功劳,我们将 `register` 函数传递给 `Input` 组件时,我们的组件就知道了这次表单的类型。并且确定了 `name` 属性的类型。
+
+
+
+这是因为 `register` 函数本身的签名:`const register: UseFormRegister` 。这才使得我们的组件成功接受到了泛型。
+
+再添加一些 `rules` 以及验证未通过时的提示,这样一个可复用的 React hook form 组件就封装好了。
+
+```tsx
+
+```
+
+
diff --git a/pages/p/generic-component-encapsulate-reusable-component/react-generic/App.ts b/pages/p/generic-component-encapsulate-reusable-component/react-generic/App.ts
new file mode 100644
index 0000000..4224cbe
--- /dev/null
+++ b/pages/p/generic-component-encapsulate-reusable-component/react-generic/App.ts
@@ -0,0 +1,22 @@
+const app = `import "./styles.css";
+import Child from "./Child";
+
+const testData = {
+ name: "xfy",
+ age: 18
+};
+
+export default function App() {
+ return (
+
+
Hello CodeSandbox
+
Start editing to see some magic happen!
+
+
+
+
+
+ );
+}`;
+
+export default app;
diff --git a/pages/p/generic-component-encapsulate-reusable-component/react-generic/Child.ts b/pages/p/generic-component-encapsulate-reusable-component/react-generic/Child.ts
new file mode 100644
index 0000000..0e3de8a
--- /dev/null
+++ b/pages/p/generic-component-encapsulate-reusable-component/react-generic/Child.ts
@@ -0,0 +1,27 @@
+const child = `import { useState } from "react";
+
+type Props> = {
+ name: keyof T;
+ data: T;
+};
+
+const Child = >({ name, data }: Props) => {
+ const [showName, setShowName] = useState();
+ const valid = () => {
+ console.log(data[name]);
+ setShowName(data[name]);
+ };
+
+ return (
+ <>
+ {name}
+
+
+ {JSON.stringify(showName)}
+ >
+ );
+};
+
+export default Child;`;
+
+export default child;