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

React 条件渲染与自定义 Hooks 的应用

2021-05-021.7k 阅读

React 条件渲染

在 React 应用开发中,条件渲染是一项非常重要的技术,它允许我们根据不同的条件来决定渲染哪些内容。这在处理动态 UI 时尤为关键,例如根据用户的登录状态展示不同的界面,或者根据数据的加载状态显示加载指示器等。

1. 使用 if 语句进行条件渲染

最常见的条件渲染方式就是使用 JavaScript 的 if 语句。假设我们有一个 React 组件,根据用户是否登录来显示不同的内容。

import React from'react';

function UserGreeting() {
  return <p>欢迎回来!</p>;
}

function GuestGreeting() {
  return <p>请登录。</p>;
}

function Greeting({ isLoggedIn }) {
  if (isLoggedIn) {
    return <UserGreeting />;
  }
  return <GuestGreeting />;
}

export default function App() {
  const isLoggedIn = true;
  return (
    <div>
      <Greeting isLoggedIn={isLoggedIn} />
    </div>
  );
}

在上述代码中,Greeting 组件接收一个 isLoggedIn 属性,通过 if 语句判断该属性的值,然后决定渲染 UserGreeting 还是 GuestGreeting 组件。

2. 使用逻辑与(&&)运算符进行条件渲染

逻辑与(&&)运算符也可用于条件渲染。这种方式在需要在满足某个条件时才渲染某个元素时非常方便。

import React from'react';

function LoadingIndicator() {
  return <p>加载中...</p>;
}

function DataDisplay({ data }) {
  return (
    <div>
      {data? <p>{data}</p> : <LoadingIndicator />}
    </div>
  );
}

export default function App() {
  const data = null;
  return (
    <div>
      <DataDisplay data={data} />
    </div>
  );
}

DataDisplay 组件中,通过 data? <p>{data}</p> : <LoadingIndicator /> 这样的表达式,当 data 存在时显示数据,否则显示加载指示器。

3. 使用三目运算符进行条件渲染

三目运算符(condition? trueExpression : falseExpression)同样可以实现条件渲染,它在简单的条件判断场景下非常直观。

import React from'react';

function Button({ isDisabled }) {
  return (
    <button disabled={isDisabled}>
      {isDisabled? '按钮不可用' : '点击我'}
    </button>
  );
}

export default function App() {
  const isDisabled = true;
  return (
    <div>
      <Button isDisabled={isDisabled} />
    </div>
  );
}

Button 组件中,通过三目运算符根据 isDisabled 的值来设置按钮的文本和禁用状态。

React 自定义 Hooks

Hooks 是 React 16.8 引入的新特性,它允许我们在不编写类的情况下使用状态和其他 React 特性。自定义 Hooks 则是一种更高层次的抽象,让我们可以复用状态逻辑。

1. 自定义 Hooks 基础

自定义 Hooks 本质上是一个 JavaScript 函数,其名称以 use 开头,并且可以调用其他 Hooks。例如,我们来创建一个简单的 useToggle 自定义 Hook,用于切换布尔值状态。

import { useState } from'react';

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = () => setValue(!value);
  return [value, toggle];
}

function ToggleButton() {
  const [isOn, toggle] = useToggle();
  return (
    <button onClick={toggle}>
      {isOn? '关闭' : '打开'}
    </button>
  );
}

export default function App() {
  return (
    <div>
      <ToggleButton />
    </div>
  );
}

在上述代码中,useToggle 自定义 Hook 使用了 useState Hook 来管理状态,并返回当前状态值和一个切换状态的函数。ToggleButton 组件使用 useToggle Hook 来实现按钮的开关切换功能。

2. 自定义 Hooks 与条件渲染结合

我们可以将自定义 Hooks 和条件渲染结合使用,以实现更复杂的功能。比如,我们创建一个 useFetch 自定义 Hook 用于数据请求,并根据请求状态进行条件渲染。

import { useState, useEffect } from'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [url]);

  return { data, loading, error };
}

function DataComponent() {
  const { data, loading, error } = useFetch('https://example.com/api/data');

  if (loading) {
    return <p>加载中...</p>;
  }

  if (error) {
    return <p>加载数据出错: {error.message}</p>;
  }

  return (
    <div>
      <p>{JSON.stringify(data)}</p>
    </div>
  );
}

export default function App() {
  return (
    <div>
      <DataComponent />
    </div>
  );
}

useFetch 自定义 Hook 中,通过 useStateuseEffect 实现数据的异步请求,并管理请求状态(加载中、数据、错误)。DataComponent 组件使用 useFetch Hook,并根据不同的状态进行条件渲染,加载中显示加载提示,出错显示错误信息,数据加载成功则显示数据。

3. 自定义 Hooks 的复用性

