繁体   English   中英

为什么我的 React 组件渲染了两次?

[英]Why is my React component is rendering twice?

我不知道为什么我的 React 组件会渲染两次。 所以我从参数中提取一个电话号码并将其保存到状态,以便我可以搜索 Firestore。 一切似乎都工作正常,除了渲染两次......第一个渲染电话号码和零点。 第二次渲染时所有数据都正确显示。 有人可以指导我找到解决方案。

class Update extends Component {
constructor(props) {
    super(props);
    const { match } = this.props;
    this.state = {
        phoneNumber: match.params.phoneNumber,
        points: 0,
        error: ''
    }
}

getPoints = () => {
    firebase.auth().onAuthStateChanged((user) => {
        if(user) {
            const docRef = database.collection('users').doc(user.uid).collection('customers').doc(this.state.phoneNumber);
            docRef.get().then((doc) => {
                if (doc.exists) {
                const points = doc.data().points;
                this.setState(() => ({ points }));
                console.log(points);
                } else {
                // doc.data() will be undefined in this case
                console.log("No such document!");
                const error = 'This phone number is not registered yet...'
                this.setState(() => ({ error }));
                }
                }).catch(function(error) {
                console.log("Error getting document:", error);
                });
        } else {
            history.push('/')
        }
    });
}

componentDidMount() {
    if(this.state.phoneNumber) {
        this.getPoints();
    } else {
        return null;
    }
}

render() {
    return (
        <div>
            <div>
                <p>{this.state.phoneNumber} has {this.state.points} points...</p>
                <p>Would you like to redeem or add points?</p>
            </div>
            <div>
                <button>Redeem Points</button>
                <button>Add Points</button>
            </div>
        </div>
    );
  }
}

export default Update;

您正在严格模式下运行您的应用程序。 转到 index.js 并注释严格模式标签。 你会发现一个单一的渲染。

发生这种情况是 React.StrictMode 的有意功能。 它只发生在开发模式下,应该有助于在渲染阶段发现意外的副作用。

从文档:

严格模式不能自动为您检测副作用,但它可以通过使它们更具确定性来帮助您发现它们。 这是通过有意双重调用以下函数来完成的:...

^ 在这种情况下, render功能。

使用 React.StrictMode 时可能导致重新渲染的官方文档:

https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects

React 在getPoints完成异步操作之前渲染组件。

因此,第一个render显示points的初始状态为0 ,然后调用componentDidMount并触发异步操作。
当异步操作完成并更新状态时,将使用新数据触发另一个render

如果需要,您可以显示加载器或指示数据正在被获取并且尚未准备好使用条件渲染显示

只需添加另一个布尔键,例如isFetching ,在调用服务器时将其设置为 true,在接收到数据时将其设置为 false。

你的渲染看起来像这样:

  render() {
    const { isFetching } = this.state;
    return (
      <div>
        {isFetching ? (
          <div>Loading...</div>
        ) : (
          <div>
            <p>
              {this.state.phoneNumber} has {this.state.points} points...
            </p>
            <p>Would you like to redeem or add points?</p>
            <div>
              <button>Redeem Points</button>
              <button>Add Points</button>
            </div>
          </div>
        )}
      </div>
    );
  }

这是因为 React Strict Mode 代码。

从 ReactDOM.render 代码中移除 -> React.StrictMode。

每次重新渲染都会渲染 2 次:

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

将渲染 1 次:

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

PS 对那些说不要担心“它重新渲染多少次”的人没有冒犯……如果您通过 setTimeout 每 1 秒运行一次永久 API 获取代码,并且每次使用该 API 都会花费您 0.01 美分,每次重新渲染时,它都会触发 setTimeout 函数 2 次(这意味着您每秒将调用加倍),这意味着在 5 秒后,您将在同一时间和 1 小时后运行 1000+ setTimeouts 每个调用 API,这个数字将超过一万亿,所以如果你弄错了,成本将变成天文数字。 此问题已通过删除 React.StrictMode 得到解决,因此代码将 WAI。

React.StrictMode,让它渲染两次,这样我们就不会在以下位置放置副作用

constructor
componentWillMount (or UNSAFE_componentWillMount)
componentWillReceiveProps (or UNSAFE_componentWillReceiveProps)
componentWillUpdate (or UNSAFE_componentWillUpdate)
getDerivedStateFromProps
shouldComponentUpdate
render
setState updater functions (the first argument)

所有这些方法都被多次调用,因此避免它们产生副作用很重要。 如果我们忽略这个原则,很可能会导致不一致的状态问题和内存泄漏。

React.StrictMode 不能立即发现副作用,但它可以通过有意调用两次关键函数来帮助我们找到它们。

这些功能是:

Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer

这种行为肯定会对性能产生一些影响,但我们不必担心,因为它只发生在开发中而不是生产中。
信用: https ://mariosfakiolas.com/blog/my-react-components-render-twice-and-drive-me-crazy/

它是通过反应故意完成的,以避免这个删除

  <React.StrictMode>     </React.StrictMode>

来自 index.js

React 在内部使用它的虚拟 dom 和它的 diffing 算法监控和管理它的渲染周期,所以你不必担心重新渲染的数量。 让重新渲染由 React 管理。 即使调用了渲染函数,如果其中没有道具或状态更改,也有不会在 ui 上刷新的子组件。 每个 setstate 函数调用都会通知 react 检查差异算法,并调用渲染函数。

因此,在您的情况下,由于您在 getPoints 函数中定义了一个 setstate,它告诉 react 通过 render 函数重新运行差异过程。

我通过提供一个自定义钩子来解决这个问题。 将下面的钩子放入您的代码中,然后:

// instead of this:
useEffect( ()=> {
    console.log('my effect is running');
    return () => console.log('my effect is destroying');
}, []);

// do this:
useEffectOnce( ()=> {
    console.log('my effect is running');
    return () => console.log('my effect is destroying');
});

这是钩子的代码:

export const useEffectOnce = ( effect => {

    const destroyFunc = useRef();
    const calledOnce = useRef(false);
    const renderAfterCalled = useRef(false);

    if (calledOnce.current) {
        renderAfterCalled.current = true;
    }

    useEffect( () => {
        if (calledOnce.current) { 
            return; 
        }

        calledOnce.current = true;
        destroyFunc.current = effect();

        return ()=> {
            if (!renderAfterCalled.current) {
                return;
            }

            if (destroyFunc.current) {
                destroyFunc.current();
            }
        };
    }, []);
};

有关说明,请参阅此博客

好吧,我为此创建了一个解决方法挂钩。 检查这个,如果它有帮助:

import { useEffect } from "react";

const useDevEffect = (cb, deps) => {
  let ran = false;
  useEffect(() => {
    if (ran) return;
    cb();
    return () => (ran = true);
  }, deps);
};

const isDev = !process.env.NODE_ENV || process.env.NODE_ENV === "development";

export const useOnceEffect = isDev ? useDevEffect : useEffect;

CodeSandbox 演示: https ://github.com/akulsr0/react-18-useeffect-twice-fix

基于 React 18 的更新:

import { useEffect, useRef } from "react";

export default function Component() {
    const isRunned = useRef(false);

    useEffect(() => {
        !isRunned.current &&
            {
                /* CODE THAT SHOULD RUN ONCE */
            };

        return () => {
            isRunned.current = true;
        };
    }, []);

    return <div> content </div>;
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM