I'm working on component that works with MobX store and I ran into a problem. When I go to a page with a different item id, after loading the data, I see the previous data before the new one is rendered. So I wrote this check for the first render, which seems to work good for preparing component to render, but it seems to me that it might be bad code. Is it so?
"use client"
const Component = (props) => {
const isFirstRender = useRef(true)
if (isFirstRender.current) {
/* some action */
isFirstRender.current = false
}
return (
/* some jsx */
)
})
This might the same scenario which has been documented here Resetting all state when a prop changes
Discussing below some points relevant to this question.
Now coming to this question:
Now this post is asking for a solution to avoid the stale or outdated render which always happens prior to every useEffect, the very same point we have discussed above in point 7.
Some background information of a possible solution
Please note that components will preserve state between renders. It means normally when a function object terminates its invocation, all variables declared inside the function object will be lost. The same variables will be newly created when the same function is invoked again. It means the variables local to a function are always reset.
However React functional object has the ability to retain state values between renders. The default state retention rule of React is that, it will retain the state as longs as the same component renders in the same position in the UI Render tree. For more about this, can be read here Preserving and Resetting State.
Though the default behaviour will suiting in most use-cases, and therefore it has become the default behaviour of React, the context which we are now in does not suit to this behaviour. We do not want React to retain the previous fetch.
It means we want a way to tell react that please reset the state along with the change in the props. Please note that, even if we are success to reset the state along with the prop change, the render process and useEffect are still going to run in the same normal order. There will be an initial render with the latest state and a useEffect run as the follow up of render. However the improvement we may achieve here is that this initial render always will be with the initial value which we have passed into useState. Since this initial value is fixed, always, we can use it to build a conditional rendering - Loading status or fetched data.
The following two sample codes, demo the same.
The first code below, demoes the issue we have been discussing.
app.js
import { useEffect, useState } from 'react';
export default function App() {
return <Page />;
}
function Page() {
const [contentId, setContentId] = useState(Math.random());
return (
<>
<Content contentId={contentId} />
<br />
<button onClick={() => setContentId(Math.random())}>
Change contentId
</button>
</>
);
}
function Content({ contentId }) {
const [mockData, setMockData] = useState(null);
useEffect(() => {
new Promise((resolve) => {
setTimeout(() => {
resolve(`some mock data 1,2,3,4.... for ${contentId}`);
}, 2000);
}).then((data) => setMockData(data));
}, [contentId]);
return <>mock data : {mockData ? mockData : 'Loading data..'}</>;
}
Test run
Test plan : Clicking the button to change contentId
The UI Before clicking
The UI for less than 2 seconds, just after clicking
Observation
The previous data retained for less than 2 seconds, this is not the desired UI. The UI should change to inform user that data loading is going on. And upon 2 seconds, the mock data should come into the UI.
The second code below, addresses the issue. This code remains the same as the first code, except on the usage of the property key.
It addresses the issue by using the property key. This property has great significance in the state retention scheme. For more about this, can be read here Preserving and Resetting State.
In brief, what happens now is that, React will reset the state if the key changes between two renders.
App.js
import { useEffect, useState } from 'react';
export default function App() {
return <Page />;
}
function Page() {
const [contentId, setContentId] = useState(Math.random());
return (
<>
<Content contentId={contentId} key={contentId} />
<br />
<button onClick={() => setContentId(Math.random())}>
Change contentId
</button>
</>
);
}
function Content({ contentId }) {
const [mockData, setMockData] = useState(null);
useEffect(() => {
new Promise((resolve) => {
setTimeout(() => {
resolve(`some mock data 1,2,3,4.... for ${contentId}`);
}, 2000);
}).then((data) => setMockData(data));
}, [contentId]);
return <>mock data : {mockData ? mockData : 'Loading data..'}</>;
}
Test run
Test plan : Clicking the button to change contentId
The UI before clicking
The UI for less than 2 seconds, after clicking
The UI after seconds
Observation
The previous data did not retain, instead the IU displayed the loading status and updated it as soon as the actual data had come. This may be the UI desired in this use-case.
Citation
How does React determine which state to apply when children are conditionally rendered?