自定义 Hooks 的一大优势就是其复用性。我们可以在多个组件中使用同一个自定义 Hook,而不需要重复编写相同的状态逻辑。例如,我们有多个需要进行数据请求的组件,都可以使用 useFetch 自定义 Hook。

import { useState, useEffect } from'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [url]);

  return { data, loading, error };
}

function FirstDataComponent() {
  const { data, loading, error } = useFetch('https://example.com/api/firstData');

  if (loading) {
    return <p>加载中...</p>;
  }

  if (error) {
    return <p>加载数据出错: {error.message}</p>;
  }

  return (
    <div>
      <p>{JSON.stringify(data)}</p>
    </div>
  );
}

function SecondDataComponent() {
  const { data, loading, error } = useFetch('https://example.com/api/secondData');

  if (loading) {
    return <p>加载中...</p>;
  }

  if (error) {
    return <p>加载数据出错: {error.message}</p>;
  }

  return (
    <div>
      <p>{JSON.stringify(data)}</p>
    </div>
  );
}

export default function App() {
  return (
    <div>
      <FirstDataComponent />
      <SecondDataComponent />
    </div>
  );
}

在上述代码中,FirstDataComponentSecondDataComponent 组件都使用了 useFetch 自定义 Hook 来进行数据请求,极大地提高了代码的复用性。

深入理解 React 条件渲染与自定义 Hooks 的原理

1. 条件渲染原理

React 条件渲染的实现依赖于 JavaScript 的条件判断机制。当 React 渲染组件时,它会计算条件表达式的值,根据结果决定渲染哪些内容。在虚拟 DOM 的构建过程中,不同条件下的渲染结果会生成不同的虚拟 DOM 树结构。

例如,当使用 if 语句进行条件渲染时:

function ConditionalComponent({ condition }) {
  if (condition) {
    return <p>条件为真</p>;
  }
  return <p>条件为假</p>;
}

React 在渲染 ConditionalComponent 时,会首先判断 condition 的值。如果 condition 为真,就会构建包含 <p>条件为真</p> 的虚拟 DOM 节点;如果为假,则构建包含 <p>条件为假</p> 的虚拟 DOM 节点。之后,React 会将虚拟 DOM 与实际 DOM 进行对比,只更新那些有变化的部分,从而高效地更新 UI。

2. 自定义 Hooks 原理

自定义 Hooks 基于 React 的 Hooks 机制。Hooks 本质上是利用了闭包和函数作用域来实现状态和副作用的管理。当我们调用 useState 等内置 Hooks 时,React 会在内部维护一个 Hook 链表,每个 Hook 在链表中都有自己的位置。

useState 为例,每次调用 useState 时,React 会从链表中取出对应位置的状态和更新函数。对于自定义 Hooks,由于它们可以调用其他 Hooks,同样遵循这个机制。例如在 useToggle 自定义 Hook 中:

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = () => setValue(!value);
  return [value, toggle];
}

useToggle 调用 useState 时,React 会为其在 Hook 链表中分配一个位置,用于存储 valuesetValue。当 toggle 函数被调用时,它通过闭包访问到 setValue,从而更新状态。

3. 两者结合的原理

当条件渲染与自定义 Hooks 结合使用时,条件渲染会影响自定义 Hooks 的执行和状态的更新。例如在 DataComponent 组件中使用 useFetch 自定义 Hook 并进行条件渲染:

function DataComponent() {
  const { data, loading, error } = useFetch('https://example.com/api/data');

  if (loading) {
    return <p>加载中...</p>;
  }

  if (error) {
    return <p>加载数据出错: {error.message}</p>;
  }

  return (
    <div>
      <p>{JSON.stringify(data)}</p>
    </div>
  );
}

useFetch Hook 在组件挂载时开始执行数据请求,随着请求状态的变化(loading、data、error),组件会重新渲染。条件渲染根据这些状态值决定渲染不同的内容。在这个过程中,自定义 Hook 的状态变化驱动了条件渲染的结果,而条件渲染则根据不同的状态展示合适的 UI。

实践中的优化与注意事项

1. 条件渲染的优化

在条件渲染中,尽量减少不必要的渲染。例如,如果某个组件在条件为真时渲染,并且该组件的 props 没有变化,我们可以使用 React.memo 来优化。

import React from'react';

const ExpensiveComponent = React.memo(({ value }) => {
  console.log('渲染 ExpensiveComponent');
  return <p>{value}</p>;
});

function ConditionalRendering({ condition }) {
  return (
    <div>
      {condition && <ExpensiveComponent value="条件为真" />}
    </div>
  );
}

export default function App() {
  const [condition, setCondition] = useState(false);
  return (
    <div>
      <button onClick={() => setCondition(!condition)}>切换条件</button>
      <ConditionalRendering condition={condition} />
    </div>
  );
}

