React Native - how do you call the function of a child component from its parent (without triggering an infinite loop)? - javascript

I am trying to create a React Native e-commerce app where the featured products are shown, but then the user can view a list of categories via a sheet popping up from the bottom, which will load the products from said category.
I have managed to create such a bottom sheet using react-native-btr's BottomSheet. However, the function to show/hide the component (simply toggling a boolean in state) needs to be available to the component itself (to handle the back button and backdrop presses).
This is the component code:
const TestNav = (props, ref) => {
const [visible, setVisible] = useState(false);
const toggleVisible = () => {
setVisible(!visible);
};
useImperativeHandle(ref, () => toggleVisible());
return (
<BottomSheet
visible={visible}
//setting the visibility state of the bottom shee
onBackButtonPress={toggleVisible}
//Toggling the visibility state on the click of the back botton
onBackdropPress={toggleVisible}
//Toggling the visibility state on the clicking out side of the sheet
>
<View style={styles.bottomNavigationView}>
<View
style={{
flex: 1,
flexDirection: 'column',
}}
>
{DummyData.map((item) => {
return (
<Button
key={item.id}
title={item.name}
type="clear"
buttonStyle={styles.button}
onPress={() => console.log(item.name)}
/>
);
})}
</View>
</View>
</BottomSheet>
);
};
export default React.forwardRef(TestNav);
And here is the code for the screen where it's being used (it's called ChatScreen as I'm using it as a testing ground since I haven't implemented that feature yet)
import React, { useRef } from 'react';
import {SafeAreaView,StyleSheet,View,Text} from 'react-native';
import TestNav from '../components/TestNav';
import { Button } from 'react-native-elements';
const ChatScreen = () => {
const childRef = useRef(null);
const toggleBottomNavigationView = () => {
if (myRef.current) {
childRef.current.toggleVisible;
}
};
return (
<SafeAreaView style={styles.container}>
<View style={styles.container}>
<Text
style={{
fontSize: 20,
marginBottom: 20,
textAlign: 'center',
}}
>
Content goes here
</Text>
<Button
onPress={() => toggleBottomNavigationView()}
title="Show Bottom Sheet"
/>
<TestNav ref={childRef} />
</View>
</SafeAreaView>
);
};
export default ChatScreen;
However, this code has somehow triggered an infinite loop, as I get this message:
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
How do I go about fixing this?

I think the issue lies with how you define the imperative handle. Hook callbacks are called each time the component renders and so () => toggleVisible() is called each render and creates a render loop. It should be passed a callback that returns the imperative functions/values to be made available to callees.
const toggleVisible = () => {
setVisible(visible => !visible);
};
useImperativeHandle(ref, () => ({
toggleVisible,
}));
In ChatScreen you then need to invoke the function. I'll assume the myRef in your snippet was a typo since it's not declared in the component and the usage appears to be similar to the guard pattern.
const toggleBottomNavigationView = () => {
childRef.current && childRef.current.toggleVisible();
// or childRef.current?.toggleVisible();
};

Related

Reusing Component State issue - State always retained for last reference loaded

