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

161

我不知道为什么我的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;

3
componentDidMount会在组件挂载后触发。因此,getPoints()只有在第一次渲染后才会被调用。 - Nsevens
2
嗯,当你的组件首次挂载时,它并没有所有的数据,只有在它挂载后,你才会加载数据并更新状态,因此它将首先在没有数据的情况下进行渲染,然后再次渲染,此时已经将获取到的数据添加到状态中。 - Icepickle
我只是在这里添加了组件生命周期文档部分。 https://reactjs.org/docs/state-and-lifecycle.html#adding-lifecycle-methods-to-a-class - Striped
可能是为什么React组件会渲染两次?的重复问题。 - Striped
在仍处于beta版本的新React文档中,有一个非常好的解释和示例:https://beta.reactjs.org/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development - Samuli Asmala
显示剩余2条评论
10个回答

276

您正在以严格模式运行应用程序。前往index.js并注释掉“strict mode”标签。您将找到一个单独的渲染。

这是React.StrictMode的一个有意特性。它只在开发模式下发生,并应该帮助查找渲染阶段中的意外副作用。

从文档中得知:

严格模式无法自动检测到副作用,但可以通过使它们更加确定性来帮助您发现它们。这是通过有意地调用以下函数两次完成的:...

^ 在这种情况下为render函数。

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

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


5
你能否详细说明一下这个问题? - Sai Saagar Sharman
1
也许添加到相关文档/文章的链接请返回翻译后的文本内容: - lordvcs
1
不确定为什么,但这对我来说是这种情况。移除了严格的情况,双重渲染停止了!现在我会重新激活它,因为我知道这不是一个错误。 - Loïc V
1
这不是一个 bug。事实上,它们进行额外渲染的原因是为了在开发中“捕获 bug”。(额外渲染不会在生产环境中发生。)如果额外渲染破坏了你的组件,那么根据 React 团队的理念,这意味着你的组件有一个 bug(很可能)。 - aderchox
谢谢,我移除了React.StrictMode,问题解决了。 - GBg
显示剩余5条评论

53

这是由于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')
);

3
重要的侧面说明:严格模式只在开发模式下呈现两次。因此,额外的 API 请求只会在开发模式下发生(通常只有几个开发人员),而不会在生产环境中发生(可能有很多访问者)。 - Flion
4
移除严格模式是最后要做的事情。更详细的答案请参见 https://dev59.com/OlEG5IYBdhLWcg3wJlLh#72238236 - Youssouf Oumar

20

在完成异步操作之前,React会渲染组件。因此,第一次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>
    );
  }

14

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/


5

这是React故意这样做的,以避免此操作被删除。

  <React.StrictMode>     </React.StrictMode>

来自index.js


2
我通过提供一个自定义钩子来解决这个问题。将以下钩子放入你的代码中,然后:
// 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();
            }
        };
    }, []);
};

请参见此博客了解详细信息。


1

我已经为此创建了一个解决方法的钩子。如果有帮助,请检查:

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


0

React通过其虚拟dom和差分算法内部监测和管理其渲染周期,因此您无需担心重新渲染的数量。让React来管理重新渲染。即使渲染函数被调用,如果其中没有props或state更改,则子组件不会在UI上刷新。每个setstate函数调用都将通知React检查差分算法,并调用渲染函数。

因此,在您的情况下,由于getPoints函数中定义了setstate,它会告诉React通过渲染函数重新运行差分过程。


0

0

在重写一些组件以使用props后,我注意到了这种行为。这样做破坏了页面的功能。我正在修复我的props使用方式,但如果我从子组件中删除它们,我的组件的双倍就会消失,并且实际副本将与我正在使用的新进程一起工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接