React学习:React.StrictMode 渲染2次的问题
由于用脚手架创建项目,默认会自动启用严格模式,所以你会发现双调用现象
React.StrictMode
- 严格模式检查只在开发模式下运行,不会与生产模式冲突。
StrictMode
是一个用以标记出应用中潜在问题的工具。就像Fragment/StrictMode
不会渲染任何真实的UI。它为其后代元素触发额外的检查和警告。- 因为严格模式不能自动检测到你的副作用, 但它可以帮助你发现它们, 使其更具确定性。这是通过有意地双调用以下方法来完成的,但是你可能不太习惯,那就关了它
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');
}
}