React项目开发时值得注意的一些问题记录

本文主要介绍了使用react开发项目时比较容易出错或者是说需要注意的一些问题。

一、重复渲染问题

React组件可能会在不同时间点重新渲染,如果不小心,某些操作可能会在每次渲染时都重复执行,这会降低性能。

解决办法: 可以使用useMemouseCallback来记忆化操作,以便不会在每次渲染时重新计算。

示例: 用useMemo,避免在每次渲染时重复执行工作

function List({
     
      items }) {
    
    
  // 不使用useMemo - 每次渲染都会重新过滤
  const filteredItems = items.filter(item => item.active);
 
  // 使用useMemo - 只在items发生变化时重新过滤,
  const filteredItems = useMemo(() => items.filter(item => item.active), [items]);
}

useMemo 和 useCallback

useMemo是缓存函数本身,而useCallback是缓存函数的返回值。

什么时候应该使用useMemo而不是useCallback?答案是:尽可能使用useMemo,只有在必要时才使用useCallback

一个微妙但重要的区别:当函数本身被重复实例化并导致性能损耗时,使用useMemo;否则,使用useCallback是更好的选择。

当我们需要避免在渲染过程中创建函数本身时,useMemo是合适的选择,而useCallback无法阻止函数在每次出现时被重新创建。然而,useMemo会确保如果依赖项没有发生变化,函数会返回缓存的值。

二、改变Hooks调用的顺序

React hooks的内部工作原理要求组件在每次渲染的时候总是按相同的顺序执行hooks。

React依赖于Hooks调用的顺序来正确地关联Hook的状态。如果在循环或条件语句中调用Hooks,每次迭代或条件判断都可能产生新的Hook状态,这会导致React无法正确地关联状态和更新。

示例:

function fetchData() {
    
    
  let [data, setData] = useState(null);
  
  useEffect(() => {
    
    
    fetch('api')
      .then(response => response.json())
      .then(data => setData(data));
  }, []); // 依赖项为空,表示这个Effect只会在组件挂载时运行一次

  // 错误写法:在循环中调用useState
  for (let i = 0; i < 5; i++) {
    
    
    useState(() => ({
    
     count: 0 }));
  }
}

React为Hooks定义的两条基本规则:

  • 只能在函数组件的最顶层调用Hooks,不要在循环、条件或嵌套函数中调用Hooks。
  • 只能在React的函数组件或自定义Hook中调用Hooks。

关于React的调度模型和Hooks的执行机制:

React的调度模型是基于Fiber架构的,它负责追踪和协调组件的更新,Hooks的执行也遵循这个模型。

当React渲染组件时,它会按照声明的顺序执行Hooks。如果在条件语句或嵌套函数中调用Hooks,React就无法保证这些Hooks的执行顺序,从而导致状态管理的混乱。

三、使用useState()不当

1、更新state对象属性时只改属性

使用setState() 更新对象属性时,非常容易犯的错误就是,只修改对象或数组的属性而不修改引用本身。

示例1:修改对象类型

import {
    
     useState, useEffect } from "react";

export default function App() {
    
    
  const [user, setUser] = useState({
    
    
    name: "vickie",
    age: 18,
  });

  // 出错处:更新用户状态属性,修改后user不再是一个对象,而是被改写为字符串"Cythia",而不是特定的属性被修改
  // 原因:setState() 将返回或传递给它的任何值赋值为新状态
  const changeName = () => {
    
    
    setUser((user) => (user.name = "Cythia"));
  };
  
  // 正确做法
  const changeName = () => {
    
    
    // 方法1:创建一个新的对象引用,并将前一个用户对象分配给它
    setUser((user) => Object.assign({
    
    }, user, {
    
     name: "Cythia" }));
    
    // 方法2:推荐使用es6的扩展运算符(推荐)
    setUser((user) => ({
    
     ...user, name: "Cythia" }));
  };
  
  return (
    <div className='App'>
      <p>User: {
    
    user.name}</p>
      <p>Age: {
    
    user.age}</p>
      <button onClick={
    
    changeName}>Change name</button>
    </div>
  );
}

