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

React 条件渲染的常见坑与解决办法

2021-06-204.5k 阅读

一、React 条件渲染基础回顾

在 React 中,条件渲染是根据不同条件来决定是否渲染某些组件或者渲染不同的组件。这是前端开发中非常基础且常用的功能,它让我们能够根据应用的状态动态地展示 UI。

最常见的条件渲染方式是使用 JavaScript 的 if 语句。例如,假设有一个表示用户登录状态的变量 isLoggedIn,我们想要根据这个状态来显示不同的内容:

import React from 'react';

function App() {
    const isLoggedIn = true;
    let content;
    if (isLoggedIn) {
        content = <p>欢迎,用户已登录</p>;
    } else {
        content = <p>请登录</p>;
    }
    return <div>{content}</div>;
}

export default App;

这里通过 if 语句判断 isLoggedIn 的值,然后根据不同情况给 content 赋值不同的 JSX 元素,最后在 return 中渲染 content

除了 if 语句,还可以使用三元运算符来进行简洁的条件渲染:

import React from 'react';

function App() {
    const isLoggedIn = true;
    return (
        <div>
            {isLoggedIn? <p>欢迎,用户已登录</p> : <p>请登录</p>}
        </div>
    );
}

export default App;

三元运算符的形式更加紧凑,适用于简单的条件判断。

另外,在 React 中还可以使用 && 运算符来进行条件渲染。这种方式常用于只在某个条件为真时渲染某个元素的场景。例如:

import React from 'react';

function App() {
    const isLoggedIn = true;
    return (
        <div>
            {isLoggedIn && <p>欢迎,用户已登录</p>}
        </div>
    );
}

export default App;

isLoggedIntrue 时,&& 后面的 <p>欢迎,用户已登录</p> 会被渲染;当 isLoggedInfalse 时,&& 后面的内容不会被渲染,也不会在 DOM 中产生任何节点。

二、常见坑点及解决办法

2.1 错误使用三元运算符导致的 UI 闪烁

坑点描述:在一些复杂的条件渲染场景中,错误地使用三元运算符可能会导致 UI 闪烁的问题。例如,在一个根据用户角色来显示不同菜单的应用中,当角色信息在组件挂载后异步获取并更新时,可能会出现闪烁现象。

代码示例

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

function App() {
    const [userRole, setUserRole] = useState(null);

    useEffect(() => {
        // 模拟异步获取用户角色
        setTimeout(() => {
            setUserRole('admin');
        }, 2000);
    }, []);

    return (
        <div>
            {userRole === 'admin'? <p>显示管理员菜单</p> : <p>显示普通用户菜单</p>}
        </div>
    );
}

export default App;

在这个例子中,组件挂载时 userRolenull,会显示 “显示普通用户菜单”,两秒后 userRole 更新为 admin,会显示 “显示管理员菜单”,这个过程中会出现 UI 闪烁。

解决办法:在异步获取数据时,可以先显示一个加载状态,避免直接在 null 值和实际值之间切换。

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

function App() {
    const [userRole, setUserRole] = useState(null);
    const [isLoading, setIsLoading] = useState(true);

    useEffect(() => {
        setTimeout(() => {
            setUserRole('admin');
            setIsLoading(false);
        }, 2000);
    }, []);

    return (
        <div>
            {isLoading? <p>加载中...</p> : (
                userRole === 'admin'? <p>显示管理员菜单</p> : <p>显示普通用户菜单</p>
            )}
        </div>
    );
}

export default App;

这样在数据加载过程中,始终显示 “加载中...”,避免了 UI 闪烁。

2.2 && 运算符渲染非布尔值的意外情况

坑点描述:当使用 && 运算符进行条件渲染时,如果条件判断后的表达式返回的不是 null 或实际要渲染的元素,可能会出现意外的渲染结果。

代码示例

import React from'react';

function App() {
    const someValue = 0;
    return (
        <div>
            {someValue && <p>这个值应该为真才渲染</p>}
        </div>
    );
}

export default App;

