React Native bottom sheet opens when TextInput has autofocus prop - react-native

I am using react-native-bottom-sheet for choosing a category and when the Amount input has the autofocus property the bottom sheet appears on the screen. And it looks like this
The bottom sheet appears behind the keyboard, and it doesn't follow the snap point I want, it should be at the middle of the screen but it is always slightly above the keyboard. This doesn't happen all the time, like 2/5 tries, and only when I open this screen for the first time. The sheet should only open when I tap on the category and it should be hidden when entering the screen.
I tried to use some variable render to check if the bottom sheet even needs to render or not, so I don't render it at all but then I could see it just flicker for a second.
This is my bottom sheet component. Because I want to create a reusable component I used ref to pass the function for opening the bottom sheet to the parent.
const TransactionBottomSheet: React.ForwardRefRenderFunction<refProps, Props> = (props, ref) => {
const { onSelect } = props;
const sheetRef = useRef<BottomSheet>(null);
const [data, setData] = useState<Transaction[]>(categoriesData);
const [selectedCategory, setSelectedCategory] = useState<Category | null>(null);
const openSheet = useCallback(() => {
sheetRef.current?.expand();
}, []);
const closeSheet = useCallback(() => {
sheetRef.current?.close();
}, []);
useImperativeHandle(ref, () => ({
openSheet: () => openSheet(),
}));
const setTypeData = (id: number) => {
const types = transactionCategories[id].types ?? [];
setData(Object.values(types));
};
const clearCategory = () => {
setData(categoriesData);
setSelectedCategory(null);
};
const onRowPress = (item: Transaction | Category) => {
if (!selectedCategory) {
setTypeData(item.id);
setSelectedCategory(item as Category);
} else {
onSelect(selectedCategory, item);
closeSheet();
}
};
const onClose = () => {
setData(categoriesData);
setSelectedCategory(null);
};
const renderItem = ({ item }: { item: Transaction | Category }) => (
<TransactionRowSelect item={item} hideIcon={!!selectedCategory} onPress={onRowPress} />
);
const renderBackdrop = useCallback(
(props) => <BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} />,
[]
);
return (
<BottomSheet
ref={sheetRef}
snapPoints={snapPoints}
enablePanDownToClose
index={-1}
onClose={onClose}
backdropComponent={renderBackdrop}
handleStyle={styles.handle}
>
<Label style={styles.title}>{"Pick category"}</Label>
{!!selectedCategory && (
<>
<TransactionRowSelect item={selectedCategory} onPress={clearCategory} />
<Separator />
</>
)}
<BottomSheetFlatList
data={data}
keyExtractor={(item) => `${item.id}`}
renderItem={renderItem}
/>
</BottomSheet>
);
};
const styles = StyleSheet.create({
title: {
textAlign: "center",
fontSize: 16,
fontWeight: "bold",
paddingBottom: 10,
backgroundColor: colors.grey3,
},
handle: {
backgroundColor: colors.grey3,
borderTopLeftRadius: 10,
borderTopRightRadius: 10,
},
});
export default React.forwardRef(TransactionBottomSheet);
And this is Add transaction screen
const TransactionForm: React.FC<Props> = ({ navigation }) => {
const [date, setDate] = useState(new Date());
const sheetRef = useRef<TransactionBottomSheetType>(null);
const [category, setCategory] = useState<Category | null>(null);
const [type, setType] = useState<Transaction | null>(null);
const [amount, setAmount] = useState("");
const [description, setDescription] = useState("");
const [tryCreateNewTransaction, { isLoading }] = useCreateNewTransactionMutation();
const onAdd = async () => {
Keyboard.dismiss();
try {
if (type && category) {
await tryCreateNewTransaction({
amount: Number(amount),
description,
date: formatIsoDate(date),
user_id: 1,
type_id: type.id,
category_id: category.id,
}).unwrap();
navigation.goBack();
}
} catch (error) {
Alert.alert("An error occurred while adding transaction", "Please try again");
}
};
const onSelectCategory = (category: Category, type: Transaction) => {
setCategory(category);
setType(type);
};
const setCategoryText = () => {
if (!category && !type) {
return "";
}
return `${category?.label}, ${type?.label}`;
};
const openSheet = () => {
if (sheetRef?.current) {
Keyboard.dismiss();
sheetRef?.current?.openSheet();
}
};
return (
<View style={styles.container}>
<DatePickerInput date={date} maximumDate={new Date()} onDateSelect={setDate} />
// This is just TextInput component with some custom style, here is the autofocus props
<LabelInput
value={amount}
placeholder='Amount'
onChangeText={setAmount}
keyboardType='decimal-pad'
style={styles.marginTop}
icon={<FontAwesome5 name='coins' size={24} color={colors.greenMint} />}
autoFocus
/>
{/* <InputErrorLabel text={errors.amount} isVisible={!!errors.amount} /> */}
<TouchableOpacity onPress={openSheet}>
<LabelInput
value={setCategoryText()}
icon={<MaterialIcons name='category' size={24} color={colors.greenMint} />}
disabled
placeholder='Category'
style={styles.marginTop}
inputStyle={styles.category}
/>
</TouchableOpacity>
<TextBox
placeholder='Transaction comment'
style={styles.marginTop}
numberOfLines={6}
maxLength={300}
value={description}
onChangeText={setDescription}
/>
<CustomButton title='Submit' onPress={onAdd} style={styles.marginTop} />
<AppActivityIndicator isLoading={isLoading} />
<TransactionBottomSheet ref={sheetRef} onSelect={onSelectCategory} />
</View>
);
};
export default TransactionForm;
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 16,
paddingTop: 20,
},
marginTop: {
marginTop: 20,
},
category: {
color: colors.black,
},
});