示例2: 修改数组类型

  // 图片压缩组件
  function Compress() {
    
    
    
    const [files, setFiles] = useState([])
    
    // 错误写法: 直接修改state的值
    const handleChange = (newFiles) => {
    
    
      api(newFiles).then((res)=>{
    
    
        const cloneFiles = [...files] // 这里的files始终是[]
        cloneFiles.map( /* 一些逻辑...*/ )
        setFiles(cloneFiles)
      })
    }
    
    // 正确写法:通过setState修改state的值
    const handleChange = (newFiles) => {
    
    
    	api(newFiles).then((res)=>{
    
    
	      	setFiles((oldFiles) => {
    
    
	        	const cloneFiles = [...files] // 这里的files才是最新的
	        	return cloneFiles.map( /*一些逻辑...*/ )
	      	})
    	})
    }
    
    return <input type="upload" multiple onChange={
    
    handleChange}/>
  }

2、乱用useState()

1)在不需要render的场景下使用useState

// 不推荐,因为 return 部分并没有用到count状态,而每次setCount都会使组件重新渲染一次
function ClickButton(props){
    
    
  const [count, setCount] = useState(0)
  
  const onClickCount = () => {
    
    
    setCount((c) => c + 1)
  }
  
  const onClickRequest = () => {
    
    
    apiCall(count)
  }
  
  return (
    <div>
      <button onClick={
    
    onClickCount}>Add</button>
      <button onClick={
    
    onClickRequest}>Submit</button>
    </div>
  )
}

// 正确做法
function ClickButton(props){
    
    
  const count = useRef(0) // ref有个好处就是不会触发组件的重新渲染,从而避免了不必要的性能问题。
  
  const onClickCount = () => {
    
    
    count.current++ // ref本身是一个变化的对象,在组件渲染时使用ref.current来获取当前的值
  }
  
  const onClickRequest = () => {
    
    
    apiCall(count.current)
  }

  return (
    <div>
      <button onClick={
    
    onClickCount}>Click</button>
      <button onClick={
    
    onClickRequest}>Submit</button>
    </div>
  )
}

2)基础设施的数据,比如:渲染周期的细节(是否首次渲染,渲染次数)、计时器setTimeout(), setInterval()、对DOM元素的直接引用等,应该使用引用useRef()来保存和更新。

因为当useRef的值发生变化时,不会导致React引擎重新渲染,而useState会触发重新渲染。

function MyComponent() {
    
    
	const [count, setCount] = useState(0);
	
  	// const [isFirst, setIsFirst] = useState(true);
    const isFirstRef = useRef(true); // 问题解决:将首次渲染的信息保存在引用isFirstRef中,isFirstRef.current属性用于访问和更新引用的值
    
    useEffect(() => {
    
    
      // 出现的问题:一旦更新了setIsFirst(false),就会触发重新渲染
      // if (isFirst) {
    
    
      //  setIsFirst(false);
      //  return;
      // }
      
      if (isFirstRef.current) {
    
    
        isFirstRef.current = false;
        return;
      }
    }, [count]);
    
    return (
      <button onClick={
    
    () => setCounter(count => count + 1)}>
        Increase
      </button>
    );
}

四、使用 Ref 时出现的问题

1、忘记使用fowardRef

React 不允许将 ref 传递给函数组件,除非它被forwardRef包装起来

import {
    
     useRef } from'react';

// 错误写法
const CustomInput = ({
     
      ref,...rest }) => {
    
    
  const [value, setValue] = useState('');
  useEffect(() => {
    
    
    if(ref.current === null) return;
    ref.current.focus();
  }, [ref]);
  return (
    <input ref={
    
    ref} {
    
    ...rest} value={
    
    value} onChange={
    
    e => setValue(e.target.value)} />
  );
}