I tried creating a reusable component in React. Which has a textInput and secure text entry handled in the state of the reusable component. But the state is not getting maintained differently when reusing always the last state is updated,
Issue: If i call the reusable const two times on a single screen or on the next screen in stack. The toggle for secure entry keeps changing for the last field loaded and earlier loaded fields state is lost.
i.e., when i click on toggle of Password, text change from hidden to visible or vice-versa happens for Confirm password field.
This is how i call
<View style={styles.inputContainerView}>
<InputTextFieldView
enteredText={passwordEntered}
setEnteredText={setPasswordEntered}
title={Constants.registerPasswordPlaceholder} icon={lockIcon}
isSecureEntry={true}
placeholder={Constants.registerPasswordPlaceholder} />
</View>
<View style={styles.inputContainerView}>
<InputTextFieldView
enteredText={confirmPasswordEntered}
setEnteredText={setConfirmPasswordEntered}
title={Constants.registerConfirmPasswordPlaceholder} icon={lockIcon}
isSecureEntry={true}
placeholder={Constants.registerConfirmPasswordPlaceholder} />
</View>
My component:
const InputTextFieldView = ({ enteredText, setEnteredText, title, icon, isSecureEntry, placeholder }) => {
const [isSecureEntryEnabled, setIsSecureEntryEnabled] = useState(isSecureEntry)
const eyeOpenIcon = require('../../../assets/visibility.png')
const eyeCloseIcon = require('../../../assets/close-eye.png')
useEffect(() => {
console.log('called')
}, [])
toggleSecureTextEntry = () => {
console.log('title', title)
setIsSecureEntryEnabled(!isSecureEntryEnabled)
}
return (
<View style={styles.fieldsContainerView}>
<Text style={styles.titleStyle}>{title}</Text>
<View style={[styles.fieldInputContainerView, {padding: Platform.OS === 'ios' ? 12 : 0}]}>
<Image source={icon} style={styles.fieldIconView} />
<TextInput secureTextEntry={isSecureEntryEnabled} style={{ width: isSecureEntry ? '75%' : '85%' }} onChange={() => setEnteredText()} value={enteredText} placeholder={placeholder} />
{isSecureEntry &&
<TouchableWithoutFeedback onPress={() => toggleSecureTextEntry()}>
<Image source={isSecureEntryEnabled ? eyeOpenIcon : eyeCloseIcon} style={styles.fieldIconView} />
</TouchableWithoutFeedback>
}
</View>
</View>
)
}
I'm guessing that you are using isSecureEntry as the hook to toggle the password fields? If so, it looks like you are passing the same state to both
the password field and the confirm password field. Right now, you essentially have one light switch that controls two different lamps. So you are going to want to have separate separate useState hooks for the password field and confirm password field. Then pass each one to the correct component.
const [passwordSecure, togglePasswordSecure] = useState(true);
const [confirmPasswordSecure, toggleConfirmPasswordSecure] = useState(true);
const togglePasswordField = () => {
togglePasswordSecure(!passwordSecure)
};
const toggleConfirmPasswordField = () => {
toggleConfirmPasswordSecure(!confirmPasswordSecure)
};
Issue was happening due to TouchableWithoutFeedback. Now used TouchableOpacity and it started to work. Not sure why but it may help someone

Why can't I call the parent function passed as props to child component?