Found a fix for the problem, it looks like it had to do with the keyboard and expo. I noticed that when the keyboard is shown or dismissed there is a white background showing behind it for a split second. I fixed this by adding this to app.json
"android": {
"softwareKeyboardLayoutMode": "pan"
},
This fixed problem with the keyboard and bottom sheet also. Didn't see it appear anymore.

Listen to the keyboard show/hide and do whatever you want with the bottom sheet.
for example, here I'm changing the snapping point of the bottom sheet.
React.useEffect(() => {
let keyboardHideListener = Keyboard.addListener('keyboardDidHide', () => {
if (isBottomSheetOpen.current) {
bottomSheetRef.current?.snapToIndex(0);
}
});
let keyboardShowListener = Keyboard.addListener('keyboardDidShow', () => {
if (isBottomSheetOpen.current) {
bottomSheetRef.current?.snapToIndex(1);
}
});
return () => {
keyboardHideListener.remove();
keyboardShowListener.remove();
};
}, []);

Related

Get current index/visible item of FlatList

I have a scrolling view of posts. Each post has a corresponding user and I have a header that shows the user info of the current visible post. With Flutter this was simple, I just wrapped the post widget with a visibility detector. With React Native this is not very easy. I've tried onViewableItemsChanged but, since I am using a fuction not a class, that causes an error. I also tried some solutions that used onScroll and onMomentumScrollEnd but those all just stayed at index 0. How can I get the current index that is fully visible? If needed, I am fine with splitting up the pagination functions so I can just have a class with the UI and use onViewableItemsChanged but I don't know how to do that because the handleLoadMore function is used in the UI.
export default function PostsListView() {
const [users, setUsers] = useState<User[]>([]);
const [posts, setPosts] = useState<Post[]>([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
const [hasMore, setHasMore] = useState(true);
const onScroll = useCallback((event: any) => {
const slideSize = event.nativeEvent.layoutMeasurement.width;
const index = event.nativeEvent.contentOffset.x / slideSize;
const roundIndex = Math.round(index);
console.log("roundIndex:", roundIndex);
currentItem = roundIndex;
}, []);
useEffect(() => {
async function fetchPosts() {
setLoading(true);
const { data, error } = await supabase
.from("posts")
.select("*")
.order("date", { ascending: false })
.range((page - 1) * PAGE_SIZE, page * PAGE_SIZE - 1);
if (error) {
console.error(error);
showAlert();
setLoading(false);
return;
}
const newPosts = data.map((post: any) => new Post(post));
setPosts((prevPosts) => [...prevPosts, ...newPosts]);
setLoading(false);
setHasMore(data.length === PAGE_SIZE);
}
async function fetchUsers() {
const { data, error } = await supabase.from("posts").select("*");
if (error) {
showAlert();
console.error(error);
return;
}
const newUsers = data.map((user: any) => new User(user));
newUsers.forEach((user) => {
const userPosts = posts.filter((post) => post.uid === user.uid);
user.posts = [...user.posts, ...userPosts];
});
setUsers((prevUsers) => [...prevUsers, ...newUsers]);
}
fetchPosts();
fetchUsers();
}, [page]);
const handleLoadMore = () => {
if (!loading && hasMore) {
setPage((prevPage) => prevPage + 1);
}
};
const handleScroll = (event: any) => {
const index = Math.floor(
Math.floor(event.nativeEvent.contentOffset.x) /
Math.floor(event.nativeEvent.layoutMeasurement.width)
);
currentItem = index;
};
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
{loading ? (
<Text>Loading...</Text>
) : (
<FlatList
data={posts}
horizontal={false}
directionalLockEnabled={true}
renderItem={({ item }) => (
<View>
<HomePost
post={item}
user={
users.filter(function (u) {
return u.uid == item.uid;
})[0]
}
index={posts.indexOf(item)}
loading={loading}
/>
<SizedBox vertical={5} />
</View>
)}
keyExtractor={(item) => item.postId}
onEndReached={handleLoadMore}
onEndReachedThreshold={0.1}
onScroll={onScroll}
onMomentumScrollEnd={onScroll}
/>
)}
</View>
);
}
If you provide a viewabilityConfig to the FlatList, you can use the onViewableItemsChanged event to learn which items are on screen. You just have to make sure that both the viewabilityConfig and onViewableItemsChanged values never change:
import { useState, useEffect, useRef, useCallback } from 'react';
import { Text, View, StyleSheet, FlatList, Image } from 'react-native';
import Constants from 'expo-constants';
// You can import from local files
import AssetExample from './components/AssetExample';
// or any pure javascript modules available in npm
import { Card } from 'react-native-paper';
const API_URL = 'https://random-data-api.com/api/v2/users?size=25';
export default function App() {
const [posts, setPosts] = useState([]);
const [visibleItems, setVisibleItems] = useState([]);
// wrapped in ref so that re-renders doesnt recreate it
const viewabilityConfig = useRef({
minimumViewTime: 100,
itemVisiblePercentThreshold: '90%',
}).current;
// wrapped in useCallback so that re-renders doesnt recreate it
const onViewableItemsChanged = useCallback(({ viewableItems }) => {
setVisibleItems(viewableItems.map(({ item }) => item));
}, []);
useEffect(() => {
fetch(API_URL)
.then((data) => data.json())
.then(setPosts);
}, []);
return (
<View style={styles.container}>
{visibleItems.length > 0 && (
<Text>
Currently visible:{' '}
{visibleItems
.map((item) => item.first_name + ' ' + item.last_name)
.join(', ')}
</Text>
)}
<View style={styles.flatlistContainer}>
<FlatList
data={posts}
renderItem={(props) => <Item {...props} />}
viewabilityConfig={viewabilityConfig}
onViewableItemsChanged={onViewableItemsChanged}
/>
</View>
</View>
);
}
const Item = ({ item }) => {
return (
<View style={styles.itemContainer}>
<Text>
{item.first_name} {item.last_name}
</Text>
<Image
source={{ uri: item.avatar }}
style={{ width: 100, height: 100 }}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
},
flatlistContainer: {
width: '100%',
height: 500,
backgroundColor: 'lightblue',
},
itemContainer: {
justifyContent: 'center',
alignItems: 'center',
margin: 10,
},
});
Demo
You could get the currently viewable items with onViewableItemsChanged where you should get your information.
<FlatList
data={posts}
horizontal={false}
directionalLockEnabled={true}
// this should return an array with following infos
// [{
// item: {key: "key-12"},
// key: "key-12",
// index: 11,
// isViewable: true
// }]
onViewableItemsChanged={({changed, viewableItems}) => console.log(changed, viewableItems)}
....
I've gone through and created a supabase app that resembles your use case and the onViewableItemsChanged approach should work:
import { useContext, useEffect, useState, useRef, useCallback } from 'react';
import {
View,
StyleSheet,
Dimensions,
FlatList,
ActivityIndicator,
} from 'react-native';
import { Text } from 'react-native-paper';
import { SessionContext } from '../../Context';
import { supabase } from '../../initSupabase';
import PostItem from '../../components/PostItem';
const { height } = Dimensions.get('screen');
const totalPostsToGet = 2;
type Post = {
post: {
text: string;
media: {
type: string;
source: string;
};
};
id: number;
created_at: string;
uid: string;
};
export default function PostScreen(props) {
const { session } = useContext(SessionContext);
const [posts, setPosts] = useState<Post[]>([]);
const [page, setPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [visibleItems, setVisibleItems] = useState([]);
const viewabilityConfig = useRef({
// minimumViewTime: 100,
itemVisiblePercentThreshold: 50,
}).current;
// wrapped in useCallback so that re-renders doesnt recreate it
const onViewableItemsChanged = useCallback(({ viewableItems }) => {
setVisibleItems(viewableItems.map(({ item }) => item));
}, []);
const handleLoadMore = () => {
if (!isLoading && hasMore) {
setPage((prevPage) => prevPage + 1);
}
};
useEffect(() => {
if (!session) return;
const fetchLastPost = async () => {
const { data, error } = await supabase
.from('posts')
.select('*')
.order('created_at')
.range(0, 1);
return data[0];
};
const fetchPosts = async () => {
setIsLoading(true);
const lastPost = await fetchLastPost();
console.log('last post', lastPost);
const rangeStart = (page - 1) * totalPostsToGet;
const { data, error } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
.range(rangeStart, rangeStart + totalPostsToGet - 1);
if (error) {
console.log('error retrieving profile data', error);
}
if (data) {
setPosts((prev) => prev.concat(data));
// I couldnt figure out how PAGE_SIZE could be used to know that the last post was reached
// so I just grab the last post and look to see if its id is in current data
const hasLastPost = Boolean(
data.find((post) => post.id == lastPost.id)
);
setHasMore(!hasLastPost);
}
setIsLoading(false);
};
fetchPosts();
// subscribe to database changes
const subscription = supabase
.channel(`Posts`)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
// filter: `id=eq.${session.user.id}`,
},
(payload) => {
setIsLoading(true);
console.log('Post update');
setPosts((prev) => {
// either push new post or update existing one
const postIndex = prev.findIndex(
(post) => post.id == payload.new.id
);
if (postIndex < 0) return [...prev, payload.new];
else {
const newPosts = [...prev];
newPosts[postIndex] = payload.new;
return newPosts;
}
});
setIsLoading(false);
}
)
.subscribe();
return () => {
supabase.removeChannel(subscription);
};
}, [session, page]);
return (
<View style={styles.container}>
<Text>Currently visible:</Text>
<View style={{ padding: 5, margin: 5 }}>
{visibleItems.map((item) => (
<Text>{item.post.text.substring(0, 30)}</Text>
))}
</View>
<View style={styles.flatlistContainer}>
{isLoading && <ActivityIndicator />}
<FlatList
data={posts}
keyExtractor={(item: Post) => item.id}
horizontal={false}
directionalLockEnabled
onEndReached={handleLoadMore}
renderItem={(props) => <PostItem {...props} />}
viewabilityConfig={viewabilityConfig}
onViewableItemsChanged={onViewableItemsChanged}
/>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
flatlistContainer: {
height: height * 0.45,
},
});
Demo
Take a look at the Intersection Observer API documentation which is an implementation you can use to detect when an element is visible on the screen or not.
Here's a very simple example where the green div is "observed". Whether it is visible or not is marked in state.
Here's a working sandbox
package.json
{
"name": "react18-intersection-observer",
"version": "1.0.0",
"description": "Detect visible screen elemenet using intersection observer",
"keywords": [
"react",
"starter"
],
"main": "src/index.js",
"dependencies": {
"react": "18.2.0",
"react-bootstrap": "^2.7.1",
"react-dom": "18.2.0",
"react-scripts": "^5.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"author": "Wesley LeMahieu"
}
index.js
import { createRoot } from "react-dom/client";
import App from "./App";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(<App />);
app.js
import { useEffect, useRef, useState } from "react";
const App = () => {
const observer = useRef(null);
const observerRef = useRef(null);
const [isVisible, setIsVisible] = useState(false);
const observerCallback = async (e) => {
if (e.length) {
setIsVisible(e[0].isIntersecting);
} else {
setIsVisible(false);
}
};
useEffect(() => {
if (observerRef.current) {
if (observer.current) {
observer.current.disconnect();
observer.current.observe(observerRef.current);
} else {
observer.current = new IntersectionObserver(observerCallback);
observer.current.observe(observerRef.current);
}
}
return () => observer.current.disconnect();
}, []);
useEffect(() => {
if (isVisible) {
alert("Green box visible");
} else {
alert("Grey box visible");
}
console.log(isVisible ? "GREEN BOX VISIBLE" : "GREEN BOX NOT VISIBLE");
}, [isVisible]);
return (
<div style={{ display: "flex" }}>
<div
style={{
backgroundColor: isVisible ? "green" : "red",
width: "10%",
height: "3000px",
}}
>
Visible
</div>
<div style={{ width: "90%" }}>
<div
style={{ backgroundColor: "grey", opacity: 0.6, height: "1000px" }}
>
Other div
</div>
<div
ref={observerRef}
style={{ backgroundColor: "green", opacity: 0.6, height: "1000px" }}
>
Observed Div
</div>
<div
style={{ backgroundColor: "grey", opacity: 0.6, height: "1000px" }}
>
Other div
</div>
</div>
</div>
);
};
export default App;

how to use react native redux in react navigation

I am using redux persist with asyncstorage save items in a bookmarks list. The items are in a flatlist and when I click on one item, it navigates me to another screen. I would like to implement the functional bookmark in the header of that screen.
When I tried doing this, and clicked the bookmark in the header, and go back to the bookmarks, it just shows a blank card. It looks like it is not updating the state properly. How can I fix this?
StackNavigator.tsx
const MainStackNavigator = () => {
const { books, bookmarks } = useAppSelector((state) => state.booksReducer);
const dispatch = useDispatch();
const fetchBooks = () => dispatch(getBooks());
const addToBookmarkList = (book) => dispatch(addBookmark(book));
const removeFromBookmarkList = (book) => dispatch(removeBookmark(book));
useEffect(() => {
fetchBooks();
}, []);
const handleAddBookmark = (book) => {
addToBookmarkList(book);
};
const handleRemoveBookmark = (book) => {
removeFromBookmarkList(book);
};
const handleSwapBookmark = (book) => {
removeFromBookmarkList(book);
};
const RenderItem = () => {
const ifExists = (book) => {
if (bookmarks.filter((item) => item.id === book.id).length > 0) {
return true;
}
return false;
};
return (
<TouchableOpacity
onPress={() =>
ifExists(i) ? handleRemoveBookmark(i) : handleAddBookmark(i)
}
activeOpacity={0.7}
style={{
flexDirection: "row",
padding: 2,
backgroundColor: ifExists(i) ? "#F96D41" : "#2D3038",
borderRadius: 20,
alignItems: "center",
justifyContent: "center",
height: 40,
width: 40,
}}
>
<MaterialCommunityIcons
color={ifExists(i) ? "white" : "#64676D"}
size={24}
name={ifExists(i) ? "bookmark-outline" : "bookmark"}
/>
</TouchableOpacity>
);
};
return (
<AppStack.Navigator>
<AppStack.Screen
name="BookmarksScreen"
component={BookmarksScreen}
options={{
title: "Search",
statusBarColor: isDarkMode ? "white" : "black",
headerLargeTitle: true,
headerTranslucent: true,
headerLargeTitleHideShadow: true,
}}
/>
<AppStack.Screen
name="Screen2"
component={Screen2}
options={({ route }) => ({
headerLargeTitle: false,
title: route.params.name,
headerTranslucent: true,
headerRight: () => <RenderItem item={route.params.name} />,
})}
/>
</AppStack.Navigator>
);
};
actions.js
import axios from "axios";
import { BASE_URL } from "../config";
// Define action types
export const GET_BOOKS = "GET_BOOKS";
export const ADD_TO_BOOKMARK_LIST = "ADD_TO_BOOKMARK_LIST";
export const REMOVE_FROM_BOOKMARK_LIST = "REMOVE_FROM_BOOKMARK_LIST";
export const SWAP_IN_BOOKMARK_LIST = "SWAP_IN_BOOKMARK_LIST";
export const getBooks = () => {
try {
return async (dispatch) => {
const response = await axios.get(`${BASE_URL}`);
if (response.data) {
dispatch({
type: GET_BOOKS,
payload: response.data,
});
} else {
console.log("Unable to fetch data from the API BASE URL!");
}
};
} catch (error) {
console.log(error);
}
};
export const addBookmark = (book) => (dispatch) => {
dispatch({
type: ADD_TO_BOOKMARK_LIST,
payload: book,
});
};
export const removeBookmark = (book) => (dispatch) => {
dispatch({
type: REMOVE_FROM_BOOKMARK_LIST,
payload: book,
});
};
hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./store";
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
reducers.ts
import {
GET_BOOKS,
ADD_TO_BOOKMARK_LIST,
REMOVE_FROM_BOOKMARK_LIST,
} from "./actions";
const initialState = {
books: [],
bookmarks: [],
};
function booksReducer(state = initialState, action) {
switch (action.type) {
case GET_BOOKS:
return { ...state, books: action.payload };
case ADD_TO_BOOKMARK_LIST:
return { ...state, bookmarks: [...state.bookmarks, action.payload] };
case REMOVE_FROM_BOOKMARK_LIST:
return {
...state,
bookmarks: state.bookmarks.filter(
(book) => book.id !== action.payload.id
),
};
default:
return state;
}
}
export default booksReducer;
store.ts
import { createStore, combineReducers, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import AsyncStorage from "#react-native-async-storage/async-storage";
import { persistStore, persistReducer } from "redux-persist";
import booksReducer from "./reducers";
const persistConfig = {
key: "root",
storage: AsyncStorage,
whitelist: ["bookmarks"],
};
const rootReducer = combineReducers({
booksReducer: persistReducer(persistConfig, booksReducer),
});
export const store = createStore(rootReducer, applyMiddleware(thunk));
export const persistor = persistStore(store);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
BookmarksScreen.tsx
const BookmarksScreen = () => {
return (
<View>
<FlatList
data={bookmarks}
keyExtractor={(item) => item.id}
renderItem={renderItem}
showsVerticalScrollIndicator={false}
/>
</View>
);
}
renderItem
const renderItem = ({ item }) => {
return (
<TouchableOpacity
onPress={() =>
navigation.navigate("Screen2", {name: item.name})}
>
<View style={{ flexDirection: "row", flex: 1 }}>
<View>
<Text
style={{
fontSize: 22,
paddingRight: 16,
color: "black",
fontFamily: "Medium",
left: 45,
top: 6,
}}
>
{item.country}
</Text>
</View>
</View>
</View>
</TouchableOpacity>
);
};
I think you forgot to access props that's the reason booked-marked not working i change some code please check it's working or not.
StackNavigator.tsx
const RenderItem = (item) => {
const ifExists = (book) => {
if (bookmarks.filter((item) => item.id === book.id).length > 0) {
return true;
}
return false;
};
return (
<TouchableOpacity
onPress={() =>
ifExists(item) ? handleRemoveBookmark(item) : handleAddBookmark(item)
}
activeOpacity={0.7}
style={{
flexDirection: "row",
padding: 2,
backgroundColor: ifExists(i) ? "#F96D41" : "#2D3038",
borderRadius: 20,
alignItems: "center",
justifyContent: "center",
height: 40,
width: 40,
}}
>
<MaterialCommunityIcons
color={ifExists(i) ? "white" : "#64676D"}
size={24}
name={ifExists(i) ? "bookmark-outline" : "bookmark"}
/>
</TouchableOpacity>
);
};

React-Native FlatList item clickable with data to another screen

I'm trying to access a screen when you click on an item in my flatlist by passing the date I retrieved from the firebase before, I've tried several things without success so I come to you.
Basically when I click on one of the elements -> A screen with details should appear.
export default function Notifications() {
const dbh = firebase.firestore();
const [loading, setLoading] = useState(true); // Set loading to true on component mount
const [deliveries, setDeliveries] = useState([]); // Initial empty array of users
useEffect(() => {
const subscriber = dbh
.collection("deliveries")
.onSnapshot((querySnapshot) => {
const deliveries = [];
querySnapshot.forEach((documentSnapshot) => {
deliveries.push({
...documentSnapshot.data(),
key: documentSnapshot.id,
});
});
setDeliveries(deliveries);
setLoading(false);
});
// Unsubscribe from events when no longer in use
return () => subscriber();
}, []);
if (loading) {
return <ActivityIndicator />;
}
return (
<FlatList
style={{ flex: 1 }}
data={deliveries}
renderItem={({ item }) => (
<TouchableOpacity
onPress={() => { * HERE I NEED TO PASS DATA AND SHOW AN ANOTHER SCREEN FOR DETAILS * }}>
<View style={styles.container}>
<Text>DATE: {item.when}</Text>
<Text>ZIP DONATEUR: {item.zip_donator}</Text>
<Text>ZIP BENEFICIAIRE: {item.zip_tob_deliv}</Text>
</View>
</TouchableOpacity>
)}
/>
);
}
EDIT: Small precision this screen is located in a Tab.Navigator
you can pass params in navigation,
export default function Notifications(props) {
const { navigation } = props
const dbh = firebase.firestore();
const [loading, setLoading] = useState(true); // Set loading to true on component mount
const [deliveries, setDeliveries] = useState([]); // Initial empty array of users
useEffect(() => {
const subscriber = dbh
.collection("deliveries")
.onSnapshot((querySnapshot) => {
const deliveries = [];
querySnapshot.forEach((documentSnapshot) => {
deliveries.push({
...documentSnapshot.data(),
key: documentSnapshot.id,
});
});
setDeliveries(deliveries);
setLoading(false);
});
// Unsubscribe from events when no longer in use
return () => subscriber();
}, []);
if (loading) {
return <ActivityIndicator />;
}
return (
<FlatList
style={{ flex: 1 }}
data={deliveries}
renderItem={({ item }) => (
<TouchableOpacity
onPress={() => {
navigation.navigate('screenName', {
//pass params here
})
}}>
<View style={styles.container}>
<Text>DATE: {item.when}</Text>
<Text>ZIP DONATEUR: {item.zip_donator}</Text>
<Text>ZIP BENEFICIAIRE: {item.zip_tob_deliv}</Text>
</View>
</TouchableOpacity>
)}
/>
);
}
you can access params in the navigated screen by props.route.params

Search engine in Expo

I follow this tutorial to add search to my Expo (React Native) app. After the last step I have this mistake:photo.
What should I do?
This is the part of the program.
This is one of my navigation screens, where I have links for other screens:
function InfoScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button title="Go back" onPress={() => navigation.goBack()} />
<View style={styles.container_new}>
<Text style={styles.text}>Basic FlatList Example</Text>
<FlatList
ListHeaderComponent={renderHeader}
...
</View>
)}
/>
</View>
</View>
);
}
Function renderHeader from the tutorial:
function renderHeader() {
return (
...
);
}
Part from the tutorial (last steps)
const [query, setQuery] = useState('');
const [fullData, setFullData] = useState([]);
useEffect(() => {
setIsLoading(true);
fetch(API_ENDPOINT)
.then(response => response.json())
.then(response => {
setData(response.results);
// ADD THIS
setFullData(response.results);
setIsLoading(false);
})
.catch(err => {
setIsLoading(false);
setError(err);
});
}, []);
const handleSearch = text => {
const formattedQuery = text.toLowerCase();
const filteredData = filter(fullData, user => {
return contains(user, formattedQuery);
});
setData(filteredData);
setQuery(text);
};
const contains = ({ name, email }, query) => {
const { first, last } = name;
if (first.includes(query) || last.includes(query) || email.includes(query)) {
return true;
}
return false;
};