在上述代码中,ExpensiveComponent 使用 React.memo 进行包裹,只有当 value prop 变化时才会重新渲染,即使 condition 变化导致组件的显示与否改变,只要 value 不变,ExpensiveComponent 就不会重新渲染,从而提高性能。

2. 自定义 Hooks 的注意事项

  • 只能在函数最外层调用 Hooks:不能在循环、条件判断或者嵌套函数中调用 Hooks,这是因为 React 依靠调用顺序来正确地识别和管理 Hooks。例如:
// 错误示例
function WrongUsage() {
  let shouldUseHook = true;
  if (shouldUseHook) {
    const [value, setValue] = useState(0); // 错误:不能在条件判断中调用 useState
    return <p>{value}</p>;
  }
  return null;
}
  • 自定义 Hooks 的命名规范:自定义 Hooks 名称必须以 use 开头,这样 React 可以识别它们并应用相关规则。

3. 两者结合的优化

当条件渲染与自定义 Hooks 结合时,要注意避免无效的 Hook 调用。例如,如果某个自定义 Hook 在条件渲染的某个分支中调用,并且该分支在某些情况下不会执行,可能会导致 Hook 调用顺序不一致。

// 错误示例
function WrongCombination({ condition }) {
  if (condition) {
    const [value, setValue] = useState(0); // 错误:在条件分支中调用 useState
    return <p>{value}</p>;
  }
  return null;
}

为了避免这种情况,可以将自定义 Hook 的调用放在组件的顶层,通过条件渲染来控制基于 Hook 状态的 UI 显示。

function CorrectCombination({ condition }) {
  const [value, setValue] = useState(0);
  return (
    <div>
      {condition && <p>{value}</p>}
    </div>
  );
}

这样可以确保 useState Hook 的调用顺序始终一致,避免潜在的错误。

复杂场景下的应用

1. 多条件复杂判断的条件渲染

在实际应用中,可能会遇到需要进行多条件复杂判断的情况。例如,根据用户的角色、权限以及数据状态来决定渲染的内容。

import React from'react';

function AdminDashboard() {
  return <p>这是管理员仪表盘</p>;
}

function UserDashboard() {
  return <p>这是普通用户仪表盘</p>;
}

function GuestView() {
  return <p>请登录以查看更多内容</p>;
}

function Dashboard({ userRole, hasData, isLoggedIn }) {
  if (!isLoggedIn) {
    return <GuestView />;
  }

  if (userRole === 'admin') {
    return <AdminDashboard />;
  }

  if (hasData) {
    return <UserDashboard />;
  }

  return <p>数据加载中...</p>;
}

export default function App() {
  const userRole = 'user';
  const hasData = true;
  const isLoggedIn = true;
  return (
    <div>
      <Dashboard userRole={userRole} hasData={hasData} isLoggedIn={isLoggedIn} />
    </div>
  );
}

Dashboard 组件中,通过多个 if 语句对 isLoggedInuserRolehasData 进行多条件判断,根据不同的组合渲染不同的内容。

2. 自定义 Hooks 在复杂业务逻辑中的应用

自定义 Hooks 在处理复杂业务逻辑时非常有用。比如,实现一个具有缓存功能的 useCachedFetch 自定义 Hook。

import { useState, useEffect } from'react';

const cache = {};

function useCachedFetch(url) {
  const [data, setData] = useState(cache[url] || null);
  const [loading, setLoading] = useState(cache[url] === undefined);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (cache[url]) {
      return;
    }

    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const result = await response.json();
        cache[url] = result;
        setData(result);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [url]);

  return { data, loading, error };
}

function CachedDataComponent() {
  const { data, loading, error } = useCachedFetch('https://example.com/api/cachedData');

  if (loading) {
    return <p>加载中...</p>;
  }

  if (error) {
    return <p>加载数据出错: {error.message}</p>;
  }

  return (
    <div>
      <p>{JSON.stringify(data)}</p>
    </div>
  );
}

export default function App() {
  return (
    <div>
      <CachedDataComponent />
    </div>
  );
}

useCachedFetch 自定义 Hook 中,通过一个全局的 cache 对象来缓存数据。当再次请求相同的 URL 时,如果缓存中已有数据,则直接使用缓存数据,避免重复请求,提高性能。

3. 条件渲染与自定义 Hooks 在表单处理中的应用

在表单处理中,条件渲染和自定义 Hooks 也能发挥重要作用。例如,根据用户输入的内容动态显示不同的表单字段,并使用自定义 Hook 管理表单状态。

import { useState, useEffect } from'react';

function useForm(initialState = {}) {
  const [formData, setFormData] = useState(initialState);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({...formData, [name]: value });
  };

  return { formData, handleChange };
}

