Comments (29)
This has been my solution so far. Wait until interactions are done, then fade in the screen.
I can put it into an npm
package if that would be useful.
1. use-after-interactions.ts
A hook that updates the interaction state and runs a transition when it's done.
import { useState, useEffect, useRef } from 'react'
import { InteractionManager } from 'react-native'
import { TransitioningView } from 'react-native-reanimated'
export const useAfterInteractions = () => {
const [interactionsComplete, setInteractionsComplete] = useState(false)
const subscriptionRef = useRef(null)
const transitionRef = useRef<TransitioningView>(null)
useEffect(() => {
subscriptionRef.current = InteractionManager.runAfterInteractions(() => {
transitionRef.current?.animateNextTransition()
setInteractionsComplete(true)
subscriptionRef.current = null
})
return () => {
subscriptionRef.current?.cancel()
}
}, [])
return {
interactionsComplete,
transitionRef,
}
}
2 with-interactions-managed.tsx
Higher-order component that renders your screen after transitions are done. If they aren't done, it renders an optional placeholder.
import React, { ComponentType } from 'react'
import { Transition, Transitioning } from 'react-native-reanimated'
import { useAfterInteractions } from '../hooks/use-after-interactions'
export function withInteractionsManaged<Props>(
Component: ComponentType<Props>,
Placeholder: ComponentType | null = null
) {
return (props: Props) => {
const { transitionRef, interactionsComplete } = useAfterInteractions()
return (
<Transitioning.View
transition={
<Transition.Together>
<Transition.Change interpolation="easeInOut" />
<Transition.In type="fade" />
</Transition.Together>
}
style={{ flex: 1 }}
ref={transitionRef}
>
{interactionsComplete ? (
<Component {...props} />
) : (
Placeholder && <Placeholder />
)}
</Transitioning.View>
)
}
}
And then when exporting your screen, wrap it with this HOC:
SomeScreen.tsx
import { withInteractionsManaged } from './with-interactions-managed'
...
export default withInteractionsManaged(SomeScreen)
// or, with a placeholder:
export default withInteractionsManaged(SomeScreen, Placeholder)
from rfcs.
willFocus
to show a loading spinner anddidFocus
to show the calendar? Will that not be the same asInteractionManager.runAfterInteractions
example?
yeah it would be very similar.
the issue is basically the following - if you push a screen and it is computationally expensive enough to render that it is going to block the main thread, you have three options (as far as i know):
- break it up into chunks and render incrementally so it doesn't block the main thread enough to interrupt the transition
- render either some placeholder content or just partial / minimal content, then render full content once transition is complete (can use focus events or InteractionManager or whatever for this)
- render the screen first (block the main thread briefly before you push the screen) then animate it in
react-navigation doesn't do any of those three for you out of the box. react-native-navigation, as of v2 it would seem, does number 3 for you out of the box.
in the case of this calendar, perhaps the implementation could be changed so that it uses the first strategy. i'm not familiar with the implementation but based on what i saw above it looks like it renders everything at once
from rfcs.
I put the above solution into an npm package called react-navigation-heavy-screen
. Figure it might be useful until there is a more official solution.
import { optimizeHeavyScreen } from 'react-navigation-heavy-screen'
const Screen = () => ...
export default optimizeHeavyScreen(Screen, OptionalPlaceHolderScreen)
from rfcs.
@slorber - i'd be open to adding an experimental option for that so we can try it out, maybe something like this:
createStackNavigator({
Home: { screen: HomeScreen},
Calendar: {
screen: CalendarScreen,
loadingPlaceholderExperimental: ScreenWithSpinner,
}
}}
from rfcs.
My workaround is listening to the focus
and optionally to transitionEnd
events, in my components, and while it's not ready I render a placeholder. The screen transition will be smooth.
// useIsReady.ts
import { useNavigation } from '@react-navigation/native'
import React from 'react'
const useIsReady = (stack: boolean = true) => {
const navigation = useNavigation()
const [isReady, setIsReady] = React.useState(false)
React.useEffect(() => {
const unsubscribeFocus = navigation.addListener('focus', () => {
if (!isReady) setIsReady(true)
})
const unsubscribeTransitionEnd = stack
? // @ts-ignore
navigation.addListener('transitionEnd', () => {
if (!isReady) setIsReady(true)
})
: undefined
return () => {
unsubscribeFocus()
unsubscribeTransitionEnd && unsubscribeTransitionEnd()
}
}, [])
return isReady
}
export default useIsReady
Some component...
const HeavyComponentThatMakeNavigationLooksCrap = () => {
const isReady = useIsReady()
return isReady ? ... : <Placeholder />
}
If you have multiple heavy components in the screen, it is better to use it directly in the screen:
const ScreenWithMultipleHeavyComponents = () => {
const isReady = useIsReady()
return isReady ? ... : <ScreenPlaceholder />
// or
return (
<>
...
{isReady ? ... : <ComponentsPlaceholder />}
</>
)
}
Just a workaround...
It's not a solution because if your component is really heavy, it is still blocking the js thread, which is also true for the react-navigation-heavy-screen
solution above. Although the page transition will be smooth, again, the same as the above solution.
from rfcs.
this approach works well to selectively render some content before transition, then render the rest after: https://snack.expo.io/rkM0mmgGQ - notice that when the transition starts, 'willFocus' fires but 'didFocus' does not until the transition is completed
we don't yet support giving a screen the opportunity to render before beginning the animation (like in react-native-navigation v2) but this is pretty neat and we probably should come up with a way to make that possible.
from rfcs.
I updated withInteractionsManaged
in the answer above to look like this:
import hoistNonReactStatics from 'hoist-non-react-statics'
...
const Wrapped = (props: Props) => {
const { transitionRef, interactionsComplete } = useAfterInteractions()
return (
<Transitioning.View
transition={
<Transition.Together>
<Transition.Change interpolation="easeInOut" />
<Transition.In type="fade" />
</Transition.Together>
}
style={{ flex: 1 }}
ref={transitionRef}
>
{interactionsComplete ? (
<Component {...props} />
) : (
Placeholder && <Placeholder />
)}
</Transitioning.View>
)
}
// forward navigationOptions, and other statics
hoistNonReactStatics(Wrapped, Component)
return Wrapped
from rfcs.
This has been my solution so far. Wait until interactions are done, then fade in the screen.
I can put it into an
npm
package if that would be useful.1.
use-after-interactions.ts
A hook that updates the interaction state and runs a transition when it's done.
import { useState, useEffect, useRef } from 'react' import { InteractionManager } from 'react-native' import { TransitioningView } from 'react-native-reanimated' export const useAfterInteractions = () => { const [interactionsComplete, setInteractionsComplete] = useState(false) const subscriptionRef = useRef(null) const transitionRef = useRef<TransitioningView>(null) useEffect(() => { subscriptionRef.current = InteractionManager.runAfterInteractions(() => { transitionRef.current?.animateNextTransition() setInteractionsComplete(true) subscriptionRef.current = null }) return () => { subscriptionRef.current?.cancel() } }, []) return { interactionsComplete, transitionRef, } }2
with-interactions-managed.tsx
Higher-order component that renders your screen after transitions are done. If they aren't done, it renders an optional placeholder.
import React, { ComponentType } from 'react' import { Transition, Transitioning } from 'react-native-reanimated' import { useAfterInteractions } from '../hooks/use-after-interactions' export function withInteractionsManaged<Props>( Component: ComponentType<Props>, Placeholder: ComponentType | null = null ) { return (props: Props) => { const { transitionRef, interactionsComplete } = useAfterInteractions() return ( <Transitioning.View transition={ <Transition.Together> <Transition.Change interpolation="easeInOut" /> <Transition.In type="fade" /> </Transition.Together> } style={{ flex: 1 }} ref={transitionRef} > {interactionsComplete ? ( <Component {...props} /> ) : ( Placeholder && <Placeholder /> )} </Transitioning.View> ) } }And then when exporting your screen, wrap it with this HOC:
SomeScreen.tsx
import { withInteractionsManaged } from './with-interactions-managed' ... export default withInteractionsManaged(SomeScreen) // or, with a placeholder: export default withInteractionsManaged(SomeScreen, Placeholder)
This is not working with react native 0.67.3, I guess its not supported on latest react native versions
from rfcs.
@ferrannp - can you provide a runnable example that you used for the gifs above?
from rfcs.
also it looks like on the react-native-navigation issue you're looking at transitions on iOS but in this issue you're looking at Android, is there a reason for that difference @ferrannp?
from rfcs.
Hey @brentvatne! Thanks for moving this.
First, ok I will provide an example just for this case. I'll prepare the code, push it to a repo and publish it here.
About iOS stuff, not really, I was just using my own device (Android) when checking with react-navigation
because with react-native-navigation
I was having real big troubles to make it work for both platforms with the latest version of react-native
. But anyway, it behaves the same in both platforms.
from rfcs.
willFocus
to show a loading spinner and didFocus
to show the calendar? Will that not be the same as InteractionManager.runAfterInteractions
example?
Here you got the calendar: https://snack.expo.io/r1rdvmeGQ. It seems a bit better that my examples? In my app in calendar screen the only difference I have is the withTheme
HOC from react-native-paper
(and I still need to add a connect
from redux there to mark days in calendar).
from rfcs.
Hi @brentvatne , as stated on https://facebook.github.io/react-native/docs/performance#slow-navigator-transitions:
One solution to this is to allow for JavaScript-based animations to be offloaded to the main thread.
Wondering what is the status of this?
If animation is hanged over to UI thread, then regardless a screen is computationally expensive or not, navigation animation should never be blocked, right? FYI, @adamgins
from rfcs.
the status is that it happens automatically in react-navigation. if there is a slowdown in the animation then it is because there is too much work being done on the ui thread
from rfcs.
Hi,
I'd be interested to work on this kind of feature but wonder if it's not more flexible to support an additional community package to help doing that, as it could be handy outside of react-navigation too. But in the same way some tab navs are external, it could be possible to make an adaptation layer and offer a simple interface in react-navigation.
Any idea what kind of api we should build?
I'm thinking we could have some kind of placeholderScreen attribute to screen config
createStackNavigator({
Home: { screen: HomeScreen},
Calendar: {
screen: CalendarScreen,
screenPlaceholder: {
screen: ScreenWithSpinner,
animation: { ...},
}
}
}}
I've already some working code in some apps that use runAfterInteraction and a quite generic Overlay component with animated transitions from the placeholder screen to the heavy screen
from rfcs.
@slorber
Can we use loadingPlaceholderExperimental
in v3 now?
from rfcs.
Hi, no sorry didn't have time to work on this unfortunately
from rfcs.
would be neat for someone to have a go at this
from rfcs.
Hey Guys, any update in a plugin native workaround?
from rfcs.
any news?
at the moment we should use one of @brentvatne 's solutions or delay the full render of heavy screens using timeout.
If we don't. It takes seconds to get to the destination screen.
it is ugly, bad and ... !
from rfcs.
3. render the screen first (block the main thread briefly before you push the screen) then animate it in
@brentvatne This feels like the best solution to me. Seems to follow the suspense
-ful direction react is going towards.
from rfcs.
@nandorojo this is a great solution and shows a marked improvement for my heavier screens.
I am however struggling to get the Title to show as it did.. The setup of the screen itself looks like this;
TitleScreen.navigationOptions = ({ navigation }) => ({
title: navigation.getParam('name'),
headerRight: null,
})
the name
is passes into the screen on navigation. I also noted that the headerRight
is also ignored - so it seems that none of that static function is respected
from rfcs.
The solution would be to hoist non react statics. I’ll edit my answer shortly.
https://github.com/mridgway/hoist-non-react-statics/blob/master/README.md
from rfcs.
Implemented good workaround.
Basically the ChunkView will not render whole screen all together. It will render chunk by chunk
import React, { useEffect, useState, useRef } from 'react';
import { InteractionManager } from 'react-native';
const BATCH_SIZE = 4;
export default function ChunkView({ children }) {
const [batchIndex, setBatchIndexRaw] = useState(1);
const focusedRef = useRef(true);
const batchIndexRef = useRef(1);
const reachedEndRef = useRef(false);
const childrenChunk = reachedEndRef.current
? children
: children.slice(0, BATCH_SIZE * batchIndex);
const setBatchIndex = (index) => {
batchIndexRef.current = index;
setBatchIndexRaw(index);
};
const loadNextBatch = (timeout = 800) => {
InteractionManager?.runAfterInteractions(() => {
setTimeout(() => {
if (focusedRef.current) {
const nextBatchIndex = batchIndexRef.current + 1;
if (nextBatchIndex * BATCH_SIZE >= children.length) {
reachedEndRef.current = true;
} else {
loadNextBatch();
}
setBatchIndex(nextBatchIndex);
}
}, timeout);
});
return () => (focusedRef.current = true);
};
useEffect(() => {
loadNextBatch(1000);
}, []);
return <>{childrenChunk}</>;
}
from rfcs.
I'm using https://github.com/th3rdwave/react-native-incremental which used to be part of react-native but was removed. Screens using FlatList should not need anything special as it already does incremental rendering.
<>
<MainContent />
<IncrementalGroup name="detail_content">
<Incremental>
<ContentA />
</Incremental>
<Incremental>
<ContentB />
</Incremental>
</IncrementalGroup>
</>
Incremental components inside a IncrementalGroup will be rendered one after the other inside InteractionManager callbacks. This assure JS is not blocked for long periods of time.
from rfcs.
Would it be worth breaking up the page into subcomponents and rendering with a SectionList
?
from rfcs.
Today, using native-stack should be enough.
from rfcs.
Today, using native-stack should be enough.
Still not working.
from rfcs.
@md-ajju
Try this #51 (comment) .
from rfcs.
Related Issues (20)
- Action property that indicates to routes that they should not change index HOT 1
- Improve ergonomics of back HOT 1
- Navigator config to pass params down HOT 1
- Add unsetParams to navigation prop HOT 2
- Reset state action HOT 2
- Expose logic for initializing state of createAppContainer HOT 4
- On will/didFocus subscribe, stop firing the listener if current screen is focused HOT 2
- Idea: Preloading HOT 6
- <StaticNavigator> for tests/storybook HOT 14
- StackActions.popToRouteName(routeName) HOT 2
- Alternative API for defining navigators HOT 12
- how to send navigation events between peer navigators? HOT 4
- Deep linking with authentication
- How to hide tab bar item n react-navigation HOT 3
- Ability to show screens on top of native modals
- useNavigationParams hook HOT 1
- Add useMaterialTabsHeight HOT 1
- Typesafe Stack & Navigation HOT 1
- devTools prop HOT 5
- [RFC] TabView API change
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from rfcs.