When unfreezing the FlatList returns the scroll to the top
yepMad opened this issue · 5 comments
Hello,
My current component is structured as follows, take into account that isFocused comes from the React Navigation hook:
<Freeze freeze={!isFocused}>
<FlatList
data={data}
removeClippedSubviews
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemLayout={getItemLayout}
ItemSeparatorComponent={Divider}
/>
</Freeze>
When the user returns to this screen after being frozen, the FlatList scroll is at the top, is this the expected behavior? From the README I understood that it shouldn't happen. Here is a video to illustrate the problem. For tabs I'm using @react-navigation/material-top-tabs.
screencap-2022-12-08T023104.731Z.mp4
This also happens to <ScrollView>
, it freezes the component as intended but every time you unfreeze it, it scrolls to the top.
This issue appears to occur because Suspense
is adding the style display: 'none'
to its suspended children. We worked around the issue with something like the following.
const [freeze, setFreeze] = useState(false);
const [placeholder, setPlaceholder] = useState(null);
const handleFreeze = (value: boolean) => {
setFreeze(value);
if (value) {
const __html = document.getElementById('freezeBlock')?.innerHTML ?? '';
setPlaceholder(<div dangerouslySetInnerHTML={{ __html }} />);
} else {
setPlaceholder(null);
}
};
return (
<Freeze freeze={freeze} placeholder={placeholder}>
<div id="freezeBlock">{children}</div>
</Freeze>
);
I think the root of the issue lies with React.Suspense
not providing a better way to opt out of having suspended children hidden by default. There is the useTransition
hook, but I wasn't able to figure out how to get that to work with the concept of a boolean value that triggers suspension.
This issue appears to occur because
Suspense
is adding the styledisplay: 'none'
to its suspended children. We worked around the issue with something like the following.const [freeze, setFreeze] = useState(false); const [placeholder, setPlaceholder] = useState(null); const handleFreeze = (value: boolean) => { setFreeze(value); if (value) { const __html = document.getElementById('freezeBlock')?.innerHTML ?? ''; setPlaceholder(<div dangerouslySetInnerHTML={{ __html }} />); } else { setPlaceholder(null); } }; return ( <Freeze freeze={freeze} placeholder={placeholder}> <div id="freezeBlock">{children}</div> </Freeze> );
I think the root of the issue lies with
React.Suspense
not providing a better way to opt out of having suspended children hidden by default. There is theuseTransition
hook, but I wasn't able to figure out how to get that to work with the concept of a boolean value that triggers suspension.
Thank you so much for this! Any idea how it can be solved on mobile?
Thank you so much for this! Any idea how it can be solved on mobile?
By mobile, do you mean something like react-native? I've only worked with React for strict web developement. I'm not sure if this is available in react-native or not, but I had a coworker just mention Mutation Observers. It's funny all the basic things you keep learning about in the javascript ecosystem. Anyways, I was able to refactor the previous code so that the dom isn't being duplicated anymore. Using the mutation observer, we can catch Suspense adding display: 'none !important
and prevent it from happening. This could could probably be refined, but hopefully the gist of it is clear enough.
let observer: MutationObserver | undefined;
export function FreezeProvider({ children, freeze }: { children: React.ReactElement, freeze?: boolean }) {
useEffect(() => {
if (observer) return;
observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type !== 'attributes' || mutation.attributeName !== 'style') continue;
const target = mutation.target as HTMLElement;
if (target.getAttribute('style')?.includes('display: none !important;'))
target.setAttribute('style', '');
}
});
// this is used to ensure the mutation observer is correctly assigned
const fetchFreezeBlockInterval = setInterval(() => {
const freezeBlockEl = document.getElementById('freezeBlock');
if (!freezeBlockEl || !observer) return;
observer.observe(freezeBlockEl, {
attributeFilter: ['style'],
});
clearInterval(fetchFreezeBlockInterval);
}, 1000);
}, []);
return (
<Freeze freeze={freeze} placeholder={null}>
<div id="freezeBlock">{children}</div>
</Freeze>
);
}