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

React 列表渲染中的常见错误与解决

2022-12-187.5k 阅读

React 列表渲染中的常见错误与解决

未使用 key 属性

在 React 中进行列表渲染时,key 属性是非常重要的。当 React 渲染列表时,它需要一种方法来跟踪每个列表项,以便在数据变化时有效地更新 DOM。key 就是用来帮助 React 识别哪些列表项发生了变化、被添加或被删除。

错误示例

import React from 'react';

const MyComponent = () => {
  const items = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' }
  ];

  return (
    <ul>
      {items.map(item => (
        <li>{item.name}</li>
      ))}
    </ul>
  );
};

export default MyComponent;

在上述代码中,map 方法遍历 items 数组并渲染 <li> 元素,但没有给 <li> 元素添加 key 属性。这在 React 开发工具中会看到警告,并且在某些复杂的更新场景下可能导致 UI 渲染异常。

解决方案

给每个列表项添加唯一的 key,通常使用数据项中的唯一标识作为 key

import React from 'react';

const MyComponent = () => {
  const items = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' }
  ];

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
};

export default MyComponent;

这样 React 就能准确地识别每个列表项,确保在数据变化时正确地更新 DOM。

key 属性使用不当

虽然知道要使用 key 属性,但如果使用不当,同样会引发问题。常见的错误是使用数组索引作为 key,尤其是在列表项顺序可能改变或者列表项可能增删的情况下。

错误示例

import React, { useState } from'react';

