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

我不知道为什么我的 React 组件渲染了两次。所以我从 params 中提取了一个电话号码,并将其保存到 state 中,这样我就可以通过 Firest 进行搜索。一切似乎工作正常,除了它呈现两次... 第一次呈现电话号码和零点。第二次呈现所有数据时正确显示。谁能指引我找到解决之道。

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;
101852 次浏览

React is rendering the component before getPoints finishing the asynchronous operation.

So the first render shows the initial state for points which is 0, then componentDidMount is called and triggers the async operation.
When the async operation is done and the state been updated, another render is triggered with the new data.

If you want, you can show a loader or an indicator that the data is being fetched and is not ready yet to display with conditional rendering.

Just add another Boolean key like isFetching, set it to true when you call the server and set it to false when the data is received.

Your render can look something like this:

  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 internally monitors & manages its render cycles using its virtual dom and its diffing algorithms, so you need not worry about the number of re-renders. Let the re-renders to be managed by react. Even though the render function is getting invoked, there are sub components which doesn't gets refreshed on ui, if there is no props or state change inside it. Every setstate function call will inform react to check the diffing algorithm, and invoke the render function.

So in your case, since you have a setstate defined inside the getPoints function, it tells react to rerun the diffing process through the render function.

You are running your app in strict mode. Go to index.js and comment strict mode tag. You will find a single render.

This happens is an intentional feature of the React.StrictMode. It only happens in development mode and should help to find accidental side effects in the render phase.

From the docs:

Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:...

^ In this case the render function.

Official documentation of what might cause re-rendering when using React.StrictMode:

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

This is because of React Strict Mode code.

Remove -> React.StrictMode, from ReactDOM.render code.

Will render 2 times on every re-render:

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

Will render 1 time:

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

React.StrictMode, makes it render twice, so that we do not put side effects in following locations

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

All these methods are called more than once, so it is important to avoid having side-effects in them. If we ignore this principle it is likely to end up with inconsistent state issues and memory leaks.

React.StrictMode cannot spot side-effects at once, but it can help us find them by intentionally invoking twice some key functions.

These functions are:

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

This behaviour definitely has some performance impact, but we should not worry since it takes place only in development and not in production.
credit: https://mariosfakiolas.com/blog/my-react-components-render-twice-and-drive-me-crazy/

I worked around this by providing a custom hook. Put the hook below into your code, then:

// 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');
});

Here is the code for the hook:

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();
}
};
}, []);
};

See this blog for the explanation.

Well, I have created a workaround hook for this. Check this, if it helps:

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 Demo: https://github.com/akulsr0/react-18-useeffect-twice-fix

it is done intentionally by react to avoid this remove

  <React.StrictMode>     </React.StrictMode>

from index.js