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

React 错误边界与全局状态管理的结合

2021-04-106.2k 阅读

React 错误边界简介

在 React 应用的开发过程中,错误处理是至关重要的一环。React 错误边界是一种 React 组件,它可以捕获其子组件树中 JavaScript 错误,记录这些错误,并展示降级 UI,而不是崩溃整个应用。错误边界只能捕获其子组件树中的错误,无法捕获自身的错误或者异步代码(如 setTimeoutrequestAnimationFrame 回调函数)中的错误。

如何创建错误边界

错误边界组件需要继承 React.Component 并实现 componentDidCatchgetDerivedStateFromError 生命周期方法。下面是一个简单的错误边界示例:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, errorInfo) {
    // 记录错误信息,例如发送到日志服务
    console.log('Error caught:', error, errorInfo);
    this.setState({ hasError: true });
  }

  render() {
    if (this.state.hasError) {
      // 展示降级 UI
      return <div>Something went wrong.</div>;
    }
    return this.props.children;
  }
}

然后在应用中使用这个错误边界:

function App() {
  return (
    <ErrorBoundary>
      <MyComponentThatMightThrow />
    </ErrorBoundary>
  );
}

全局状态管理概述

在 React 应用中,随着应用规模的增长,管理状态变得越来越复杂。全局状态管理可以帮助我们在整个应用中共享和管理状态,避免状态通过多层组件传递(prop drilling)。常见的全局状态管理库有 Redux、MobX 等。

Redux 简介

Redux 是一个流行的状态管理库,它遵循单向数据流的原则。应用的状态存储在一个单一的 store 中,通过 dispatch action 来更新状态,reducer 根据 action 来决定如何更新状态。

以下是一个简单的 Redux 示例:

首先,安装 Redux 和 React - Redux:

npm install redux react-redux

创建一个 reducer:

// counterReducer.js
const initialState = { value: 0 };

const counterReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { value: state.value + 1 };
    case 'DECREMENT':
      return { value: state.value - 1 };
    default:
      return state;
  }
};

export default counterReducer;

创建 store:

// store.js
import { createStore } from'redux';
import counterReducer from './counterReducer';

const store = createStore(counterReducer);

export default store;

在 React 组件中使用 Redux:

import React from'react';
import { useSelector, useDispatch } from'react-redux';

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
    </div>
  );
};

export default Counter;

在应用入口连接 Redux store:

import React from'react';
import ReactDOM from'react-dom';
import { Provider } from'react-redux';
import store from './store';
import App from './App';