// 正确写法:用forwardRef包裹组件
const CustomInput = forwardRef((props, ref) => {
    
    
  const [value, setValue] = useState('');
  useEffect(() => {
    
    
    if(ref.current === null) return;
    ref.current.focus();
  }, [ref]);
  return (
    <input ref={
    
    ref} {
    
    ...props} value={
    
    value} onChange={
    
    e => setValue(e.target.value)} />
  );
})

export const CustomInputElement = () => {
    
    
  const ref = useRef();
  return (
    <CustomInput ref={
    
    ref} />
  );
}

2、不缓存 ref 的值

调用函数来初始化ref的值时,需要缓存该函数或在渲染期间初始化 ref(在检查值尚未设置之后)

示例:

import {
    
     useState, useRef, useEffect } from "react";

const useOnBeforeUnload = (callback) => {
    
    
  useEffect(() => {
    
    
    window.addEventListener("beforeunload", callback);
    return () => window.removeEventListener("beforeunload", callback);
  }, [callback]);
}

export const App = () => {
    
    
  
  // 错误写法:函数开销很大时,将不必要地影响应用性能
  // const ref = useRef(window.localStorage.getItem("cache-date"));
  
  // 正确写法
  const ref = useRef(null);
  
  if (ref.current === null) {
    
    
    ref.current = window.localStorage.getItem("cache-date");
  }
  
  const [inputValue, setInputValue] = useState("");

  useOnBeforeUnload(() => {
    
    
    const date = new Date().toUTCString();
    console.log("Date", date);
    window.localStorage.setItem("cache-date", date);
  });

  return (
    <>
      <div>缓存的时间: <strong>{
    
    ref.current}</strong></div>
      用户名:{
    
    " "}
      <input value={
    
    inputValue} onChange={
    
    (e) => setInputValue(e.target.value)} />
    </>
  );
}

五、不正确的使用useEffect()

class 组件时,componentDidMount 是一个通用的生命周期函数,用来做一些数据请求,事件绑定等。在使用Hooks时,useEffect 则取代了生命周期函数。但是不正确的使用 useEffect 可能会导致一些问题。

示例:不正确的使用 useEffect 可能会导致最终创建多个事件绑定

// 不良写法 - 每次渲染都会创建新的事件监听器
useEffect(() => {
    
    
  window.addEventListener('resize', handleResize);
  return () => {
    
    
	window.removeEventListener('resize', handleResize);
  };
});
 
// 推荐写法 - 只在组件挂载时创建事件监听器
useEffect(() => {
    
    
  window.addEventListener('resize', handleResize);
	return () => {
    
    
	  window.removeEventListener('resize', handleResize); // 不要忘记清理副作用,如果用到的话
	};
}, []); // 空的依赖数组,通过使用空的依赖数组,确保了事件监听器只在组件挂载时创建一次

useEffect钩子用于处理副作用,但如果不正确使用它,可能会导致创建多个事件监听器,这会引发问题。在上述示例中,可以看到正确使用useEffect的方法包括将清理函数返回以取消订阅,以及使用空的依赖数组以确保只运行一次。

示例2:

function FetGame({
     
      id }) {
    
    
    const [game, setGame] = useState({
    
     
      name: '',
      description: '' 
    });
    useEffect(() => {
    
    
      const fetchGame = async () => {
    
    
        const response = await fetch(`/api/game/${
      
      id}`);
        const fetchedGame = await response.json();
        setGame(fetchedGame);
      };
      if (id) {
    
     
        fetchGame(); 
      }
    }, [id]);
  	// 避免条件渲染hook的实用性建议:在组件主体的顶部执行hooks,逻辑渲染语句移到组件底部。
    if (!id) {
    
    
      return 'Please select a game to fetch';
    }
    return (
      <div>
        <div>Name: {
    
    game.name}</div>
        <div>Description: {
    
    game.description}</div>
      </div>
    );
}

useEffect用法

useEffect()可以看做是类组件中componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个生命周期函数的组合。

useEffect(effect, deps)接收 2 个参数:

  • effect副作用函数;
  • deps依赖项数组。

useEffect 使用的 4 种情况:

// 1、第二个参数不传:任何状态更新,都会触发useEffect的副作用函数
useEffect(() => {
    
    
  setCount(count + 1);
});