const MyComponent = () => {
  const [items, setItems] = useState([
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' }
  ]);

  const addItem = () => {
    const newItem = { id: 4, name: 'New Item' };
    setItems([newItem, ...items]);
  };

  return (
    <div>
      <button onClick={addItem}>Add Item</button>
      <ul>
        {items.map((item, index) => (
          <li key={index}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default MyComponent;

在上述代码中,使用 index 作为 key。当点击 “Add Item” 按钮添加新项时,新项会被插入到列表开头。由于 key 是基于索引的,所有列表项的 key 都会改变,React 会认为所有列表项都是新的,从而重新渲染整个列表,而不是只更新新增的项,这会导致性能问题,并且在某些情况下可能丢失列表项的状态(比如输入框的焦点等)。

解决方案

始终使用数据项中的唯一标识作为 key

import React, { useState } from'react';

const MyComponent = () => {
  const [items, setItems] = useState([
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' }
  ]);

  const addItem = () => {
    const newItem = { id: 4, name: 'New Item' };
    setItems([newItem, ...items]);
  };

  return (
    <div>
      <button onClick={addItem}>Add Item</button>
      <ul>
        {items.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default MyComponent;

这样,无论列表项的顺序如何变化或者是否有增删操作,React 都能通过唯一的 key 准确识别每个列表项,只对实际变化的部分进行高效更新。

列表渲染中的作用域问题

在列表渲染中,作用域问题也可能导致一些难以察觉的错误。例如,在 map 方法内部使用函数时,要注意函数内部 this 的指向以及变量的作用域。

错误示例

import React from'react';

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      items: [
        { id: 1, name: 'Item 1' },
        { id: 2, name: 'Item 2' },
        { id: 3, name: 'Item 3' }
      ]
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick(item) {
    console.log(`Clicked on ${item.name}`);
  }

  render() {
    return (
      <ul>
        {this.state.items.map(item => (
          <li onClick={this.handleClick(item)} key={item.id}>{item.name}</li>
        ))}
      </ul>
    );
  }
}

export default MyComponent;

在上述代码中,onClick={this.handleClick(item)} 这样的写法会导致 handleClick 函数在渲染时就被调用,而不是在点击时调用。这是因为在 JSX 中,() 表示立即执行函数。而且,由于 handleClick 函数在渲染时就被调用,item 的值会是数组中最后一个元素的值,因为此时 map 循环已经结束。

解决方案

使用箭头函数或者 bind 方法来正确绑定事件处理函数。

import React from'react';

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      items: [
        { id: 1, name: 'Item 1' },
        { id: 2, name: 'Item 2' },
        { id: 3, name: 'Item 3' }
      ]
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick(item) {
    console.log(`Clicked on ${item.name}`);
  }

  render() {
    return (
      <ul>
        {this.state.items.map(item => (
          <li onClick={() => this.handleClick(item)} key={item.id}>{item.name}</li>
        ))}
      </ul>
    );
  }
}

export default MyComponent;

通过箭头函数 () => this.handleClick(item),在点击时才会调用 handleClick 函数,并且正确传递当前的 item 对象。或者也可以使用 bind 方法,如 <li onClick={this.handleClick.bind(this, item)} key={item.id}>{item.name}</li>,同样能达到在点击时调用函数并传递正确参数的目的。

列表渲染中的嵌套循环问题

当需要在列表渲染中使用嵌套循环时,很容易出现逻辑错误,尤其是在处理复杂数据结构时。

错误示例

import React from'react';

const data = [
  {
    group: 'Group 1',
    items: [
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2' }
    ]
  },
  {
    group: 'Group 2',
    items: [
      { id: 3, name: 'Item 3' },
      { id: 4, name: 'Item 4' }
    ]
  }
];

const MyComponent = () => {
  return (
    <div>
      {data.map(group => (
        <div key={group.group}>
          <h2>{group.group}</h2>
          {group.items.map(item => (
            <div key={item.id}>{item.name}</div>
          ))}
        </div>
      ))}
    </div>
  );
};

export default MyComponent;

虽然上述代码看起来正常,但如果数据结构发生变化,比如 items 数组中可能包含子数组,情况就会变得复杂。假设数据结构变为:

const data = [
  {
    group: 'Group 1',
    items: [
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2', subItems: [
        { subId: 11, subName: 'Sub Item 1' },
        { subId: 12, subName: 'Sub Item 2' }
      ]}
    ]
  },
  {
    group: 'Group 2',
    items: [
      { id: 3, name: 'Item 3' },
      { id: 4, name: 'Item 4' }
    ]
  }
];

如果仍然按照之前的方式渲染,就无法处理 subItems

解决方案

需要根据数据结构的变化,正确嵌套 map 方法。

import React from'react';

const data = [
  {
    group: 'Group 1',
    items: [
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2', subItems: [
        { subId: 11, subName: 'Sub Item 1' },
        { subId: 12, subName: 'Sub Item 2' }
      ]}
    ]
  },
  {
    group: 'Group 2',
    items: [
      { id: 3, name: 'Item 3' },
      { id: 4, name: 'Item 4' }
    ]
  }
];

const MyComponent = () => {
  return (
    <div>
      {data.map(group => (
        <div key={group.group}>
          <h2>{group.group}</h2>
          {group.items.map(item => (
            <div key={item.id}>
              {item.name}
              {item.subItems && item.subItems.map(subItem => (
                <div key={subItem.subId}>{subItem.subName}</div>
              ))}
            </div>
          ))}
        </div>
      ))}
    </div>
  );
};

export default MyComponent;

通过增加一层 map 来处理 subItems,确保在复杂数据结构下列表渲染的正确性。

列表渲染与状态更新

在列表渲染时,状态更新的方式也很关键。不正确的状态更新可能导致列表渲染不符合预期。

错误示例

import React, { useState } from'react';

const MyComponent = () => {
  const [items, setItems] = useState([
    { id: 1, name: 'Item 1', isChecked: false },
    { id: 2, name: 'Item 2', isChecked: false },
    { id: 3, name: 'Item 3', isChecked: false }
  ]);

  const handleCheckboxChange = (id) => {
    const index = items.findIndex(item => item.id === id);
    items[index].isChecked =!items[index].isChecked;
    setItems(items);
  };

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          <input type="checkbox" checked={item.isChecked} onChange={() => handleCheckboxChange(item.id)} />
          {item.name}
        </li>
      ))}
    </ul>
  );
};

export default MyComponent;

在上述代码中,直接修改 items 数组中的元素状态,然后调用 setItems(items) 更新状态。这种方式是错误的,因为 React 的状态更新应该是不可变的。直接修改数组元素违反了不可变性原则,React 可能无法检测到状态的变化,导致 UI 不会更新。

解决方案

使用不可变的方式更新状态。

import React, { useState } from'react';

const MyComponent = () => {
  const [items, setItems] = useState([
    { id: 1, name: 'Item 1', isChecked: false },
    { id: 2, name: 'Item 2', isChecked: false },
    { id: 3, name: 'Item 3', isChecked: false }
  ]);

  const handleCheckboxChange = (id) => {
    setItems(items.map(item =>
      item.id === id
      ? {...item, isChecked:!item.isChecked }
       : item
    ));
  };

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          <input type="checkbox" checked={item.isChecked} onChange={() => handleCheckboxChange(item.id)} />
          {item.name}
        </li>
      ))}
    </ul>
  );
};

