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

React 如何通过 Props 传递函数

2021-06-123.8k 阅读

React 如何通过 Props 传递函数

一、Props 基础概念回顾

在 React 中,Props(Properties 的缩写)是一种将数据从父组件传递到子组件的方式。React 组件就像一个黑盒子,它接收外部传入的数据(props),并根据这些数据渲染出对应的 UI。例如,我们有一个 Button 组件,可能会通过 props 传递按钮的文本:

// 父组件
import React from'react';
import Button from './Button';

function App() {
  return (
    <div>
      <Button text="点击我" />
    </div>
  );
}

export default App;

// Button 子组件
import React from'react';

function Button(props) {
  return <button>{props.text}</button>;
}

export default Button;

在上述代码中,App 组件作为父组件,向 Button 子组件传递了 text 这个 prop。Button 组件通过 props.text 来获取并展示这个文本。

二、为什么要传递函数作为 Props

  1. 实现组件间交互 当子组件需要触发父组件中的某些操作时,传递函数作为 props 是一种常见的解决方案。比如在一个待办事项列表应用中,子组件是单个待办事项项,当用户点击删除按钮(在子组件中)时,需要从父组件的待办事项列表中移除该项。这就需要子组件能够调用父组件提供的删除函数。
  2. 动态配置子组件行为 父组件可以根据不同的业务场景,传递不同的函数给子组件,从而动态改变子组件的行为。例如,在一个通用的表格组件中,父组件可以传递不同的排序函数给表格子组件,以实现不同列的排序功能。

三、如何传递函数作为 Props

1. 简单示例:子组件触发父组件函数

首先,我们创建一个父组件 Parent 和一个子组件 Child。在父组件中定义一个函数,并将其作为 prop 传递给子组件。

// 父组件 Parent.js
import React, { useState } from'react';
import Child from './Child';

function Parent() {
  const [count, setCount] = useState(0);

  const incrementCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>计数: {count}</p>
      <Child incrementFunction={incrementCount} />
    </div>
  );
}

export default Parent;

// 子组件 Child.js
import React from'react';

function Child(props) {
  return (
    <button onClick={props.incrementFunction}>
      增加计数
    </button>
  );
}

export default Child;

在上述代码中,Parent 组件通过 useState 钩子来维护一个计数器 count,并定义了 incrementCount 函数用于增加计数。然后将 incrementCount 函数作为 incrementFunction prop 传递给 Child 组件。Child 组件通过 props.incrementFunction 来获取这个函数,并将其绑定到按钮的 onClick 事件上。当用户点击按钮时,就会调用父组件中的 incrementCount 函数,从而更新父组件中的 count 状态。

2. 传递带参数的函数

有时候,传递的函数可能需要接收参数。例如,在待办事项列表应用中,删除单个待办事项时,需要知道要删除的事项的唯一标识。

// 父组件 TodoList.js
import React, { useState } from'react';
import TodoItem from './TodoItem';

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习 React' },
    { id: 2, text: '完成项目' }
  ]);

  const deleteTodo = (todoId) => {
    const newTodos = todos.filter(todo => todo.id!== todoId);
    setTodos(newTodos);
  };

  return (
    <div>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          deleteFunction={deleteTodo}
        />
      ))}
    </div>
  );
}

export default TodoList;

// 子组件 TodoItem.js
import React from'react';

function TodoItem(props) {
  return (
    <div>
      <p>{props.todo.text}</p>
      <button onClick={() => props.deleteFunction(props.todo.id)}>
        删除
      </button>
    </div>
  );
}

export default TodoItem;

在这个例子中,TodoList 组件作为父组件,维护一个待办事项列表 todos,并定义了 deleteTodo 函数用于删除指定 id 的待办事项。在渲染 TodoItem 子组件时,将 deleteTodo 函数和每个待办事项对象传递给子组件。TodoItem 组件在按钮的 onClick 事件中,通过箭头函数调用 props.deleteFunction,并传递当前待办事项的 id。这样,当用户点击删除按钮时,就会调用父组件的 deleteTodo 函数,从列表中删除对应的待办事项。

3. 传递函数给多个子组件

在实际应用中,可能需要将同一个函数传递给多个子组件。例如,在一个表单应用中,父组件可能有一个验证函数,需要传递给多个输入框子组件。

// 父组件 Form.js
import React from'react';
import Input from './Input';

function Form() {
  const validateEmail = (email) => {
    const re = /\S+@\S+\.\S+/;
    return re.test(email);
  };

  return (
    <div>
      <Input label="邮箱" type="email" validateFunction={validateEmail} />
      <Input label="确认邮箱" type="email" validateFunction={validateEmail} />
    </div>
  );
}

export default Form;

// 子组件 Input.js
import React, { useState } from'react';

