React学习:React.StrictMode 渲染2次的问题

由于用脚手架创建项目,默认会自动启用严格模式,所以你会发现双调用现象
更新于: 2022-04-26 11:35:25

React.StrictMode

  1. 严格模式检查只在开发模式下运行,不会与生产模式冲突。
  2. StrictMode 是一个用以标记出应用中潜在问题的工具。就像 Fragment/StrictMode不会渲染任何真实的UI。它为其后代元素触发额外的检查和警告。
  3. 因为严格模式不能自动检测到你的副作用, 但它可以帮助你发现它们, 使其更具确定性。这是通过有意地双调用以下方法来完成的,但是你可能不太习惯,那就关了它
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

关于 React.StrictMode 作用

  • 识别不安全的生命周期
  • 关于使用过时字符串 ref API 的警告
  • 关于使用废弃的 findDOMNode 方法的警告
  • 检测意外的副作用
  • 检测过时(遗留)的 context API

useAxios/useFetch(稍稍改进)

  • useAxios
    • data/loading/error 三个状态
    • source.cancel(); 利用 abort 处理 umount 状态
  • useFetch(usehooks-ts版本)
    • 利用 reducer 方式代替3个state
    • 逻辑更加的清晰
    • 问题:当这个useFetch 多次调用的时候,会导致 cancelRequest.current = true 被执行,后面的逻辑不会再执行了
    • 改进版:process.env.NODE_ENV === ‘development’ 的时候,且 React.StrictMode 下,不用这个逻辑
    • 不过,这里更好的实现方式是利用 AbortControl 来完成真正的副作用的清除,这样还可以保证,组件销毁的时候,请求被 abort 掉
// unmount,在 production 的时候,才会执行正确的逻辑
// React.StrictMode 执行2次,会导致第2次不会取到数据 
return () => {
    console.log('unmounting')
    if (process.env.NODE_ENV === 'production') {
        cancelRequest.current = true
    }
}
import {useState, useEffect} from 'react';
import axios from 'axios';

function useAxios(url: string) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState<boolean>(false);
  const [error, setError] = useState<any>(null);

  useEffect(() => {
    setLoading(true)
    setData(null);
    setError(null);
    const source = axios.CancelToken.source();
    axios.get(url, {cancelToken: source.token})
      .then((res: any) => {
        setLoading(false);
        //checking for multiple responses for more flexibility
        //with the url we send in.
        res.data.content && setData(res.data.content);
        res.content && setData(res.content);
      })
      .catch(err => {
        setLoading(false)
        setError('An error occurred. Awkward..')
      })
    return () => {
      source.cancel();
    }
  }, [url])

  return {data, loading, error}
}

export default useAxios;
import {useEffect, useReducer, useRef} from 'react'

interface State<T> {
  data?: T
  error?: Error
}

type Cache<T> = { [url: string]: T }

// discriminated union type
type Action<T> =
  | { type: 'loading' }
  | { type: 'fetched'; payload: T }
  | { type: 'error'; payload: Error }

function useFetch<T = unknown>(url?: string, options?: RequestInit): State<T> {
  const cache = useRef<Cache<T>>({})

  // Used to prevent state update if the component is unmounted
  const cancelRequest = useRef<boolean>(false)

  const initialState: State<T> = {
    error: undefined,
    data: undefined,
  }

  // Keep state logic separated
  const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {
    switch (action.type) {
      case 'loading':
        return {...initialState}
      case 'fetched':
        return {...initialState, data: action.payload}
      case 'error':
        return {...initialState, error: action.payload}
      default:
        return state
    }
  }

  const [state, dispatch] = useReducer(fetchReducer, initialState)

  useEffect(() => {
    // Do nothing if the url is not given
    if (!url) return

    const fetchData = async () => {
      dispatch({type: 'loading'})

      // If a cache exists for this url, return it
      if (cache.current[url]) {
        dispatch({type: 'fetched', payload: cache.current[url]})
        return
      }

      try {
        const response = await fetch(url, options)
        if (!response.ok) {
          throw new Error(response.statusText)
        }

        const data = (await response.json()) as T
        cache.current[url] = data
        if (cancelRequest.current) return

        dispatch({type: 'fetched', payload: data})
      } catch (error) {
        if (cancelRequest.current) return

        dispatch({type: 'error', payload: error as Error})
      }
    }

    void fetchData()

    // Use the cleanup function for avoiding a possibly...
    // ...state update after the component was unmounted
    return () => {
      console.log('unmounting')
      if (process.env.NODE_ENV === 'production') {
        cancelRequest.current = true
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [url])

  return state
}

export default useFetch

useFetch abortControl 版本

import React, { useState, useEffect } from "react";
const useFetch = (url, options) => {
  const [response, setResponse] = useState(null)
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);
useEffect(() => {
    const abortController = new AbortController();
    const signal = abortController.signal;
    const doFetch = async () => {
      setLoading(true);
      try {
        const res = await fetch(url, options);
        const json = await res.json();
        if (!signal.aborted) {
          setResponse(json);
        }
      } catch (e) {
        if (!signal.aborted) {
          setError(e);
        }
      } finally {
        if (!signal.aborted) {
          setLoading(false);
        }
      }
    };
    doFetch();
    return () => {
      abortController.abort();
    };
  }, []);
  return { response, error, loading };
};
export default useFetch;

官方的例子

这段代码看起来似乎没有问题。但是如果 SharedApplicationState.recordEvent 不是幂等的情况下,多次实例化此组件可能会导致应用程序状态无效。这种小 bug 可能在开发过程中可能不会表现出来,或者说表现出来但并不明显,并因此被忽视。
严格模式采用故意重复调用方法(如组件的构造函数)的方式,使得这种 bug 更容易被发现。
class TopLevelRoute extends React.Component {
  constructor(props) {
    super(props);
    SharedApplicationState.recordEvent('ExampleComponent');
  }
}

参考