React.useMemo doesn't prevent re-rendering - reactjs

I have a functional component UserInfoInput and another functional component UserInfo including many UserInfoInput components. Here my code:
const UserInfoInput = React.memo((props) => {
useEffect(() => {
console.log('changed ');
})
return (
<View style={styles.container} >
<View style={{ ...styles.iconContainer, backgroundColor: props.color }}>
{props.name ? <Icon type={props.type} name={props.name} size={props.size} color='white' /> :
<Image source={props.image} style={props.imageStyle} />}
</View>
//input goes there
<TextInput style={styles.input} placeholder={props.title} onChangeText={props.changeText} />
{props.divider && <View style={{ ...RootStyle.divider, position: 'absolute', width: '85%', bottom: 0, right: 5 }}></View>}
</View>
)
});
and UserInfo component:
const [text, setText] = useState('');
const [second, setSecond] = useState('');
const update = text => setText(text);
const updt = text1 => setSecond(text1);
return (
// <UserMenu />
<View style={{ flex: 1 }}>
<View style={styles.navigator}>
</View>
<View style={styles.inputList}>
<UserInfoInput name="bookmark" color='#FF6C88' type='font-awesome' size={18} title={UserMenuTitle.bookmark} divider={true} changeText={update} />
<UserInfoInput name="star" color='#8BD8FB' type='font-awesome' size={18} title={UserMenuTitle.achieve} divider={true} changeText={updt} />
...
</View>
</View>
Whenever I enter one of those inputs, both two UserInfoInput components still re-render even I already wrap it inside React.memo function. I want my UserInfoInput component only re-render when I enter exactly it, not from other UserInfoInput components but I don't know how to do it. Could anyone help me to solve this problem ?

According to react official docs:
By default it will only shallowly compare complex objects in the props object. If you want control over the comparison, you can also provide a custom comparison function as the second argument.
This is the reason why your component is re-rendering again.
What you can do pass custom callback function and check your self:
function areEqual(prevProps, nextProps) {
/*
return true if passing nextProps to render would return
the same result as passing prevProps to render,
otherwise return false
*/
// prevProps.title===nextProps.title(add your condition return true if values are equal. It ensure it will not re-render again)
}
const UserInfoInput = React.memo((props) => {
useEffect(() => {
console.log('changed ');
})
return (
<View style={styles.container} >
<View style={{ ...styles.iconContainer, backgroundColor: props.color }}>
{props.name ? <Icon type={props.type} name={props.name} size={props.size} color='white' /> :
<Image source={props.image} style={props.imageStyle} />}
</View>
//input goes there
<TextInput style={styles.input} placeholder={props.title} onChangeText={props.changeText} />
{props.divider && <View style={{ ...RootStyle.divider, position: 'absolute', width: '85%', bottom: 0, right: 5 }}></View>}
</View>
)
},areEqual);

Related

#gorrhom React Native Bottom Sheet - Calling from another component

Im trying to implement this relatively popular bottom sheet in React Native by #gorhom.
My aim is to open the bottom sheet from another component. I've tried to follow this answer - here . However, i do not understand what goes in the touchable opacity onPress in my component where it is being called from.
Code below
BottomSheetModalComponent
export default function BottomSheetModalComponent({ref}) {
// ref
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
// variables
const snapPoints = useMemo(() => ['25%', '50%'], []);
// callbacks
const handlePresentModalPress = useCallback(() => {
ref.current?.present();
}, []);
const handleSheetChanges = useCallback((index: number) => {
console.log('handleSheetChanges', index);
}, []);
// renders
return (
<BottomSheetModalProvider>
<View>
<BottomSheetModal
ref={ref}
snapPoints={snapPoints}
onChange={handleSheetChanges}
>
<View>
<Text>Awesome 🎉</Text>
</View>
</BottomSheetModal>
</View>
</BottomSheetModalProvider>
);
};
Location Component
export default function LocationBar() {
// Create Ref
const userBottomSheetRef = useRef<BottomSheetModal>(null);
// Pass ref into the bottom sheet component
<LocationBottomSheet ref={userBottomSheetRef} snapPoints={["30%"]}/>
return (
<>
<View style={{ flexDirection: "row" }}>
<View style={styles.locationBar}>
<Octicons style={styles.locationIcon} name="location" size={18} />
<TouchableOpacity onPress={() => {
//WHAT GOES HERE?
}}>
<Text style={{fontSize: 17, fontWeight: '600'}}>{userLocation}</Text>
</TouchableOpacity>
<Ionicons style={styles.chevronIcon} name="chevron-down" size={12} />
</View>
</View>
</>
);
}
Thanks in advance

React-Native: RefreshControl on ScrollView with nested View won't show animation

So I am pretty new to React Native and I'm trying to implement a list of contacts which is refreshable. However I cannot get it to work (probably because I missed something).
I don't get any errors, I simply cannot pull down to get the animation.
I have created a wrapper component which is used as a basis for all my other stuff besides the contact-list as well.
const Wrapper = ({ refreshing, onRefresh, children }) => {
const theme = useColorScheme();
return refreshing === null || onRefresh == null ? (
<SafeAreaView style={{ flex: 1 }}>
<ScrollView
style={theme === "light" ? styles.container_light : styles.container_dark}
contentContainerStyle={{ flexGrow: 1 }}
>
<View style={styles.wrapper}>{children}</View>
</ScrollView>
</SafeAreaView>
) : (
<SafeAreaView style={{ flex: 1 }}>
<ScrollView
style={theme === "light" ? styles.container_light : styles.container_dark}
contentContainerStyle={{ flexGrow: 1 }}
>
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
<View style={styles.wrapper}>{children}</View>
</ScrollView>
</SafeAreaView>
);
};
export default Wrapper;
As seen above, refreshing & onRefresh get passed to the Wrapper, because the ScrollView is located there.
Now in my contact list, I put all my contacts inside that wrapper element and pass refreshing & onRefresh to the Wrapper. I also have a header and a FloatingActionButton on that screen. The screen takes up 100% of the height because I want that FloatingActionButton on the bottom right of my screen.
// outside my contact-list component:
const wait = (timeout) => {
return new Promise((resolve) => setTimeout(resolve, timeout));
};
// in my contact-list component:
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(() => {
setRefreshing(true);
wait(2000).then(() => setRefreshing(false));
}, []);
<Wrapper refreshing={refreshing} onRefresh={onRefresh}>
<Text style={theme === "light" ? globalStyles.heading_light : globalStyles.heading_dark}>
{t("ContactsIndexHeader")}
</Text>
<View style={{ marginTop: 30 }}>
{contacts.length > 0 ? letters.map((letter) => {
return (...); // Check last name of contact & display if equal to letter
})
: null}
</View>
<FloatingActionButton icon="plus" onPress={() => navigation.navigate("ContactsCreate")} />
</Wrapper>
I believe that refresh control should be specified as a prop and not as a child of scroll view, example:
<ScrollView
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
/>
}
/>