ReactDOM.render(
  <Provider store = {store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

MobX 简介

MobX 采用的是响应式编程模型,它自动跟踪状态变化并更新依赖的组件。在 MobX 中,状态被定义为 observable,计算值为 computed,副作用操作放在 action 中。

首先,安装 MobX 和 React - MobX:

npm install mobx mobx-react

创建 observable 状态和 action:

// store.js
import { makeObservable, observable, action } from'mobx';

class CounterStore {
  constructor() {
    this.value = 0;
    makeObservable(this, {
      value: observable,
      increment: action,
      decrement: action
    });
  }

  increment() {
    this.value++;
  }

  decrement() {
    this.value--;
  }
}

const counterStore = new CounterStore();
export default counterStore;

在 React 组件中使用 MobX:

import React from'react';
import { observer } from'mobx-react';
import counterStore from './store';

const Counter = observer(() => {
  return (
    <div>
      <p>Count: {counterStore.value}</p>
      <button onClick={() => counterStore.increment()}>Increment</button>
      <button onClick={() => counterStore.decrement()}>Decrement</button>
    </div>
  );
});

export default Counter;

React 错误边界与全局状态管理的结合方式

将 React 错误边界与全局状态管理结合可以有效地处理应用中的错误,并在错误发生时更新全局状态,以便更好地进行错误处理和恢复。

使用 Redux 与错误边界结合

  1. 在错误边界中更新 Redux 状态

    • 当错误发生时,我们可以通过 dispatch action 来更新 Redux store 中的错误状态。首先,定义一个 action 和 reducer 来处理错误。
    // errorActions.js
    const SET_ERROR = 'SET_ERROR';
    
    const setError = (error, errorInfo) => ({
      type: SET_ERROR,
      payload: { error, errorInfo }
    });
    
    export { setError };
    
    // errorReducer.js
    const initialState = { error: null, errorInfo: null };
    
    const errorReducer = (state = initialState, action) => {
      switch (action.type) {
        case 'SET_ERROR':
          return {
            error: action.payload.error,
            errorInfo: action.payload.errorInfo
          };
        default:
          return state;
      }
    };
    
    export default errorReducer;
    
    • 在 store 中合并这个错误 reducer:
    // store.js
    import { createStore, combineReducers } from'redux';
    import counterReducer from './counterReducer';
    import errorReducer from './errorReducer';
    
    const rootReducer = combineReducers({
      counter: counterReducer,
      error: errorReducer
    });
    
    const store = createStore(rootReducer);
    
    export default store;
    
    • 在错误边界中 dispatch 错误 action:
    import React from'react';
    import { useDispatch } from'react-redux';
    import { setError } from './errorActions';
    
    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
      }
    
      componentDidCatch(error, errorInfo) {
        const dispatch = useDispatch();
        dispatch(setError(error, errorInfo));
      }
    
      render() {
        return this.props.children;
      }
    }
    
    export default ErrorBoundary;
    
  2. 在组件中使用 Redux 错误状态

    • 我们可以在组件中通过 useSelector 获取错误状态,并根据错误状态展示不同的 UI。
    import React from'react';
    import { useSelector } from'react-redux';
    
    const App = () => {
      const error = useSelector(state => state.error.error);
      const errorInfo = useSelector(state => state.error.errorInfo);
    
      if (error) {
        return (
          <div>
            <p>Error: {error.message}</p>
            <pre>{JSON.stringify(errorInfo, null, 2)}</pre>
          </div>
        );
      }
      return (
        <div>
          {/* 正常的应用内容 */}
        </div>
      );
    };
    
    export default App;
    

使用 MobX 与错误边界结合

  1. 在错误边界中更新 MobX 状态

    • 在 MobX store 中定义错误相关的 observable 状态和 action 来更新它。
    // store.js
    import { makeObservable, observable, action } from'mobx';
    
    class AppStore {
      constructor() {
        this.error = null;
        this.errorInfo = null;
        makeObservable(this, {
          error: observable,
          errorInfo: observable,
          setError: action
        });
      }
    
      setError = (error, errorInfo) => {
        this.error = error;
        this.errorInfo = errorInfo;
      };
    }
    
    const appStore = new AppStore();
    export default appStore;
    
    • 在错误边界中调用 MobX action:
    import React from'react';
    import appStore from './store';
    
    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
      }
    
      componentDidCatch(error, errorInfo) {
        appStore.setError(error, errorInfo);
      }
    
      render() {
        return this.props.children;
      }
    }
    
    export default ErrorBoundary;
    
  2. 在组件中使用 MobX 错误状态

    • 使用 observer 包裹组件,使其能够响应 MobX 状态变化。
    import React from'react';
    import { observer } from'mobx-react';
    import appStore from './store';
    
    const App = observer(() => {
      if (appStore.error) {
        return (
          <div>
            <p>Error: {appStore.error.message}</p>
            <pre>{JSON.stringify(appStore.errorInfo, null, 2)}</pre>
          </div>
        );
      }
      return (
        <div>
          {/* 正常的应用内容 */}
        </div>
      );
    });
    
    export default App;
    

结合后的优势

  1. 统一错误处理:通过将错误边界与全局状态管理结合,我们可以在整个应用中统一处理错误。无论是哪个组件发生错误,都可以在全局状态中记录并展示相应的错误信息,方便用户了解问题所在,也便于开发者进行调试。
  2. 更好的用户体验:当错误发生时,错误边界展示降级 UI,同时全局状态更新可以触发其他相关组件的更新,例如显示错误提示框,告知用户发生了错误以及可能的解决方法,避免用户在错误发生后不知所措,提升了用户体验。
  3. 错误恢复与重试:基于全局状态中的错误信息,我们可以实现错误恢复机制。例如,某些错误可能是由于网络问题导致的,当网络恢复后,我们可以根据全局状态中的错误信息进行重试操作,而不需要用户手动刷新页面或进行复杂的操作。

