React面试基础考点:模块化引入CSS和forwardRef传递ref

89 阅读6分钟

React面试基础考点:模块化引入CSS和forwardRef传递ref

在React开发中,我们经常会遇到两个看似简单却容易踩坑的问题:样式冲突组件ref传递。今天我们就来深入剖析这两个面试高频考点,看看它们背后的原理和解决方案!

一、CSS模块化:告别样式冲突的烦恼

1.1 为什么需要CSS模块化?

想象一下,你加入了一个新公司,负责开发一个Button组件。但当你兴冲冲地写好了代码,却发现公司里已经有一个类似的按钮组件了。于是,你决定同时使用这两个组件:一个叫Button(之前的),一个叫AnotherButton(你写的)。

根组件App.js如下:

import { useState } from 'react'
import './App.css'
import Button from './components/Button'
import AnotherButton from './components/AnotherButton'

function App() {
  return (
    <>
      <Button />
      <AnotherButton />
    </>
  )
}

export default App

你的AnotherButton组件代码:

import './another-button.css'

const AnotherButton = () => {
  return (
    <button className='another-button'>Another-Button</button>
  )
}
export default AnotherButton

你的样式文件another-button.css

.another-button{
  background-color: green; 
  color:white;
  padding: 10px 20px;
}
.button{
  background-color: green; 
  color:white;
  padding: 10px 20px;
}

而之前同事写的Button组件:

import './button.css'
const Button = () => {
  return (
    <button className='button'>Button</button>
  )
}
export default Button

样式文件button.css

.button{
  background-color: red; 
  color:white;
  padding: 10px 20px;
}
.another-button{
  background-color: red; 
  color:white;
  padding: 10px 20px;
}

你期望的效果是:Button显示红色,AnotherButton显示绿色。然而,现实却给了你当头一棒——两个按钮都变成了绿色!😱

两个按钮都是绿色

为什么会这样?

打开开发者工具一看,真相大白:后引入的样式文件覆盖了前面的样式!

样式覆盖

因为AnotherButtonButton之后引入,所以another-button.css中的样式覆盖了button.css中的样式。更糟糕的是,两个样式文件中都定义了.button.another-button,这就导致了样式冲突。

这时候你可能会想:我只要给类名取不同的名字不就行了?但现实是,随着项目越来越大,组件越来越多,类名冲突几乎是不可避免的。这时候,CSS模块化就闪亮登场了!✨

1.2 如何实现CSS模块化?

CSS模块化的核心思想是:将CSS文件作用域限制在单个组件内。这样,即使不同组件使用了相同的类名,它们也不会互相影响。