error in passing data through navigation in react native

I'm new to react native world and I'm trying to integrate a calendar with a time slot picker, so I'm trying to pass the selected date from the calendar to the slot picker page but I'm having this Error when I press on a date in the calendar and I couldn't fix it.
TypeError: undefined is not an object (evaluating 'navigation.navigate')
This is my calendar function:
const RequestMeeting = ({ navigation }) => {
const [isModalVisible, setModalVisible] = useState(false);
const toggleModal = () => {
setModalVisible(!isModalVisible);
};
return (
<View style={{ margin: 100, }}>
<Button title="Show modal" onPress={toggleModal} />
<Modal isVisible={isModalVisible} avoidKeyboard={true} scrollHorizontal={true} propagateSwipe={true}>
<ScrollView>
<View style={{ margin: 50, backgroundColor: 'gray', borderRadius: 20, padding: 20, margin: 20 }}>
<Text style={styles.heading}>Request to Buy/Rent</Text>
<View style={{ paddingBottom: 10 }}></View>
<View >
<Calendar
onDayPress={(day) => navigation.navigate("Slot", { bookingDate: day })}
style={styles.calendar}
hideExtraDays
theme={{
selectedDayBackgroundColor: 'green',
todayTextColor: 'green',
arrowColor: 'green',
}}
/>
</View>
<Button
buttonStyle={styles.register}
title="Send Buy/Rent request"
/>
<Button
buttonStyle={styles.cancelbtn}
title="Cancel"
onPress={toggleModal}
/>
</View>
</ScrollView>
</Modal>
</View>
);
};
And this is my time slot picker function:
const jsonData = {
"slots": {
"slot1": "9:00am to 9:30am",
"slot2": "9:30am to 10:00am",
"slot3": "10:00am to 10:30am",
"slot4": "10:30am to 11:00am",
"slot5": "11:00am to 11:30am",
"slot6": "11:30am to 12:00pm"
}
}
const Slot = ({ navigation }) => {
const onPressBack = () => {
const { goBack } = navigation
goBack()
}
const slots = jsonData.slots
const slotsarr = Object.keys(slots).map(function (k) {
return (
<View key={k} style={{ margin: 5 }}>
<TouchableOpacity >
<Text>{slots[k]}</Text>
</TouchableOpacity>
</View>)
});
return (
<View style={styles.container}>
<StatusBar barStyle="light-content" />
<View >
<TouchableOpacity onPress={() => onPressBack()}><Text >Back</Text></TouchableOpacity>
<Text ></Text>
<Text ></Text>
</View>
{ slotsarr}
</View>
);
}
This error means that wherever you are rendering RequestMeeting or Slot (whichever one has the error) it's not getting the navigation prop. If it is rendered as a Screen then it will get the prop from the Navigator. If it's not a top-level screen then you need to pass down the prop from a parent or access it with the useNavigation hook.
Docs: Access the navigation prop from any component

