React: Avoid nondeterministic first render (hack with "mounted" state) - javascript

TL;DR React sometimes renders a loading state and sometimes not, without changes in the UI. This is probably due to batched updates.
I would like to know if the problem below is due to batched updates. If the answer is "yes", I would like to know if there's preferred way to opt-out of batched updates in React to get deterministic render behavior. Go down to "Experiment" if you want to skip the setup.
Setup
Here's the setup, a chart that takes a long time to render. So long that the render is blocking. There are three different ways to render the chart here:
one is the normal way
one with a "mounted" render hack
one with the same "mounted" render hack, but with an additional setTimeout
Option 2 & 3 both have a small useState to check whether they've been mounted. I do this to show a "Loading" state conditionally:
function ChartWithMountHack({ data }: { data: Data }) {
// initially not mounted
const [isMounted, setIsMounted] = useState<boolean>(false);
useEffect(() => {
// "Now I've been mounted!"
setIsMounted(true);
}, []);
return !isMounted ? <p>Loading</p> : <Chart data={data} />;
}
I did this, because I want to show a "Loading" state instead of a blocking render, so e.g. page switches or ternary rendering (e.g. hasData ? <p>No data</p> : <Chart />) are shown immediately, instead of blocking. (If there are better ways, please let me know!)
Experiment
Now, each button will render one of the three options/charts. Again, the second and third chart have a small hack to check whether they're mounted or not.
Try clicking on the first button and the second button back & forth quickly.
You will see that sometimes the "Chart with mount hack" will ("correctly") render the "Loading" state, but sometimes it just doesn't render the "Loading" - instead it blocks the render up until the chart is finished rendering (skips the "Loading" state).
I think this is due to the render cycles and whether you get the two updates in one cycle of the batching. (first: isMounted === false -> second: isMounted === true)
I can't really tell how to reproduce this, hence the "nondeterministic" in the title. Sometimes you also have to click on "Regenerate data" and click back & forth after that.
Cross-check
Option 3 ("Chart with mount hack with timeout") ALWAYS gives me the "Loading" state, which is exactly what I want. The only difference to option 2 is using a setTimeout in the useEffect where isMounted is set to true. setTimeout is used here to break out of the update batching.
Is there a better way to opt-out of the batching, so isMounted will always render with its initial value (false)? Using setTimeout here feels like a hack.

React has concurrent features to handle these sort of things, for example React Suspense tags or you make use of Subscription libraries like Rxjs, which its subscription should be done in the componentDidMount and componentWillUnmount to unsubscribe the data.
Then the isMounted is just a work around for a pending issue, probably from the library you're using or sometimes just your bundler/build tool acting out a bit.
lastly to avoid unnecessary re-render, you can use React memoization of component using React.Memo.
Kindly read more on these.

Related

React 18 StrictMode first useEffect wrong state