function Input(props) {
  const [value, setValue] = useState('');
  const [isValid, setIsValid] = useState(true);

  const handleChange = (e) => {
    const inputValue = e.target.value;
    setValue(inputValue);
    const isValid = props.validateFunction(inputValue);
    setIsValid(isValid);
  };

  return (
    <div>
      <label>{props.label}</label>
      <input
        type={props.type}
        value={value}
        onChange={handleChange}
      />
      {!isValid && <p>邮箱格式不正确</p>}
    </div>
  );
}

export default Input;

在这个例子中,Form 组件定义了 validateEmail 函数用于验证邮箱格式。然后将这个函数传递给两个 Input 子组件。Input 子组件在输入框的 onChange 事件中,调用 props.validateFunction 来验证输入的值,并根据验证结果更新 isValid 状态,从而显示相应的提示信息。

四、注意事项

1. 函数引用一致性

在 React 中,每次父组件重新渲染时,都会生成新的函数引用(除非使用 useCallback 钩子)。例如:

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

function Parent() {
  const [count, setCount] = useState(0);

  const incrementCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>计数: {count}</p>
      <Child incrementFunction={incrementCount} />
      <button onClick={() => setCount(count + 1)}>直接增加</button>
    </div>
  );
}

export default Parent;

在上述代码中,如果 Parent 组件由于某些原因(比如点击了“直接增加”按钮,导致 count 状态更新,从而触发 Parent 组件重新渲染)重新渲染,incrementCount 函数就会有一个新的引用。这可能会导致一些问题,比如子组件在使用 shouldComponentUpdateReact.memo 进行性能优化时,因为函数引用的改变而不必要地重新渲染。

为了解决这个问题,可以使用 useCallback 钩子。useCallback 会返回一个 memoized(记忆化)的回调函数,只有当依赖项发生变化时,才会返回新的函数。

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

function Parent() {
  const [count, setCount] = useState(0);

  const incrementCount = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return (
    <div>
      <p>计数: {count}</p>
      <Child incrementFunction={incrementCount} />
      <button onClick={() => setCount(count + 1)}>直接增加</button>
    </div>
  );
}

export default Parent;

在这个修改后的代码中,useCallback 的第二个参数 [count] 表示只有当 count 状态发生变化时,incrementCount 函数才会有新的引用。这样,当“直接增加”按钮被点击时,incrementCount 函数引用不会改变,子组件如果使用了 React.memo 等优化手段,就不会因为函数引用的变化而不必要地重新渲染。

2. 避免在 render 方法中定义函数

不要在组件的 render 方法中定义要传递给子组件的函数,因为每次 render 时都会创建新的函数实例。例如:

import React from'react';
import Child from './Child';

function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>计数: {count}</p>
      <Child incrementFunction={() => setCount(count + 1)} />
    </div>
  );
}

export default Parent;

在上述代码中,每次 Parent 组件 render 时,都会创建一个新的箭头函数 () => setCount(count + 1) 并传递给 Child 组件。这同样会导致子组件不必要的重新渲染问题。应该像前面的示例一样,提前在组件内部定义好函数,并使用 useCallback 进行优化。

3. 理解函数作用域

在传递函数作为 props 时,要注意函数的作用域。特别是在使用 ES5 语法的 function 定义函数时,this 的指向可能会不符合预期。例如:

// 父组件 Parent.js
import React from'react';
import Child from './Child';

function Parent() {
  const [count, setCount] = React.useState(0);

  function incrementCount() {
    // 这里的 this 指向可能不是你期望的,在严格模式下可能为 undefined
    this.setState({ count: count + 1 });
  }

  return (
    <div>
      <p>计数: {count}</p>
      <Child incrementFunction={incrementCount} />
    </div>
  );
}

export default Parent;

在上述代码中,incrementCount 函数使用 ES5 的 function 定义,在函数内部使用 this.setState 时,this 的指向可能不是 Parent 组件实例(在严格模式下,this 会是 undefined)。为了避免这种问题,推荐使用箭头函数定义函数,因为箭头函数没有自己的 this,它会从父作用域继承 this

// 父组件 Parent.js
import React, { useState } from'react';
import Child from './Child';

function Parent() {
  const [count, setCount] = useState(0);

  const incrementCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>计数: {count}</p>
      <Child incrementFunction={incrementCount} />
    </div>
  );
}

export default Parent;

这样,incrementCount 函数中的 this 会正确地指向父组件作用域,从而可以正确地更新状态。

五、实际应用场景

1. 表单提交

在一个登录表单中,父组件管理表单的状态和提交逻辑,子组件是各个输入框和提交按钮。父组件可以将提交函数传递给提交按钮子组件。

// 父组件 LoginForm.js
import React, { useState } from'react';
import Input from './Input';
import Button from './Button';

function LoginForm() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(`用户名: ${username}, 密码: ${password}`);
    // 实际应用中可以进行登录请求等操作
  };

  return (
    <form onSubmit={handleSubmit}>
      <Input label="用户名" value={username} onChange={(e) => setUsername(e.target.value)} />
      <Input label="密码" value={password} onChange={(e) => setPassword(e.target.value)} type="password" />
      <Button text="登录" />
    </form>
  );
}