What I am trying To Do
I am building a simple expo managed audio player app. On my App Screen, I need display a list of songs. When a user clicks on the song, it plays and once the play finishes, the "Songs Played" at the bottom of the page should increase. I am using expo-av API for this.
Here is the breakdown of the app:
App.js
Here I have an array (Data) that holds the songs. To keep it simple, I am using the same song for all elements. count variable holds the count of songs and there is a function (IncreaseCount) which is passed to the ChildComponent as prop. Flatlist is used to render the ChildComponents
import { View, Text, FlatList } from 'react-native'
import React, {useState} from 'react'
import ChildComponent from './ChildComponent';
const Data = [
{
key: "1",
song: "https://www2.cs.uic.edu/~i101/SoundFiles/CantinaBand3.wav"
},
{
key: "2",
song: "https://www2.cs.uic.edu/~i101/SoundFiles/CantinaBand3.wav"
},
{
key: "3",
song: "https://www2.cs.uic.edu/~i101/SoundFiles/CantinaBand3.wav"
}
]
export default function App() {
const [count, setcount] = useState(0);
const IncreaseCount = ()=>{
setcount(count + 1);
}
const renderItem = ({item, index})=>{
return(
<View style={{marginTop: 10}} >
<ChildComponent path={item.path} IncreaseCount={()=>IncreaseCount} index={index} songURL={item.song}/>
</View>
)
}
return (
<View style={{justifyContent: "center", alignItems: "center", marginTop: 200}}>
<FlatList
data={Data}
renderItem={renderItem}
extraData={count}
/>
<Text style={{marginTop: 30}}> Number of Songs Played: {count} </Text>
</View>
)
}
ChildComponent
Here I use expo-av API. Using the loadAsync() method, I Initially load the songs upon first render using useEffect hook. Then using onPress method of the button I invoke the playAsync() method of the playBackObject.
Using the setOnPlayBackStatusUpdate method, I listen for status changes. When playBackObjectStatus.didJustFinish becomes true, I call the props.IncreaseCount().
import { View, Button } from 'react-native'
import React, {useRef, useEffect} from 'react'
import { Audio } from 'expo-av';
export default function ChildComponent(props) {
const sound = useRef(new Audio.Sound());
const PlayBackStatus = useRef();
useEffect(()=>{
LoadAudio();
return ()=> sound.current.unloadAsync()
},[])
const LoadAudio = async ()=>{
PlayBackStatus.current = sound.current.loadAsync({uri: props.songURL})
.then((res)=>{
console.log(`load result : ${res}`)
})
.catch((err)=>console.log(err))
}
const PlayAuido = async ()=>{
PlayBackStatus.current = sound.current.playAsync()
.then((res)=>console.log(`result of playing: ${res}`))
.catch((err)=>console.log(`PlayAsync Failed ${err}`))
}
sound.current.setOnPlaybackStatusUpdate(
(playBackObjectStatus)=>{
console.log(`Audio Finished Playing: ${playBackObjectStatus.didJustFinish}`)
if(playBackObjectStatus.didJustFinish){
console.log(`Inside the If Condition, Did the Audio Finished Playing?: ${playBackObjectStatus .didJustFinish}`)
props.IncreaseCount();
}
}
)
return (
<View >
<Button title="Play Sound" onPress={PlayAuido} />
</View>
);
}
Problem I am facing
No matter what I do, I can't get the props.IncreaseCount to be called in App.js. Using console.log inside the if condition of setOnPlayBackStatusUpdate, I know that the props.IncreaseCount() method is being called, but the IncreaseCount() function in App.js is never called. Any help is greatly appreciated!
Here is the snack
Inside here please do this
<ChildComponent path={item.path} IncreaseCount={IncreaseCount} index={index} songURL={item.song}/>
Ive changed IncreaseCount={IncreaseCount}
DO lemme know if this helps
You have two ways to call the IncreaseCount function, in the ChildComponent
<ChildComponent IncreaseCount={IncreaseCount} path={item.path} .......
or
<ChildComponent IncreaseCount={() => IncreaseCount()} path={item.path} .......
You made a mistake while passing increaseCount prop to the ChildComponent
Here are to correct ways to do it:
return(
<View style={{marginTop: 10}} >
<ChildComponent path={item.path} IncreaseCount={IncreaseCount} index={index} songURL={item.song}/>
</View>
)
or: IncreaseCount={() => IncreaseCount()}

Cannot update a component (`ForwardRef(BaseNavigationContainer)`) while rendering a different component (`Home_Profile`)

