MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 使用 Refs 操作列表元素

2021-11-272.2k 阅读

理解 React 中的 Refs

在 React 开发中,Refs 提供了一种访问 DOM 元素或在 render 方法中创建的 React 元素的方式。通常情况下,在 React 中数据是单向流动的,通过 props 将数据从父组件传递到子组件。然而,有些场景下我们需要直接操作 DOM 元素或者获取子组件的实例,这时候 Refs 就派上用场了。

Refs 的基本使用

React 提供了 React.createRef() 方法来创建 Refs。一个典型的例子是获取输入框的焦点:

import React, { Component } from 'react';

class InputFocus extends Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }

  componentDidMount() {
    this.inputRef.current.focus();
  }

  render() {
    return <input type="text" ref={this.inputRef} />;
  }
}

export default InputFocus;

在上述代码中,我们首先通过 React.createRef() 创建了一个 inputRef。在 componentDidMount 生命周期方法中,当组件挂载到 DOM 后,我们通过 this.inputRef.current 访问到实际的 DOM 元素,并调用 focus 方法使其获得焦点。

Refs 在函数组件中的使用

在函数组件中,由于没有实例,不能像类组件那样在构造函数中创建 Refs。React 提供了 useRef Hook 来解决这个问题。例如:

import React, { useRef, useEffect } from'react';

const InputFocusFunction = () => {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <input type="text" ref={inputRef} />;
};

export default InputFocusFunction;

这里通过 useRef Hook 创建了 inputRef,在 useEffect Hook 中,当组件挂载后(依赖数组为空),我们调用 inputRef.current.focus() 使输入框获得焦点。

在列表中使用 Refs

当涉及到操作列表元素时,Refs 的使用变得稍微复杂一些,但同样非常有用。例如,我们有一个列表,每个列表项都有一个按钮,点击按钮后需要获取该列表项的 DOM 元素。

为列表项创建 Refs 数组

假设我们有一个简单的待办事项列表:

import React, { Component } from'react';

class TodoList extends Component {
  constructor(props) {
    super(props);
    this.todoRefs = [];
    this.state = {
      todos: ['Learn React', 'Build a project', 'Deploy app']
    };
  }

  handleClick = (index) => {
    console.log(this.todoRefs[index].current);
  }

  render() {
    return (
      <ul>
        {this.state.todos.map((todo, index) => (
          <li key={index} ref={(el) => this.todoRefs[index] = el}>
            {todo}
            <button onClick={() => this.handleClick(index)}>Click me</button>
          </li>
        ))}
      </ul>
    );
  }
}

export default TodoList;

在上述代码中,我们在构造函数中初始化了一个空数组 todoRefs。在 map 方法中,通过 ref={(el) => this.todoRefs[index] = el} 为每个列表项创建 Refs,并将其存储在 todoRefs 数组中。当点击按钮时,handleClick 方法通过传入的 index 来访问对应的列表项的 DOM 元素。

使用 useRef 在函数组件列表中

在函数组件中实现类似功能,我们可以这样做:

import React, { useRef, useState } from'react';

const TodoListFunction = () => {
  const todoRefs = useRef([]);
  const [todos, setTodos] = useState(['Learn React', 'Build a project', 'Deploy app']);

  const handleClick = (index) => {
    console.log(todoRefs.current[index].current);
  }

  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={index} ref={(el) => {
          if (!todoRefs.current[index]) {
            todoRefs.current[index] = React.createRef();
          }
          todoRefs.current[index].current = el;
        }}>
          {todo}
          <button onClick={() => handleClick(index)}>Click me</button>
        </li>
      ))}
    </ul>
  );
}

export default TodoListFunction;

这里使用 useRef 创建了 todoRefs,由于 useRef 返回的是一个可变的 ref 对象,我们在 map 中需要为每个列表项创建并赋值 Refs。当按钮点击时,handleClick 方法通过 index 访问对应的列表项的 DOM 元素。

操作列表元素的样式和属性

通过 Refs 获取到列表项的 DOM 元素后,我们可以对其样式和属性进行操作。