export default MyComponent;

通过 map 方法创建一个新的数组,在新数组中对目标元素进行状态更新,而不是直接修改原数组,确保了状态更新的不可变性,React 能够正确检测到状态变化并更新 UI。

列表渲染中的性能问题

当列表数据量较大时,性能问题就会凸显出来。如果处理不当,可能导致页面卡顿。

原因分析

React 在渲染列表时,即使只有个别项发生变化,默认情况下也会重新渲染整个列表。这在数据量小的时候可能不明显,但随着数据量的增加,性能开销会变得很大。

解决方案

  1. 使用 shouldComponentUpdateReact.memo:对于类组件,可以使用 shouldComponentUpdate 方法来控制组件是否需要更新。对于函数组件,可以使用 React.memo 高阶组件。例如:
import React from'react';

const ListItem = React.memo(({ item }) => {
  return <li>{item.name}</li>;
});

const MyComponent = () => {
  const items = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' }
  ];

  return (
    <ul>
      {items.map(item => (
        <ListItem key={item.id} item={item} />
      ))}
    </ul>
  );
};

export default MyComponent;

React.memo 会对 ListItem 组件的 props 进行浅比较,如果 props 没有变化,组件就不会重新渲染。

  1. 虚拟列表:当列表数据量非常大时,可以使用虚拟列表技术。虚拟列表只渲染当前可见区域内的列表项,当用户滚动时,动态加载新的列表项。常见的库如 react - virtualizedreact - window 可以帮助实现虚拟列表。例如,使用 react - virtualized 中的 List 组件:
import React from'react';
import { List } from'react - virtualized';

const rowCount = 1000;
const rowHeight = 35;

const rowRenderer = ({ index, key, style }) => {
  return (
    <div key={key} style={style}>
      Item {index + 1}
    </div>
  );
};

const MyComponent = () => {
  return (
    <List
      height={400}
      rowCount={rowCount}
      rowHeight={rowHeight}
      rowRenderer={rowRenderer}
      width={300}
    />
  );
};

export default MyComponent;

这样,无论列表数据量有多大,始终只渲染当前可见区域内的少量列表项,大大提高了性能。

列表渲染中的样式问题

在列表渲染中,样式的设置也可能出现问题,比如样式重复或者样式不生效等情况。

样式重复问题

当为列表项设置样式时,如果不小心,可能会导致所有列表项都应用相同的样式,而不是根据每个列表项的不同状态或属性来设置不同样式。

错误示例

import React from'react';

