解决组件样式变化的问题
之前在使用Radix组件库时,遇到了一个关于样式动态变化的问题。本文将分享我所遇到的问题、解决方法、尝试过程以及最终的结果。
遇到的问题
在这个项目中,我们使用了一个主打 UI 形式变化的组件库。我们的目标是实现点击元素后,从灰色变成蓝色的效果。这种样式变化并不是组件状态的改变,而是纯粹的视觉效果。因此,我们需要找到一种方法,使得点击元素后能够直接改变其样式,而不涉及到组件状态的管理。
解决方法
我们选择使用 HTML 的 <label> 标签配合组件库中的特性来实现这一目标。在这个组件库中,组件结构可以被视为一个接一个浮起来的盒子。我们将根节点设立为 <label>,并利用 for 属性来控制对应的 id。这种方法允许我们在元素被选中时,直接改变其样式。
在具体实现中,我们使用了以下结构:
- Root(根) :作为整个组件的容器。
- Item(每一栏的内容) :包裹需要添加样式的组件。
- Indicator(指示器) :包裹需要更新样式的组件。
在组件库中,可以通过以下代码实现:
<RadioGroupPrimitive.Item value={value} id={value} asChild>
<div>
<RadioCheckIcon className="text-slate-12 size-8"/>
<RadioGroupPrimitive.Indicator>
<RadioSelectIcon className="text-slate-12"/>
</RadioGroupPrimitive.Indicator>
</div>
</RadioGroupPrimitive.Item>
在这个示例中,asChild 使得 RadioGroupPrimitive.Item 的行为和样式直接应用于其子元素 <div>,从而影响内部的 RadioCheckIcon 和 RadioSelectIcon。
Key Value:
- 使用
asChild属性:在Item中使用asChild,可以让被选中的元素直接传递给子组件。当需要嵌套button时,必须使用asChild,以确保正确的样式传递。 - 样式更新:通过
Indicator来包裹需要更新样式的组件,确保在元素被选中后,样式能够及时更新。
一些样式尝试细节
在实现过程中,我尝试了多种方法来覆盖选中状态下的样式。具体尝试包括:
-
加className样式:我在
Item、Icon、Indicator和SelectIcon上分别添加了样式,但都未能成功覆盖原有样式。 -
使用包裹:我尝试用
Icon包裹Indicator,但这导致复选框消失。 -
调整定位:最终,我决定使用绝对定位来解决问题:
- 在
Item上添加className="relative"。 - 在
Indicator上添加className="absolute top-0"。
- 在
通过这种方式,样式得以正确显示。
用asChild,在item中,让它被选中后直接传递给子类【我要添加样式的组件】
asChild 属性通常用于将父组件的某些特性传递给子组件,尤其是在需要嵌套按钮或其他交互元素时,以确保样式和行为的正确传递。
可控组件 和 不可控组件
问题描述
在一个项目中,我们使用了 Radio Group 来显示选项。最初的实现中,点击 Radio Group 后,新出现的两个选项中,第一个选项会被默认选中。然而,当再次点击其他选项时,第一个选项仍然保持选中状态。这显然是一个 bug。
问题根源
经过分析,我们发现问题出在对组件的控制方式上。在初始实现中,我们将 defaultValue 用于 Radio Group,使其成为不可控组件。然而,实际上我们需要一个可控组件来动态响应用户交互。
可控组件与不可控组件
在理解这个问题时,我们需要区分可控组件和不可控组件:
- 可控组件:需要与事件绑定,通常通过
value和onValueChange来管理状态。 - 不可控组件:初始值固定,通常通过
defaultValue设置。
在需要根据不同状态改变变量的情况下,使用可控组件是更好的选择。
解决方案
我们将 Radio Group 改为可控组件,使用 value 和 onValueChange 来管理选中状态。然而,这带来了一个新的问题:类型不匹配。我们的类型定义如下:
type Space = "meetingRoom" | "privateSpace";
但 onValueChange 期望接收一个 string 类型。为了解决这个问题,我们引入了 zod 进行类型检查,并封装了 spaceSchema:
import { z } from "zod";
const spaceSchema = z.enum(["meetingRoom", "privateSpace"]);
onValueChange={(value) => {
setValue(spaceSchema.parse(value));
}}
这种方法不仅解决了类型问题,还提高了代码的可读性和维护性。我们还可以在使用 value 时进行严格的类型检查:
value={spaceSchema.enum.publicArea}
代码优化与封装
为了避免代码冗余,我们对 Radio Group 进行了封装和复用。通过创建一个自定义组件 HomeScreenRadioGroup,我们可以轻松地管理和使用不同的选项:
const WrappedHomeScreenRadioGroup = ({ children }) => {
return (
<HomeScreenRadioGroup
value={value}
onValueChange={(value) => {
setValue(spaceSchema.parse(value));
}}
>
{children}
</HomeScreenRadioGroup>
);
};
这样,我们可以在代码中简洁地使用:
<WrappedHomeScreenRadioGroup>
{PublicAreaItem}
{MeetingRoomItem}
</WrappedHomeScreenRadioGroup>
总结
在 HTML 中,表单元素通常会自行管理状态,并根据用户输入自动更新其 UI,这一过程不受程序的直接控制。然而,在 React 中,我们可以通过将组件的状态(state)与表单元素的值关联起来,并使用 onChange 事件结合 setState() 方法来更新状态,从而实现对用户输入过程的控制。通过这种方式管理值的表单输入元素被称为受控组件。
对于受控组件,我们需要为每个状态更新(例如this.state.username)编写一个事件处理程序(例如this.setState({ username: e.target.value }))。
如果,我们仅仅是想要获取某个表单元素的值,而不关心它是如何改变的。
当在输入框中输入内容并点击提交按钮时,我们可以通过 this.inputRef 直接获取 input 元素的 DOM 属性信息,包括用户输入的值。这种方式使我们无需像使用受控组件那样,为每个表单元素单独维护一个状态。此外,我们可以使用 defaultValue 属性来设置表单元素的默认值。
这种方法的表单元素被称为不受控组件。不受控组件适用于那些不需要频繁更新或验证的场景,因为它们减少了状态管理的复杂性,简化了代码,同时仍然允许我们在需要时访问用户输入的数据。
在面试中可以这样简短地解释:
在 React 中,受控组件通过将状态与表单元素的值绑定,并使用 onChange 事件更新状态,从而精确控制用户输入。这需要为每个状态更新编写事件处理程序。
不受控组件则通过引用直接访问 DOM 元素的值,无需维护状态,适用于不需要频繁更新或验证的场景。这种方式简化了代码,同时允许在需要时获取用户输入的数据。