React Native: Keyboard keeps closing on key press when typing in TextInput

I have a functional component Login Screen and have 4 Input Fields in them, along with a login button.
Here is the Full Code for my component Screen:
export default function Login() {
//Configs
const navigation = useNavigation();
const orientation = useDeviceOrientation();
const { colors, dark } = useTheme();
const InputTheme = {
colors: {
placeholder: colors.accent,
primary: colors.accent,
error: "red",
},
};
/** State Codes */
//States
const [login, setLogin] = useState({
email: "",
password: "",
licenseKey: "",
deviceName: "",
});
const [loading, setLoading] = useState(false);
const [secureEntry, setSecureEntry] = useState(true);
//Errors
const [errorEmail, setEmailError] = useState(false);
const [errorPWD, setPWDError] = useState(false);
const [errorLicense, setLicenseError] = useState(false);
const [errorDevice, setDeviceError] = useState(false);
//Error Messages
const [messageEmail, setEmailMessage] = useState("Looks Good");
const [messagePWD, setPWDMessage] = useState("All Good");
async function VerifyInputs() {
var pattern = /^[a-zA-Z0-9\-_]+(\.[a-zA-Z0-9\-_]+)*#[a-z0-9]+(\-[a-z0-9]+)*(\.[a-z0-9]+(\-[a-z0-9]+)*)*\.[a-z]{2,4}$/;
if (login.email == "") {
//Email cannot be empty
setEmailMessage("Email cannot be Blank!");
setEmailError(true);
return;
} else if (login.email != "" && !pattern.test(login.email)) {
//Email is not valid
setEmailMessage("This is not a valid email address!");
setEmailError(true);
return;
} else {
console.log("resolved email");
setEmailMessage("");
setEmailError(false);
}
if (login.password == "") {
//Password cannot be empty
setPWDMessage("Password cannot be Empty!");
setPWDError(true);
return;
} else if (login.password.length < 5) {
//Password must be minimum 5 characters.
setPWDMessage("Password must be of minimum 5 characters!");
setPWDError(true);
return;
} else {
console.log("resolved password");
setPWDMessage("");
setPWDError(false);
}
if (login.licenseKey == "") {
//License Key can't be Empty
setLicenseError(true);
return;
} else {
console.log("License resolved");
setLicenseError(false);
}
if (login.deviceName == "") {
//Device Name can't be empty as well
setDeviceError(true);
return;
} else {
console.log("Device name resolved");
setDeviceError(false);
}
Toast.show("Validation Successful");
}
function MobileContent() {
console.log("mobile_content rerendered");
return (
<View style={{ flex: 1, backgroundColor: colors.accent }}>
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}></View>
<View style={{ flex: 3, justifyContent: "center" }}>
{/**Main Content */}
<View style={[styles.content, { backgroundColor: colors.primary }]}>
<View style={{ flex: 1 }}>
{/**For Header */}
<Header />
</View>
<View style={{ flex: 5 }}>
{/**For Content */}
<ScrollView style={{ flex: 1 }}>
<LoginContent />
</ScrollView>
</View>
<View style={{ flex: 1 }}>
{/**For Footer */}
<Footer />
</View>
</View>
</View>
</View>
);
}
function TabContent() {
console.log("tab_content rerendered");
return (
<View
style={{
flex: 1,
backgroundColor: colors.accent,
flexDirection: orientation.landscape ? "row" : "column",
}}>
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}></View>
<View style={{ flex: 1.5, justifyContent: "center" }}>
{/**Main Content */}
<View style={[styles.content, { backgroundColor: colors.primary }]}>
{/**Header Wrapper */}
<View style={{ justifyContent: "center" }}>
{/**Header Title */}
<Header />
</View>
{/**Content Wrapper */}
<LoginContent />
{/**Footer Wrapper */}
<View style={{ justifyContent: "center" }}>
{/** Login Button */}
<Footer />
</View>
</View>
</View>
</View>
);
}
function Header() {
console.log("header_component rerendered");
return (
<View style={{ margin: "5%" }}>
<Title>Welcome User</Title>
<Subheading>Please Sign In to Continue..</Subheading>
</View>
);
}
function Footer() {
console.log("footer_component rerendered");
return (
<View style={{ margin: isTablet ? "5%" : "3.5%" }}>
<Button
title="Login"
loading={false}
ViewComponent={LinearGradient}
containerStyle={{ maxWidth: isTablet ? "45%" : "100%" }}
buttonStyle={{ height: 50, borderRadius: 10 }}
linearGradientProps={{
colors: [colors.accent, colors.accentLight],
start: { x: 1, y: 1 },
end: { x: 1, y: 0 },
}}
onPress={() => {
VerifyInputs();
//navigation.navigate("verify");
}}
/>
</View>
);
}
function LoginContent() {
console.log("login_component rerendered");
return (
<View style={{ margin: "3%" }}>
{/**Login & Email Wrapper */}
<View style={{ flexDirection: isTablet ? "row" : "column" }}>
<View style={styles.input}>
<TextInput
mode="outlined"
label="Email"
value={login.email}
error={errorEmail}
theme={InputTheme}
onChangeText={(text) => setLogin({ ...login, email: text })}
/>
{errorEmail ? (
<HelperText visible={true} type="error" theme={{ colors: { error: "red" } }}>
{messageEmail}
</HelperText>
) : null}
</View>
<View style={styles.input}>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<TextInput
mode="outlined"
label="Password"
value={login.password}
error={errorPWD}
theme={InputTheme}
secureTextEntry={secureEntry}
style={{ flex: 1, marginBottom: 5, marginEnd: isTablet ? 15 : 5 }}
onChangeText={(text) => setLogin({ ...login, password: text })}
/>
<Button
icon={
<Icon
name={secureEntry ? "eye-off-outline" : "eye-outline"}
size={30}
color={colors.primary}
/>
}
buttonStyle={{
width: 55,
aspectRatio: 1,
backgroundColor: colors.accent,
borderRadius: 10,
}}
containerStyle={{ marginStart: 5 }}
onPress={async () => setSecureEntry(!secureEntry)}
/>
</View>
{errorPWD ? (
<HelperText visible={true} type="error" theme={{ colors: { error: "red" } }}>
{messagePWD}
</HelperText>
) : null}
</View>
</View>
{/**License & Device Wrapper */}
<View style={{ flexDirection: isTablet ? "row" : "column" }}>
<View style={styles.input}>
<TextInput
mode="outlined"
label="License Key"
value={login.licenseKey}
error={errorLicense}
theme={InputTheme}
onChangeText={(text) => setLogin({ ...login, licenseKey: text })}
/>
{errorLicense ? (
<HelperText visible={true} type="error" theme={{ colors: { error: "red" } }}>
License Key cannot be Empty!
</HelperText>
) : null}
</View>
<View style={styles.input}>
<TextInput
mode="outlined"
label="Device Name"
value={login.deviceName}
error={errorDevice}
theme={InputTheme}
onChangeText={(text) => setLogin({ ...login, deviceName: text })}
/>
{errorDevice ? (
<HelperText visible={true} type="error" theme={{ colors: { error: "red" } }}>
Device Name cannot be empty
</HelperText>
) : null}
</View>
</View>
</View>
);
}
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle={"dark-content"} backgroundColor={colors.accent} />
{isTablet ? <TabContent /> : <MobileContent />}
</SafeAreaView>
);
}
As you can see by looking at the code.. my Login component's rendering responsive component layouts based on device type being phone or tablet, the following components MobileContent or TabContent.
MobileContent functional component wraps my main content in a ScrollView & TabContent functional component doesn't need a scrollview but has responsive scaling views.
Both of these parent components display my common components, Header,LoginContent & Footer.
My Header component just displays a standard title & subheading. My LoginContent component has all the TextInput fields in them with validation logic setup as well. My Footer component has the Submit/Login button. That's All.
So we can summarize the components tree for screen as:
Login Screen/Parent Component => Mobile Content/Tab Content => Header, LoginContent & Footer (Common Components)
Alright, so now what is the issue? the major issue for me lies for TextInputs in LoginContent component, but I assume the underlying issue is applicable for entire Login Screen component.
The ISSUE: When I click on TextInput fields, the input gets focus and Keyboard appears. Everything is fine till this point. As soon as I type even a single letter, single key press on keyboard.. The Keyboard closes immediately and the text input is lost focus.
Why it could be happening? I believe this might be the classic 'React Component Re-render' problem, which causes by TextInput to be re-rendered when state is updated by the onChangeText for the TextInput and hence it loses focus & that's why the Keyboard closes.
So, I hope to resolve atleast first issue from these two issues.
Issue 1: How do I prevent the Keyboard from constantly closing when I try to type something in any of the TextInput fields.
Issue 2: How can I better optimize my Parent & Children components rendering on every state update using the any of these two React Hooks useCallback() and useMemo()?
I've read the documentation for these two hooks useCallback & useMemo multiple times and still haven't grasped the concept of these hooks. All posted examples deal with a Counter Exmaple optimized using useCallback or useMemo but I'm not working with Counters here in my Screen.
I need a more practical exmaple of useCallback() & useMemo() in screen components. Perhaps an implementation of these two hooks in my current Login Screen component will help me understand & grasp the concept better.
Like I mentioned, solving Issue 1 is my highest priority atm, but I know a solution for Issue 2 will help me in the long run as well. It will be very helpful if you can resolve both of my issues here. Thanks for helping.

