英文:
Render is called before useEffect
问题
我正在尝试使用React和Leva js创建一个简单的应用程序。
基本上有一个项目数组,每个项目都有一个名称和一个数字数组。Leva面板包含两个选择框,用户可以从项目数组中选择两个项目。
如果选择的两个项目具有相同的长度,那么一切正常;否则应用程序应该返回一个错误。
以下是主要代码和一个工作演示。
App.jsx:
export const App = () => {
const [haveSameNumberOfValues, setHaveSameNumberOfValues] = useState(true);
const [showResult, setShowResult] = useState(haveSameNumberOfValues);
const { startValuesName, endValuesName } = useControls({
startValuesName: {
value: VALUES[0].name,
options: VALUES.map((d) => d.name)
},
endValuesName: { value: VALUES[1].name, options: VALUES.map((d) => d.name) }
});
useEffect(() => {
const startValuesItem = getValuesFromName(startValuesName);
const endValuesItem = getValuesFromName(endValuesName);
const startValues = startValuesItem.values;
const endValues = endValuesItem.values;
const values = [startValues, endValues];
const valuesLength = values.map((path) => path.length);
const haveValuesTheSameNumberOfItems = valuesLength.every(
(pathLength) => pathLength === valuesLength[0]
);
setHaveSameNumberOfValues(haveValuesTheSameNumberOfItems);
setShowResult(haveValuesTheSameNumberOfItems);
}, [startValuesName, endValuesName]);
console.log("\n");
console.log("haveSameNumberOfValues:", haveSameNumberOfValues);
console.log("showResult:", showResult);
return (
<div className="w-screen h-screen flex flex-col justify-center items-center">
{!haveSameNumberOfValues && <div>:( Error.</div>}
{showResult && (
<Result
startValues={getValuesFromName(startValuesName)}
endValues={getValuesFromName(endValuesName)}
/>
)}
</div>
);
};
Result.jsx:
export const Result = ({ startValues, endValues }) => {
console.log("startValues:", startValues.values.length);
console.log("endValues:", endValues.values.length);
return (
<div className="border-4 border-green-400 px-5 py-3">
<div>:)</div>
<div>{startValues.name}</div>
<div>{endValues.name}</div>
</div>
);
};
data.js:
export const VALUES = [
{
name: "carrot (3)",
values: [0, 4, 45]
},
{
name: "apple (3)",
values: [20, 20, 10]
},
{
name: "salad (4)",
values: [30, 0, 2, 1]
},
{
name: "chicken (6)",
values: [40, 1, 3, 20, 3, 1]
}
];
export function getValuesFromName(name) {
return VALUES.find((d) => d.name === name);
}
问题在于当用户选择两个具有不同长度的项目时(例如Carrot和Chicken),代码将showResult
设置为true,因此即使不应该渲染Result
组件也会渲染它。
你可以通过查看日志消息来验证这一点。
以下是我尝试使用整个示例流程更好地解释自己的问题。
- 刷新页面,选择的项目是
carrot (3)
和apple (3)
。这些值具有相同的长度,在控制台中可以看到:
haveSameNumberOfValues: true
showResult: true
startValues: 3
endValues: 3
showResult
是true,因此渲染了Result
组件。一切正常。
- 用户将
chicken (6)
选择为endValuesName
。控制台打印:
haveSameNumberOfValues: true
showResult: true
startValues: 3
endValues: 6
haveSameNumberOfValues: false
showResult: false
showResult
在第一次为true,所以渲染了Result
组件,然后它改变为false。这很奇怪,因为我不希望如此,我希望立即showResult=false
。这是因为在我的简单示例中,这不会引起大问题,但在我的实际应用程序中,它会破坏应用程序。
我的代码有什么问题?
我再次重申我的要求:
用户使用Leva更改值 - showResult
应该在第一次正确更新,然后再调用Result
。
英文:
I'm trying to create a simple application usign React and Leva js.
Basically there is an array of items and each item has a name and an array of numbers. The Leva panel contains two selects and the user can selects two items from the array of items.
If the two selected items have the same lenght, it's ok, otherwise the app should returns an error.
Here the main code and here a working demo.
App.jsx:
export const App = () => {
const [haveSameNumberOfValues, setHaveSameNumberOfValues] = useState(true);
const [showResult, setShowResult] = useState(haveSameNumberOfValues);
const { startValuesName, endValuesName } = useControls({
startValuesName: {
value: VALUES[0].name,
options: VALUES.map((d) => d.name)
},
endValuesName: { value: VALUES[1].name, options: VALUES.map((d) => d.name) }
});
useEffect(() => {
const startValuesItem = getValuesFromName(startValuesName);
const endValuesItem = getValuesFromName(endValuesName);
const startValues = startValuesItem.values;
const endValues = endValuesItem.values;
const values = [startValues, endValues];
const valuesLenght = values.map((path) => path.length);
const haveValuesTheSameNumberOfItems = valuesLenght.every(
(pathLength) => pathLength === valuesLenght[0]
);
setHaveSameNumberOfValues(haveValuesTheSameNumberOfItems);
setShowResult(haveValuesTheSameNumberOfItems);
}, [startValuesName, endValuesName]);
console.log("\n");
console.log("haveSameNumberOfValues:", haveSameNumberOfValues);
console.log("showResult:", showResult);
return (
<div className="w-screen h-screen flex flex-col justify-center items-center">
{!haveSameNumberOfValues && <div>:( Error.</div>}
{showResult && (
<Result
startValues={getValuesFromName(startValuesName)}
endValues={getValuesFromName(endValuesName)}
/>
)}
</div>
);
};
Result.jsx:
export const Result = ({ startValues, endValues }) => {
console.log("startValues:", startValues.values.length);
console.log("endValues:", endValues.values.length);
return (
<div className="border-4 border-green-400 px-5 py-3">
<div>:)</div>
<div>{startValues.name}</div>
<div>{endValues.name}</div>
</div>
);
};
data.js:
export const VALUES = [
{
name: "carrot (3)",
values: [0, 4, 45]
},
{
name: "apple (3)",
values: [20, 20, 10]
},
{
name: "salad (4)",
values: [30, 0, 2, 1]
},
{
name: "chicken (6)",
values: [40, 1, 3, 20, 3, 1]
}
];
export function getValuesFromName(name) {
return VALUES.find((d) => d.name === name);
}
The problem is that when user selects two items with values length not equals (for example Carrot and Chicken), the code set showResult
as true so the Result
component is rendered even if it shouldn't.
You can checkit reading the log messagges.
I'm trying to explain myself better using an entire example flow.
- refresh page, the selected items are
carrot (3)
andapple (3)
. The values have the same lenght and in the console you can see:
haveSameNumberOfValues: true
showResult: true
startValues: 3
endValues: 3
showResult
is true so the Result
component is rendered. Ok, it works
- user selects
chiken (6)
asendValuesName
. the console prints:
haveSameNumberOfValues: true
showResult: true
startValues: 3
endValues: 6
haveSameNumberOfValues: false
showResult: false
showResult
is true the first time, so the Result
component is rendered and then it changes and become false. It's strange because I don't want that, I'd like to have immediatly showResult=false
. This because in my simple example, that do not cause a big problem but in my real application it breaks the app.
What's wrong with my code?
I repeat what I would like to have:
user changes values using Leva -> showResult
should be updated in the right way the first time, before call Result
答案1
得分: 4
Render is called before useEffect
渲染在 useEffect 之前被调用。
This is the nature of React you cannot change it!. learn more about using useEffect
hook and react lifecycle methods.
这是 React 的本质,你无法改变它!了解更多关于使用 useEffect
钩子和 React 生命周期方法的信息。
What's wrong with my code?
我的代码有什么问题?
Basically nothing. your code is fine, however it can be better
基本上没有问题。你的代码没什么问题,但还有改进的余地。
Here is a brief explanation:
这里有一个简要的解释:
What you should keep in mind is that when you update a state, this latter does not updates immediately but when the code finishes executing the component rerenders, and only when this happens, the value of the state is updated.
你应该记住的是,当你更新状态时,状态不会立即更新,而是在代码执行完毕后组件重新渲染,只有在这种情况下,状态的值才会更新。
Then when the component has finished rendering, if there is a useEffect
hook, it will look inside its dependency array and check if one or more of those values is different from what it was during the last render and if it's the case useEffect
runs (it will do anyway during the first render) and if the code included inside the useEffect
hook is updating a state, then this will lead to a new render, so the component rerenders again, that's why you got this output in the console :
然后,当组件完成渲染后,如果有一个 useEffect
钩子,它会查看它的依赖数组,并检查其中一个或多个值是否与上次渲染时的值不同,如果是这样,useEffect
会运行(它在第一次渲染时也会运行),如果在 useEffect
钩子内部包含的代码更新了一个状态,那么这将导致新的渲染,因此组件再次重新渲染,这就是为什么你在控制台中看到这个输出:
// 组件重新渲染
haveSameNumberOfValues: true
showResult: true
startValues: 3
endValues: 6
// useEffet 被调用(因为 endValues 从 3 变为 6),并且由于在 useEffect 内部更新了状态(setShowResult(x) 和 setHaveSameNumberOfValues(y)),组件再次重新渲染
// 新渲染
haveSameNumberOfValues: false
showResult: false
// <Result/> 未显示,因为 showResult 为 false
// useEffet 未被调用(因为 startValuesName 和 endValuesName 未被更新)
Now, that being said, you understood "the issue", what you have to do, is to avoid triggering useEffect
to update haveSameNumberOfValues
and showResult
, there is no need for that, also this will avoid this extra render.
现在,说了这么多,你已经理解了“问题”,你需要做的是避免触发 useEffect
来更新 haveSameNumberOfValues
和 showResult
,没有必要这样做,而且这将避免额外的渲染。
Since useControls
of Leva is "similar" to the react useState
hook each time its value is updated the component rerenders so you'll have the updated values in this new render. then, when it comes to JSX to display the ui, you call this function (it returns either TRUE
or FALSE
) and based on its result you decide either to show <Result />
or <div>Error</div>
由于 Leva 的 useControls
“类似”于 React 的 useState
钩子,每次其值更新时组件都会重新渲染,因此您将在这个新的渲染中获得更新后的值。然后,在显示用户界面的 JSX 时,您调用此函数(它返回 TRUE
或 FALSE
),根据其结果来决定是显示 <Result />
还是 <div>Error</div>
。
The function includes the same logic of your useEffect
:
这个函数包括与你的 useEffect
相同的逻辑:
function haveValuesTheSameNumberOfItems(first, second) {
const startValuesItem = getValuesFromName(first);
const endValuesItem = getValuesFromName(second);
const startValues = startValuesItem.values;
const endValues = endValuesItem.values;
const values = [startValues, endValues];
const valuesLength = values.map((path) => path.length);
return valuesLength.every((pathLength) => pathLength === valuesLength[0]);
}
Full working example:
完整的工作示例:
import { useControls } from "leva";
const VALUES = [
{
name: "carrot (3)",
values: [0, 4, 45],
},
{
name: "apple (3)",
values: [20, 20, 10],
},
{
name: "salad (4)",
values: [30, 0, 2, 1],
},
{
name: "chicken (6)",
values: [40, 1, 3, 20, 3, 1],
},
];
const Result = ({ startValues, endValues }) => {
console.log("startValues:", startValues.values.length);
console.log("endValues:", endValues.values.length);
return (
<div className="border-4 border-green-400 px-5 py-3">
<div>{startValues.name}</div>
<div>{endValues.name}</div>
</div>
);
};
const App = () => {
function getValuesFromName(name) {
return VALUES.find((d) => d.name === name);
}
function haveValuesTheSameNumberOfItems(first, second) {
const startValuesItem = getValuesFromName(first);
const endValuesItem = getValuesFromName(second);
const startValues = startValuesItem.values;
const endValues = endValuesItem.values;
const values = [startValues, endValues];
const valuesLength = values.map((path) => path.length);
return valuesLength.every((pathLength) =>
<details>
<summary>英文:</summary>
>Render is called before useEffect
This is the nature of React you cannot change it!. [learn](https://reactjs.org/docs/hooks-effect.html) more about using `useEffect` hook and react lifecycle methods.
>What's wrong with my code?
Basically nothing. your code is fine, however it can be better
---
**Here is a brief explanation:**
What you should keep in mind is that when you update a state, this latter does not updates immediately but when the code finishes executing the component rerenders, and only when this happens, the value of the state is updated.<br>
Then when the component has finished rendering, if there is a `useEffect` hook, it will look inside its dependency array and check if one or more of those values is different from what it was durring the last render and if its the case `useEffect` runs (it will do anyway during the first render) and if the code included inside the `useEffect` hook is updating a state, then this will lead to a new render, so the component rerenders again, that's why you got this output in the console :
````json
// component rerendered
haveSameNumberOfValues: true
showResult: true
startValues: 3
endValues: 6
// useEffet is called (because endValues was 3 and become 6), and since inside useEffect you are updating the state (setShowResult(x) and setHaveSameNumberOfValues(y)) the component rerenders again
// new render
haveSameNumberOfValues: false
showResult: false
// <Result/> is not displayed because showResult is false
// useEffet is not called (because startValuesName and endValuesName are not updated)
Now, that being said, you understood "the issue", what you have to do, is to avoid triggering useEffect
to update haveSameNumberOfValues
and showResult
, there is no need for that, also this will avoid this extra render.
Since useControls
of Leva is "similar" to the react useState
hook each time its value is updated the component rerenders so you'll have the updated values in this new render. then, when it comes to JSX to display the ui, you call this function (it returns either TRUE
or FALSE
) and based on its result you decide either to show <Result />
or <div>Error</div>
The function includes the same logic of your useEffect
:
function haveValuesTheSameNumberOfItems(first, second) {
const startValuesItem = getValuesFromName(first);
const endValuesItem = getValuesFromName(second);
const startValues = startValuesItem.values;
const endValues = endValuesItem.values;
const values = [startValues, endValues];
const valuesLenght = values.map((path) => path.length);
return valuesLenght.every((pathLength) => pathLength === valuesLenght[0]);
}
Full working example:
import { useControls } from "leva";
const VALUES = [
{
name: "carrot (3)",
values: [0, 4, 45],
},
{
name: "apple (3)",
values: [20, 20, 10],
},
{
name: "salad (4)",
values: [30, 0, 2, 1],
},
{
name: "chicken (6)",
values: [40, 1, 3, 20, 3, 1],
},
];
const Result = ({ startValues, endValues }) => {
console.log("startValues:", startValues.values.length);
console.log("endValues:", endValues.values.length);
return (
<div className="border-4 border-green-400 px-5 py-3">
<div>{startValues.name}</div>
<div>{endValues.name}</div>
</div>
);
};
const App = () => {
function getValuesFromName(name) {
return VALUES.find((d) => d.name === name);
}
function haveValuesTheSameNumberOfItems(first, second) {
const startValuesItem = getValuesFromName(first);
const endValuesItem = getValuesFromName(second);
const startValues = startValuesItem.values;
const endValues = endValuesItem.values;
const values = [startValues, endValues];
const valuesLenght = values.map((path) => path.length);
return valuesLenght.every((pathLength) => pathLength === valuesLenght[0]);
}
const { startValuesName, endValuesName } = useControls({
startValuesName: {
value: VALUES[0].name,
options: VALUES.map((d) => d.name),
},
endValuesName: {
value: VALUES[1].name,
options: VALUES.map((d) => d.name),
},
});
return (
<div className="w-screen h-screen flex flex-col justify-center items-center">
{haveValuesTheSameNumberOfItems(startValuesName, endValuesName) ? (
<Result
startValues={getValuesFromName(startValuesName)}
endValues={getValuesFromName(endValuesName)}
/>
) : (
<div>: Error.</div>
)}
</div>
);
};
export default App;
Note: you can have a look at useMemo to keep a memorized version of the function result including startValuesName
and endValuesName
in its dependency array, so then if there is another state that can trigger component rerender, React doesn't have to calculate the result on every render but only when startValuesName
or endValuesName
are updated.
答案2
得分: 3
问题在于您的代码中,状态变量showResult由于使用了useState hook而异步更新。当您在初始状态中将showResult设置为true时,它立即生效并渲染Result组件。稍后,在useEffect hook中,您将showResult设置为haveValuesTheSameNumberOfItems,但此更新需要一些时间才能反映在状态中。
要解决这个问题,您可以使用一个单独的状态变量来跟踪数据是否准备好显示。您可以将此状态变量初始化为false,并在useEffect hook中与haveSameNumberOfValues一起更新它。一旦两个状态变量都准备好,您可以将showResult设置为true。
英文:
The problem with your code is that the state variable showResult is updated asynchronously due to the use of useState hook. When you set showResult to true in the initial state, it takes effect immediately and renders the Result component. Later, in the useEffect hook, you set showResult to haveValuesTheSameNumberOfItems, but this update takes time to be reflected in the state.
To solve this issue, you can use a separate state variable to keep track of whether the data is ready to be shown or not. You can initialize this state variable to false and update it in the useEffect hook along with haveSameNumberOfValues. Once both the state variables are ready, you can set showResult to true.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论