another React 18 strict mode question. I'm aware React will call the render and effect functions twice to highlight potential memory leaks with the upcoming features. What I yet don't understand is how to properly handle that. My issue is that I can't properly unmount the first render result as the two useEffect calls are performed with the state of the 2nd render. Here is an example to showcase what I mean.
const ref = useRef(9);
const id = useId();
console.log('## initial id', id);
console.log('## initial ref', ref.current);
ref.current = Math.random();
console.log('## random ref', ref.current);
useEffect(() => {
console.log('## effect id', id);
console.log('## effect ref', ref.current);
return () => {
console.log('## unmount id', id);
console.log('## unmount ref', ref.current);
};
});
and here is the log output
## initial id :r0:
## initial ref 9
## random ref 0.26890444169781214
## initial id :r1:
## initial ref 9
## random ref 0.7330565878991766
## effect id :r1: <<--- first effect doesn't use data of first render cycle
## effect ref 0.7330565878991766
## unmount id :r1:
## unmount ref 0.7330565878991766
## effect id :r1:
## effect ref 0.7330565878991766
As you can see there is no useEffect call with the state of the first render cycle and as well the 2nd render cycle doesn't provide you with the ref of the first render cycle (it is initialized with 9 again and not 0.26890444169781214. Also the useId hook returns two different ids where the 2nd Id is kept also in further render cycles.
Is this a bug or expected behavior? If it is expected, is there a way to fix this?
Before React 18's StrictMode, your components would only mount once. But now, they mount, are unmounted, and then remounted. So its not only the effects that are ran twice - your entire component is rendered twice.
That means your state is re-initialized and your refs are also reinitialized. Obviously, your effects will run twice as well.
About effects running twice, you need to properly cleanup async effects - any effect that does something asynchronously, like fetching data from the server, adding an event listener etc. Not all effects need a cleanup.
Also, the effects are meant to run twice in development (they only run once in production). Some people try to prevent effects from running twice, but that is not okay. If you cleanup an effect properly, there should be no difference in its execution when it runs once in production or twice in development.
Also the useId hook returns two different ids where the 2nd Id is kept
also in further render cycles. Is this a bug or expected behavior? If
it is expected, is there a way to fix this?
The second value will be the one that is used. It is not a bug, and you can keep using that as the "true" value.
You can readup more on StrictMode here.
Edit: Detecting an unmount.
// Create a ref to track unmount
const hasUnmounted = useRef(false)
useEffect(() => {
return () => {
// Set ref value to true in cleanup. This will run when the component is unmounted. If this is true, your component has unmounted OR the effect has run at least once
hasUnmounted.current = true;
}
}, [])

How to specify the rendering order in React

I have a .tsx file that renders two component:
export default observer(function MyModule(props: MyModuleProps) {
....
return (
<div>
<TopPart></TopPart>
<LowerPart></LowerPart>
</div>
);
});
The problem I have is that the TopPart contains lots of sub components, so it takes much longer time to render, and the LowerPart is more important and I want to render it first, but in this code, the LowerPart won't be available until the TopPart has been rendered.
What I want to do is to first render the LowerPart, then the TopPart, without changing the layout. I am wondering how I can achieve this goal properly.
Disclaimer: this is a hack.
If the problem is server side, this is easy for react. Just throw up a placeholder while data is loading, then save it in state when loading finishes and render.
The following answer assumes this is a rendering performance problem. If it is, then you look at that rendering performance. Paginate your lists, simplify your CSS rules, profile react and see what is taking the time.
What follows may be interesting, but is probably a bad idea. React is declarative, meaning you tell the result you want and then let it crunch things to deliver that. As soon as you start telling it what order to do things in, you break that paradigm and things may get painful for you.
If you want to break up rendering you could use state to prevent the expensive component from rendering, and then use an effect to update that state after the first render, which then renders both components.
You could make a custom hook like this:
function useDeferredRender(): boolean {
const [doRender, setDoRender] = useState(false);
useEffect(() => {
if (!doRender) {
setTimeout(() => setDoRender(true), 100);
}
}, [doRender]);
return doRender;
}
This hook create the doRender state, initialized to false. Then it has an effect which sets the state to true after a brief timeout. This means that doRender will be false on the first render, and then the hook will immediately set doRender to true, which triggers a new render.
The timeout period is tricky. Too small and React may decide to batch the render, too much and you waste time. (Did I mention this was a hack?)
You would this like so:
function App() {
const renderTop = useDeferredRender();
return (
<div className="App">
{renderTop ? <TopPart /> : "..."}
<LowerPart />
</div>
);
}
Working example
One last time: this is probably a bad idea.

How do I show an activity indicator every time my flatlist data changes?

