Error in Reanimated "tried to synchronously call function res from a different thread " - react-native

Have some gesture handlers that work fine in the browser but I am getting this error on iOS in my onEnd callback in the useAnimatedGestureHandler hook.
Here is all the code related to the gesture I am trying to add
const headerHeight = useSharedValue(176)
const outerStyle = useAnimatedStyle(() => ({
minHeight: 176,
maxHeight: 416,
height: headerHeight.value,
borderBottomLeftRadius: 20,
borderBottomRightRadius: 20,
position: 'relative',
overflow: 'visible',
zIndex: 502,
}))
const innerStyle = useAnimatedStyle(() => ({
overflow: 'hidden',
height: headerHeight.value,
minHeight: 176,
maxHeight: 416,
borderBottomLeftRadius: 20,
borderBottomRightRadius: 20,
}))
const resizeHeaderHeight = useAnimatedGestureHandler({
onStart: () => {},
onActive: (event) => {
headerHeight.value = event.absoluteY
},
onEnd: () => {
if(headerHeight.value < 305) {
headerHeight.value = withTiming(176, {
duration: 500,
})
setHeaderExpanded(false)
} else {
headerHeight.value = withTiming(416, {
duration: 500,
})
setHeaderExpanded(true)
}
},
})
return <>
<PanGestureHandler onGestureEvent={resizeHeaderHeight}>
<Animated.View style={outerStyle}>
<Animated.View style={innerStyle}>
<HeaderComponent
expandable={true}
hideContentCollapsed={false}
onClickExpand={() => {
// setHeaderExpanded(!headerExpanded)
}}
onClickTitle={openMonthPicker}
>{{
title: <Title />,
content: <HeaderCalendar />,
buttons: [
<RefreshButton key='refresh' />,
<AssignmentOffersButton key='assignment-offers' navigation={navigation} />,
<FiltersButton key='filters' navigation={navigation} />,
],
}}</HeaderComponent>
</Animated.View>
<ExpandButton isExpanded={headerExpanded} onClick={()=> {}} />
</Animated.View>
</PanGestureHandler>
{headerExpanded && <Overlay onClick={() => {
setHeaderExpanded(!headerExpanded)
}} />}
</>
}
export default observer(Header)
Have tried defining the onEnd as a 'worklet' and using the runOnJs function suggested to solve this but I am not sure I am doing it correctly since I still have the error every time the onEnd runs.

I'm sorry for the late response.
I think that, as you have already mentioned, the problem lies in not using runOnJS.
Basically onStart, onActive and onEnd are full-fledged worklets, i.e. javascript functions that will be executed on the UI Thread.
Consequently if you have functions that can only be executed on the javascript thread and need to be launched from a worklet it must always be specified with runOnJS.
To be more concrete, in the onEnd function you should wrap the setHeaderExpanded function like this:
runOnJS(setHeaderExpanded)(true) // the boolean value you want to use.

Related

Expo react native custom button text not working