I get this error while trying to navigate between screens with react native stack navigator. I think this was working in my previous project which had react 17.0.1 and react native 0.64.2, this project is react 17.0.2 and react native 0.66.4, helps would be appreciated.
log
Warning: Cannot update a component (`ForwardRef(BaseNavigationContainer)`) while rendering a different component (`Home_Profile`). To locate the bad setState() call inside `Home_Profile`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
error comes when I try to call navigation in an onPress prop on a Flatlist render item component.
renderItem={({ item }) => (
<View style={{ backgroundColor: "#f6f6f6" }}>
<PostCard
item={item}
onPress={() => navigation.navigate("Home_Profile")}
/>
</View>
)}
const PostCard = ({ item, onPress }) => {
React.useEffect(() => {
async function fetchUserData() {
const data = await getUserData(item.userid);
setProfileimg(data.userimg);
setName(data.name);
setLocation(data.location);
}
fetchUserData();
}, []);
return (
<TouchableOpacity onPress={onPress}>
<Text
style={{
color: "#231454",
fontWeight: "bold",
fontSize: 15,
marginBottom: 3,
}}
>
{name}
</Text>
</TouchableOpacity>
)
};
export default PostCard;
Home_Profile
import React from "react";
import { View, Text, Button } from "react-native";
const Home_Profile = ({ navigation }) => {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text>Home_Profile.js</Text>
<Button title="back" onPress={navigation.navigate("Home")} />
</View>
);
};
export default Home_Profile;
I found the cause of the problem, when you render the second screen, it is executing the onPress method of the button that goes back home, and that navigation causes the render method in that home screen to execute, which means you are trying to execute two renders at the same time, to fix that, just add an arrow function to the navigate home function as shown bellow:
onPress={()=>{navigation.navigate("Home")}}
You have forgot to add an arrow function before navigation.navigation('nameOFComponent')

Update Child Component Data on Parent Refresh React Native

I am fairly new to React Native and currently try to implement pull-to-refresh functionality in my app. Here is my Parent component snippet:
function MainScreenBoss({ navigation }) {
const [refreshing, setRefreshing] = useState(false);
//here I'm trying to refresh (example from docs)
const onRefresh = React.useCallback(async () => {
setRefreshing(true);
wait(2000).then(setRefreshing(false));
}, [refreshing]);
return (
<ScrollView
contentContainerStyle={styles.containerScroll}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}>
<View style={styles.container}>
//other components here
<View style={styles.containerWidget}>
//need to refresh this component
<Widget style={styles.widget} />
</View>
</View>
</ScrollView>
);
}
I am trying to refresh Widget component as it has the call to API url in it with loading animation. Here is the snipped of Widget component:
function Widget() {
const [isLoading, setLoading] = useState(true);
const [data, setData] = useState([]);
//call to api returns data
useEffect(() => {
getData(API_GET_WIDGET_DATA, setLoading, setData);
}, []);
return (
<View style={styles.container}>
{isLoading ? (
<ActivityIndicator
loader="blue"
visible={isLoading}
style={styles.animation}
/>
) : (
<>
<View style={styles.Contents}>
//need to refresh data is it used in here when loading is finished
</View>
</>
)}
</View>
);
}
I guess I need to force update the widget component or launch the loading function again somehow, but I do not quite understand what should I do.
Edit:
the api function looks like this:
export function getData(reqOptions, setLoading, setData) {
fetch(apiURL, reqOptions)
.then((response) => response.json())
.then((json) => setData(json.data))
.catch((error) => console.error(error))
.finally(() => setLoading(false));
}
If I understand well, in order to update your widget, you gotta re-do the fetch that you have inside your useEffect.
The useEffect you currently have only executes on mount of the component, as the dep. array is empty. From the looks of your parent component, the Widget does not get unmounted, therefore your useEffect is only called once.
What you have to do is to take your refreshing state and pass it to the Widget, so that it knows when it has to refetch the data.
Try something like this:
function MainScreenBoss({ navigation }) {
const [refreshing, setRefreshing] = useState(false);
//here I'm trying to refresh (example from docs)
const onRefresh = React.useCallback(async () => {
setRefreshing(true);
wait(2000).then(setRefreshing(false));
}, [refreshing]);
return (
<ScrollView
contentContainerStyle={styles.containerScroll}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}>
<View style={styles.container}>
//other components here
<View style={styles.containerWidget}>
//need to refresh this component
<Widget style={styles.widget} refreshing={refreshing} /> // pass this prop
</View>
</View>
</ScrollView>
);
}
Widget:
function Widget(props) {
const [isLoading, setLoading] = useState(true);
const [data, setData] = useState([]);
//call to api returns data
useEffect(() => {
getData(API_GET_WIDGET_DATA, setLoading, setData);
}, []);
useEffect(() => {
if (props.refreshing) {
getData(API_GET_WIDGET_DATA, setLoading, setData);
}
}, [props.refreshing]); // on every refreshing `true` state
return (
<View style={styles.container}>
{isLoading ? (
<ActivityIndicator
loader="blue"
visible={isLoading}
style={styles.animation}
/>
) : (
<>
<View style={styles.Contents}>
//need to refresh data is it used in here when loading is finished
</View>
</>
)}
</View>
);
}
Edit:
You can either modify the existing useEffect to include the check of the data, or else create another one which runs only on mount, with another useEffect running on update state. I've added the second case.
Edit 2:
There is nothing wrong with that, actually that is better because the refreshing is handled at a correct time, when the call is finished and the data is fulfilled. With your current config the call may not be finished, but after 2 seconds you're setting refreshing to false, this may bring problems.
The beauty of hooks is that you can use as many of them as you want, as compared to the class methods of React before hooks introduction. So there is no problem with that, but you can actually change it around and have something like:
useEffect(() => {
if (!data || props.refreshing) {
getData(API_GET_WIDGET_DATA, setLoading, setData);
}
}, [data, props.refreshing]);
If you may, one last thing: I would not pass state setters to your fetch fn, I would handle the state updates in the component and just make sure that the fn returns your data. It's just for separation of concerns causes.

