Why useState hook doesn't update in WebView onScroll event? - react-native

I'm trying to create a workaround to calculate/get content height which should be rendered inside React Native WebView component.
For that Im artificially calling onScroll event with Javascript , then trying to update state with the actual content height. But even console log shows different values for content height, which should trigger state update , it doesnt happen. Only when really scrolling it gets updated. But its for sure that even without scrolling the event is triggered as console log is working.
Any idea why here state update fails? (In the code snippet some backticks are missing)
const [contentHeight, setContentHeight] = useState(10);
const webViewScript =
setTimeout(function() {
window.postMessage(document.body.scrollDown);
}, 1000);
true; // note: this is required, or you'll sometimes get silent failures
;
<WebView
injectedJavaScript={window.ReactNativeWebView.postMessage(${webViewScript})}
domStorageEnabled={true}
automaticallyAdjustContentInsets={false}
onScroll={(event) =>
setContentHeight(Number(event.nativeEvent.contentSize.height));
}
javaScriptEnabled
style={{ height: contentHeight }}
source={{ html: htmlTempl, baseUrl: '' }}
/>

Related

React Native Testing Library: scroll FlatList to bottom

I have a react-native FlatList component with onEndReached event used to load more items.
I am testing the component with react-native-testing-library. The test is as follows:
Component mounted.
Requests 10 items to mockedBackend.
Expect FlatList to render those 10 items.
fireEvent onEndReached to ask for next page with some more items.
Expect FlatList to render new items.
Test was failing because FlatList was not rendering new items even thought FlatList's data property was being updated (I used act and waitFor as needed).
I managed to make the test pass when I fired onScroll event to bottom after some tries with hardcoded values.
Is there a way to make a helper to scroll to bottom taking into account proper FlatList size?
Helper to improve:
export const scrollListToBottom = (list: ReactTestInstance) => {
// FIXME: improve to get real list size
// After some tries these hardcoded values allowed to pass the test
act(() => {
fireEvent.scroll(list, {
nativeEvent: {
contentSize: {height: 500, width: 100},
contentOffset: {y: 400, x: 0},
layoutMeasurement: {height: 100, width: 100},
}
})
})
// FIXME: if onScroll works perfectly is this even needed?
act(() => {
fireEvent(list, 'onEndReached')
})
}
Thank you.

How to make a React Native TextInput change Opacity just like TouchableOpacity?

My code looks something like this currently:
<View>
<TextInput placeholder='PlaceholderText'>
</TextInput>
</View>
I want to make a TextInput component that has an opacity animation on click (exactly like TouchableOpacity changes opacity on click).
I tried wrapping the TextInput inside TouchableOpacity, but it doesn't work since the touchable component surrounds text input. Is there a standard React Native or StyleSheet way of doing this or would I have to manually create an animation to mimic that effect?
Simply wrap your TextInput in a View element and animate the View's opacity color from the onFocs event of the TextInput. TextInput doesn't have the opacity attribute, therefore the additional view is required for your goal.
Following code may give you an idea how to solve it. Have to admit, that I haven't tested the code, it may contain some error, so use it carefully.
// First create an animated value to use for the view's opacity.
const textInputAnimOpacity = useRef(new Animated.Value(1.0)).current;
// create a method to set the opacity back to 1.0
const showTxtInput = () => {
Animated.timing(textInputAnimOpacity, {
toValue: 1.0, // value to reach
duration: 250 // time in ms
}).start();
};
// this animated method differs from the first one by (obviously the value 0.7)
//and the callback part that goes into the start()-method
const blurTxtInput = () => {
Animated.timing(textInputAnimOpacity, {
toValue: 0.7, // value to reach
duration: 250 // time in ms
}).start(({ finished }) => {
showTxtInput(); // Callback after finish, setting opacity back to 1.0
});
};
/*Set the opacity to the value, we want to animate*/
<View style={{opacity:textInputAnimOpacity}}>
/* call blurTxtInput to set the value to 0.7 and again to 1.0 once it reaches 0.7*/
<TextInput onPressIn={blurTxtInput} placeholder='PlaceholderText'>
</TextInput>
</View>
If you just want to set opacity, make your styles change using the onPressIn and onPressOut props:
const [pressed, setPressed] = useState(false);
// in render
<TextInput
onPressIn={() => setPressed(true)}
onPressOut={() => setPressed(false)}
style={pressed ? styles.textInputPressed : styles.textInput}
// ...
/>
If you need the changes to animate, you can do that with the built-in RN Animated component or react-native-reanimated, using the same props to trigger the animations.

react native can't dynamically change Image component source uri