I'm setting the data that my flatlist component displays using a state called selectedStream. selectedStream changes every time the user presses a different group option. I've noticed that the flatlist takes 1-3 seconds to refresh all the posts that it's currently displaying already. I want there to be a loading indicator so that by the time the indicator goes away, the list is already properly displayed with the newly updated data.
<FlatList
maxToRenderPerBatch={5}
bounces={false}
windowSize={5}
ref={feedRef}
data={selectedStream}/>
Whenever we are working with anything related to the UI, sometimes we may face delays in UI re-rendering. However, we need to first figure out what is actually causing the delay.
The right question to ask about your code would be:
Is the rendering of items taking longer than expected? Or, is the data being passed with a delay because it is dependant on an API call or any other async task?
Once you answer that question, you may end up with two scenarios:
1. FlatList taking longer to render views
This doesn't usually happen as the RN FlatList will only render views that are visible to the user at any given time and will keep rendering new views as the user scrolls through the list. However, there may be some flickering issues for which you can refer to the below article:
8 Ways to optimise your RN FlatList
2. Passing the data causes the delay
This is the most common scenario, where we may call an API endpoint and get some data and then do setState to update any view/list accordingly. A general approach is to show some sort of a progress-bar that would indicate that the application is busy and thus maintaining a proper user-experience. The easiest way to do that is by conditional rendering.
A general example would be:
const [myList, setMyList] = useState();
function callAPIforMyList(){
// logic goes here
}
return {
{myList ? <ActivityIndicator .../> : <Flatlist .... />
}
The above code will check if myList is undefined or has a value. If undefined, it will render the ActivityIndicator or else the FlatList.
Another scenario could be when myList may have existing data but you need to update/replace it with new data. This way the above check may fail, so we can put another check:
const [myList, setMyList] = useState();
const [isAPIbusy, setAPIBusy] = useState(false)
function callAPIformyList() {
setAPIBusy(true)
/// other logics or async calls or redux-dispatch
setAPIBusy(false)
}
return {
{!isAPIBusy && myList ? (<Flatlist .... />) : (<ActivityIndicator .../>)
}
You can add multiple conditions using more turneries such as isAPIBusy ? <View1> : otherBoolean ? <View2> : <Default_View_When_No_Conditions_Match)/>
Hope this helps clarify your needs.

Calling React setState hook from outside react (DOM/Google Maps)

I'm building a function React component which uses various useState() hooks. Each of them providing a state and a function which can be called to update it. This works beautifully from within react itself. But in my scenario I have to deal with other (DOM/Google Maps) environments as well.
As a callback from a DOM element put on the Google map, I want to call setState(!state) to flip a boolean. However, this works only once.
I think that the problem is that the setState hook fails to fetch the latest state but uses the initial state instead. Flipping a bool 1 will invert it, but the flipping it again without taking the former change into account does not update anything.
I've managed to solve this by implementing a state that sets the boolean on a data attribute in the DOM (and then flip that bool) but I think that's a rather ugly solution.
How should I update the state in functional React component from a callback function provided by something not React?
You'll want to use functional updates.
const [bool, setBool] = React.useState(false);
// Flip bool depending on latest state.
setBool((prevBool) => !prevBool);
As opposed to using the latest state in the component itself, which can use the wrong state depending on the memoization / state life cycle:
const [bool, setBool] = React.useState(false);
// bool could be behind here. DON'T do this.
setBool(!bool);
If the problem is setState using the wrong state, you can pass a function to setState instead:
setState(state => !state);
This will use the latest state instead of the state which occurred at the React render. Not sure how this will play with the weird outside-of-React situation here, but it may help out. If this page isn't even using React (the component with the state you want to edit isn't even rendered) then HTML LocalStorage might be your best bet for persisting information.

Dealing with parallel requests in React.JS / Redux ( Flux )

Scenario
2 Container components rendered
Each container is subscribed to the "Basket" piece of state. Within their componentDidUpdate they should go and re-fetch data from the pricing API and display the results ( i.e running totals with offers etc )
Image of multiple containers all trying to fetch data # same time
Issue
The issue is that a race condition will happen. When basket updates the flow looks something like this
Component1 compenentDidUpdate is triggered due to basket update
Check if pricingDetails.isLoading is false, if not fetch data
Dispatch fetchPricingData() - this will set pricingDetails.isLoading to false
Component2 componentDidUpdate is triggered
Check if pricingDetails.isLoading is false ( IT is still is false at this point, as Component2 subscription update to the isLoading state hasn't happened quick enough )
Perform a second call to fetchPricingData - meaning now we've got 2 API calls on the go!
Solution?
Currently there seems a couple of ways I can think to tackle this, but none particularly elegantly
1) Delegate responsibility of fetching data to a higher level container where only a single instance of it will ever exist.
2) Have each container somehow use a "slave/master" setup, where only 1 initial instance is allowed to fetch data.

Categories