// 2、第二个参数为空数组:仅在挂载和卸载的时触发useEffect的副作用函数
useEffect(() => {
    
    
  setCount(count + 1);
}, []);

// 3、第二个参数为单值数组:仅在该值变化,才会触发useEffect的副作用函数
useEffect(() => {
    
    
  setCount(count + 1);
}, [name]);

// 4、第二个参数为多值数组:仅在传入的值发生变化,才会触发useEffect的副作用函数
useEffect(() => {
    
    
  setCount(count + 1);
}, [name, age]);

// 当需要实现componentWillUnmount 生命周期函数的效果,可以在useEffect()函数返回一个函数即可,该函数会在组件卸载时执行
export default function App() {
    
    
  const [count, setCount] = useState(0);
  useEffect(() => {
    
    
    setCount(count+1);
    return () => {
    
    
      console.log('[组件已卸载]');
    }
  }, []);
  return <div className="App">{
    
    count}</div>;
}

六、props 透传

props 透传是指将单个 props 从父组件向下多层传递的做法。理想状态下,props 不应该超过两层,当我们选择多层传递时,会导致一些性能问题。

props 透传会导致不必要的重新渲染:因为React 组件总会在 props 发生变化时重新渲染,而那些不需要 props,只是提供传递作用的中间层组件都会被渲染。

解决办法: 方法有很多,比如 React Context Hook,或者类似 Redux 的库,简单的项目选择使用 Context Hook 是更好的选择,使用 Redux 需要额外编写一些代码,更适合单个状态改变很多东西的复杂场景。

示例1:用React的上下文context来共享数据

import React, {
    
     Component } from 'react';
import {
    
     createContext, useContext, useState } from 'react';

// 创建了一个名为MyContext的上下文
const MyContext = createContext(); 

// 创建了一个名为MyProvider的组件来提供共享的数据
const MyProvider = ({
     
      children }) => {
    
    
  const [data, setData] = useState('Hello, World!');
  return (
    <MyContext.Provider value={
    
    {
    
     data, setData }}>
      {
    
    children}
    </MyContext.Provider>
  );
};

const MyConsumer = () => {
    
    
  // 用useContext钩子来获取上下文中的数据,并使用setData函数来更新数据
  const {
    
     data, setData } = useContext(MyContext);
  return (
    <div>
      <h1>{
    
    data}</h1>
      <button onClick={
    
    () => setData('Hello, Button!')}>Change Data</button>
    </div>
  );
};

// 在App组件中包装了MyProvider和MyConsumer组件。
const App = () => {
    
    
  return (
    <MyProvider>
      <MyConsumer />
    </MyProvider>
  );
};

context共享数据总结:

  • 16.3 之前的老版本
    • 提供者:getChildContext + static childContextType
    • 消费者:this.context.xx 取值
  • 16.3 之后的版本:
    • 类组件:React.createContext
      • 提供者:TopContext.Provider
      • 消费者:TopContext.Consumer
        • 优化方式一:组件.contextType= TopContext; this.context.xx取值
        • 优化方式二:封装connect函数,直接从组件参数获取
    • 函数组件(hook):
      • 提供者:createContext.Provider
      • 消费者:useContext(TopContext)返回值

示例2:用hooks实现数据传递

// 父组件:创建+包裹
export const ContactContext = createContext({
    
    });

function parent(){
    
    
    <ContactContext.Provider value={
    
    {
    
     itemData: res }} >                     
        <ShowName />
        <ShowSex/>
    </ContactContext.Provider>
}

// 子组件接收数据:useContext(父定义的context)
const ShowName = () => {
    
    
    const {
    
    itemData:{
    
    name}} = useContext(ContactContext);
    return ( <span className='name'>{
    
    name}</span> )
}

React的数据传递方式:

  1. props:只能父传子
  2. redux:数据传递不多的时候使用会有点重
  3. 数据共享:context 上下文

猜你喜欢

转载自blog.csdn.net/ganyingxie123456/article/details/145606859