I'm facing a weird issue/bug with react native (0.61.5). I have a screen where a user can change his profile picture. The picture is fetched from redux.
const [pp, setPp] = useState(useSelector(state => state.profile.pp))
const ppChangeHandler = async () => {
try {
req = await fetch(...)
res = await req.json()
// res.url contains the url to the new uploaded image on the server
setPp(res.url)
} catch (e) {
console.log(e)
}
}
As you can see, I send a request to the server containing the new image (this is not included in the code, but it works), and as a response, I receive the newly generated url to the image. I want to update the profile picture on the screen to the new image once I receive the image url. But this does not work. The image dissapears and is not updated.
I'm thinking I'm facing this bug: Image Component will not update correctly when passing in new url #9195
I have tried the following things:
Adding a "key" prop to containing 'pp': <Image key={pp} source={{ uri: pp }} style={{ width: 100, height: 100 }} />
Adding "cache: reload" to the source: <Image source={{ uri: pp, cache: reload }} style={{ width: 100, height: 100 }} />
Does anyone has a tip/workaround for this problem? The "update" of the image works, when I refresh the app, the new profile picture is showed, but I want it to show immediately, not after refreshing the app.
Edit: I also tried it with FastImage (https://github.com/DylanVann/react-native-fast-image) but I get the same result.

How to prerender a component in react-navigation before switching to it?

Inside my StackNavigator, one of the components includes web content with a long loading time. However, this screen will only be shown late in my navigation flow.
How can I use this time to render my component in the background before finally switching to it?
I couldn't find anything comparable to ReactDOM.render in React Native that would allow me to render it manually.
I am not aware of any option in react-navigation to preload a screen that is not displayed, except maybe when the screen is part of a tab navigator.
Depending on what is slowing down the rendering, you might be able to do some actions in the first screen and later pass the results to the second screen using a navigation parameter.
For instance, if you are fetching data from an api in the second screen, you could fetch this data in the first screen and pass it to the second one:
this.props.navigation.navigate('SecondScreen', { data: this.data });
If it is a component, you could also try to build it in the first screen and pass it in the same fashion:
this.props.navigation.navigate('SecondScreen', { component: this.component });
If you are rendering a WebView in the second screen, what can help is to render the WebView in the first screen too, but with no width or height. The WebView will not be displayed but the website data will be fetched and cached, making the real render more efficient:
render() {
return (
<View>
<WebView source={{ uri: 'https://github.com/facebook/react-native' }} style={{ height: 0, width: 0 }} />
{this.renderCurrentScreen()}
</View>
);
}

In react-native-video how can you disable the seek function?

I am trying to disable the seek function on react native video. I have a full video that I want to preview for 30 seconds. In order to do this I want to disable the seek button so a user cannot skip through the video.
I have tried giving onSeek the value of function that exits the video player however this does not seem to do anything.
if(!loading) {
return <Video source={{uri: uri}} // Can be a URL or a local file.
onFullscreenPlayerDidDismiss={this.onDismiss}
preferredPeakBitrate={this.state.preferredPeakBitrate}
ref={(player) => {
if(!this.state.playing && player) {
player.presentFullscreenPlayer()
this.setState({ playing: true })
}
}} // Store reference
rate={1.0} // 0 is paused, 1 is normal.
volume={1.0} // 0 is muted, 1 is normal.
muted={false} // Mutes the audio entirely.
paused={false} // Pauses playback entirely.
resizeMode="cover" // Fill the whole screen at aspect ratio.*
repeat={false} // Repeat forever.
playInBackground={true} // Audio continues to play when app entering background.
playWhenInactive={true} // [iOS] Video continues to play when control or notification center are shown.
ignoreSilentSwitch={"ignore"} // [iOS] ignore | obey - When 'ignore', audio will still play with the iOS hard silent switch set to silent. When 'obey', audio will toggle with the switch. When not specified, will inherit audio settings as usual.
progressUpdateInterval={PROGRESS_MILLISECONDS} // [iOS] Interval to fire onProgress (default to ~250ms)
onError={this.onVideoError} // Callback when video cannot be loaded
onProgress={this.onProgress}
onLoadStart={this.onStart}
onEnd={this.stopPlaybackPing}
/>
} else {
return <View />
}
}
Short answer: No, you can't.
You called presentFullscreenPlayer() to play the video, unfortunately, you can't disable any buttons on the player. Because that's the default player made by Apple if you're running your app on iPhone, not by the people who created react-native-video, and I don't believe there's any public API that allows you to do so.
What you can do, however, is to write your own full screen player, with any button you want/don't want on there. Here's some hint:
Create a custom component called CustomVideo, which takes the url of the video as a prop:
// CustomVideo.js file
import React, { PureComponent } from 'react';
import { ... } from 'react-native';
import Video from 'react-native-video';
export class CustomVideo extends PureComponent {
constructor(props) {
super(props)
this.state = {
// Have any state you want here, for example
paused: false,
played: 0,
duration: 0,
isFullscreen: false
}
}
render() {
const { url } = this.props;
const { paused, played, duration, isFullscreen } = this.state;
return(
<View style={{ ... }}>
<Video
source={{ uri: url }}
...
/>
// =======> Here, you add your custom buttons <=======
// Suppose you want a pause/play button
<TouchableOpacity onPress={this.toggleVideo}>
<Text>{paused ? "Play" : "Pause"}</Text>
</TouchableOpacity>
// If you want a progress indicator, which users
// can use to skip videos, then use `Slider` component
<Slider
value={...}
step={...}
onValueChange={(value) => ...}
/>
// Here, you toggle whether you want to play the video
// in full screen mode, if so, render it in a modal
// Also, add a full screen toggle button to the video
// the same way you add a play/pause button
<Modal visible={isFullscreen}>
<View>
<Video ... />
</View>
</Modal>
</View>
);
}
}
So, next time, when you want render a video, instead of calling <Video source={{ uri: '...' }} />, you can call your <CustomVideo url='https://....' /> component.