how to write a react checkbox component

预期的 Checkbox 使用方法



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Checkbox from '@/components'
const { CheckboxItem } = Checkbox

export default () => {
const onChange = (values) => {
console.log(values)
}
return (
<Checkbox onChange={onChange}>
<CheckboxItem label="angular" />
<CheckboxItem label="react" />
<CheckboxItem label="vue" />
</Checkbox>
)
}


两种实现方式

通过 React.cloneElement

1
2
3
4
5
React.cloneElement(
element,
[props],
[...children]
)

Checkbox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Checkbox
import React from "react";
import CheckboxItem from "./CheckboxItem";
import { uid } from "../../utils";
import useCheckbox from "../../hooks/useCheckbox";

export interface CheckboxProps {
children: React.ReactNode | React.ReactNode[];
onChange: (val: unknown) => void;
className?: string;
}

const Checkbox = (props: CheckboxProps) => {
const { children, onChange, className } = props;
const [val, setVal] = useCheckbox([]);
const handleChange = (checked: boolean, value: string) => {
setVal(checked, value);
onChange(Array.from(val));
};
// 为每个子组件添加 onChange 回调函数
const childList = React.Children.map(children, (child: any) =>
React.cloneElement(child, { onChange: handleChange, key: uid() })
);
return <div className={className}>{childList}</div>;
};

Checkbox.CheckboxItem = CheckboxItem;

export default Checkbox;

CheckboxItem



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// CheckboxItem
import React from "react";
import { uid } from "../../utils";

export interface CheckboxItemProps {
onChange?: (checked: boolean, val: unknown) => void;
block?: boolean;
label?: string;
}

const CheckboxItem = (props: CheckboxItemProps) => {
const { onChange, block = false, label, ...rest } = props;
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (onChange) {
onChange(e.currentTarget.checked, label);
}
};
const uuid = uid();
const item = (
<>
<input {...rest} type="checkbox" onChange={handleChange} id={`${uuid}`} />
{label && <label htmlFor={`${uuid}`}>{label}</label>}
</>
);
if (block) {
return <div>{item}</div>;
}
return item;
};

export default CheckboxItem;


使用 Checkbox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import * as React from "react";
import "./styles.css";
import { Checkbox } from "./components";

const { CheckboxItem } = Checkbox;

export default function App() {
const onChange = (...args: unknown[]) => console.log.apply(null, args);
return (
<div className="App">
<h1 style={{ textAlign: "center" }}>Checkbox Component</h1>
<Checkbox onChange={onChange}>
<div>
<CheckboxItem label="angular" block />
</div>
<CheckboxItem label="react" block />
<CheckboxItem label="vue" block />
</Checkbox>
</div>
);
}

通过 React.createContext

1
2
const MyContext = React.createContext(defaultValue);
<MyContext.Provider value={/* some value */}>

CheckBox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { createContext } from "react";
import CheckboxItem from "./CheckboxItem";
import useCheckbox from "../../hooks/useCheckbox";

export interface CheckboxProps {
children: React.ReactNode | React.ReactNode[];
onChange: (val: unknown) => void;
className?: string;
}

export const CheckboxContext = createContext({
onContextChange(bool: boolean, val?: string): void {
console.log(val, bool);
}
});

const Checkbox = (props: CheckboxProps) => {
const { children, onChange, className } = props;
const [val, setVal] = useCheckbox([]);
const handleChange = (checked: boolean, value: string) => {
setVal(checked, value);
onChange(Array.from(val));
};
return (
<CheckboxContext.Provider value={{ onContextChange: handleChange }}>
<div className={className}>{children}</div>
</CheckboxContext.Provider>
);
};

Checkbox.CheckboxItem = CheckboxItem;

export default Checkbox;

CheckboxItem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React, { useContext } from "react";
import { uid } from "../../utils";
import { CheckboxContext } from "./index";

export interface CheckboxItemProps {
onChange?: (val: unknown, value: unknown) => void;
block?: boolean;
label?: string;
}

const CheckboxItem = (props: CheckboxItemProps) => {
const { onContextChange } = useContext(CheckboxContext);
const { onChange, block = false, label, ...rest } = props;
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (onChange) {
onChange(e.currentTarget.checked, label);
}
if (onContextChange) {
onContextChange(e.currentTarget.checked, label);
}
};
const uuid = uid();
const item = (
<>
<input {...rest} type="checkbox" onChange={handleChange} id={`${uuid}`} />
{label && <label htmlFor={`${uuid}`}>{label}</label>}
</>
);
if (block) {
return <div>{item}</div>;
}
return item;
};

export default CheckboxItem;

总结

使用 React.createContext 的实现方式, 可以支持嵌套的组件结构, 非常灵活。
通过 React.cloneElement 的实现方式不支持组件嵌套的结构, 可以考虑使用 children 的方式, 使组件更灵活;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// CheckboxItem
import React from "react";
import { uid } from "../../utils";

export interface CheckboxItemProps {
onChange?: (checked: boolean, val: unknown) => void;
block?: boolean;
label?: string;
children? React.ReactNode
}

const CheckboxItem = (props: CheckboxItemProps) => {
const { onChange, block = false, label, children, ...rest } = props;
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (onChange) {
onChange(e.currentTarget.checked, label);
}
};
const uuid = uid();
const item = (
<>
<input {...rest} type="checkbox" onChange={handleChange} id={`${uuid}`} />
{label && <label htmlFor={`${uuid}`}>{children || label}</label>}
</>
);
if (block) {
return <div>{item}</div>;
}
return item;
};

export default CheckboxItem;

一个人活在世上就是为了忍受一切摧残,想通了这点,任何事情都能泰然处之。 ——王小波