how to show loading only for one item in flatlist?

i'm getting a list of products and i show it in a flatlist. when somebody wants to add quantity of an item or remove an item i will show a loading only on that specific card.
i shouldn't add property to list object models.
const Counter = ({ quantity, itemId }) => {
console.log(quantity);
console.log('ITEM ID ',itemId);
return (
<View style={{flexDirection:'row', alignItems:'center'}}>
<Icon style={{fontSize: 25, color: '#059b9a',marginRight:25}} name='md-trash' onPress={()=> removeItem(itemId)} />
<Icon style={{fontSize: 25, color: '#059b9a',marginRight:10}} name='md-remove'/>
<View style={{marginRight:10, alignItems:'center',justifyContent:'center'}}>
<CustomText value={`${numTranslate(quantity)}`} style={{color:'#059b9a', fontSize:22, fontWeight:'bold'}}/>
<CustomText style={{fontSize:10}} value={'حداکثر'} />
</View>
<Icon style={{fontSize: 25, color: '#059b9a', marginRight:10}} name='md-add' />
</View>
);
}
i only want to show loading instead of last icon until fetch response comes.
You need some kind of prop that shows rather or not your data has fetched yet. Since you did not attach your data fetch, i will just assume, that you can add it to your component.
const Counter = ({ quantity, itemId, isLoading }) => {
console.log(quantity);
console.log('ITEM ID ',itemId);
//declare conditional constant for icon
const icon = isLoading ? <div><p>Loading...</p></div> : <Icon style={{fontSize: 25, color: '#059b9a', marginRight:10}} name='md-add' />
return (
<View style={{flexDirection:'row', alignItems:'center'}}>
<Icon style={{fontSize: 25, color: '#059b9a',marginRight:25}} name='md-trash' onPress={()=> removeItem(itemId)} />
<Icon style={{fontSize: 25, color: '#059b9a',marginRight:10}} name='md-remove'/>
<View style={{marginRight:10, alignItems:'center',justifyContent:'center'}}>
<CustomText value={`${numTranslate(quantity)}`} style={{color:'#059b9a', fontSize:22, fontWeight:'bold'}}/>
<CustomText style={{fontSize:10}} value={'حداکثر'} />
</View>
{/* render conditional expression instead of jsx element */}
{icon}
</View>
);
}
your main component where using Counter :
// Call setState here or anywhere you want to fetch data or...
fetchData =()=> {
this.setState({isLoading: true});
axios.post(url).then(response=>{
this.setState({isLoading: false});
})
}
render(){
return(
<Counter quantity={quantity} itemId={itemId} isLoading={this.state.isLoading} />
)
}
your Counter object:
const Counter = ({ quantity, itemId, isLoading }) => {
console.log(quantity);
console.log('ITEM ID ',itemId);
return (
<View style={{flexDirection:'row', alignItems:'center'}}>
<Icon style={{fontSize: 25, color: '#059b9a',marginRight:25}} name='md-trash' onPress={()=> removeItem(itemId)} />
<Icon style={{fontSize: 25, color: '#059b9a',marginRight:10}} name='md-remove'/>
<View style={{marginRight:10, alignItems:'center',justifyContent:'center'}}>
<CustomText value={`${numTranslate(quantity)}`} style={{color:'#059b9a', fontSize:22, fontWeight:'bold'}}/>
<CustomText style={{fontSize:10}} value={'حداکثر'} />
</View>
{isLoading ?
<LoadingIcon />
:
<Icon style={{fontSize: 25, color: '#059b9a', marginRight:10}} name='md-add' />
}
</View>
);
}
1) Maintain an array in the state like loadingItems. In the onPress, push the itemId into loadingItems before the try block. whether if the call succeed or not, pop the itemId that is within the try or catch block.
2) Add a loading prop to your component and check if the itemId is in the loadingItems array.
<Card
isLoading={this.state.loadingItems.includes(item.Id)
...
/>
Hope this helps.
Create a new variable loadingItem.
Store the itemId in loadingItem before you call your fetchData method
Check if loading is true and itemId equals loadingItem
{loading && itemId === loadingItem ? <LoadingIcon />
:
<Icon style={{fontSize: 25, color: '#059b9a', marginRight:10}} name='md-add' />
}

Resources