改变列表项的样式

例如,我们点击按钮后改变列表项的背景颜色:

import React, { Component } from'react';

class StyleChangeList extends Component {
  constructor(props) {
    super(props);
    this.itemRefs = [];
    this.state = {
      items: ['Item 1', 'Item 2', 'Item 3']
    };
  }

  handleClick = (index) => {
    this.itemRefs[index].current.style.backgroundColor = 'lightblue';
  }

  render() {
    return (
      <ul>
        {this.state.items.map((item, index) => (
          <li key={index} ref={(el) => this.itemRefs[index] = el}>
            {item}
            <button onClick={() => this.handleClick(index)}>Change Style</button>
          </li>
        ))}
      </ul>
    );
  }
}

export default StyleChangeList;

在上述代码中,handleClick 方法通过 Refs 获取到对应的列表项 DOM 元素,并修改其 backgroundColor 样式属性。

修改列表项的属性

假设我们的列表项是图片,点击按钮后修改图片的 src 属性:

import React, { Component } from'react';

class ImageList extends Component {
  constructor(props) {
    super(props);
    this.imageRefs = [];
    this.state = {
      images: [
        { id: 1, src: 'image1.jpg' },
        { id: 2, src: 'image2.jpg' },
        { id: 3, src: 'image3.jpg' }
      ]
    };
  }

  handleClick = (index) => {
    this.imageRefs[index].current.src = 'newImage.jpg';
  }

  render() {
    return (
      <ul>
        {this.state.images.map((image, index) => (
          <li key={index}>
            <img ref={(el) => this.imageRefs[index] = el} src={image.src} alt={`Image ${image.id}`} />
            <button onClick={() => this.handleClick(index)}>Change Image</button>
          </li>
        ))}
      </ul>
    );
  }
}

export default ImageList;

这里 handleClick 方法通过 Refs 访问到图片的 DOM 元素,并修改其 src 属性。

处理列表元素的滚动和位置

Refs 还可以用于处理列表元素的滚动和位置相关的操作。

滚动到特定列表项

假设我们有一个较长的列表,希望点击按钮后滚动到特定的列表项:

import React, { Component } from'react';

class ScrollToList extends Component {
  constructor(props) {
    super(props);
    this.itemRefs = [];
    this.state = {
      items: Array.from({ length: 50 }, (_, i) => `Item ${i + 1}`)
    };
  }

  handleClick = (index) => {
    this.itemRefs[index].current.scrollIntoView({ behavior:'smooth' });
  }