在 JavaScript 中,0 是假值,但由于 && 运算符的特性,它会返回最后一个被求值的表达式。这里 someValue0&& 运算符会返回 0,而 React 会尝试将 0 渲染到 DOM 中,这可能会导致不符合预期的结果。

解决办法:确保 && 运算符右侧的表达式返回的是 null 或者是合法的 JSX 元素。可以先进行类型判断:

import React from'react';

function App() {
    const someValue = 0;
    const shouldRender = typeof someValue === 'number' && someValue > 0;
    return (
        <div>
            {shouldRender && <p>这个值应该为真才渲染</p>}
        </div>
    );
}

export default App;

通过这种方式,只有当 someValue 是大于 0 的数字时,才会渲染 <p>这个值应该为真才渲染</p>

2.3 条件渲染中函数调用的性能问题

坑点描述:在条件渲染中,如果频繁调用函数来生成要渲染的内容,可能会导致性能问题。例如,在一个列表项根据不同条件显示不同格式化文本的场景中,每次渲染都调用格式化函数。

代码示例

import React from'react';

function formatText(text) {
    // 模拟复杂的文本格式化操作
    return text.toUpperCase();
}

function ListItem({ item }) {
    return (
        <li>
            {item.isSpecial? formatText(item.text) : item.text}
        </li>
    );
}

function App() {
    const items = [
        { text: '普通文本', isSpecial: false },
        { text: '特殊文本', isSpecial: true }
    ];
    return (
        <ul>
            {items.map((item, index) => (
                <ListItem key={index} item={item} />
            ))}
        </ul>
    );
}

export default App;

在这个例子中,每次 ListItem 渲染时,如果 item.isSpecialtrue,都会调用 formatText 函数。如果列表项很多,这个函数调用会带来性能开销。

解决办法:可以使用 useMemo 来缓存函数调用的结果。

import React, { useMemo } from'react';

function formatText(text) {
    // 模拟复杂的文本格式化操作
    return text.toUpperCase();
}

function ListItem({ item }) {
    const formattedText = useMemo(() => {
        return item.isSpecial? formatText(item.text) : item.text;
    }, [item.isSpecial, item.text]);
    return (
        <li>
            {formattedText}
        </li>
    );
}

function App() {
    const items = [
        { text: '普通文本', isSpecial: false },
        { text: '特殊文本', isSpecial: true }
    ];
    return (
        <ul>
            {items.map((item, index) => (
                <ListItem key={index} item={item} />
            ))}
        </ul>
    );
}

export default App;

通过 useMemo,只有当 item.isSpecialitem.text 变化时,才会重新调用 formatText 函数,从而提高性能。

2.4 条件渲染与组件生命周期的冲突

坑点描述:在 React 组件中,条件渲染可能会与组件的生命周期方法产生冲突。例如,在一个组件根据条件渲染或卸载的过程中,可能会导致生命周期方法调用顺序不符合预期,从而引发一些逻辑错误。

代码示例

import React, { Component } from'react';

class ConditionalComponent extends Component {
    componentDidMount() {
        console.log('组件挂载');
    }
    componentWillUnmount() {
        console.log('组件卸载');
    }
    render() {
        return <p>条件渲染的组件</p>;
    }
}

class App extends Component {
    constructor(props) {
        super(props);
        this.state = { shouldRender: true };
    }
    toggleRender = () => {
        this.setState({ shouldRender:!this.state.shouldRender });
    };
    render() {
        return (
            <div>
                <button onClick={this.toggleRender}>切换渲染</button>
                {this.state.shouldRender && <ConditionalComponent />}
            </div>
        );
    }
}

export default App;

在这个例子中,当点击按钮切换 shouldRender 状态时,ConditionalComponent 会被挂载或卸载。如果在 ConditionalComponent 的生命周期方法中有一些重要的初始化或清理逻辑,可能会因为频繁的挂载和卸载而出现问题。

解决办法:可以使用 React.memoshouldComponentUpdate 来优化组件的更新。对于类组件,可以通过 shouldComponentUpdate 方法来控制组件是否需要更新。