This is how I load fonts:
const [IsReady, SetIsReady] = useState(false);
const LoadFonts = async () => {
await useFonts();
};
if (!IsReady) {
return (
<AppLoading
startAsync={LoadFonts}
onFinish={() => SetIsReady(true)}
onError={() => {}}
/>
);
}
It's working, but I have a button:
export const StartButton = ({
style = {},
textStyle = {},
size = 125,
...props
}) => {
return (
<TouchableOpacity
style={[styles(size).radius, style]}
onPress={props.onPress}
>
<Text style={[styles(size).text, textStyle]}>{props.title}</Text>
</TouchableOpacity>
);
};
const styles = (size) =>
StyleSheet.create({
radius: {
borderRadius: size / 2,
width: size,
height: size,
alignItems: "center",
justifyContent: "center",
borderColor: "#FFFFFF",
borderWidth: 1,
fontFamily: "nevrada",
backgroundColor: "#252250AA ",
},
text: { color: "#FFFFFF", fontSize: size / 5 },
});
App.js component
<StartButton size={100} title="START" fontFamily="Heavitas" />`
Here is my useFonts.js file:
import * as Font from "expo-font";
export default useFonts = async () =>
await Font.loadAsync({
Heavitas: require("./../assets/fonts/Heavitas.ttf"),
nevrada: require("./../assets/fonts/nevrada.ttf"),
gazebo: require("./../assets/fonts/gazebo.otf"),
});
The custom font doesn't work on the button. Everywhere else it is.
Various loading technics none did work for the button.
SOLVED
It needed to set fontFamily in component.
Thanks anyway.

【ReactNative】How to execute display: "none" before rendering a screen

I want to execute display: "none" before rendering a screen by few code changes.
Now {goodbotton} appears temporarily when the screen is renderd.
Currently my code is the below.
...
const [ goodbotton, setGoodBotton ] = useState(<Image source={require('../../images/good_botton_inactive.png')} style={ {width: 65, height: 65} }/>);
<TouchableOpacity
onPress={() => {
if (image.is_my_good) {
postGood(image.id, false)
} else {
setGoodBotton(<Image source={require('../../images/good_botton.png')} style={ {width: 65, height: 65}}/>)
postGood(image.id, true)
setTimeout(() => {
setGoodBotton(<Image source={require('../../images/good_botton_inactive.png')} style={ {width: 65, height: 65}}/>)
}, 500)
}
}}>
<View style={[image?.is_my_good ? styles.hide_good_botton : {}, commonStyles.shadow]} >
{goodbotton}
</View>
</TouchableOpacity>
const styles = StyleSheet.create({
hide_good_botton: {
display: "none"
}
});
Do you have any ideas?
The most common way I've seen in projects is to have a state variable that controls when the application is fetching or processing something. Something similar to:
[isLoading, setIsLoading] = useState(true);
// enter code here
if (isLoading) { return (<View / Spinner / LoadingIcon / PlaceHolderView />) }
return (
<TouchableOpacity
onPress={() => {
....

react native top tab bar navigator: indicator width to match text

I have three tabs in a top tab bar navigation with different width text. Is it possible to make the indicator width match the text? On a similar note, how can I make the tabs match the width of the text too without making it display weird. I've tried width auto but it doesn't stay center.
This is how it looks with auto width:
<Tab.Navigator
initialRouteName="Open"
tabBarOptions={{
style: {
backgroundColor: "white",
paddingTop: 20,
paddingHorizontal: 25
},
indicatorStyle: {
borderBottomColor: colorScheme.teal,
borderBottomWidth: 2,
width: '30%',
left:"9%"
},
tabStyle : {
justifyContent: "center",
width: tabBarWidth/3,
}
}}
>
<Tab.Screen
name="Open"
component={WriterRequestScreen}
initialParams={{ screen: 'Open' }}
options={{
tabBarLabel: ({focused}) => <Text style = {{fontSize: 18, fontWeight: 'bold', color: focused? colorScheme.teal : colorScheme.grey}}> Open </Text>,
}}
/>
<Tab.Screen
name="In Progress"
component={WriterRequestScreen}
initialParams={{ screen: 'InProgress' }}
options={{
tabBarLabel: ({focused}) => <Text style = {{fontSize: 18, fontWeight: 'bold', color: focused? colorScheme.teal : colorScheme.grey}}> In Progress </Text>}}
/>
<Tab.Screen
name="Completed"
component={WriterRequestScreen}
initialParams={{ screen: 'Completed' }}
options={{ tabBarLabel: ({focused}) => <Text style = {{fontSize: 18, fontWeight: 'bold', color: focused? colorScheme.teal : colorScheme.grey}}> Completed </Text>}}
/>
</Tab.Navigator>
I also needed to make the indicator fit the text size, a dynamic width for the labels, and a scrollable top bar because of long labels. The result looks like this:
tab bar with dynamic indicator width
If you don't care about the indicator width fitting the labels, you can simply use screenOptions.tabBarScrollEnabled: true in combination with width: "auto" in screenOptions.tabBarIndicatorStyle.
Otherwise, you'll need to make your own tab bar component and pass it to the tabBar property of your <Tab.Navigator>. I used a ScrollView but if you have only a few tabs with short labels, a View would be more simple. Here is the Typescript code for this custom TabBar component:
import { MaterialTopTabBarProps } from "#react-navigation/material-top-tabs";
import { useEffect, useRef, useState } from "react";
import {
Animated,
Dimensions,
View,
TouchableOpacity,
StyleSheet,
ScrollView,
I18nManager,
LayoutChangeEvent,
} from "react-native";
const screenWidth = Dimensions.get("window").width;
const DISTANCE_BETWEEN_TABS = 20;
const TabBar = ({
state,
descriptors,
navigation,
position,
}: MaterialTopTabBarProps): JSX.Element => {
const [widths, setWidths] = useState<(number | undefined)[]>([]);
const scrollViewRef = useRef<ScrollView>(null);
const transform = [];
const inputRange = state.routes.map((_, i) => i);
// keep a ref to easily scroll the tab bar to the focused label
const outputRangeRef = useRef<number[]>([]);
const getTranslateX = (
position: Animated.AnimatedInterpolation,
routes: never[],
widths: number[]
) => {
const outputRange = routes.reduce((acc, _, i: number) => {
if (i === 0) return [DISTANCE_BETWEEN_TABS / 2 + widths[0] / 2];
return [
...acc,
acc[i - 1] + widths[i - 1] / 2 + widths[i] / 2 + DISTANCE_BETWEEN_TABS,
];
}, [] as number[]);
outputRangeRef.current = outputRange;
const translateX = position.interpolate({
inputRange,
outputRange,
extrapolate: "clamp",
});
return Animated.multiply(translateX, I18nManager.isRTL ? -1 : 1);
};
// compute translateX and scaleX because we cannot animate width directly
if (
state.routes.length > 1 &&
widths.length === state.routes.length &&
!widths.includes(undefined)
) {
const translateX = getTranslateX(
position,
state.routes as never[],
widths as number[]
);
transform.push({
translateX,
});
const outputRange = inputRange.map((_, i) => widths[i]) as number[];
transform.push({
scaleX:
state.routes.length > 1
? position.interpolate({
inputRange,
outputRange,
extrapolate: "clamp",
})
: outputRange[0],
});
}
// scrolls to the active tab label when a new tab is focused
useEffect(() => {
if (
state.routes.length > 1 &&
widths.length === state.routes.length &&
!widths.includes(undefined)
) {
if (state.index === 0) {
scrollViewRef.current?.scrollTo({
x: 0,
});
} else {
// keep the focused label at the center of the screen
scrollViewRef.current?.scrollTo({
x: (outputRangeRef.current[state.index] as number) - screenWidth / 2,
});
}
}
}, [state.index, state.routes.length, widths]);
// get the label widths on mount
const onLayout = (event: LayoutChangeEvent, index: number) => {
const { width } = event.nativeEvent.layout;
const newWidths = [...widths];
newWidths[index] = width - DISTANCE_BETWEEN_TABS;
setWidths(newWidths);
};
// basic labels as suggested by react navigation
const labels = state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const label = route.name;
const isFocused = state.index === index;
const onPress = () => {
const event = navigation.emit({
type: "tabPress",
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
// The `merge: true` option makes sure that the params inside the tab screen are preserved
// eslint-disable-next-line
// #ts-ignore
navigation.navigate({ name: route.name, merge: true });
}
};
const inputRange = state.routes.map((_, i) => i);
const opacity = position.interpolate({
inputRange,
outputRange: inputRange.map((i) => (i === index ? 1 : 0.5)),
});
return (
<TouchableOpacity
key={route.key}
accessibilityRole="button"
accessibilityState={isFocused ? { selected: true } : {}}
accessibilityLabel={options.tabBarAccessibilityLabel}
onPress={onPress}
style={styles.button}
>
<View
onLayout={(event) => onLayout(event, index)}
style={styles.buttonContainer}
>
<Animated.Text style={[styles.text, { opacity }]}>
{label}
</Animated.Text>
</View>
</TouchableOpacity>
);
});
return (
<View style={styles.contentContainer}>
<Animated.ScrollView
horizontal
ref={scrollViewRef}
showsHorizontalScrollIndicator={false}
style={styles.container}
>
{labels}
<Animated.View style={[styles.indicator, { transform }]} />
</Animated.ScrollView>
</View>
);
};
const styles = StyleSheet.create({
button: {
alignItems: "center",
justifyContent: "center",
},
buttonContainer: {
paddingHorizontal: DISTANCE_BETWEEN_TABS / 2,
},
container: {
backgroundColor: "black",
flexDirection: "row",
height: 34,
},
contentContainer: {
height: 34,
marginTop: 30,
},
indicator: {
backgroundColor: "white",
bottom: 0,
height: 3,
left: 0,
position: "absolute",
right: 0,
// this must be 1 for the scaleX animation to work properly
width: 1,
},
text: {
color: "white",
fontSize: 14,
textAlign: "center",
},
});
export default TabBar;
I managed to make it work with a mix of:
react navigation example
react-native-tab-view original indicator
jsindos answer
Please let me know if you find a more convenient solution.
You have to add width:auto to tabStyle to make tab width flexible.
Then inside each tabBarLabel <Text> component add style textAlign: "center" and width: YOUR_WIDTH .
YOUR_WIDTH can be different for each tab and can be your text.length * 10 (if you want to make it depended on your text length) or get screen width from Dimensions and divide it by any other number to make it equal widths in screen. Example:
const win = Dimensions.get('window');
...
bigTab: {
fontFamily: "Mulish-Bold",
fontSize: 11,
color: "#fff",
textAlign: "center",
width: win.width/2 - 40
},
smallTab: {
fontFamily: "Mulish-Bold",
fontSize: 11,
color: "#fff",
textAlign: "center",
width: win.width / 5 + 10
}
Remove width from indicatorStyle and use flex:1
indicatorStyle: { borderBottomColor: colorScheme.teal,
borderBottomWidth: 2,
flex:1,
left:"9%"
},
I've achieved this using some hacks around onLayout, please note I've made this with the assumptions of two tabs, and that the second tabs width is greater than the first. It probably will need tweaking for other use cases.
import React, { useEffect, useState } from 'react'
import { createMaterialTopTabNavigator } from '#react-navigation/material-top-tabs'
import { Animated, Text, TouchableOpacity, View } from 'react-native'
const Stack = createMaterialTopTabNavigator()
const DISTANCE_BETWEEN_TABS = 25
function MyTabBar ({ state, descriptors, navigation, position }) {
const [widths, setWidths] = useState([])
const [transform, setTransform] = useState([])
const inputRange = state.routes.map((_, i) => i)
useEffect(() => {
if (widths.length === 2) {
const [startingWidth, transitionWidth] = widths
const translateX = position.interpolate({
inputRange,
outputRange: [0, startingWidth + DISTANCE_BETWEEN_TABS + (transitionWidth - startingWidth) / 2]
})
const scaleX = position.interpolate({
inputRange,
outputRange: [1, transitionWidth / startingWidth]
})
setTransform([{ translateX }, { scaleX }])
}
}, [widths])
return (
<View style={{ flexDirection: 'row' }}>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key]
const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name
const isFocused = state.index === index
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true
})
if (!isFocused && !event.defaultPrevented) {
// The `merge: true` option makes sure that the params inside the tab screen are preserved
navigation.navigate({ name: route.name, merge: true })
}
}
const onLayout = event => {
const { width } = event.nativeEvent.layout
setWidths([...widths, width])
}
const opacity = position.interpolate({
inputRange,
outputRange: inputRange.map(i => (i === index ? 0.87 : 0.53))
})
return (
<TouchableOpacity
key={index}
accessibilityRole='button'
accessibilityState={isFocused ? { selected: true } : {}}
accessibilityLabel={options.tabBarAccessibilityLabel}
testID={options.tabBarTestID}
onPress={onPress}
style={{ marginRight: DISTANCE_BETWEEN_TABS }}
>
<Animated.Text
onLayout={onLayout}
style={{
opacity,
color: '#000',
fontSize: 18,
fontFamily: 'OpenSans-Bold',
marginBottom: 15
}}
>
{label}
</Animated.Text>
</TouchableOpacity>
)
})}
<View style={{ backgroundColor: '#DDD', height: 2, position: 'absolute', bottom: 0, left: 0, right: 0 }} />
<Animated.View style={{ position: 'absolute', bottom: 0, left: 0, width: widths.length ? widths[0] : 0, backgroundColor: '#222', height: 2, transform }} />
</View>
)
}
export default () => {
return (
<>
<Stack.Navigator tabBar={props => <MyTabBar {...props} />} style={{ paddingHorizontal: 25 }}>
<Stack.Screen name='Orders' component={() => <Text>A</Text>} />
<Stack.Screen name='Reviews' component={() => <Text>B</Text>} />
</Stack.Navigator>
</>
)
}
Update:
If the menu names are static, it is probably a more robust solution to hard code the widths inside of widths, although this is a little more costly to maintain.
Resources:
https://reactnavigation.org/docs/material-top-tab-navigator/#tabbar
https://github.com/facebook/react-native/issues/13107
In the screenOptions, add following props for
tabBarScrollEnabled: true
tabBarItemStyle: {{width: "auto", minWidht: "100"}}
minWidth is just to keep the design consistent.
Please note, I am using react-navigation 6.x and Camille Hg answer was really helpful.
I had the same issue and I was finally able to make the indicator takes exactly the text size.
I don't know in which version this was possible .. but apparently you can add a custom indicator component (beside the ability to add a custom tabBar component)
when creating the TopTabNavigator it is important to add the properties as described in the code under
// assuming that you want to add paddingHorizontal: 10 for each item!
const TAB_BAR_ITEM_PADDING = 10;
const Tab = createMaterialTopTabNavigator();
function TopTabNavigator() {
return (
<Tab.Navigator
.....
... .
screenOptions={{
....
...
tabBarItemStyle: {
// these properties are important for this method to work !!
width: "auto",
marginHorizontal: 0, // this is to make sure that the spacing of the item comes only from the paddingHorizontal!.
paddingHorizontal: TAB_BAR_ITEM_PADDING, // the desired padding for the item .. stored in a constant to be passed in the custom Indicator
},
tabBarIndicator: props => {
return (
<CustomTabBarIndicator
// the default props
getTabWidth={props.getTabWidth}
jumpTo={props.jumpTo}
layout={props.layout}
navigationState={props.state}
position={props.position}
width={props.width}
style={{
left: TAB_BAR_ITEM_PADDING,
backgroundColor: Colors.primary,
}}
// this is an additional property we will need to make the indicator exactly
tabBarItemPadding={TAB_BAR_ITEM_PADDING}
/>
);
},
}}
>
<Tab.Screen .... />
<Tab.Screen ..... />
<Tab.Screen .... />
</Tab.Navigator>
);
}
now for the CustomTabBarIndIndicator component we simply go to the official github repository for react-native-tab-view and then go to TabBarIndicator.tsx and copy the component over in a file called CustomTabBarIndicator "just to be consistence with the example, but you can call it what ever you want", and don't forget to add the additional property to the Props type for tabBarItemPadding "if you are using typescript"
and now make this small change to the line that is highlighted in the image
change:
const outputRange = inputRange.map(getTabWidth);
to be:
const outputRange = inputRange.map(x => {
// this part is customized to get the indicator to be the same width like the label
// subtract the tabBarItemPadding from the tabWidth
// so that you indicator will be exactly the same size like the label text
return getTabWidth(x) - this.props.tabBarItemPadding * 2;
});
and that was it :)
P.S. I added the image because I didn't know how to exactly describe where to make the change
and if you don't want the typescript .. jsut remove all the types from the code and you are good to go :)

Why is my message not getting displayed in React Native FlatList?

Im making a dating app in react native and im using FlatList to display messages. I implemented inverted and onEndReached props and messages are getting fetched when scrolling up no problem. but when sending a message, (im using socket.io-client with my nodejs server), the new message does not get displayed. Here is my code:
sendMessage func
const sendMessage = async () => {
if (!currentConversation) return;
if (isUserBlocked(getRecipient(currentConversation))) return;
if (isRecipientBlocked(getRecipient(currentConversation)._id)) return;
if (!newMessage.trim()) return;
if (user && currentConversation) {
const v4 = uuid.v4();
const message = {
text: newMessage,
user,
uuid: v4,
};
socket.emit("message", {
message: { ...message, conversation: currentConversation },
});
socket.emit("conversation", {
userId: getRecipient(currentConversation)._id,
message: { ...message, conversation: currentConversation },
});
setNewMessage("");
await (await HttpClient()).post(config.SERVER_URL + "/api/message/send", {
...message,
conversationId: currentConversation._id,
});
}
};
useEffect to get new Message
the setMessages() func doesnt work
useEffect(() => {
const handler = (message) => {
const conversationIndex = conversations.findIndex(
(c) => c._id === message.conversation._id
);
if (conversationIndex >= 0) {
const _conversations = [...conversations];
message.conversation.latestMessage = message.text;
_conversations.splice(conversationIndex, 1);
_conversations.unshift(message.conversation);
setConversations(_conversations);
setDefaultConversations(_conversations);
}
if (message.conversation._id !== currentConversation._id) return;
console.log(message.text); //works
setMessages([...messages, { ...message }]); //doesnt work
};
socket.on("message", handler);
return () => {
socket.off("message", handler);
};
}, [messages]);
flatlist
<FlatList
onEndReached={() =>
getMessages(currentConversation, messages.length)
}
onEndReachedThreshold={0.8}
inverted
data={messages}
renderItem={(message) => (
<View
style={{
flexDirection: "row",
padding: 5,
justifyContent: isOwnMessage(message.item)
? "flex-end"
: "flex-start",
}}
>
<View
style={{
padding: 10,
backgroundColor: "#2196F3",
marginBottom: 5,
borderRadius: 5,
maxWidth: "50%",
}}
>
<Text style={{ fontWeight: "bold" }}>
{message.item.user.displayName}
</Text>
<Text>{message.item.text}</Text>
<Text style={{ fontSize: 10 }}>
{moment(message.item.createdAt).fromNow()}
</Text>
</View>
</View>
)}
keyExtractor={(item) => item.uuid}
/>
I solved this issue. The new message WAS being added, but to the top, due to the inverted property. So I just changed the setMessages to first include the message, and then the rest of the messages.

How to re-use the useEffect hook properly

I'm now switching from a class based component to a function with hooks, and I need some time getting used to it.
I want the user to be able to upload a main profile picture and 6 secondary images, the images will be uploaded to a server. I have created the logic that handles the main profile picture upload, but now I want to re-use that logic to also upload the other pictures to the server with the same piece of code.
My logic now is:
Get userData and main profile image from server.
When clicked on the edit button it will open a image picker.
When the picture has been chosen it changes the state called imageChanged to true and then the useEffect activates and executes the function called _uploadImage(); Then is check if the image has been set or if its just a false flag.
The image will be uploaded to the server by using PHP.
Here is my profile screen:
const [selectedImage, setSelectedImage] = useState(null);
const [imageChanged, setimageChanged] = useState(null);
useEffect(() => {
function _uploadImage() {
if (imageChanged !== null) {
var data = new FormData();
data.append('action', 'uploadProfileImage');
data.append('profileID', userInfo.ProfileId);
data.append('file', {
uri: selectedImage.uri,
type: 'image/jpg',
name: 'tempimage.jpg'
});
fetch('', {
method: 'POST',
body: data,
headers: {
Accept: 'application/json'
}
})
.then(response => response.json())
.then(responseJson => {
if (responseJson[0].Result == 1) {
console.log(responseJson)
} else {
//alert('Niet gelukt, probeer het later opnieuw')
}
setimageChanged(false);
});
}
};
_uploadImage();
}, [selectedImage])
async function _pickImage() {
let result = await ImagePicker.launchImageLibraryAsync({
allowsEditing: true,
aspect: [4, 3],
mediaTypes: 'Images',
quality: 0.7
});
if (!result.cancelled) {
if (result.type === 'image') {
setSelectedImage(result);
setimageChanged(true);
} else {
alert('Upload een geldige image');
console.log('No image found');
}
}
};
And here is the render:
<Card
containerStyle={{
padding: 0,
marginTop: 30,
marginRight: 0,
marginLeft: 0,
alignItems: 'center'
}}
>
<Avatar
source={{ uri: '?time=' + Date.now() }}
size="xlarge"
rounded
showEditButton={true}
editButton={{ size: 30 }}
onEditPress={() => {
_pickImage();
}}
title={'n'}
/>
<Text>
{userInfo.FirstName + ' ' + userInfo.LastName}
</Text>
<Text>
{userInfo.BirthDate + ' Jaar, ' + userInfo.City}
</Text>
</Card>
<Card
containerStyle={{
padding: 0,
marginTop: 30,
marginRight: 0,
marginLeft: 0,
alignItems: 'center'
}}
>
<FlatList style={{ margin: 5 }}
data={[0, 1, 2, 3, 4, 5]}
numColumns={3}
keyExtractor={(item, index) => item.id}
renderItem={(item) => <TouchableOpacity onPress={() => { _pickImage() }}><View key={item.id} style={{ width: 100, margin: 3.5, borderRadius: 10, justifyContent: 'center', backgroundColor: '#ddd', height: 100 }}><Text style={{ textAlign: 'center', fontSize: 20 }}>+</Text></View></TouchableOpacity>}
/>
</Card>
So to summarize I want to be able to re-use the pickimage and upload image code to upload the other photos, but I want to avoid declaring 6 states at the top and constantly checking if one of those change, and the problem then is that i don't know the index of the picked image.