Make the RBSheet reusable as a widget React-Native

I am using RBSheet Package for showing up the bottom sheet in my project. I have understood the concept of how to use it from the documentation. Is there a way that I can make the bottom sheet reusable like a Widget? I have tried doing this:
BottomSheetComponent.js
const BottomSheet = ({ message, buttonText }) => {
// to open this sheet as soon as someone call this Component
this.RBSheet.open();
return(
<RBSheet ref={ ref => { this.RBSheet = ref; }}
customStyles={{mask: { backgroundColor: COLORS.dark }, container: { elevation: 100 }}}>
<View style={styles.messageContainer}>
{/* Add more data later */}
</View>
</RBSheet>
);
}
export default BottomSheet;
MainComponent.js
const MainComponent = () => {
const bottomSheet = () => {
// Trying to call the bottom sheet here
<BottomSheet />
}
return(
<View>
<Button onPress={() => bottomSheet()} title="Bottom Sheet" />
</View>
);
}
export default MainComponent;
I have failed to get the bottom sheet. I have just started working on React Native. I don't want to go with the generic way which is explained in the Package's docs, it is not a good practice to make the same bottom sheet in two different pages from scratch.
So, after a lot of research, I finally figured out what I was doing wrong.
I was using it for the Class Component, although I was using Functional Component
The reference was missing, for which, Metro Build Runner, was giving me errors.
Solution
To solve the issue:
I need to create my own reference
Pass it to the ref props of the Package
MainComponent.js
import React, { useRef } from 'react';
const MainComponent = () => {
// To be used for the reference for the bottom sheet
const sheetRef = useRef();
const bottomSheet = () => {
// Here how you open the bottom sheet right
sheetRef.current.open();
}
return(
<View>
<Button onPress={() => bottomSheet()} title="Bottom Sheet" />
{/* Passing the sheet ref for the binding */}
<BottomSheet sheetRef={sheetRef} />
</View>
);
}
export default MainComponent;
BottomSheet.js
const BottomSheet = ({ sheetRef }) => {
return(
{/* This is the place to make it work */}
<RBSheet ref={sheetRef}
customStyles={{mask: { backgroundColor: COLORS.dark }, container: { elevation: 100 }}}>
<View style={styles.messageContainer}>
{/* Add more data later */}
</View>
</RBSheet>
);
}
export default BottomSheet;
Can you try this?
const bottomSheet = () => {
// Trying to call the bottom sheet here
return <BottomSheet />;
}

Categories