Avoid TextInput re render on text change - react-native

I have a simple TextInput and a submit button and I want to handle text change without re render the componenent. On a class component I use a private variable "textContent" triggering OnChangeText event instead of a state to prevent forced rendering but I don't know how to do that with function component. Is it possible to store the text value without state on function component ? Thanks
Here is the component (that re render on text change) :
const Search = () => {
const [_films, setFilms] = useState([]);
const [_text, setText] = useState('');
const _loadFilms = () => {
console.log(_text);
if (_text.length > 0) {
getFilmsFromApiWithSearchedText(_text).then(data => {
setFilms(data.results);
});
}
}
console.log("rendering");
return (
<View style={styles.main_container}>
<TextInput style={styles.textinput} placeholder='Titre du film' onChangeText={(text) => setText(text)} />
<Button title='Rechercher' onPress={() => { _loadFilms() }} />
<FlatList
data={_films}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => <FilmItem film={item} />}
/>
</View>
);
}
const styles = StyleSheet.create({
main_container: {
flex: 1,
marginTop: 20
},
textinput: {
marginLeft: 5,
marginRight: 5,
height: 50,
borderColor: '#000000',
borderWidth: 1,
paddingLeft: 5
}
});
export default Search;`

On a class component you could store the text in a private variable of the class. With functional components you can use refs for something equivalent. More info about how, available in this SO question

Related

React-Native: Ref is missing scrollToIndex() for FlatList

I am passing a ref to a FlatList and expecting to access the scrollToIndex() function, but it doesn't appear in the console and it throws an error when I try to use it saying "scrollTo() is not a function", it doesn't even recognise that I'm trying to use scrollToIndex().
This is a simple example of usage in my code:
const ref = useRef()
ref.current?.scrollToIndex({index:itemIndex,animated:true})
<FlatList ref={ref} .../>
Has anyone experienced this and knows how to make it work? I've looked over the documentation and examples and everything seems to be fine. Any help would be appreciated!
UPDATE:
Component:
import React from "react";
import { View, FlatList, ImageBackground,TouchableOpacity,ScrollView,StyleSheet, Text} from "react-native";
const Projects = ({ index, navigation }) => {
const {width} = Dimensions.get("window")
const [imageIndex, setIndex] = React.useState(0)
const ref = React.useRef()
const wItem = (width - 63) / 2;
console.log("index projects", imageIndex)
const scrollToIndex = (index)=>{
if(ref.current){
ref.current.scrollToIndex({index:index})
}
}
const goToIndex = React.useCallback((info) => {
const wait = new Promise(resolve => setTimeout(resolve, 200));
wait.then(() => {
ref.current?.scrollToIndex({ index: info.index, animated: true });
})
}, [])
return (
<ScrollView >
<TouchableOpacity onPress={()=> scrollToIndex(5)}><Text>Scroll to</Text></TouchableOpacity>
<FlatList ref={ref}
nestedScrollEnabled
numColumns={2}
data={DATA}
onScrollToIndexFailed={(index)=>goToIndex(index)}
style={styles.flatList}
keyExtractor={(i, index) => index.toString()}
renderItem={({ item, index }) => (
<View style={styles.item}>
<ImageBackground
source={item.image}
/* #ts-ignore */
imageStyle={styles.imgStyle}
style={{ width: wItem, height: wItem, alignItems: "flex-end" }}>
</ImageBackground>
</TouchableOpacity>
<Text>{item.title}</Text>
</View>
)}
/>
</ScrollView>
);
};
export default Projects;
const styles = StyleSheet.create({
container: {
flex: 1,
},
item: {
marginRight: 15,
marginBottom: 24,
alignItems:'center'
},
flatList: {
marginHorizontal: 24,
marginTop: 16,
},
imgStyle: {
borderRadius: 12,
},
});
const DATA = [//list of local images]
It is unable to call scrollTo because the ref has only been initialized and not assigned at the time of use:
const ref = useRef()
// ref = null
ref.current?.scrollToIndex({index:itemIndex,animated:true})
// ref = null
<FlatList ref={ref} .../>
// ref != null
So ref is not assigned until you render for the first time. This means that if you want to call .scrollTo() on the ref you can do it in 2 different ways:
1 - If you want to scroll to a certain index initially:
useEffect(() => {
const initialIndex = 34 //random number
if (ref.current){
ref.current.scrollToIndex({index: initialIndex})
}
}, [ref])
The ref in the dependency array will make sure that the useEffect will run whenever the value of ref changes (in this case it will change once it is assigned to a value by rendering the <Flatlist />
2 - If you want to scroll to a certain index on demand:
const handleScrollTo = (index) => {
if (ref.current && typeof ref.current.scrollToIndex === 'function'){
ref.current.scrollToIndex({index: index})
}
}
This function should be connected to something like an onPress or similar, which means the ref should be defined whenever the user presses the button because being able to see the button and press it requires that the component has rendered. Even though ref.current should already be assigned once it is used here, it is always good practice to make sure that it doesn't crash in cases where it is not, which is why we are adding the if-statement.

How can I change background color of a FlatList row?

I'm learning a about lists in React-Native but I can't seem to make my code work. I want to change the background color of a certain row in a FlatList when the button inside that row is pressed. I found some answers online but most of them were complex for me to understand and didn't work(I'm very new to React-Native)
This is my code so far:
export default function Listt () {
const [click, setClick] = useState(null);
const [productsData, setproductsData] = useState([]);
const ItemView = ({item, index}) => {
return (
// FlatList Item
<TouchableOpacity style={styles.row} onPress={() => {setClick(index);}}>
<View>
<Text style={styles.rowtext}>
{item[0]+ ' ' + item[1]}
</Text>
</View>
</TouchableOpacity>
);
};
async function loadInState() {
const keys =await AsyncStorage.getAllKeys();
const result = await AsyncStorage.multiGet(keys);
setproductsData([...productsData, ...result]);
}
useFocusEffect(
React.useCallback(() => {
loadInState()
}, [])
);
return (
<View style={styles.container}>
<FlatList
data={productsData}
renderItem={ItemView}
keyExtractor={(item, index) => index.toString()}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
row: {
width:widtht,
height:45,
borderWidth: 2,
borderColor: 'black',
textAlign: 'center',
justifyContent: 'center',
alignItems:'center',
textAlign:'center'
//backgroundColor:'blue',
},
rowtext:{
fontSize:17,
},
});
You can simply do it with index .which uniquely identify the each item in the list
here is the code you can use.
const ItemView = ({item, index}) => {
return (
// FlatList Item
<TouchableOpacity style={[styles.row,{backgroundColor: click===index ? 'tomato':'transparent'}]} onPress={() => {setClick(index);}}>
<View>
<Text style={styles.rowtext}>
{item[0]+ ' ' + item[1]}
</Text>
</View>
</TouchableOpacity>
);
};
Here I used my fav color tomato when you press the button but you can use as per your need and where I used transparent you can use rest of the items colors like white or whatever you want.

Change border color text input When its empty in react native

I want when text input is empty change border color to red with press button:
const post = () => {
let list = [];
if (homeAge === '') {
list.push('homeage')
}
}
<TextInput
style={[Styles.TextInput, { borderColor: list.includes('homeage') ? 'red' : '#006d41' }]}
onChangeText={(event) => homeAgeHandler(event)}
/>
<Button style={Styles.Button}
onPress={() => post()}>
<Text style={Styles.TextButton}>ثبت اطلاعات</Text>
</Button>
Use a useRef hook :
const ref=useRef(0);
const post = () => {
let list = [];
if (homeAge === '') {
list.push('homeage')
}
}
useEffect(()=>{
if(list.size==0&&ref.current)
{
ref.current.style.borderColor = "red";
}
},[list,ref]);
<TextInput ref={ref}
onChangeText={(event) => homeAgeHandler(event)}
/>
<Button style={Styles.Button}
onPress={() => post()}>
<Text style={Styles.TextButton}>ثبت اطلاعات</Text>
</Button>
Here is a simple example to validate text and change styling based on validation,
const App = () => {
const [text, setText] = useState("");
const [error, setError] = useState(false);
const validateText = () => {
if (text === "") {
setError(true);
} else {
setError(false);
}
};
return (
<View>
<TextInput style={[Styles.TextInput, { borderColor: error ? 'red' : '#006d41', borderWidth:'1px'}]}
onChangeText={setText}
/>
<Button style={Styles.Button}
onPress={validateText}>
<Text style={Styles.TextButton}>ثبت اطلاعات</Text>
</Button>
</View>
);
};
export default App;
TextInput empty:
TextInput not empty:
Use state instead.
Also, In the given example, you are trying to access the list which is the local variable of the post() method.
Here is the alternate solution:
export default function App() {
const [homeAge, setHomeAge] = useState('');
return (
<View style={styles.container}>
<TextInput
value={homeAge}
style={[
styles.textInput,
{ borderColor: !homeAge ? 'red' : '#006d41' },
]}
onChangeText={(text) => setHomeAge(text)}
/>
<Button title={'ثبت اطلاعات'} style={styles.button} onPress={() => {}} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
},
textInput: {
padding: 10,
borderWidth: 1,
},
});
Working example: Expo Snack

React Native: Is it possible to TextInput keep on focus when the Modal is opened?

I need to keep input text in TextInput while the Modal is opened. FYI, TextInput is not a child component of Modal
I know it doesn't normal but the situation is pushing me to do this.
Please help me if you have experience in solving this kind of problem.
Use can use Ref object to focus the text input every time modal will be visible
check this code,,,,
export default function App() {
const [visible, setVisible] = React.useState(false);
const input = React.createRef();
React.useEffect(() => {
if (visible) {
input.current.focus();
}
}, [visible]);
return (
<View style={styles.container}>
<Button
title="Click"
onPress={() => {
setVisible(true);
}}
/>
<Modal
visible={visible}
onRequestClose={() => {
setVisible(false);
}}>
<View style={styles.modal}>
<TextInput ref={input} />
</View>
</Modal>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
},
modal: {
width: '100%',
height: '100%',
},
});
Use this way
CustomModal.js
const CustomModal = React.forwardRef((props, ref) => (
React.useEffect(() => {
ref.current.focus();
}, []);
return (
<Modal isActive={props.isActive}>
<ModalClose onClick={props.handleClose} />
</Modal>
)
))
App.js
export default function App() {
const [visible, setVisible] = React.useState(false);
const input = React.createRef();
return (
<View >
<TextInput ref={input} /> // Create textinput outside here
<Button
title="Click"
onPress={() => {
setVisible(true);
}}
/>
<CustomModal
isActive={visible}
handleClose={()=>{}}
ref={input}
/>
</View>
);
}

FlatList ScrollView Error on any State Change - Invariant Violation: Changing onViewableItemsChanged on the fly is not supported

onViewableItemsChanged does not seem to work when there is a state change in the app. Is this correct?
Seems like it wouldn't be very useful if this were the case....
Otherwise, users will be forced to us onScroll in order to determine position or something similar...
Steps to Reproduce
Please refer to snack
Repo has also been uploaded at github
Any state change produces an error when using onViewableItemsChanged
What does this error even mean?
Note: Placing the onViewableItemsChanged function in a const outside the render method also does not assist...
<FlatList
data={this.state.cardData}
horizontal={true}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
onViewableItemsChanged={(info) =>console.log(info)}
viewabilityConfig={{viewAreaCoveragePercentThreshold: 50}}
renderItem={({item}) =>
<View style={{width: width, borderColor: 'white', borderWidth: 20,}}>
<Text>Dogs and Cats</Text>
</View>
}
/>
Actual Behavior
Error
Based on #woodpav comment. Using functional components and Hooks.
Assign both viewabilityConfig to a ref and onViewableItemsChanged to a useCallback to ensure the identities are stable and use those. Something like below:
const onViewCallBack = React.useCallback((viewableItems)=> {
console.log(viewableItems)
// Use viewable items in state or as intended
}, []) // any dependencies that require the function to be "redeclared"
const viewConfigRef = React.useRef({ viewAreaCoveragePercentThreshold: 50 })
<FlatList
horizontal={true}
onViewableItemsChanged={onViewCallBack}
data={Object.keys(cards)}
keyExtractor={(_, index) => index.toString()}
viewabilityConfig={viewConfigRef.current}
renderItem={({ item, index }) => { ... }}
/>
The error "Changing onViewableItemsChanged on the fly is not supported" occurs because when you update the state, you are creating a new onViewableItemsChanged function reference, so you are changing it on the fly.
While the other answer may solve the issue with useRef, it is not the correct hook in this case. You should be using useCallback to return a memoized callback and useState to get the current state without needing to create a new reference to the function.
Here is an example that save all viewed items index on state:
const MyComp = () => {
const [cardData] = useState(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']);
const [viewedItems, setViewedItems] = useState([]);
const handleVieweableItemsChanged = useCallback(({ changed }) => {
setViewedItems(oldViewedItems => {
// We can have access to the current state without adding it
// to the useCallback dependencies
let newViewedItems = null;
changed.forEach(({ index, isViewable }) => {
if (index != null && isViewable && !oldViewedItems.includes(index)) {
if (newViewedItems == null) {
newViewedItems = [...oldViewedItems];
}
newViewedItems.push(index);
}
});
// If the items didn't change, we return the old items so
// an unnecessary re-render is avoided.
return newViewedItems == null ? oldViewedItems : newViewedItems;
});
// Since it has no dependencies, this function is created only once
}, []);
function renderItem({ index, item }) {
const viewed = '' + viewedItems.includes(index);
return (
<View>
<Text>Data: {item}, Viewed: {viewed}</Text>
</View>
);
}
return (
<FlatList
data={cardData}
onViewableItemsChanged={handleVieweableItemsChanged}
viewabilityConfig={this.viewabilityConfig}
renderItem={renderItem}
/>
);
}
You can see it working on Snack.
You must pass in a function to onViewableItemsChanged that is bound in the constructor of the component and you must set viewabilityConfig as a constant outside of the Flatlist.
Example:
class YourComponent extends Component {
constructor() {
super()
this.onViewableItemsChanged.bind(this)
}
onViewableItemsChanged({viewableItems, changed}) {
console.log('viewableItems', viewableItems)
console.log('changed', changed)
}
viewabilityConfig = {viewAreaCoveragePercentThreshold: 50}
render() {
return(
<FlatList
data={this.state.cardData}
horizontal={true}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
onViewableItemsChanged={this.onViewableItemsChanged}
viewabilityConfig={this.viewabilityConfig}
renderItem={({item}) =>
<View style={{width: width, borderColor: 'white', borderWidth: 20,}}>
<Text>Dogs and Cats</Text>
</View>}
/>
)
}
}
In 2023 with react-native version 0.71.2, the following code seems to work better than the older answers.
// 1. Define a function outside the component:
const onViewableItemsChanged = (info) => {
console.log(info);
};
// 2. create a reference to the function (above)
const viewabilityConfigCallbackPairs = useRef([
{ onViewableItemsChanged },
]);
<FlatList
data={this.state.cardData}
horizontal={true}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
viewabilityConfig={{viewAreaCoveragePercentThreshold: 50}}
// remove the following statement
// onViewableItemsChanged={(info) =>console.log(info)}
// 3. add the following statement, instead of the one above
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
renderItem={({item}) =>
<View style={{width: width, borderColor: 'white', borderWidth: 20,}}>
<Text>Dogs and Cats</Text>
</View>
}
/>
Source: https://github.com/facebook/react-native/issues/30171#issuecomment-820833606
const handleItemChange = useCallback( ({viewableItems}) => {
console.log('here are the chaneges', viewableItems);
if(viewableItems.length>=1)
viewableItems[0].isViewable?
setChange(viewableItems[0].index):null;
},[])
try this one it work for me
Setting both onViewableItemsChanged and viewabilityConfig outside the flatlist solved my problem.
const onViewableItemsChanged = useCallback(({ viewableItems }) => {
if (viewableItems.length >= 1) {
if (viewableItems[0].isViewable) {
setItem(items[viewableItems[0].index]);
setActiveIndex(viewableItems[0].index);
}
}
}, []);
const viewabilityConfig = {
viewAreaCoveragePercentThreshold: 50,
};
I'm using functional component and my flatlist looks like this
<Animated.FlatList
data={items}
keyExtractor={item => item.key}
horizontal
initialScrollIndex={activeIndex}
pagingEnabled
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
ref={flatlistRef}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { x: scrollX } } }],
{ useNativeDriver: false },
)}
contentContainerStyle={{
paddingBottom: 10,
}}
showsHorizontalScrollIndicator={false}
renderItem={({ item }) => {
return (
<View style={{ width, alignItems: 'center' }}>
<SomeComponent item={item} />
</View>
);
}}
/>
Try using viewabilityConfigCallbackPairs instead of onViewableItemsChanged.
import React, {useRef} from 'react';
const App = () => {
// The name of the function must be onViewableItemsChanged.
const onViewableItemsChanged = ({viewableItems}) => {
console.log(viewableItems);
// Your code here.
};
const viewabilityConfigCallbackPairs = useRef([{onViewableItemsChanged}]);
return (
<View style={styles.root}>
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
viewabilityConfigCallbackPairs={
viewabilityConfigCallbackPairs.current
}
/>
</View>
);
}
Move the viewabilityConfig object to the constructor.
constructor() {
this.viewabilityConfig = {
viewAreaCoveragePercentThreshold: 50
};
}
render() {
return(
<FlatList
data={this.state.cardData}
horizontal={true}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
onViewableItemsChanged={(info) =>console.log(info)}
viewabilityConfig={this.viewabilityConfig}
renderItem={({item}) =>
<View style={{width: width, borderColor: 'white', borderWidth: 20,}}>
<Text>Dogs and Cats</Text>
</View>
}
/>
)
}
Sombody suggest to use extraData property of Flatlist to let Flatlist notice, that something changed.
But this didn't work for me, here is what work for me:
Use key={this.state.orientation} while orientation e.g is "portrait" or "landscape"... it can be everything you want, but it had to change, if the orientation changed.
If Flatlist notice that the key-property is changed, it rerenders.
works for react-native 0.56
this works for me, is there any way to pass an additional argument to onViewRef? Like in the below code how can i pass type argument to onViewRef.
Code:
function getScrollItems(items, isPendingList, type) {
return (
<FlatList
data={items}
style={{width: wp("100%"), paddingLeft: wp("4%"), paddingRight: wp("10%")}}
horizontal={true}
keyExtractor={(item, index) => index.toString()}
showsHorizontalScrollIndicator={false}
renderItem={({item, index}) => renderScrollItem(item, index, isPendingList, type)}
viewabilityConfig={viewConfigRef.current}
onViewableItemsChanged={onViewRef.current}
/>
)
}
Remove your viewabilityConfig prop to a const value outside the render functions as well as your onViewableItemsChanged function