I have a basic WebView:
const WebViewComponent = () => {
function sendDataToWebView() {
webviewRef.current.postMessage('Hello world');
}
const webviewRef = useRef();
return (
<SafeAreaView>
<WebView
ref={webviewRef}
source={{
uri: 'https://www.google.com/',
}}
/>
</SafeAreaView>
);
}
Calling sendDataToWebView() works in this component since i have the reference. (useRef()).
But in another component:
const anotherComponent = () => {
const webviewRef = useRef();
webviewRef.current.postMessage('Hello world');
}
webviewRef is undefined.
How can i access my WebView from another component?
There are 2 ways to do this, with and without using name ref for the reference property.
Without using ref prop name, i.e. use any other name.
const WebViewComponent = ({ webviewRef }) => {
return (
<WebView
ref={webviewRef}
source={{ uri: 'https://www.google.com/' }}
/>
);
};
const AnotherComponent = () => {
const webviewRef = useRef();
useEffect(() => {
const test = () => {
const run = "window.alert('haha');";
webviewRef.current?.injectJavaScript(run);
};
setTimeout(test, 3000);
}, []);
return (
<SafeAreaView>
<WebViewComponent
webviewRef={webviewRef}
// ....
/>
</SafeAreaView>
);
}
Using ref. This is possible using forwardRef feature of React. You can read more about it here.
const WebViewComponent = React.forwardRef((props, ref) => {
return (
<WebView
ref={ref}
source={{ uri: 'https://www.google.com/' }}
/>
);
});
const AnotherComponent = () => {
const webviewRef = useRef();
// .....
return (
<SafeAreaView>
<WebViewComponent
ref={webviewRef}
// ....
/>
</SafeAreaView>
);
}
you can't just create a new ref and use that in your anotherComponent. If you want to display the webview in your anotherComponent, you need to import it and return is as part of the anotherComponent.
Related
I have an React Native app with two pages. On the first page I have a picker from which I need the data from in the second page. I try to use Context for making sate globally available but I didn't get it to work till now because I only get undefined types at the position where I wanna insert the global state and not the value who was selected from the picker. I dont't get any errors but the field where the picker value should be represented is empty.
File from which I wanna get state from:
const FirstFile = () => {
const [selectedValueRound, setSelectedValueRound] = useState("10 rounds");
return (
<View>
<RoundContext.Provider
value={[selectedValueRound, setSelectedValueRound]}
>
<View>
<Picker
selectedValue={selectedValueRound}
onValueChange={(itemValue, itemIndex) =>
setSelectedValueRound(itemValue)
}
>
<Picker.Item label="1 round" value="0"></Picker.Item>
<Picker.Item label="2 rounds" value="1"></Picker.Item>
</Picker>
</View>
</RoundContext.Provider>
</View>
);
};
Context file:
export const RoundContext = createContext(false);
Navigation file where I wrap my context around
const Stack = createNativeStackNavigator();
const {selectedValueRound, setSelectedValueRound} = useContext(RoundContext);
const MyStack = () => {
return (
<RoundContext.Provider value={[selectedValueRound, setSelectedValueRound]}>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="FirsFile" component={FirsFile} />
<Stack.Screen name="SecondFile" component={SecondFile} />
</Stack.Navigator>
</NavigationContainer>
</RoundContext.Provider>
);
};
File where I try to insert the global value:
const SecondFile = () => {
const [selectedValueRound, setSelectedValueRound] = useContext(RoundContext);
return (
<View>
<Text>{selectedValueRound}</Text>
</View>
);
};
export default SomeFile;
You also need to define context provider and wrap your app into it.
export const RoundContextProvider = ({children}) => {
const stateTuple = useState(false);
return <RoundContext.Provider value={stateTuple}>{children}</RoundContext.Provider>;
}
<RoundContextProvider>
<YourApp/>
</RoundContextProvider>
then you can use it as you described in the question: const [selectedValueRound, setSelectedValueRound] = useContext(RoundContext);
You must declare the state and the context provider in the top parent component. The children should only consume the values from the context.
The parent component
const MyStack = () => {
const [selectedValueRound, setSelectedValueRound] = useState("10 rounds");
const contextValue = useMemo(
() => [selectedValueRound, setSelectedValueRound],
[selectedValueRound]
);
return (
<RoundContext.Provider value={contextValue}>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="FirsFile" component={FirsFile} />
<Stack.Screen name="SecondFile" component={SecondFile} />
</Stack.Navigator>
</NavigationContainer>
</RoundContext.Provider>
);
};
Note that I used useMemo to prevent passing a new array to the context when selectedValueRound did not change.
The children
const FirstFile = () => {
const [selectedValueRound, setSelectedValueRound] = useContext(RoundContext);
return (
<View>
<View>
<Picker
selectedValue={selectedValueRound}
onValueChange={itemValue => setSelectedValueRound(itemValue)}
>
<Picker.Item label="1 round" value="0"></Picker.Item>
<Picker.Item label="2 rounds" value="1"></Picker.Item>
</Picker>
</View>
</View>
);
};
const SecondFile = () => {
const [selectedValueRound] = useContext(RoundContext);
return (
<View>
<Text>{selectedValueRound}</Text>
</View>
);
};
I'm trying to pass a ref to a component with a similar approach to the following code block, but the current value always returns undefined. This approach works fine with a plain FlatList from react-native, however it doesn't work once I'm using either an Animated.FlatList or an Animated.createAnimatedComponent(FlatList) :
const Parent = () => {
const flatListRef = useRef();
useEffect(() => {
console.log(flatListRef.current) // undefined
})
return (
<View>
<Child ref={flatListRef} />
</View>
)
}
const Child = React.forwardRef((props, ref) => {
return (
<Animated.FlatList
ref={ref}
/>
)
})
The library react-native-reanimated works a little bit different in comparison to react-native-animated.
If we create the animated component via Animated.createAnimatedComponent(FlatList), then everything works as expected.
Here is a working version of your code. I have logged the function scrollToIndex of the FlatList ref for testing purposes.
import Animated from "react-native-reanimated"
const ReanimatedFlatList = Animated.createAnimatedComponent(FlatList);
const Parent = (props) => {
const flatListRef = useRef(null);
useEffect(() => {
console.log(flatListRef.current.scrollToIndex)
}, [])
return (
<View>
<Child ref={flatListRef} />
</View>
)
}
const Child = React.forwardRef((props, ref) => {
return (
<ReanimatedFlatList
ref={ref}
/>
)
})
I am new to react native here I tried to convert class components to functional components, I have tried to pass ref in the functional component in several ways also I have used hooks to handle the state but I am unable to do so please help me out thanks in advance.
export default class AddClick extends Component {
constructor(props) {
super(props);
this.state = {
changeAnim: false,
};
}
componentDidMount() {
setTimeout(() => {
// handleScreenNavigation("OtpScreen", {});
this.setState({ changeAnim: true }, () => {
if (this.state.changeAnim) {
this.animation.play(48, 48);
}
});
}, 1500);
this.animation.play();
}
render() {
return (
<View style={styles.container}>
<View>
<Animation
ref={(animation) => {
this.animation = animation;
console.log("------#######");
}}
style={styles.imageStyle}
resizeMode="cover"
loop={true}
source={anim}
/>
</View>
</View>
);
}
}
here i have mentioned my attempt by functional component.
const AddClick = (props) => {
const [changeAnimation, setChangeAnimation] = useState(false)
useEffect(() => {
setTimeout(()=>{
setChangeAnimation(true),()=>{
if(changeAnimation){
animation.play(48,48)
}
}
},1500)
animation.play();
}, [])
return (
<View style={styles.container}>
<View>
<Animation
ref={(animation) => {
this.animation = animation;
console.log("------#######");
}}
style={styles.imageStyle}
resizeMode="cover"
loop={true}
source={anim}
/>
</View>
</View>
);
}
AppRegistry.registerComponent("AddClick", () => AddClick);
You cannot use this in a functional component. You can find the updated code here:
const AddClick = (props) => {
const [changeAnimation, setChangeAnimation] = useState(false)
let animation; // Create a local variable
useEffect(() => {
setTimeout(()=>{
setChangeAnimation(true),()=>{
if(changeAnimation){
animation.play(48,48)
}
}
},1500)
animation.play(); // Make sure to check if animation is defined before calling any methods
}, [])
return (
<View style={styles.container}>
<View>
<Animation
ref={(anim) => {
animation = anim;
console.log("------#######");
}}
style={styles.imageStyle}
resizeMode="cover"
loop={true}
source={anim}
/>
</View>
</View>
);
}
AppRegistry.registerComponent("AddClick", () => AddClick);
I am new to react native and my JS is a bit rusty. I need to be able to change the value of my collection for the firestore. I have two buttons that will change the value of typeOfPost by setting the state. Component1 can successfully get "this.state.typeOfPost". However, when I click one of the buttons and update the state my log inside of the async function is not being called. It is only called when the app initially renders. What I find weird is that my log on the top of Component1 will display as expected. Is there any better way of doing this?
class Forum extends Component {
state = {
typeOfPost: ' '
}
onPressSitter = () => {
this.setState({
typeOfPost: 'sitterPosts'
})
}
onPressNeedSitter = () => {
this.setState({
typeOfPost: 'needPosts'
})
}
render() {
return (
<View style={styles.container}>
<View style={styles.row}>
<TouchableOpacity
style={styles.button}
onPress={this.onPressSitter}
>
<Text>I am a sitter</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.button}
onPress={this.onPressNeedSitter}
>
<Text>Need a sitter</Text>
</TouchableOpacity>
</View>
<View>
<Component1 typeOfPost = {this.state.typeOfPost}> </Component1>
</View>
</View>
)
}
}
const Component1 = (props) => {
console.log("type of post " + props.typeOfPost);
const [loading, setLoading] = useState(true); // Set loading to true on component mount
const [data, setData] = useState([]); // Initial empty array of data
const getData = async () => {
console.log("type of post inside async " + props.typeOfPost);
const subscriber = firestore()
.collection(props.typeOfPost) // need to be able to update this
.onSnapshot(querySnapshot => {
const data = [];
querySnapshot.forEach(documentSnapshot => {
data.push({
...documentSnapshot.data(),
key: documentSnapshot.id,
});
});
setData(data);
setLoading(false);
});
// Unsubscribe from events when no longer in use
return () => subscriber();
}
useEffect(() => {
getData();
}, [])
if (loading) {
return <ActivityIndicator />;
}
return (
<FlatList
data={data}
ListEmptyComponent={
<View style={styles.flatListEmpty}>
<Text style={{ fontWeight: 'bold' }}>No Data</Text>
</View>
}
renderItem={({ item }) => (
<View>
<Text>User ID: {item.fullName}</Text>
</View>
)}
/>
)
}
There is a difference between mount and render. I see no problem with your code except the few remarks I have made. The thing is that when you change typeOfPost, the component is rerendered, but the useEffect is not called again, since you said, it's just called when it was first mounted:
useEffect(() => {
}, []) // ---> [] says to run only when first mounted
However here, you want it to run whenever typeOfPost changes. So here is how you can do this:
useEffect(() => {
getData();
}, [typeofPost])
class Forum extends Component {
state = {
typeOfPost: ' '
}
onPressSitter = () => {
this.setState({
typeOfPost: 'sitterPosts'
})
}
onPressNeedSitter = () => {
this.setState({
typeOfPost: 'needPosts'
})
}
render() {
return (
<View style={styles.container}>
<View style={styles.row}>
<TouchableOpacity
style={styles.button}
onPress={this.onPressSitter}
>
<Text>I am a sitter</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.button}
onPress={this.onPressNeedSitter}
>
<Text>Need a sitter</Text>
</TouchableOpacity>
</View>
<View>
<Component1 typeOfPost = {this.state.typeOfPost}> </Component1>
</View>
</View>
)
}
}
const Component1 = (props) => {
const { typeOfPost } = props
console.log("type of post " + props.typeOfPost);
const [loading, setLoading] = useState(true); // Set loading to true on component mount
const [data, setData] = useState([]); // Initial empty array of data
const getData = () => {
setLoading(true)
console.log("type of post inside async " + props.typeOfPost);
const subscriber = firestore()
.collection(props.typeOfPost) // need to be able to update this
.onSnapshot(querySnapshot => {
const data = [];
querySnapshot.forEach(documentSnapshot => {
data.push({
...documentSnapshot.data(),
key: documentSnapshot.id,
});
});
setData(data);
setLoading(false);
});
// Unsubscribe from events when no longer in use
return () => subscriber();
}
useEffect(() => {
getData();
}, [typeofPost])
if (loading) {
return <ActivityIndicator />;
}
return (
<FlatList
data={data}
ListEmptyComponent={
<View style={styles.flatListEmpty}>
<Text style={{ fontWeight: 'bold' }}>No Data</Text>
</View>
}
renderItem={({ item }) => (
<View>
<Text>User ID: {item.fullName}</Text>
</View>
)}
/>
)
}
you are using a class based component to access react hook which is a bad practice, i will advice you use a functional component and you have access to react useCallback hook which will handle your request easily
const ButtonPressed = useCallback(() => {
setLoading(true);
getData()
}).then(() => setLoading(false));
}, [loading]);
I have a simple functional component with a WebView inside a TabNavigator where I'd like to inject some javascript when the tab is focused like so:
import React, {useRef, useEffect} from 'react';
import {WebView} from 'react-native-webview';
export default (props) => {
const webViewRef = useRef();
React.useEffect(() => {
const unsubscribe = props.navigation.addListener('focus', () => {
webViewRef.current.injectJavascript('console.log("Hello World")');
});
return unsubscribe;
}, []);
return (
<WebView ref={webViewRef} />
);
}
For some reason, webViewRef is always undefined in the listener.
If I do something like <WebView ref={(r) => console.log(r)} /> I get something like the following:
EDIT:
Simply adjusting the provided example -does- work, so I suppose I will have to figure out what is going on in my code:
import React, { useRef } from 'react'
import { View } from 'react-native'
import { WebView } from 'react-native-webview'
export default () => {
const webViewRef = useRef(null);
const run = `
document.body.style.backgroundColor = 'blue';
true;
`
setTimeout(() => {
webViewRef.current.injectJavaScript(run)
}, 3000)
return (
<View style={{ flex: 1 }}>
<WebView
ref={webViewRef}
source={{
uri:
'https://github.com/react-native-community/react-native-webview',
}}
/>
</View>
)
}
EDIT 2: Adjusting the simple example and attempting to use the ref inside the listener does not work:
import React, { useRef } from 'react'
import { View } from 'react-native'
import { WebView } from 'react-native-webview'
export default (props) => {
const webViewRef = useRef(null);
const run = `
document.body.style.backgroundColor = 'blue';
true;
`
const handleLoadEnd = () => {
props.navigation.addListener('focus', () => {
webViewRef.current.injectJavascript(run)
})
}
return (
<View style={{ flex: 1 }}>
<WebView
ref={webViewRef}
source={{
uri:
'https://github.com/react-native-community/react-native-webview',
}}
onLoadEnd={handleLoadEnd}
/>
</View>
)
}
Edit 3: useFocusEffect also has same problem:
useFocusEffect(
React.useCallback(() => {
webViewRef.current.injectJavascript('alert("HI")')
}, [props.navigation])
)
Sources:
React Navigation: Call a function when focused screen changes
React Native WebView: Communicating between JS and Native
Oh man, it's really another one of those epic hair pulling days:
webViewRef.injectJavascript < BAD
webViewRef.injectJavaScript < GOOD
Somebody please give the last 4 hours of my life back
The ref will be set, if the onLoadEnd event has triggered. So you must adjust your code like so:
export default (props) => {
const webViewRef = useRef();
const handleLoadEnd = () => {
props.navigation.addListener('focus', () => {
webViewRef.current.injectJavascript('console.log("Hello World")');
});
}
return (
<WebView ref={webViewRef} onLoadEnd={handleLoadEnd}/>
);
}
I have a working example. See here https://github.com/Tracer1337/MRGVP/blob/master/vertretungsplan/components/PaginatedWebview/PaginatedWebview.js