I want to move some code to inside of a component in react-native, but it will mess up the variable scopes - javascript

I have a react-native application I am writing and I want to move some code to an inner component. The problem is if I do this I can't figure out how to make it work because of variable scope. Can someone please take a look?
I have tried moving the code and it doesn't work. I don't know how to write clean code that does this.
import React, {useState} from 'react';
import { StyleSheet, Text, View, Image, Button } from 'react-native';
import { type ImageHolder } from './ImageHolder'
import OuterComponent from "./OuterComponent"
const imageholder2: ImageHolder[] = [
{
id: "1",
actualimage: require("./images/image1.jpeg"),
},
{
id: "2",
actualimage: require("./images/image2.jpg"),
},
{
id: "3",
actualimage: require("./images/image3.jpg"),
},
{
id: "4",
actualimage: require("./images/image4.jpg"),
},
];
//The following code works fine, but I want to move the code to a
//button that is inside of another component.
export default function App() {
const [currentImageIndex, setCurrentImageIndex] = useState(0)
return (
<View>
<View>
{
imageholder2[currentImageIndex] &&
<Image
key={imageholder2[currentImageIndex].id}
style = {{
width: 300,
height: 300,
}}
source={imageholder2[currentImageIndex].actualimage}
/>
}
</View>
<View> /*this button code is what I want to move because my button will be inside another component8?*/
<Button
title= "Press me"
onPress={() => setCurrentImageIndex(currentImageIndex ==
imageholder2.length - 1 ?
0 :
currentImageIndex + 1
)}
/>
</View>
</View>
);
}
/*here is my component with the button, but I don't know how to make the button functionality work because of scope.*/
import React, { Component } from "react";
import {
Image, StyleSheet, View, Text,Dimensions, Button
} from "react-native";
import Animated from "react-native-reanimated";
export default class OuterComponent extends Component {
render() {
return (
<View style ={{
backgroundColor: "#C0C0C0",
flexDirection: 'row',
}}>
<Button
title= "Press me"
onPress={() => setCurrentImageIndex(currentImageIndex ==
imageholder2.length - 1 ?
0 :
currentImageIndex + 1
)}
/>
</View>
)
}
}
/*so in my app.js file I should just be able to do
<OuterComponent /> or something similar and it should work.
The app should simply cycle through the images when the button is pressed.

You can get around variable scope issue by passing a callback function to your inner component and executing all scope specific code within the component that still requires it (in this case your images). It would look something like this:
const scopeSpecificFunction =() => {
...
}
<ParentComponent>
<ChildComponent callbackfunction={()=>scopeSpecificFunction()}>
{...children}
</ChildComponent>
<ParentComponent>
//--------------------
// then the child component (Button) in another file
export default ChildComponent(props)...
<View>
<Button onPress={()=>props.callbackfunction()} />
</View>
I hope that helps and is clear enough. Call backs in general can circumvent inter component scoping issues.

Related

how to update a component from another child component in ReactNative

Right. I've been googling for days and can't seem to find an example that works and that I understand.
I've currently got three components, ButtonComponent, SecondButton, and TextComponent.
I already have it such that tapping Button can update State and thus change the text in TextComponent (if i set setState to the text string on line 39).
How can I change it so that it will change to a string of text that I can set from the button itself rather than the function fired by the button, so that SecondButton can fire the same function as Button, but update the text to something different.
I figure it must be a small tweak to onPress? but everything I try keeps complaining about objects and things being undefined.
Thanks.
import { setStatusBarNetworkActivityIndicatorVisible, StatusBar } from 'expo-status-bar';
import React, { useState, useEffect } from 'react';
import { StyleSheet, Text, View, Button } from 'react-native';
function ButtonComponent({changeText}) {
console.log(`2. ${changeText} from inside Button Component`)
return(
<Button title="change text" onPress={changeText} />
)
}
function SecondButton({changeText}){
return(
<Button title="change text again" onPress={changeText} />
)
}
function TextComponent({text}) {
console.log(`3. ${text} from inside Text Component`) //
return (
<View>
<Text style={styles.text}>{text}</Text>
</View>
)
}
export default function App() {
console.log('1 .start')
const [text, setText] = useState('default text'); // declared in the screen
const changeText = ({newtext}) => {
setText(newtext)
console.log(`4. ${text} just after changeText`)
}
return(
<View style={styles.container}>
<ButtonComponent changeText={changeText}/>
<SecondButton changeText={changeText}/>
<TextComponent text={text}/>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
text: {
color: 'red',
}
});
function SecondButton({changeText}){
return(
<Button
title="change text again"
onPress={() => changeText( { newtext: "different string" })}
/>
)
}

React Native: Passing useState() data to unrelated screens

Explanation: I am creating a fitness app, my fitness app has a component called WorkoutTimer that connects to the workout screen, and that screen is accessed via the HomeScreen. Inside the WorkoutTimer, I have an exerciseCount useState() that counts every time the timer does a complete loop (onto the next exercise). I have a different screen called StatsScreen which is accessed via the HomeScreen tab that I plan to display (and save) the number of exercises completed.
What I've done: I have quite literally spent all day researching around this, but it seems a bit harder with unrelated screens. I saw I might have to use useContext() but it seemed super difficult. I am fairly new to react native so I am trying my best haha! I have attached the code for each screen I think is needed, and attached a screenshot of my homeScreen tab so you can get a feel of how my application works.
WorkoutTimer.js
import React, { useState, useEffect, useRef } from "react";
import {
StyleSheet,
Text,
View,
TouchableOpacity,
Button,
Animated,
Image,
SafeAreaView,
} from "react-native";
import { CountdownCircleTimer } from "react-native-countdown-circle-timer";
import { Colors } from "../colors/Colors";
export default function WorkoutTimer() {
const [count, setCount] = useState(1);
const [exerciseCount, setExerciseCount] = useState(0);
const [workoutCount, setWorkoutCount] = useState(0);
const exercise = new Array(21);
exercise[1] = require("../assets/FR1.png");
exercise[2] = require("../assets/FR2.png");
exercise[3] = require("../assets/FR3.png");
exercise[4] = require("../assets/FR4.png");
exercise[5] = require("../assets/FR5.png");
exercise[6] = require("../assets/FR6.png");
exercise[7] = require("../assets/FR7.png");
exercise[8] = require("../assets/FR8.png");
exercise[9] = require("../assets/S1.png");
exercise[10] = require("../assets/S2.png");
exercise[11] = require("../assets/S3.png");
exercise[12] = require("../assets/S4.png");
exercise[13] = require("../assets/S5.png");
exercise[14] = require("../assets/S6.png");
exercise[15] = require("../assets/S7.png");
exercise[16] = require("../assets/S8.png");
exercise[17] = require("../assets/S9.png");
exercise[18] = require("../assets/S10.png");
exercise[19] = require("../assets/S11.png");
exercise[20] = require("../assets/S12.png");
exercise[21] = require("../assets/S13.png");
return (
<View style={styles.container}>
<View style={styles.timerCont}>
<CountdownCircleTimer
isPlaying
duration={45}
size={240}
colors={"#7B4FFF"}
onComplete={() => {
setCount((prevState) => prevState + 1);
setExerciseCount((prevState) => prevState + 1);
if (count == 21) {
return [false, 0];
}
return [(true, 1000)]; // repeat animation for one second
}}
>
{({ remainingTime, animatedColor }) => (
<View>
<Image
source={exercise[count]}
style={{
width: 150,
height: 150,
}}
/>
<View style={styles.timeOutside}>
<Animated.Text
style={{
color: animatedColor,
fontSize: 18,
position: "absolute",
marginTop: 67,
marginLeft: 35,
}}
>
{remainingTime}
</Animated.Text>
<Text style={styles.value}>seconds</Text>
</View>
</View>
)}
</CountdownCircleTimer>
</View>
</View>
);
}
const styles = StyleSheet.create({})
WorkoutScreen.js
import React, { useState } from "react";
import { StyleSheet, Text, View } from "react-native";
import WorkoutTimer from "../components/WorkoutTimer";
export default function WorkoutScreen() {
return (
<View style={styles.container}>
<WorkoutTimer />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
HomeScreen.js
import React from "react";
import { StyleSheet, Text, View, SafeAreaView, Button } from "react-native";
import { TouchableOpacity } from "react-native-gesture-handler";
import { AntDesign } from "#expo/vector-icons";
import { Colors } from "../colors/Colors";
export default function HomeScreen({ navigation }) {
return (
<SafeAreaView style={styles.container}>
<Text style={styles.pageRef}>SUMMARY</Text>
<Text style={styles.heading}>STRETCH & ROLL</Text>
<View style={styles.content}>
<TouchableOpacity
style={styles.timerDefault}
onPress={() => navigation.navigate("WorkoutScreen")}
>
<Button title="START WORKOUT" color={Colors.primary} />
</TouchableOpacity>
<TouchableOpacity
style={styles.statContainer}
onPress={() => navigation.navigate("StatsScreen")}
>
<AntDesign name="barschart" size={18} color={Colors.primary} />
<Text style={{ color: Colors.primary }}>Statistics</Text>
<AntDesign name="book" size={18} color={Colors.primary} />
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({})
StatsScreen.js
import React from "react";
import { StyleSheet, Text, View } from "react-native";
import { exerciseCount, workoutCount } from "../components/WorkoutTimer";
export default function StatsScreen() {
return (
<View style={styles.container}>
<Text display={exerciseCount} style={styles.exerciseText}>
{exerciseCount}
</Text>
<Text display={workoutCount} style={styles.workoutText}>
{workoutCount}
</Text>
</View>
);
}
const styles = StyleSheet.create({});
Home Screen Image
As far as I can tell, you're almost there! You're trying to get your 2 state
variables from the WorkoutTimer like this:
import { exerciseCount, workoutCount } from "../components/WorkoutTimer";
Unfortunatly this won't work :( . These two variables change throughout your
App's life-time and that kinda makes them "special".
In React, these kinds of variables need to be declared in a parent component
and passed along to all children, which are interested in them.
So in your current Setup you have a parent child relationship like:
HomeScreen -> WorkoutScreen -> WorkoutTimer.
If you move the variables to HomeScreen (HomeScreen.js)
export default function HomeScreen({ navigation }) {
const [exerciseCount, setExerciseCount] = useState(0);
const [workoutCount, setWorkoutCount] = useState(0);
you can then pass them along to WorkoutScreen or StatsScreen with something
like:
navigation.navigate("WorkoutScreen", { exerciseCount })
navigation.navigate("StatsScreen", { exerciseCount })
You'll probably have to read up on react-navigation's documentation for .navigate I'm not sure I remember this correctly.
In order to read the variable you can then:
export default function WorkoutScreen({ navigation }) {
const exerciseCount = navigation.getParam(exerciseCount);
return (
<View style={styles.container}>
<WorkoutTimer exerciseCount={exerciseCount} />
</View>
);
}
and finally show it in the WorkoutTimer:
export default function WorkoutTimer({ exerciseCount }) {
Of course that's just part of the solution, since you'll also have to pass
along a way to update your variables (setExerciseCount and setWorkoutCount).
I encourage you to read through the links I posted and try to get this to work.
After you've accumulated a few of these stateful variables, you might also want to look at Redux, but this is a bit much for now.
Your app looks cool, keep at it!
I ended up solving this problem with useContext if anyone is curious, it was hard to solve initially. But once I got my head around it, it wasn't too difficult to understand.
I created another file called exerciseContext with this code:
import React, { useState, createContext } from "react";
const ExerciseContext = createContext([{}, () => {}]);
const ExerciseProvider = (props) => {
const [state, setState] = useState(0);
//{ exerciseCount: 0, workoutCount: 0 }
return (
<ExerciseContext.Provider value={[state, setState]}>
{props.children}
</ExerciseContext.Provider>
);
};
export { ExerciseContext, ExerciseProvider };
and in App.js I used ExerciseProvider which allowed me to pass the data over the screens.
if (fontsLoaded) {
return (
<ExerciseProvider>
<NavigationContainer>
<MyTabs />
</NavigationContainer>
</ExerciseProvider>
);
} else {
return (
<AppLoading startAsync={getFonts} onFinish={() => setFontsLoaded(true)} />
);
}
}
I could call it with:
import { ExerciseContext } from "../components/ExerciseContext";
and
const [exerciseCount, setExerciseCount] = useContext(ExerciseContext);
This meant I could change the state too! Boom, solved! If anyone needs an explanation, let me know!
I think you have to use Mobx or Redux for state management. That will be more productive for you instead built-in state.

How do you execute Javascript in React-Native WebView?

I'm trying to execute javascript in a WebView that is loaded on an iOS advice. I'm trying to get a painfully simple example to work but it is consistently throwing an undefined or TypeError.
Here is the code:
import React, { Component } from 'react';
import { StyleSheet, Text, View, Button } from 'react-native';
import { WebView } from 'react-native-webview';
export default class App extends Component {
constructor(props){
super(props)
this.onPressButton = this.onPressButton.bind(this)
}
onPressButton(){
this.webview.injectJavascript(`alert('hello')`)
}
render() {
return (
<View style={{ height: '100%' }}>
<WebView
ref={ref => (this.webview = ref)}
javaScriptEnabled={true}
source={{ uri: 'https://google.com' }}
/>
<Button onPress={this.onPressButton} style={{ height: '10%' }} title="Test Javascript" />
</View>
);
}
}
Please don't recommend to use injectedJavascript because that is not my desired functionality.
Would appreciate any other approaches as well.
According to react-native-webview, you need to use injectJavaScript, not injectJavascript. That'll fix your issue.
Also, there are few things that need to change
You don't need to bind this.onPressButton = this.onPressButton.bind(this) instead you can use an arrow function.
You don't need to use javaScriptEnabled={true}. It is already using default value as true.
After your script includes true. This is required, or you'll sometimes get silent failures.
Check complete sample code
import React, { Component } from "react";
import { View, Button } from "react-native";
import { WebView } from "react-native-webview";
const script = `
alert('hello')
true;
`;
export default class App extends Component {
onPressButton = () => {
this.webview.injectJavaScript(script);
};
render() {
return (
<View style={{ flex: 1 }}>
<WebView
ref={ref => (this.webview = ref)}
source={{ uri: "https://google.com" }}
/>
<Button onPress={this.onPressButton} title="Test Javascript" />
</View>
);
}
}
Hope this helps you. Feel free for doubts.

How can I cycle through an array of images using state/props?

I have a react-native app where I want to pass in an object array using state/props that contains images. I want to cycle through the images when a button is pressed. I have been working on this for a while now, and can't figure it out. Would someone mind taking a look? I'm new to react-native so if you think I should be doing this differently please let me know. Thanks!!
I have been going over javascript/react-native for a while now and can't figure out how I should do this.
//file1 (ImageHolder.js)
import type {ImageSourcePropType } from "react- native/Libraries/Image?ImageSourcePropType";
export type ImageHolder = {
id: string,
actualimage: ImageSourcePropType,
};
//file2(App.js)
import React from 'react';
import { StyleSheet, Text, View, Image, Button } from 'react- native';
import { type ImageHolder } from './ImageHolder'
const imageholder2: ImageHolder[] = [
{
id: "1",
actualimage: require("./images/image1.jpeg"),
},
{
id: "2",
actualimage: require("./images/image2.jpg"),
},
{
id: "3",
actualimage: require("./images/image3.jpg"),
},
{
id: "4",
actualimage: require("./images/image4.jpg"),
},
];
export default function App() {
return (
<View>
<View>
<Image
style = {{
width: 300,
height: 300,
}}
source = {require('./images/image1.jpeg')} />
</View>
<View>
<Button title= "Press me"/>
</View>
</View>
);
}
I simply want to be able to cycle through the images when pressing the button and somehow be able to access what I'm on.
Keep the current image index in a state and increase ir when you press the button.
import React, { useState } from 'react'
export default function App() {
const [currentImageIndex, setCurrentImageIndex] = useState(0)
return (
<View>
<View>
{
imageholder2[currentImageIndex] &&
<Image
key={imageholder2[currentImageIndex].id}
style = {{
width: 300,
height: 300,
}}
source={imageholder2[currentImageIndex].actualimage}
/>
}
</View>
<View>
<Button
title= "Press me"
onPress={() => setCurrentImageIndex(currentImageIndex == imageholder2.length - 1 ?
0 :
currentImageIndex + 1
)}
/>
</View>
</View>
);
}
Also, it would be better if you keep the images in a state.
Sorry for not giving the typescript answer.
This is my answer:
Call Carousel Component and pass an array of four images.
<Carousel
images={[
"https://randomuser.me/api/portraits/women/1.jpg",
"https://randomuser.me/api/portraits/men/1.jpg",
"https://randomuser.me/api/portraits/women/2.jpg",
"https://randomuser.me/api/portraits/men/2.jpg",
]}
/>
Then this is my carousel component
import React, { useState } from "react";
function Carousel(props) {
const { images } = props;
let [imageIndex, setImageIndex] = useState(0);
function increase() {
if (imageIndex <= 2) {
setImageIndex(imageIndex + 1);
} else {
setImageIndex(0);
}
}
function decrease() {
if (imageIndex > 0) {
setImageIndex(imageIndex - 1);
} else {
setImageIndex(3);
}
}
return (
<div>
<button onClick={decrease}>Left</button>
<img src={images[imageIndex]} alt="carousel" />
<button onClick={increase}>Right</button>
</div>
);
}
export default Carousel;

Getting 'undefined is not an object' when calling navigation prop methods in react native navigation

I'm having trouble calling react navigation methods from custom components outside of my original screens, specifically the one I'm working on right now is trying to call goBack() in a back arrow of a custom header component I made (code below). The error message I'm getting when I click the back arrow is:
undefined is not an object (evaluating '_this2.props.navigation.goBack')
Here is the code:
// HeaderText.js
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Platform } from 'react-native';
import { Icon } from 'expo';
export class HeaderText extends React.Component {
render() {
const needsBackButton = this.props.backIcon;
if (needsBackButton) {
return(
<View style={[styles.headerStyle,styles.buttonHeaderStyle]}>
<TouchableOpacity onPress={() => this.props.navigation.goBack()} style={styles.backButtonStyles}><Icon.Ionicons size={25} style={{ color: '#fff', fontWeight: 'bold' }} name={Platform.OS === 'ios' ? `ios-arrow-back` : 'md-arrow-back'} /></TouchableOpacity>
<Text style={styles.textStyle}>{this.props.headerText}</Text>
<View style={styles.emptyViewStyles} />
</View>
);
} else {
return(
<View style={styles.headerStyle}>
<Text style={styles.textStyle}>{this.props.headerText}</Text>
</View>
);
}
}
}
Here is the screen I'm putting that HeaderText component in:
// SubmitReferralScreen.js
import React from 'react';
import {
Image,
Platform,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
ImageBackground
} from 'react-native';
import { MonoText } from '../../components/general/StyledText';
import { HeaderText } from '../../components/general/HeaderText';
import { HomeScreenContainer } from '../../components/homeScreen/HomeScreenContainer';
import { IconButton } from '../../components/general/IconButton';
import { StatusInfo } from '../../constants/StatusInfo';
import SvgUri from 'react-native-svg-uri';
export default class SubmitReferralScreen extends React.Component {
static navigationOptions = {
header: null,
};
render() {
return (
<View style={{flex: 1, width: '100%',justifyContent: 'center', alignItems: 'center'}}>
<ImageBackground source={require('../../assets/images/background.png')} style={{width: '100%', height: '100%', flex: 1, justifyContent: 'flex-start', alignItems: 'center', backgroundColor: 'background-color: rgba(0, 0, 0, 0.5)',}}>
<HeaderText backIcon='true' headerText='New Referral' />
<Text>Submit referral here!</Text>
</ImageBackground>
</View>
);
}
}
And here is my Stack Navigator for the referral Screens:
// MainTabNavigator.js
const ReferralStack = createStackNavigator({
Referrals: ReferralScreen,
MakeReferral: SubmitReferralScreen,
});
I've looked at this StackOverflow answer: Getting undefined is not an object evaluating _this.props.navigation
And the answer there was to put only navigation.navigate(YourScreen). I tried that, and the error I got said "cannot find variable navigation".
How can I call navigation props from custom react native components?
By default only screen components are provided with the navigation prop. You can either use library provided ways of hooking up arbitrary components to the navigation state, or you can pass navigation as a prop manually.
Option #1. Using withNavigation:
React navigation exports a higher-order component through which you can inject the navigation props into any component you want. To do this, you can do something like:
Don't immediately export the HeaderText component class (remove export from that line)
At the bottom of that file add export default withNavigation( HeaderText );
or if you don't want to use a default export and keep it as a named export, instead do:
const NavigationConnected = withNavigation( HeaderText );
export { NavigationConnected as HeaderText };
Option #2. Passing navigation as prop: In your SubmitReferralScreen component you can simply pass this.props.navigation as a prop to the HeaderText component like: <HeaderText navigation={ this.props.navigation } />
It's because your navigation prop didn't found where is the navigation's value prop from the parent. Better you make HeaderText component using regular arrow function, like this;
const HeaderText = ({ needsBackButton }) => {
if(needsBackButton) {
return (
<View style={[styles.headerStyle,styles.buttonHeaderStyle]}>
<TouchableOpacity onPress={() => this.props.navigation.goBack()} style={styles.backButtonStyles}><Icon.Ionicons size={25} style={{ color: '#fff', fontWeight: 'bold' }} name={Platform.OS === 'ios' ? `ios-arrow-back` : 'md-arrow-back'} /></TouchableOpacity>
<Text style={styles.textStyle}>{this.props.headerText}</Text>
<View style={styles.emptyViewStyles} />
</View>
)
}
return (
// another component
)
}
And then, You can simply use useNavigation() to access navigation prop from any screen/component.
First, import useNavigation on component that handled function of the moving screen.
import { useNavigation } from '#react-navigation/native';
Create some constant to reference this module:
const navigation = useNavigation()
And then, simply use this on your TouchableOpacity's onPress prop like this;
<TouchableOpacity
onPress={() => navigation.goBack()}
style={styles.backButtonStyles}
>
//...
</ TouchableOpacity>
Get the complete documentation on this:
https://reactnavigation.org/docs/connecting-navigation-prop/

Categories