import React, { Component } from'react';

class ConditionalComponent extends Component {
    componentDidMount() {
        console.log('组件挂载');
    }
    componentWillUnmount() {
        console.log('组件卸载');
    }
    shouldComponentUpdate(nextProps, nextState) {
        // 这里可以根据具体情况进行判断,例如只在 props 变化时更新
        return false;
    }
    render() {
        return <p>条件渲染的组件</p>;
    }
}

class App extends Component {
    constructor(props) {
        super(props);
        this.state = { shouldRender: true };
    }
    toggleRender = () => {
        this.setState({ shouldRender:!this.state.shouldRender });
    };
    render() {
        return (
            <div>
                <button onClick={this.toggleRender}>切换渲染</button>
                {this.state.shouldRender && <ConditionalComponent />}
            </div>
        );
    }
}

export default App;

通过 shouldComponentUpdate 返回 false,可以避免不必要的更新,减少因频繁挂载和卸载带来的问题。对于函数组件,可以使用 React.memo 来达到类似的效果。

2.5 条件渲染中样式的动态切换问题

坑点描述:在条件渲染时,动态切换样式可能会遇到一些问题。例如,想要根据一个布尔值来切换元素的类名,可能会因为错误的写法导致样式无法正确应用。

代码示例

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

function App() {
    const isActive = true;
    return (
        <div className={isActive? 'active' : ''}>
            切换样式的元素
        </div>
    );
}

export default App;

styles.css 中定义了 .active 类:

.active {
    color: red;
}

这个例子看起来很简单,但是如果 isActive 频繁变化,可能会导致样式切换不流畅,特别是在复杂的样式场景下。

解决办法:可以使用 CSS 变量结合 useState 来实现更平滑的样式切换。

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

function App() {
    const [isActive, setIsActive] = useState(true);
    return (
        <div className={`dynamic - class ${isActive? 'active' : ''}`}>
            切换样式的元素
            <button onClick={() => setIsActive(!isActive)}>切换</button>
        </div>
    );
}

export default App;

styles.css 中:

:root {
    --text - color: black;
}

.dynamic - class {
    color: var(--text - color);
}

.active {
    --text - color: red;
}

通过这种方式,利用 CSS 变量的特性,可以实现更平滑的样式过渡,避免一些因直接切换类名带来的问题。

2.6 嵌套条件渲染的代码可读性问题

坑点描述:当条件渲染嵌套层次过多时,代码的可读性会变得很差。例如,在一个根据用户权限、用户角色以及业务状态来决定显示内容的复杂场景中,嵌套的条件渲染可能会让代码难以理解和维护。

代码示例

import React from'react';

function App() {
    const hasPermission = true;
    const userRole = 'admin';
    const businessStatus = 'active';
    return (
        <div>
            {hasPermission && (
                userRole === 'admin' && (
                    businessStatus === 'active'? <p>显示特定内容</p> : <p>业务状态不匹配</p>
                )
            )}
        </div>
    );
}

export default App;

这样的嵌套结构使得代码逻辑难以一眼看清,特别是当条件判断逻辑变得更加复杂时。

解决办法:可以通过提取函数或使用对象字面量来提高代码的可读性。 提取函数方式

import React from'react';

function shouldRenderContent(hasPermission, userRole, businessStatus) {
    return hasPermission && userRole === 'admin' && businessStatus === 'active';
}

function App() {
    const hasPermission = true;
    const userRole = 'admin';
    const businessStatus = 'active';
    return (
        <div>
            {shouldRenderContent(hasPermission, userRole, businessStatus)? <p>显示特定内容</p> : <p>业务状态不匹配</p>}
        </div>
    );
}

export default App;

对象字面量方式

import React from'react';

function App() {
    const hasPermission = true;
    const userRole = 'admin';
    const businessStatus = 'active';
    const conditions = {
        hasPermission,
        userRole: userRole === 'admin',
        businessStatus: businessStatus === 'active'
    };
    return (
        <div>
            {conditions.hasPermission && conditions.userRole && conditions.businessStatus? <p>显示特定内容</p> : <p>业务状态不匹配</p>}
        </div>
    );
}