  render() {
    return (
      <div>
        <ul>
          {this.state.items.map((item, index) => (
            <li key={index} ref={(el) => this.itemRefs[index] = el}>
              {item}
              {index === 20 && <button onClick={() => this.handleClick(index)}>Scroll to this item</button>}
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

export default ScrollToList;

在上述代码中,handleClick 方法通过 scrollIntoView 方法将指定的列表项滚动到视图中,behavior:'smooth' 使得滚动效果更加平滑。

获取列表项的位置

我们可以获取列表项在页面中的位置信息。例如,获取列表项距离页面顶部的距离:

import React, { Component } from'react';

class GetPositionList extends Component {
  constructor(props) {
    super(props);
    this.itemRefs = [];
    this.state = {
      items: ['Item 1', 'Item 2', 'Item 3']
    };
  }

  handleClick = (index) => {
    const rect = this.itemRefs[index].current.getBoundingClientRect();
    const top = rect.top + window.pageYOffset;
    console.log(`Item ${index + 1} is at position ${top} from the top of the page`);
  }

  render() {
    return (
      <ul>
        {this.state.items.map((item, index) => (
          <li key={index} ref={(el) => this.itemRefs[index] = el}>
            {item}
            <button onClick={() => this.handleClick(index)}>Get Position</button>
          </li>
        ))}
      </ul>
    );
  }
}

export default GetPositionList;

这里 handleClick 方法通过 getBoundingClientRect 获取列表项相对于视口的位置,再加上 window.pageYOffset 得到相对于页面顶部的位置。

注意事项和性能考量

在使用 Refs 操作列表元素时,有一些注意事项和性能方面的考量。

Refs 与 React 数据流

React 提倡单向数据流,过多地使用 Refs 来直接操作 DOM 或组件实例可能会破坏这种数据流模式,使代码难以维护和调试。因此,只有在确实需要直接操作 DOM 元素或获取子组件实例的情况下才使用 Refs。

性能影响

频繁地通过 Refs 修改 DOM 元素的样式或属性可能会导致性能问题。因为这会触发浏览器的重排和重绘。尽量减少不必要的 DOM 操作,例如,可以通过 CSS 过渡和动画来实现一些效果,而不是直接修改样式属性。

内存泄漏

如果在组件卸载时没有正确清理 Refs,可能会导致内存泄漏。在类组件中,可以在 componentWillUnmount 生命周期方法中进行清理。例如:

import React, { Component } from'react';

class MemoryLeakExample extends Component {
  constructor(props) {
    super(props);
    this.itemRef = React.createRef();
  }

  componentWillUnmount() {
    this.itemRef.current = null;
  }

  render() {
    return <div ref={this.itemRef}>Some content</div>;
  }
}

export default MemoryLeakExample;

在函数组件中,虽然没有类似 componentWillUnmount 的生命周期方法,但可以使用 useEffect 的返回函数来进行清理:

import React, { useRef, useEffect } from'react';

const MemoryLeakFunction = () => {
  const itemRef = useRef(null);

  useEffect(() => {
    return () => {
      itemRef.current = null;
    };
  }, []);

  return <div ref={itemRef}>Some content</div>;
}

export default MemoryLeakFunction;

这样在组件卸载时,将 ref.current 设置为 null,避免潜在的内存泄漏。

结合其他 React 特性使用 Refs

在实际开发中,我们通常会结合 React 的其他特性与 Refs 一起使用。

Refs 与 Context

假设我们有一个应用,通过 Context 传递一些全局状态,同时需要在列表元素中使用 Refs 进行操作。例如,我们有一个主题切换的应用,列表项的样式根据主题变化,并且可以通过 Refs 进行一些额外的操作。

首先,创建 Context:

import React from'react';

const ThemeContext = React.createContext();

export default ThemeContext;

然后,创建一个主题切换组件:

import React, { useState } from'react';
import ThemeContext from './ThemeContext';

const ThemeToggle = () => {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(theme === 'light'? 'dark' : 'light');
  }

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </ThemeContext.Provider>
  );
}

export default ThemeToggle;

最后,在列表组件中使用 Context 和 Refs:

import React, { Component } from'react';
import ThemeContext from './ThemeContext';

class ThemeList extends Component {
  constructor(props) {
    super(props);
    this.itemRefs = [];
    this.state = {
      items: ['Item 1', 'Item 2', 'Item 3']
    };
  }

  handleClick = (index) => {
    console.log(this.itemRefs[index].current);
  }

  render() {
    return (
      <ThemeContext.Consumer>
        {({ theme }) => (
          <ul>
            {this.state.items.map((item, index) => (
              <li key={index} ref={(el) => this.itemRefs[index] = el} style={{ color: theme === 'light'? 'black' : 'white' }}>
                {item}
                <button onClick={() => this.handleClick(index)}>Click me</button>
              </li>
            ))}
          </ul>
        )}
      </ThemeContext.Consumer>
    );
  }
}

export default ThemeList;

在上述代码中,我们通过 Context 获取主题信息,根据主题设置列表项的颜色,同时通过 Refs 进行按钮点击后的操作。

Refs 与 Redux

当使用 Redux 管理状态时,我们可以结合 Refs 来触发 Redux 的 action。例如,假设我们有一个待办事项列表,点击列表项的按钮可以将该项标记为已完成,并且这个操作需要更新 Redux 的状态。

首先,定义 Redux 的 action 和 reducer:

// actions.js
const MARK_TODO_COMPLETE = 'MARK_TODO_COMPLETE';

const markTodoComplete = (index) => ({
  type: MARK_TODO_COMPLETE,
  index
});

export { markTodoComplete };

// reducer.js
const initialState = {
  todos: ['Learn React', 'Build a project', 'Deploy app'],
  completed: []
};

const todoReducer = (state = initialState, action) => {
  switch (action.type) {
    case MARK_TODO_COMPLETE:
      return {
      ...state,
        completed: [...state.completed, action.index]
      };
    default:
      return state;
  }
};

export default todoReducer;

然后,在 React 组件中使用 Redux 和 Refs:

import React, { Component } from'react';
import { connect } from'react-redux';
import { markTodoComplete } from './actions';

class ReduxTodoList extends Component {
  constructor(props) {
    super(props);
    this.todoRefs = [];
  }

  handleClick = (index) => {
    this.props.markTodoComplete(index);
    console.log(this.todoRefs[index].current);
  }

  render() {
    return (
      <ul>
        {this.props.todos.map((todo, index) => (
          <li key={index} ref={(el) => this.todoRefs[index] = el}>
            {todo}
            <button onClick={() => this.handleClick(index)}>Mark Complete</button>
          </li>
        ))}
      </ul>
    );
  }
}

const mapStateToProps = (state) => ({
  todos: state.todos
});

const mapDispatchToProps = {
  markTodoComplete
};

export default connect(mapStateToProps, mapDispatchToProps)(ReduxTodoList);

在上述代码中,当点击按钮时,通过 Refs 可以进行一些操作,同时触发 Redux 的 markTodoComplete action 来更新状态。

实践案例:可编辑列表

下面通过一个可编辑列表的案例,进一步展示如何在实际项目中使用 Refs 操作列表元素。

需求分析

我们需要创建一个列表,每个列表项可以进入编辑模式,在编辑模式下,用户可以修改文本内容,点击保存按钮后,更新列表数据。

实现步骤

  1. 创建基本列表结构
import React, { Component } from'react';

class EditableList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      items: ['Item 1', 'Item 2', 'Item 3'],
      editingIndex: null
    };
  }

  render() {
    return (
      <ul>
        {this.state.items.map((item, index) => (
          <li key={index}>
            {item}
          </li>
        ))}
      </ul>
    );
  }
}

export default EditableList;
  1. 添加编辑功能
import React, { Component } from'react';

class EditableList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      items: ['Item 1', 'Item 2', 'Item 3'],
      editingIndex: null
    };
    this.inputRefs = [];
  }

  handleEdit = (index) => {
    this.setState({ editingIndex: index });
    setTimeout(() => {
      this.inputRefs[index].current.focus();
    }, 0);
  }

  handleSave = (index) => {
    const newText = this.inputRefs[index].current.value;
    const newItems = [...this.state.items];
    newItems[index] = newText;
    this.setState({ items: newItems, editingIndex: null });
  }

  render() {
    return (
      <ul>
        {this.state.items.map((item, index) => (
          <li key={index}>
            {this.state.editingIndex === index? (
              <input type="text" ref={(el) => this.inputRefs[index] = el} defaultValue={item} />
            ) : (
              item
            )}
            {this.state.editingIndex === index? (
              <button onClick={() => this.handleSave(index)}>Save</button>
            ) : (
              <button onClick={() => this.handleEdit(index)}>Edit</button>
            )}
          </li>
        ))}
      </ul>
    );
  }
}

export default EditableList;

在上述代码中,我们通过 handleEdit 方法进入编辑模式,并通过 setTimeout 在 DOM 更新后使输入框获得焦点。handleSave 方法通过 Refs 获取输入框的值,更新列表数据并退出编辑模式。

通过这个案例,我们可以看到如何在实际场景中巧妙地运用 Refs 来实现复杂的列表操作功能。

在 React 开发中,掌握如何使用 Refs 操作列表元素是一项重要的技能。它能够帮助我们实现一些通过常规数据流难以达成的功能,但同时也需要我们谨慎使用,避免破坏 React 的设计原则和影响性能。通过不断的实践和理解,我们可以更好地在项目中运用这一特性,提升用户体验和开发效率。