how to update a component from another child component in ReactNative - javascript

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" })}
/>
)
}

Related

React native useEffect does not work when it's in same screen or back to screen

I am using react naviagtion 5 and typescript for my mobile.
I have two sets of data in the database, one is date and one is time. Both are expecting strings.
I made one POST request when the user chooses the date, and that time will be selected for the user. I made a helper function for when the user's choosing time will be over, in which case I show them an alert on the front-end that shows
"Your selected time expired!"
After showing that Alert. I forcefully clear the delivery time. My logic works fine. But I am having issue that when I am in the same screen or minimise the app, the useEffect does not trigger. If I go different screen and come back to that delivery screen then it triggers. I don't know how to hot reload the app when selecting time expired even though I am in the same screen or minimise the app.
I share my code in Expo snack
This is my sample code
import * as React from 'react';
import {Text,View, Alert, Button} from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import { useNavigation } from '#react-navigation/native';
const Stack = createStackNavigator();
const deliveryDate = "2021-06-21";
const deliveryTime = "10:00-10:03";
const isTimeWindowExpired = (date: string, time: string) => {
const [startTime] = time.split('-');
const newDate = new Date(`${date}T${startTime}`);
if (newDate.getTime() < new Date().getTime()) {
return true;
}
return false;
};
function Home() {
const navigation = useNavigation()
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Home</Text>
<Button
title="Go to Details"
onPress={() => navigation.navigate('details')}
/>
</View>
);
}
function SecondHome() {
const navigation = useNavigation()
React.useEffect(() => {
const isExpired = isTimeWindowExpired(deliveryDate, deliveryTime);
const expirationTrigger = () => {
Alert.alert("Time Expired");
};
if (isExpired) {
expirationTrigger();
}
}, [deliveryDate, deliveryTime]);
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>useEffect does not trigger when it's in same screen or minimisse</Text>
<Text>Home</Text>
<Button
title="Go back Home"
onPress={() => navigation.goBack()}
/>
</View>
);
}
const App = () => {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="home" detachInactiveScreens>
<Stack.Screen name="home" component={Home} />
<Stack.Screen name="details" component={SecondHome} />
</Stack.Navigator>
</NavigationContainer>
);
};
export default App;
Using useIsFocused hook from react-navigation could help you.
It basically checks, if the screen is looked at. Adding it into useEffect and its dependencies, the useEffect will trigger every time isFocused changes its value. When you leave the screen (to another screen or minimizing the app) it's false and when you come back, it is true back again.
import { useIsFocused } from '#react-navigation/native';
function SecondHome() {
const navigation = useNavigation()
const isFocused = useIsFocused();
React.useEffect(() => {
if(isFocused) {
const isExpired = isTimeWindowExpired(deliveryDate, deliveryTime);
const expirationTrigger = () => Alert.alert("Time Expired");
if (isExpired) {
expirationTrigger();
}
}
}, [deliveryDate, deliveryTime, isFocused]);
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>useEffect does not trigger when it's in same screen or minimisse</Text>
<Text>Home</Text>
<Button
title="Go back Home"
onPress={() => navigation.goBack()}
/>
</View>
);
}
You can read more about it here https://reactnavigation.org/docs/use-is-focused/

Only last function create effect when calling same function at a time with different parameter in react useEffect