具体怎么做呢?我们只需要三步:

  1. 将样式文件后缀改为.module.css(例如button.module.css
  2. 在组件中通过import styles from './xxx.module.css'引入
  3. 在JSX中使用styles.className来设置类名

让我们来改造一下之前的代码:

Button组件:

import styles from './button.module.css'

const Button = () => {
  return (
    <button className={styles.button}>Button</button>
  )
}
export default Button

AnotherButton组件:

import styles from './another-button.module.css'

const AnotherButton = () => {
  return (
    <button className={styles.button}>Another-Button</button>
  )
}
export default AnotherButton

现在再来看效果:

两个按钮颜色不同

完美!一个红一个绿,互不干扰。🎉

背后的魔法是什么?

打开开发者工具,你会发现类名变成了这样:

<button class="_button_1hsv7_1">Button</button>
<button class="_button_1hsv8_1">Another-Button</button>

原来,CSS模块化会自动为类名生成唯一的哈希值!这样,即使不同组件都用了.button这个类名,最终在DOM中也会变成不同的哈希字符串,彻底解决了样式冲突问题。

唯一类名哈希

二、forwardRef:让ref穿越组件边界

解决了样式问题,我们再来聊聊React中的另一个常见需求:在父组件中直接操作子组件的DOM节点

2.1 为什么需要forwardRef?

假设我们有一个Guang组件,里面有一个输入框。我们希望在父组件App中,能够直接聚焦到这个输入框。听起来很简单,用ref不就行了?

但当你尝试这样做时,却遇到了问题:

function Guang(props) {
  return (
    <div>
      <input type="text" />
    </div>
  )
}

function App() {
  const ref = useRef(null)
  
  useEffect(() => {
    ref.current.focus() // 报错!ref.current为null
  }, [])
  
  return (
    <div className="App">
      <Guang ref={ref} />
    </div>
  )
}

运行后控制台会报错:ref.currentnull。为什么?

因为默认情况下,函数组件不能直接接收ref属性!当你把ref传给函数组件时,它并不会自动传递到内部的DOM节点上。

这时候你可能会想:那我用类组件不就行了?或者把函数组件改成forwardRef?Bingo!💡

2.2 使用forwardRef传递ref

forwardRef是React提供的一个高阶函数,它能够将ref属性"转发"到子组件的DOM元素上。它的用法非常简单:

import { 
  useRef,
  useEffect,
  forwardRef
} from 'react'

import './App.css'

// 使用forwardRef包装函数组件
const Guang = forwardRef((props, ref) => {
  return (
    <div>
      <input type="text" ref={ref} />
    </div>
  )
})

function App() {
  const ref = useRef(null)
  
  useEffect(() => {
    ref.current?.focus() // 安全调用,避免null错误
  }, [])
  
  return (
    <div className="App">
      <Guang ref={ref} />
    </div>
  )
}

export default App

让我们拆解一下这个过程:

  1. 创建ref:在父组件中使用useRef创建ref对象
  2. 转发ref:使用forwardRef包装子组件,使其能够接收ref参数
  3. 绑定ref:在子组件中将ref绑定到目标DOM元素上
  4. 使用ref:在父组件中通过ref.current操作子组件的DOM

小提示:使用可选链操作符?.可以避免因ref.current为null而导致的报错,是个好习惯哦!

2.3 forwardRef的工作原理

forwardRef实际上是一个高阶组件(HOC),它接收一个渲染函数作为参数,并返回一个新的组件。这个新组件能够接收ref属性,并将其传递给内部的组件。

简化版的实现原理:

function forwardRef(render) {
  return function ForwardRef(props) {
    // 从props中提取ref
    const {ref, ...otherProps} = props;
    // 调用渲染函数,传递props和ref
    return render(otherProps, ref);
  }
}

在实际项目中,forwardRef常用于:

  1. 表单组件中自动聚焦输入框
  2. 测量子组件的DOM尺寸
  3. 触发子组件的动画效果
  4. 集成第三方DOM库

三、面试考点总结

3.1 CSS模块化考点

考点答案
为什么需要CSS模块化避免全局样式冲突,特别是在大型项目和团队协作中
如何实现CSS模块化使用.module.css后缀,通过import styles导入,className={styles.className}使用
模块化的原理构建工具(如Webpack)会自动将类名转换为唯一哈希值
模块化的优点作用域隔离、避免命名冲突、提高可维护性

3.2 forwardRef考点

考点答案
为什么需要forwardRef函数组件默认不能直接接收ref属性
forwardRef的作用将ref转发到子组件内部的DOM元素或类组件实例
forwardRef的用法const MyComponent = forwardRef((props, ref) => {...})
forwardRef的应用场景表单聚焦、测量DOM尺寸、集成第三方库等

四、实战技巧与最佳实践

4.1 CSS模块化进阶技巧

  1. 组合类名:使用composes组合多个类名

    .base {
      padding: 10px 20px;
      border-radius: 4px;
    }
    
    .primary {
      composes: base;
      background-color: blue;
      color: white;
    }
    
  2. 全局样式:使用:global包裹全局样式

    :global(.ant-btn) {
      margin-right: 10px;
    }
    
  3. 变量共享:在JS和CSS之间共享变量

    /* variables.module.css */
    :export {
      primaryColor: #1890ff;
      borderRadius: 4px;
    }
    
    import variables from './variables.module.css'
    console.log(variables.primaryColor) // '#1890ff'
    

4.2 forwardRef进阶用法

  1. 转发ref到类组件

    class MyInput extends React.Component {
      focus() {
        this.inputRef.focus()
      }
      
      render() {
        return <input ref={el => this.inputRef = el} />
      }
    }
    
    export default forwardRef((props, ref) => (
      <MyInput {...props} forwardedRef={ref} />
    ))
    
  2. 配合useImperativeHandle暴露特定方法

    const FancyInput = forwardRef((props, ref) => {
      const inputRef = useRef();
      
      useImperativeHandle(ref, () => ({
        focus: () => {
          inputRef.current.focus();
        },
        scrollIntoView: () => {
          inputRef.current.scrollIntoView();
        }
      }));
      
      return <input ref={inputRef} />;
    });
    

五、总结

在React开发中,CSS模块化forwardRef是两个看似简单却非常重要的概念:

  1. CSS模块化解决了全局样式污染问题,通过唯一的哈希类名确保样式作用域隔离
  2. forwardRef解决了函数组件无法直接接收ref的问题,实现了ref的向下传递

掌握这两个知识点,不仅能让你在面试中脱颖而出,更能让你在实际开发中避免很多"坑"。

最后送大家一句话:"好的代码不是没有坑,而是知道如何优雅地避开坑" 🚀

思考题:如果要在类组件中使用forwardRef,应该如何实现?欢迎在评论区分享你的答案!