实际应用场景

  1. 数据获取错误:在使用 React 进行数据获取(如通过 fetchaxios)时,经常会遇到网络错误、服务器错误等。将错误边界与全局状态管理结合,可以在数据获取组件发生错误时,记录错误信息到全局状态,同时错误边界展示加载失败的 UI。其他组件可以根据全局状态中的错误信息,提供重试按钮或显示详细的错误原因。

    // dataFetching.js
    import React, { useState, useEffect } from'react';
    import { useDispatch } from'redux';
    import { setError } from './errorActions';
    
    const DataFetchingComponent = () => {
      const [data, setData] = useState(null);
      const dispatch = useDispatch();
    
      useEffect(() => {
        const fetchData = async () => {
          try {
            const response = await fetch('https://example.com/api/data');
            if (!response.ok) {
              throw new Error('Network response was not ok');
            }
            const result = await response.json();
            setData(result);
          } catch (error) {
            dispatch(setError(error, null));
          }
        };
        fetchData();
      }, []);
    
      return (
        <div>
          {data? (
            <p>Data: {JSON.stringify(data)}</p>
          ) : (
            <p>Loading...</p>
          )}
        </div>
      );
    };
    
    export default DataFetchingComponent;
    
  2. 复杂组件交互错误:在一个包含多个复杂交互组件的应用中,例如表单提交、拖拽排序等操作。当某个交互操作导致错误时,错误边界捕获错误,全局状态记录错误信息。这样可以在全局层面展示错误,而不仅仅是在发生错误的组件局部显示,方便用户理解整个交互流程中出现的问题。

    // formComponent.js
    import React from'react';
    import { useDispatch } from'redux';
    import { setError } from './errorActions';
    
    const FormComponent = () => {
      const dispatch = useDispatch();
      const handleSubmit = (e) => {
        e.preventDefault();
        try {
          // 模拟表单提交逻辑
          if (Math.random() > 0.5) {
            throw new Error('Form submission failed');
          }
          console.log('Form submitted successfully');
        } catch (error) {
          dispatch(setError(error, null));
        }
      };
    
      return (
        <form onSubmit={handleSubmit}>
          <input type="text" placeholder="Enter data" />
          <button type="submit">Submit</button>
        </form>
      );
    };
    
    export default FormComponent;
    

注意事项

  1. 错误边界嵌套:避免过度嵌套错误边界。虽然错误边界可以在组件树的不同层级使用,但过多的嵌套可能导致错误处理逻辑混乱。应该在合理的组件层级设置错误边界,例如在页面级组件或者功能模块的顶层组件设置,以确保错误能够被有效地捕获和处理。
  2. 性能影响:在错误边界中更新全局状态时,要注意性能问题。频繁地更新全局状态可能导致不必要的组件重新渲染。可以通过合理使用 shouldComponentUpdate 或者 React.memo 来优化组件渲染,避免性能下降。
  3. 异步错误处理:由于错误边界无法捕获异步代码中的错误,对于异步操作(如 setTimeoutPromise 等),需要手动进行错误处理,并在发生错误时通过全局状态管理更新错误信息。例如,在 Promisecatch 块中 dispatch Redux action 或者调用 MobX action 更新错误状态。
    // asyncOperation.js
    import React from'react';
    import { useDispatch } from'redux';
    import { setError } from './errorActions';
    
    const AsyncOperationComponent = () => {
      const dispatch = useDispatch();
      useEffect(() => {
        const asyncFunction = async () => {
          try {
            await new Promise((resolve, reject) => {
              setTimeout(() => {
                if (Math.random() > 0.5) {
                  reject(new Error('Async operation failed'));
                } else {
                  resolve();
                }
              }, 1000);
            });
            console.log('Async operation completed successfully');
          } catch (error) {
            dispatch(setError(error, null));
          }
        };
        asyncFunction();
      }, []);
    
      return <div>Async operation in progress...</div>;
    };
    
    export default AsyncOperationComponent;
    

通过将 React 错误边界与全局状态管理相结合,我们能够构建更加健壮、用户友好的 React 应用,有效地处理错误并提升应用的整体质量和用户体验。在实际开发中,需要根据应用的具体需求和架构特点,合理选择全局状态管理库,并精心设计错误处理和状态更新的逻辑。