How to update state react native hooks from other screen using react navigation hook param?

How to update state react native hooks from other screen using react navigation hook param?
I am trying to update state selectedHotel in screen 1 from screen 2 that provide the data, so i save data from screen 2 in parameter using react navigation hooks params, the data is update but i can't update state selectHotel if data is exist in useEffect screen 1, here the code:
screen 1:
import {useNavigation, useNavigationParam} from 'react-navigation-hooks';
const TransportScreen = () => {
const hotelParam = useNavigationParam('hotel');
const [baseLoading, setBaseLoading] = useState(true);
const [selectedHotel, setSelectedHotel] = useState(
hotelParam ? hotelParam.id : '',
);
const {navigate} = useNavigation();
useEffect(() => {
setTimeout(() => {
setBaseLoading(false);
}, 1000);
if (hotelParam) {
setSelectedHotel(hotelParam.id);
console.log('update selected hotel', selectedHotel);
}
}, []);
const redirectTransportSelectHotel = () => {
console.log('select hotel');
navigate('TransportSelectHotel');
};
const submitTransport = () => {
console.log('getHotelId ', selectedHotel);
};
const renderContent = () => {
console.log('hotelId: ', selectedHotel);
if (!baseLoading) {
return (
<View
style={{
flex: 1,
flexDirection: 'column',
justifyContent: 'flex-start',
}}>
<MenuCard
expandRight
onPress={() => redirectTransportSelectHotel()}>
{hotelParam ? hotelParam.name : 'Select Hotel'}
</MenuCard>
<View
style={{
flex: 1,
flexDirection: 'column',
justifyContent: 'flex-end',
}}>
<View
style={{
flexDirection: 'row',
marginHorizontal: 40,
marginVertical: 20,
}}>
<Button onPress={() => submitTransport()}>Submit</Button>
</View>
</View>
</View>
);
}
return <LoaderScreen visible={baseLoading} />;
};
};
screen 2:
import {useNavigation, useNavigationParam} from 'react-navigation-hooks';
import {useSelector, useDispatch} from 'react-redux';
import {getHotels} from './actions';
import _ from 'lodash';
const TransportSelectHotelScreen = () => {
const {navigate} = useNavigation();
const dispatch = useDispatch();
const [baseLoading, setBaseLoading] = useState(true);
const {hotel} = useSelector(state => ({
hotel: state.transportHotelReducer,
}));
useEffect(() => {
setTimeout(() => {
setBaseLoading(false);
}, 1000);
loadHotels();
}, [loadHotels]);
const handleRefresh = () => {
console.log('refresh');
loadHotels();
};
const loadHotels = async () => {
dispatch(getHotels());
};
const redirectTransportCallback = hotel => {
console.log('hotel detail', hotel);
navigate('Transport', {hotel: hotel});
};
const renderItem = item => {
return (
<MenuCard
expandRight
onPress={() => redirectTransportCallback(item.item)}>
{item.item.name}
</MenuCard>
);
};
const renderContent = () => {
if (!baseLoading) {
if (!hotel.hotels.baseLoading) {
if (!_.isEmpty(hotel.hotels)) {
return (
<View style={globalStyles.menuContainer}>
<FlatList
data={hotel.hotels}
renderItem={renderItem}
keyExtractor={(item, index) => index.toString()}
refreshing={hotel.isRefreshing}
onRefresh={handleRefresh}
// onEndReached={handleLoadMore}
// onEndReachedThreshold={0.1}
/>
</View>
);
} else {
return (
<View style={globalStyles.wrapperContent}>
<Text>{Lang.no_data}</Text>
</View>
);
}
}
return <LoaderScreen visible={hotel.baseLoading} />;
}
return <LoaderScreen visible={baseLoading} />;
};
};
You could try
useEffect(() => {
setSelectedHotel(hotelParam ? hotelParam.id : '')
}, [hotelParam])