I have created a component called "CommonStyleGenerator", to generate simple style object with keys like height, width and bgColor. I have created a text fields in component and whenever text change in any of text field I am calling onStyleChange with changed field key and it's value to store changed value in parent component.
Here is the CommonStyleGenerator.js component code :
import React, { useEffect, useState } from 'react';
import { View, Text, TextInput, StyleSheet } from 'react-native';
const CommonStyleGenerator = ({ style = {
height: "100%",
width: "100%",
bgColor: "#ffffff"
}, onStyleChange }) => {
useEffect(() => {
onStyleChange("height", "100%");
onStyleChange("width", "100%");
onStyleChange("bgColor", "#ffffff"); //Only this getting effect.
}, []);
return (
<View>
<TextInput
value={style?.height}
placeholder="height"
onChangeText={(text) => onStyleChange("height", text)}
style={styles.textField}
/>
<TextInput
value={style?.width}
placeholder="width"
onChangeText={(text) => onStyleChange("width", text)}
style={styles.textField}
/>
<TextInput
value={style?.bgColor}
placeholder="background color"
onChangeText={(text) => onStyleChange("bgColor", text)}
style={styles.textField}
/>
</View>
)
}
const styles = StyleSheet.create({
textField: {
borderWidth: 1,
marginVertical: 10
}
})
export default CommonStyleGenerator;
And here is the code of how I called the component in my App.js :
import React, { useEffect, useState } from 'react';
import { View, Button, Text } from 'react-native';
import CommonStyleGenerator from './components/CommonStyleGenerator';
const App = () => {
const [commonStyle, setCommonStyle] = useState(null);
return (
<View style={{flex: 1, alignItems: 'center', padding: 20}}>
<CommonStyleGenerator
style={commonStyle}
onStyleChange={(key, value) => setCommonStyle({
...commonStyle,
[key]: value
})}
/>
<Text>{JSON.stringify(commonStyle, null, 2)}</Text>
</View>
)
}
export default App;
Now, what I want is that on load CommonStyleGenerator component, generate default style if user don't change any textfield. So I called onStyleChange function on useEffect for each key. But for only last key(bgColor) function is called.
Is anyone know how I can solve this issue ?
snack.expo.io link
The commonStyle in state when the three initial onStyleChanges are called is null. Each time it's called, the new state is set with an object with a single key. The synchronous call of an onStyleChange after a previous onStyleChange hasn't updated the commonStyle variable outside yet.
Your current code is like doing:
onStyleChange({ height: '100%' });
onStyleChange({ width: '100%' });
onStyleChange({ bgColor: '#ffffff' });
so only the last object passed appears to be in state on the next render.
Use a callback instead, when setting:
onStyleChange={(key, value) => setCommonStyle(commonStyle => ({
...commonStyle,
[key]: value
}))}

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.

RN checkbox as card and multi select

I've to select one or two cards from a list of cards , where on tap of each card / checkbox , that card gets highlighted (and is selected). Each card has checkbox on it, which shows that particular card through which selection is made. You can re-tap on the same checkbox to unselect it.
I'm pretty new to react native and confused how to achieve this functionality. Here's the code for reference.
import React, { Component } from 'react';
import {View, Image, StyleSheet} from 'react-native';
import { Container, Content, ListItem, CheckBox, Text, Body } from 'native-base';
export default class Career extends Component {
topics = ['abc','def', 'ghi', 'jkl']
render() {
const extract = this.topics.map((topic, i) => {
return(
<View style={styles.cardContainer}>
<Image style={{width:50, height:50}} source={require('../../assets/images/idcLogo.png')}/>
<CheckBox checked={false}></CheckBox>
<Text>{topic}</Text>
</View>
)
});
return (
<Container>
<Content>
<View style={styles.mainContainer}>
{extract}
</View>
</Content>
</Container>
);
}
}
const styles = StyleSheet.create({
cardContainer: {
borderRadius:5,
borderColor:"#ccc",
borderWidth:2,
justifyContent:'center',
alignItems:'center',
width:'50%',
margin:5
},
mainContainer:{
justifyContent:'center',
width:'100%',
alignItems:'center',
flex:1
}
})
Please let me know if this you need any other information on this. Thanks in advance
I would change the <View style={styles.cardContainer}></View> into <TouchableOpacity style={styles.cardContainer}> then I'll add a function to monitor the changes of the checked status for the button.
I'll add this function before the render function
handleOnTap = (topic) => {
this.setState(prevState => ({ [topic]: !prevState[topic], });
}
<Checkbox onPress={() => this.handleOnTap(topic)} checked={!!this.state[topic]} />
Don't forget to add a key generating elements via this.topics.map; <TouchableOpacity style={styles.cardContainer} key={topic-${i}}>
Hope that helps

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

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.

Categories