function FormComponent() {
  const { formData, handleChange } = useForm({ type: 'text' });

  return (
    <form>
      <label>
        选择类型:
        <select name="type" onChange={handleChange}>
          <option value="text">文本</option>
          <option value="number">数字</option>
        </select>
      </label>
      {formData.type === 'text' && (
        <label>
          输入文本:
          <input type="text" name="textValue" onChange={handleChange} />
        </label>
      )}
      {formData.type === 'number' && (
        <label>
          输入数字:
          <input type="number" name="numberValue" onChange={handleChange} />
        </label>
      )}
    </form>
  );
}

export default function App() {
  return (
    <div>
      <FormComponent />
    </div>
  );
}

FormComponent 中,使用 useForm 自定义 Hook 来管理表单数据。通过条件渲染,根据用户选择的 type 值来决定显示文本输入框还是数字输入框,实现了动态表单字段的显示。

与其他 React 特性的结合

1. 与 React Router 的结合

React Router 是用于在 React 应用中实现路由功能的库。条件渲染和自定义 Hooks 可以与 React Router 很好地结合。例如,根据用户的登录状态决定是否显示某些路由。

import { BrowserRouter as Router, Routes, Route, Link } from'react-router-dom';
import React from'react';

function PrivateRoute({ isLoggedIn, children }) {
  if (!isLoggedIn) {
    return null;
  }
  return children;
}

function Login() {
  return <p>登录页面</p>;
}

function Dashboard() {
  return <p>仪表盘页面</p>;
}

function App() {
  const isLoggedIn = true;
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/login">登录</Link>
            </li>
            <li>
              <Link to="/dashboard">仪表盘</Link>
            </li>
          </ul>
        </nav>
        <Routes>
          <Route path="/login" element={<Login />} />
          <Route path="/dashboard" element={<PrivateRoute isLoggedIn={isLoggedIn}><Dashboard /></PrivateRoute>} />
        </Routes>
      </div>
    </Router>
  );
}

export default App;

在上述代码中,PrivateRoute 组件通过条件渲染,只有当 isLoggedIn 为真时才会渲染其子组件,即 Dashboard 组件对应的路由。这样实现了根据用户登录状态来控制路由的访问。

2. 与 Redux 的结合

Redux 是用于管理 React 应用状态的库。自定义 Hooks 可以与 Redux 结合使用,以更方便地获取和更新 Redux 状态。例如,使用 react - redux 库的 useSelectoruseDispatch Hooks。

import React from'react';
import { useSelector, useDispatch } from'react-redux';
import { increment, decrement } from './actions';

function Counter() {
  const count = useSelector((state) => state.count);
  const dispatch = useDispatch();

  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={() => dispatch(increment())}>增加</button>
      <button onClick={() => dispatch(decrement())}>减少</button>
    </div>
  );
}

export default Counter;

Counter 组件中,通过 useSelector Hook 获取 Redux 状态中的 count 值,通过 useDispatch Hook 获取 dispatch 函数,用于触发 incrementdecrement 等 Redux 动作,从而更新状态。条件渲染也可以基于 Redux 状态进行,例如根据 count 的值显示不同的提示信息。

3. 与 Context 的结合

React Context 用于在组件树中共享数据。自定义 Hooks 可以与 Context 结合,简化数据的获取和更新。例如,创建一个自定义 Hook 来获取特定 Context 的值。

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

const ThemeContext = createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const toggleTheme = () => setTheme(theme === 'light'? 'dark' : 'light');

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function useTheme() {
  return useContext(ThemeContext);
}

function ThemeToggleButton() {
  const { theme, toggleTheme } = useTheme();
  return (
    <button onClick={toggleTheme}>
      切换主题 ({theme === 'light'? '到黑暗' : '到明亮'})
    </button>
  );
}

function App() {
  return (
    <ThemeProvider>
      <ThemeToggleButton />
    </ThemeProvider>
  );
}

export default App;

在上述代码中,useTheme 自定义 Hook 使用 useContext 来获取 ThemeContext 的值。ThemeToggleButton 组件通过 useTheme Hook 方便地获取主题状态和切换主题的函数,实现了基于 Context 的数据共享和交互,条件渲染也可以基于 theme 值来改变组件的样式等。

通过以上对 React 条件渲染与自定义 Hooks 的深入探讨,我们可以看到它们在 React 应用开发中的强大功能和广泛应用场景。合理地运用这两项技术,能够提高代码的复用性、可维护性和性能,帮助我们构建出更加高效、灵活的 React 应用。无论是简单的 UI 切换,还是复杂的业务逻辑处理,条件渲染和自定义 Hooks 都能为我们提供有效的解决方案。同时,与其他 React 特性的结合使用,进一步拓展了它们的应用范围,使 React 应用开发更加丰富多彩。