const MyComponent = () => {
  const items = [
    { id: 1, name: 'Item 1', isSpecial: true },
    { id: 2, name: 'Item 2', isSpecial: false },
    { id: 3, name: 'Item 3', isSpecial: true }
  ];

  return (
    <ul>
      <style>{`
      .special {
        color: red;
      }
      `}</style>
      {items.map(item => (
        <li className="special" key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
};

export default MyComponent;

在上述代码中,无论 isSpecial 属性的值如何,所有列表项都应用了 .special 样式,这显然不符合预期。

解决方案

根据列表项的属性动态设置样式类名。

import React from'react';

const MyComponent = () => {
  const items = [
    { id: 1, name: 'Item 1', isSpecial: true },
    { id: 2, name: 'Item 2', isSpecial: false },
    { id: 3, name: 'Item 3', isSpecial: true }
  ];

  return (
    <ul>
      <style>{`
      .special {
        color: red;
      }
      `}</style>
      {items.map(item => (
        <li className={item.isSpecial? 'special' : ''} key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
};

export default MyComponent;

通过三元表达式 item.isSpecial? 'special' : '' 根据 isSpecial 属性的值动态设置类名,只有 isSpecialtrue 的列表项才会应用 .special 样式。

样式不生效问题

有时候,设置的样式可能不会生效,这可能是由于样式的优先级问题或者 CSS 模块的使用不当导致的。

错误示例(样式优先级问题)

import React from'react';

const MyComponent = () => {
  const items = [
    { id: 1, name: 'Item 1' }
  ];

  return (
    <div>
      <style>{`
      li {
        color: blue;
      }
      `}</style>
      <ul style={{ color: 'red' }}>
        {items.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default MyComponent;

在上述代码中,虽然为 <li> 元素设置了 color: blue 的样式,但由于 <ul> 元素的内联样式 color: red 具有更高的优先级,导致 <li> 元素的文本颜色为红色,而不是蓝色。

解决方案(解决样式优先级问题)

可以通过提高样式的特异性来解决优先级问题。例如:

import React from'react';

const MyComponent = () => {
  const items = [
    { id: 1, name: 'Item 1' }
  ];

  return (
    <div>
      <style>{`
      ul li {
        color: blue;
      }
      `}</style>
      <ul style={{ color: 'red' }}>
        {items.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default MyComponent;

通过将样式选择器改为 ul li,提高了特异性,使得 color: blue 的样式能够生效。

错误示例(CSS 模块使用不当)

假设使用 CSS 模块,项目结构如下:

src/
├── components/
│   ├── MyComponent/
│   │   ├── MyComponent.jsx
│   │   ├── MyComponent.module.css

MyComponent.module.css 内容如下:

.special {
  color: green;
}

MyComponent.jsx 内容如下:

import React from'react';
import styles from './MyComponent.module.css';

const MyComponent = () => {
  const items = [
    { id: 1, name: 'Item 1' }
  ];

  return (
    <ul>
      {items.map(item => (
        <li className="special" key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
};

export default MyComponent;

在上述代码中,没有正确使用 CSS 模块,直接写 className="special",这不会应用到 MyComponent.module.css 中的 .special 样式。

解决方案(正确使用 CSS 模块)

import React from'react';
import styles from './MyComponent.module.css';

const MyComponent = () => {
  const items = [
    { id: 1, name: 'Item 1' }
  ];

  return (
    <ul>
      {items.map(item => (
        <li className={styles.special} key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
};

export default MyComponent;

通过 styles.special 正确引用 CSS 模块中的样式类,确保样式能够正确应用。

列表渲染中的数据获取与更新

在实际应用中,列表数据通常来自后端 API,并且可能需要在某些操作后进行更新。在这个过程中,也会遇到一些常见错误。

错误示例(数据获取失败未处理)

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

const MyComponent = () => {
  const [items, setItems] = useState([]);

  useEffect(() => {
    fetch('https://example.com/api/items')
    .then(response => response.json())
    .then(data => setItems(data));
  }, []);

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
};

export default MyComponent;

在上述代码中,没有处理 fetch 请求失败的情况。如果 API 请求失败,items 会保持为空数组,并且没有给用户任何反馈。

解决方案(处理数据获取失败)

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

const MyComponent = () => {
  const [items, setItems] = useState([]);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('https://example.com/api/items')
    .then(response => {
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        return response.json();
      })
    .then(data => setItems(data))
    .catch(error => setError(error.message));
  }, []);

  return (
    <div>
      {error && <p>{error}</p>}
      <ul>
        {items.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default MyComponent;

通过捕获 fetch 请求中的错误,并将错误信息存储在 error 状态中,在 UI 中显示错误信息,给用户提供反馈。

错误示例(更新后未重新获取数据)

假设列表中有一个删除按钮,删除操作成功后需要重新获取数据以更新列表。

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

const MyComponent = () => {
  const [items, setItems] = useState([]);

  useEffect(() => {
    fetch('https://example.com/api/items')
    .then(response => response.json())
    .then(data => setItems(data));
  }, []);

  const deleteItem = (id) => {
    fetch(`https://example.com/api/items/${id}`, {
      method: 'DELETE'
    })
    .then(response => response.json());
  };

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          {item.name}
          <button onClick={() => deleteItem(item.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
};

export default MyComponent;

在上述代码中,删除操作成功后没有重新获取数据,导致列表不会更新,删除的项仍然显示在列表中。

解决方案(更新后重新获取数据)

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

const MyComponent = () => {
  const [items, setItems] = useState([]);

  useEffect(() => {
    fetch('https://example.com/api/items')
    .then(response => response.json())
    .then(data => setItems(data));
  }, []);

  const deleteItem = (id) => {
    fetch(`https://example.com/api/items/${id}`, {
      method: 'DELETE'
    })
    .then(() => {
        fetch('https://example.com/api/items')
        .then(response => response.json())
        .then(data => setItems(data));
      });
  };

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          {item.name}
          <button onClick={() => deleteItem(item.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
};

export default MyComponent;

在删除操作成功后,再次调用 fetch 获取最新数据并更新 items 状态,确保列表能够正确反映数据的变化。

通过对以上 React 列表渲染中常见错误的分析与解决,开发者能够更高效、准确地实现列表渲染功能,提升应用的性能和用户体验。在实际开发中,还需要不断积累经验,根据具体的业务场景和数据结构,灵活运用这些知识,避免和解决可能出现的问题。