Im building a chat app with React Native using Expo and I use a Flatlist as child of KeyboardAvoidingView to render the list of messages, the problem is I want to scroll to the bottom when the keyboard is triggered
So I used the Flatlist method ( scrollToEnd ) with useRef hook and my code looks like this :
const ChatBody = ({ messages }) => {
const listRef = useRef(null);
useEffect(() => {
Keyboard.addListener("keyboardWillShow", () => {
setTimeout(() => listRef.current.scrollToEnd({ animated: true }), 100);
});
return () => Keyboard.removeListener("keyboardWillShow");
}, []);
return (
<FlatList
ref={listRef}
keyboardDismissMode="on-drag"
data={messages}
keyExtractor={(item) => item.id || String(Math.random())}
renderItem={({ item }) => <Message {...item} />}
/>
}
The code works just fine at the first render, but when I leave the screen and get back again and trigger the keyboard I get this error :
TypeError : null in not an object (evaluating 'listRef.current.scrollToEnd')
*The reason I added setTimout was because the scrollToEnd for some reason does not work when the keyboard event is triggered. adding setTimeout solved that issue.
The component tree is kinda like this :
StackNavigatorScreen => KeyboardAvoidingView => FlatList
You need to pass your event handler as a second parameter to Keyboard.removeListener. Since you're only passing in the first argument, your handler is run anyway and before your ref could be set.
const ChatBody = ({ messages }) => {
const listRef = useRef(null);
useEffect(() => {
Keyboard.addListener("keyboardWillShow", onKeyboardWillShow);
return () => Keyboard.removeListener("keyboardWillShow", onKeyboardWillShow);
}, []);
function onKeyboardWillShow() {
setTimeout(() => {
listRef.current.scrollToEnd();
}, 100);
}
return (
<FlatList
ref={listRef}
keyboardDismissMode="on-drag"
data={messages}
keyExtractor={(item) => item.id || String(Math.random())}
renderItem={({ item }) => <Message {...item} />}
/>
)
}
const listRef = useRef(null); <-- this line is the one causing the problem.
You need to assign an object, null in this case cannot be put there as it is not an object.
Related
Here you can see the gif
Here is my whole Navigator functional component. I'm trying to implement two tabs using Tab Navigator. One to display the cryptos and the other to display the forex data.
The problem is, when I try to load more data on reaching the flatlist's end, the flatlist is scrolling to the top since I'm making a state change [page+1].
const Navigator = () => {
const Tab = createMaterialTopTabNavigator();
const renderItems = ({ item }) => (
<Text>{item.name}<Text>
);
const fetchMarketData = async () => {
console.log("Fetching");
const marketData = await getCryptoMarketData({ page });
if (marketData != "Network Error") {
const ids = data.map((item) => item.id);
let newData = marketData.filter((item) => !ids.includes(item.id));
setData([...data, ...newData]);
setFetching(false);
} else {
setFetching(false);
Alert.alert(marketData, "Sorry for the inconvenience");
}
};
useEffect(() => {
setFetching(true);
const data = async () => {
await fetchMarketData();
};
}, [page]);
const handleLoadMore = async () => {
setFetching(true);
setPage((page) => page + 1);
};
const ScreenA = () => (
<FlatList
data={data}
style={{ backgroundColor: "white" }}
keyExtractor={(item) => item.id}
renderItem={renderItems}
scrollEventThrottle={16}
onEndReached={handleLoadMore}
onEndReachedThreshold={0}
/>
);
return (
<Tab.Navigator
screenOptions={({ route }) => screenOptions(route)}
keyboardDismissMode="auto"
>
<Tab.Screen name="Crypto" component={ScreenA} />
<Tab.Screen name="Forex" component={ScreenC} />
</Tab.Navigator>
);
};
export default Navigator;
OnEndReached is firing the handleLoadMore function and after the state change on data, the Flatlist is scrolling to the top.
1st reason
you have typo in "fetchMarketData", how exactly u get "newData" because i cant see it anywhere, maybe it should be "marketData" if not then u adding SAME old data PLUS undefined[...data, ...undefined]
2nd reason
reason why is that u call setPage(page + 1) and then "fetchMarketData" this is bad why ? because setState is async and it can be changed instant or after 5 secound, so u dont know when its changed and this is why we have hooks, you can use "useEffect" to handle this
change your "handleLoadMore" for example like this
const handleLoadMore = () => {
setPage(page + 1);
};
add useEffect hook that runs when "page" state changes
React.useEffect(() => {
(async() => {
setFetching(true)
const marketData = await getCryptoMarketData({ page });
if (marketData != "Network Error") {
setData([...data, ...marketData]);
} else {
Alert.alert(marketData, "Sorry for the inconvenience");
}
setFetching(false)
})()
}, [page])
I have this code below, when the socket event is activated it is supposed to update the list (which is not happening), I've searched everywhere but I couldn't find how to make this list update, can anyone help me?
const [globalMessages, setGlobalMessages] = useState([]);
const chatList = useRef(null);
useEffect(()=>{
socket.on("GlobalMessage", (globalMessageData) => {
console.log(globalMessages.length)
globalMessages.push(globalMessageData)
setGlobalMessages(globalMessages)
});
},[])
<FlatList
ref={chatList}
onContentSizeChange={() => chatList.current.scrollToEnd() }
onLayout={() => chatList.current.scrollToEnd() }
data={globalMessages}
extraData={globalMessages}
keyExtractor={message => message._id}
renderItem={(message) => {
return (
<ChatBubble
text={message.item.text}
from={message.item.user.name}
datetime= {message.item.createdAt}
myId={playerData.id}
msgfromId={message.item.user._id}
containerStyle={styles.msgContainer}
smallTextColor= "#6B6B6B"
msgTextColor= "white"
/>
)
}}
/>
I changed this inside socket.on
setGlobalMessages([])
setGlobalMessages(globalMessages)
and it worked as it should
I have some question on React Native Navigations v5
I have a sub function in my main function. I also have screenOption = (navData) function for customise my navigation and have few header button here which out of my Main function. My Problem now i am not sure how to access the subFunction that located in my mainfunction from my screenOption = (navData). For example whenclick the icon on the header Button and it will trigger the dispatch function to store which located inside the main function.
const mainfunction = (props) =>{
subfunction = async(data) =>{
dispatch(action.delete(data)
}
return ()
}
export const screenOptions = (navData) => {
Item
title="Delete"
iconName={
Platform.OS === "android" ? "md-create" : "ios-trash-outline"
}
onPress={() => {
trigger subfunction here
}}
/>
}
Try creating a custom button and add the component using setOptions
React.useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => <CustomHeaderButton onPress={customOnPress} />
});
}, []);
I am trying to render a list of items in React Native with the FlatList component but every time I fetch new data it re-renders the who list of items even with React.memo.
Here is what my code looks like:
const data = [
{ _id: 1, text: 'Hello World' },
{ _id: 2, text: 'Hello' },
{ ... }
]
const renderItem = ({ item }) => (<Component item={item} />)
const loadMore = () => {
//Fetching data from db and adding to data array
}
<FlatList
data={data}
keyExtractor={item => item._id}
renderItem={renderItem}
onEndReached={loadMore}
removeClippedSubviews={true}
/>
Component.js
const Component = ({ item }) => {
console.log('I am rendering')
return (
<Text>{item.text}</Text>
)
}
const equal = (prev, next) => {
return prev.item.text === next.item.text
}
export default React.memo(Component, equal)
Every time the onEndReached function gets triggered and calls the loadMore function, all FlatList items get re-rendered, it console.log 'I am rendering' every single time and causes the error virtualizedlist you have a large list that is slow to update
Thanks to anyone who can help me!
I don't know why but I fixed it with an if statement in the equal function
//Changing this
const equal = (prev, next) => {
return prev.item.text === next.item.text
}
//To this
const equal = (prev, next) => {
if(prev.item.text !== next.item.text) {
return false;
}
return true
}
Hope this could help someone else.
I have an OTP screen, I've added a ref to the first TextInput field so that when the screen loads, it gets focused:
// ref
const pin1 = useRef();
// navigation listener
navigation.addListener('focus', () => {
pin1.current.focus();
})
But it returns an error:
TypeError: undefined is not an object (evaluating 'pin1.current.focus')
This code works pretty well:
navigation.addListener('focus', () => {
alert('Hello')
})
I think you should use autoFocus={true} with your TextInput you when you open the screen your TextInput automatically focused.
Check autoFocus
You should check the current object before calling any function.
navigation.addListener('focus', () => {
if (pin1.current) {
pin1.current.focus();
}
});
Embed the listener inside a useEffect
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
navigation.addListener('focus', () => {
pin1.current.focus();
})
}
return unsubscribe;
}, [navigation, pin1]);
also
<TextInput ref={pin1} />