组件的样式变化&可控/不可控组件

452 阅读6分钟

解决组件样式变化的问题

之前在使用Radix组件库时,遇到了一个关于样式动态变化的问题。本文将分享我所遇到的问题、解决方法、尝试过程以及最终的结果。

遇到的问题

在这个项目中,我们使用了一个主打 UI 形式变化的组件库。我们的目标是实现点击元素后,从灰色变成蓝色的效果。这种样式变化并不是组件状态的改变,而是纯粹的视觉效果。因此,我们需要找到一种方法,使得点击元素后能够直接改变其样式,而不涉及到组件状态的管理。

截屏2024-04-24 12.38.56.png

解决方法

我们选择使用 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>,从而影响内部的 RadioCheckIconRadioSelectIcon

Key Value:

  1. 使用 asChild 属性:在 Item 中使用 asChild,可以让被选中的元素直接传递给子组件。当需要嵌套 button 时,必须使用 asChild,以确保正确的样式传递。
  2. 样式更新:通过 Indicator 来包裹需要更新样式的组件,确保在元素被选中后,样式能够及时更新。

一些样式尝试细节

在实现过程中,我尝试了多种方法来覆盖选中状态下的样式。具体尝试包括:

  1. 加className样式:我在ItemIconIndicatorSelectIcon上分别添加了样式,但都未能成功覆盖原有样式。

  2. 使用包裹:我尝试用Icon包裹Indicator,但这导致复选框消失。

  3. 调整定位:最终,我决定使用绝对定位来解决问题:

    • Item上添加className="relative"
    • Indicator上添加className="absolute top-0"

通过这种方式,样式得以正确显示。

用asChild,在item中,让它被选中后直接传递给子类【我要添加样式的组件】

asChild 属性通常用于将父组件的某些特性传递给子组件,尤其是在需要嵌套按钮或其他交互元素时,以确保样式和行为的正确传递。

可控组件 和 不可控组件

问题描述

在一个项目中,我们使用了 Radio Group 来显示选项。最初的实现中,点击 Radio Group 后,新出现的两个选项中,第一个选项会被默认选中。然而,当再次点击其他选项时,第一个选项仍然保持选中状态。这显然是一个 bug。

问题根源

经过分析,我们发现问题出在对组件的控制方式上。在初始实现中,我们将 defaultValue 用于 Radio Group,使其成为不可控组件。然而,实际上我们需要一个可控组件来动态响应用户交互。

可控组件与不可控组件

在理解这个问题时,我们需要区分可控组件和不可控组件:

  • 可控组件:需要与事件绑定,通常通过 value 和 onValueChange 来管理状态。
  • 不可控组件:初始值固定,通常通过 defaultValue 设置。

在需要根据不同状态改变变量的情况下,使用可控组件是更好的选择。

解决方案

我们将 Radio Group 改为可控组件,使用 valueonValueChange 来管理选中状态。然而,这带来了一个新的问题:类型不匹配。我们的类型定义如下:

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 元素的值,无需维护状态,适用于不需要频繁更新或验证的场景。这种方式简化了代码,同时允许在需要时获取用户输入的数据。