export default App;

这两种方式都能使条件判断逻辑更加清晰,提高代码的可读性和可维护性。

2.7 条件渲染中上下文丢失问题

坑点描述:在 React 应用中,当使用上下文(Context)时,条件渲染可能会导致上下文丢失的问题。例如,在一个多层嵌套的组件结构中,某个组件根据条件渲染或不渲染,可能会影响到上下文的传递。

代码示例

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

const UserContext = createContext();

function ParentComponent() {
    const [user, setUser] = useState({ name: 'John' });
    return (
        <UserContext.Provider value={user}>
            <ChildComponent />
        </UserContext.Provider>
    );
}

function ChildComponent() {
    const shouldRenderGrandChild = false;
    return (
        <div>
            {shouldRenderGrandChild && <GrandChildComponent />}
        </div>
    );
}

function GrandChildComponent() {
    const user = React.useContext(UserContext);
    return <p>用户: {user.name}</p>;
}

function App() {
    return <ParentComponent />;
}

export default App;

在这个例子中,由于 shouldRenderGrandChildfalseGrandChildComponent 不会被渲染,此时如果 GrandChildComponent 依赖上下文来获取数据,就会出现上下文丢失的情况,因为它没有被挂载到上下文传递的组件树中。

解决办法:可以通过将上下文数据作为 props 传递给需要的组件,而不是仅仅依赖上下文。

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

const UserContext = createContext();

function ParentComponent() {
    const [user, setUser] = useState({ name: 'John' });
    return (
        <UserContext.Provider value={user}>
            <ChildComponent user={user} />
        </UserContext.Provider>
    );
}

function ChildComponent({ user }) {
    const shouldRenderGrandChild = false;
    return (
        <div>
            {shouldRenderGrandChild && <GrandChildComponent user={user} />}
        </div>
    );
}

function GrandChildComponent({ user }) {
    return <p>用户: {user.name}</p>;
}

function App() {
    return <ParentComponent />;
}

export default App;

通过这种方式,即使 GrandChildComponent 是条件渲染的,也能通过 props 获取到所需的数据,避免了上下文丢失的问题。

2.8 条件渲染与动画效果的配合问题

坑点描述:在 React 中,当需要结合条件渲染来实现动画效果时,可能会遇到一些问题。例如,使用 CSS 动画或 React 动画库时,元素的显示和隐藏可能与动画的触发和结束时机不匹配。

代码示例:假设使用 CSS 动画来实现元素的淡入淡出效果。

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

function App() {
    const [isVisible, setIsVisible] = useState(true);
    return (
        <div>
            <button onClick={() => setIsVisible(!isVisible)}>切换可见性</button>
            {isVisible && <div className="fade - in - out">动画元素</div>}
        </div>
    );
}

export default App;

styles.css 中:

.fade - in - out {
    opacity: 1;
    transition: opacity 0.5s ease - in - out;
}

.fade - in - out.hidden {
    opacity: 0;
}

这里的问题是,当 isVisible 变为 false 时,元素直接从 DOM 中移除,没有机会执行淡出动画。

解决办法:可以使用 React 动画库,如 react - transition - group 来更好地控制动画与条件渲染的配合。

import React, { useState } from'react';
import { CSSTransition } from'react - transition - group';
import './styles.css';

function App() {
    const [isVisible, setIsVisible] = useState(true);
    return (
        <div>
            <button onClick={() => setIsVisible(!isVisible)}>切换可见性</button>
            <CSSTransition
                in={isVisible}
                timeout={500}
                classNames="fade - in - out"
                unmountOnExit
            >
                <div>动画元素</div>
            </CSSTransition>
        </div>
    );
}

export default App;

styles.css 中:

.fade - in - out - enter {
    opacity: 0;
}
.fade - in - out - enter.fade - in - out - enter - active {
    opacity: 1;
    transition: opacity 0.5s ease - in - out;
}
.fade - in - out - exit {
    opacity: 1;
}
.fade - in - out - exit.fade - in - out - exit - active {
    opacity: 0;
    transition: opacity 0.5s ease - in - out;
}

通过 react - transition - group,可以在元素显示和隐藏时,正确地触发和执行动画,避免了条件渲染与动画效果配合不当的问题。

2.9 条件渲染与表单元素的联动问题

坑点描述:在 React 应用中,当条件渲染涉及到表单元素时,可能会出现表单元素与条件渲染状态联动不一致的问题。例如,根据某个条件显示或隐藏一个表单输入框,并且这个输入框的值需要与其他状态关联。

代码示例

import React, { useState } from'react';

function App() {
    const [isInputVisible, setIsInputVisible] = useState(true);
    const [inputValue, setInputValue] = useState('');
    return (
        <div>
            <input
                type="checkbox"
                onChange={() => setIsInputVisible(!isInputVisible)}
            />显示输入框
            {isInputVisible && (
                <input
                    type="text"
                    value={inputValue}
                    onChange={(e) => setInputValue(e.target.value)}
                />
            )}
            <p>输入框值: {inputValue}</p>
        </div>
    );
}

export default App;

这里当隐藏输入框后再显示,输入框的值会被重置。这是因为每次隐藏和显示时,输入框组件被重新创建,导致其状态丢失。

解决办法:可以使用 key 属性来保持组件的状态。

import React, { useState } from'react';

function App() {
    const [isInputVisible, setIsInputVisible] = useState(true);
    const [inputValue, setInputValue] = useState('');
    return (
        <div>
            <input
                type="checkbox"
                onChange={() => setIsInputVisible(!isInputVisible)}
            />显示输入框
            {isInputVisible && (
                <input
                    key="input - field"
                    type="text"
                    value={inputValue}
                    onChange={(e) => setInputValue(e.target.value)}
                />
            )}
            <p>输入框值: {inputValue}</p>
        </div>
    );
}

export default App;

通过给输入框添加 key 属性,React 可以识别这个组件,即使它被隐藏和重新显示,也能保持其状态,解决了表单元素与条件渲染状态联动不一致的问题。

2.10 条件渲染在 SSR(服务器端渲染)中的问题

坑点描述:在使用 React 进行服务器端渲染(SSR)时,条件渲染可能会导致一些问题。例如,在服务器端和客户端渲染过程中,由于环境差异,条件判断的结果可能不一致,从而导致页面闪烁或内容不匹配。

代码示例:假设在一个 SSR 应用中,根据用户代理来判断是否显示移动端特定的组件。

import React from'react';

function App() {
    const isMobile = typeof navigator!== 'undefined' && /mobile/i.test(navigator.userAgent);
    return (
        <div>
            {isMobile && <p>显示移动端组件</p>}
        </div>
    );
}

export default App;

在服务器端,navigator 对象是不存在的,这会导致在服务器端渲染时 isMobile 始终为 false,而在客户端渲染时可能为 true,从而造成页面内容的不一致。

解决办法:可以在服务器端传递一些环境相关的信息到客户端,或者使用专门的库来处理 SSR 中的条件渲染。例如,使用 next - js 框架,它提供了一些机制来处理这类问题。在 next - js 中,可以通过 getServerSideProps 方法在服务器端获取一些信息,并传递给客户端组件。

import React from'react';

function App({ isMobile }) {
    return (
        <div>
            {isMobile && <p>显示移动端组件</p>}
        </div>
    );
}

export async function getServerSideProps(context) {
    const userAgent = context.req? context.req.headers['user - agent'] : '';
    const isMobile = /mobile/i.test(userAgent);
    return {
        props: {
            isMobile
        }
    };
}

export default App;

通过这种方式,在服务器端获取 user - agent 并判断是否为移动端,然后将这个信息传递给客户端组件,保证了在服务器端和客户端渲染时条件判断的一致性,避免了因 SSR 中条件渲染不一致带来的问题。