export default LoginForm;

// 子组件 Button.js
import React from'react';

function Button(props) {
  return <button type="submit">{props.text}</button>;
}

export default Button;

// 子组件 Input.js
import React from'react';

function Input(props) {
  return (
    <div>
      <label>{props.label}</label>
      <input
        type={props.type || 'text'}
        value={props.value}
        onChange={props.onChange}
      />
    </div>
  );
}

export default Input;

在这个例子中,LoginForm 组件定义了 handleSubmit 函数用于处理表单提交。Button 子组件作为提交按钮,虽然没有直接接收 handleSubmit 函数(因为表单的 onSubmit 已经绑定了该函数),但它的 type="submit" 会触发表单提交,从而调用父组件的 handleSubmit 函数。Input 子组件用于输入用户名和密码,通过 onChange 函数更新父组件中的相应状态。

2. 列表排序

在一个商品列表应用中,父组件展示商品列表,子组件是排序按钮。父组件传递不同的排序函数给排序按钮子组件,实现不同字段的排序。

// 父组件 ProductList.js
import React, { useState } from'react';
import Product from './Product';
import SortButton from './SortButton';

function ProductList() {
  const [products, setProducts] = useState([
    { id: 1, name: '商品 A', price: 100 },
    { id: 2, name: '商品 B', price: 200 },
    { id: 3, name: '商品 C', price: 150 }
  ]);

  const sortByName = () => {
    const sortedProducts = [...products].sort((a, b) => a.name.localeCompare(b.name));
    setProducts(sortedProducts);
  };

  const sortByPrice = () => {
    const sortedProducts = [...products].sort((a, b) => a.price - b.price);
    setProducts(sortedProducts);
  };

  return (
    <div>
      <SortButton text="按名称排序" sortFunction={sortByName} />
      <SortButton text="按价格排序" sortFunction={sortByPrice} />
      <div>
        {products.map(product => (
          <Product key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}

export default ProductList;

// 子组件 SortButton.js
import React from'react';

function SortButton(props) {
  return (
    <button onClick={props.sortFunction}>
      {props.text}
    </button>
  );
}

export default SortButton;

// 子组件 Product.js
import React from'react';

function Product(props) {
  return (
    <div>
      <p>名称: {props.product.name}</p>
      <p>价格: {props.product.price}</p>
    </div>
  );
}

export default Product;

在这个例子中,ProductList 组件定义了 sortByNamesortByPrice 两个排序函数。SortButton 子组件通过 sortFunction prop 接收父组件传递的排序函数,当用户点击按钮时,调用相应的排序函数,从而更新商品列表的排序。

3. 模态框操作

在一个应用中,可能会有模态框用于显示一些信息或进行一些操作。父组件管理模态框的显示状态和相关操作,子组件是模态框组件。父组件可以传递关闭模态框的函数给模态框子组件。

// 父组件 App.js
import React, { useState } from'react';
import Modal from './Modal';

function App() {
  const [isModalOpen, setIsModalOpen] = useState(false);

  const openModal = () => {
    setIsModalOpen(true);
  };

  const closeModal = () => {
    setIsModalOpen(false);
  };

  return (
    <div>
      <button onClick={openModal}>打开模态框</button>
      {isModalOpen && <Modal closeFunction={closeModal} />}
    </div>
  );
}

export default App;

// 子组件 Modal.js
import React from'react';

function Modal(props) {
  return (
    <div className="modal">
      <div className="modal-content">
        <p>这是一个模态框</p>
        <button onClick={props.closeFunction}>关闭</button>
      </div>
    </div>
  );
}

export default Modal;

在这个例子中,App 组件通过 useState 维护模态框的显示状态 isModalOpen,并定义了 openModalcloseModal 函数。Modal 子组件通过 closeFunction prop 接收父组件传递的 closeModal 函数,当用户点击模态框中的关闭按钮时,调用该函数关闭模态框。

六、总结与深入思考

通过 props 传递函数是 React 中实现组件间交互和动态配置子组件行为的重要方式。在实际应用中,需要注意函数引用一致性、避免在 render 方法中定义函数以及理解函数作用域等问题,以确保代码的性能和正确性。同时,通过具体的应用场景,如表单提交、列表排序和模态框操作等,我们可以看到这种技术在实际项目中的广泛应用。随着 React 应用的规模不断扩大,合理地使用函数传递作为 props 将有助于构建更加灵活、可维护的应用架构。例如,在大型的企业级应用中,可能会有多个层级的组件嵌套,通过这种方式可以有效地实现跨层级的组件通信和行为控制。而且,随着 React 生态系统的不断发展,新的特性和优化手段也可能会影响到函数传递作为 props 的使用方式,开发者需要持续关注并学习,以更好地利用这一技术为项目服务。