Async await is not working. I need to wait for my image to load, move to a state, and then move on. Is not working.
export default () => {
const [imgTmp, setImgTmp] = useState(null);
const chooseImage = async () => {
await ImagePicker.showImagePicker('', r => {
if (r.uri) {
let img = {uri: r.uri};
setImgTmp(img);
}
});
alert('...');
};
return (
<View>
<View style={{height: 40, backgroundColor: '#DDDD'}}>
<Image source={imgTmp} style={{width: 100, height: 100}} />
</View>
<Button title="Select Imagem" onPress={chooseImage} />
{imgTmp !== null ? (
<DynamicCollage width={400} height={400} images={imgTmp} matrix={[1]} />
) : null}
</View>
);
};
ImagePicker.showImagePicker doesn't return a Promise, so awaiting it won't work as expected.
What you can do is to wrap it with a Promise:
const chooseImage = async () => {
try {
const img = await new Promise((resolve, reject) => {
ImagePicker.showImagePicker('', r => {
if (r.uri) {
const img = {uri: r.uri};
resolve(img);
}
reject("uri undefined");
});
});
setImgTmp(img);
alert('...');
} catch(e) {
// handle exception
console.log(e);
}
};
Related
I have a form object that holds all my input values. I am working with the react-native camera component and what I am trying to accomplish is every time a picture is added, it is added to the object. With my current code, I am getting invalid attempts to spread non-iterable instances.
import { TicketContext } from "../store/TicketContext";
function Pictures() {
const [hasCameraPermissions, setHasCameraPermissions] = useState();
const [picture, setPicture] = useState();
const { form, setForm } = useContext(TicketContext);
//Add picture to form //
const handleAddPicture = () => {
setForm([...(form ?? []), { picture: picture.uri }]);
setPicture();
};
const cameraRef = useRef();
//Get Permission to use Camera//
const handleCameraPermissions = async () => {
const cameraPermissions = await Camera.requestCameraPermissionsAsync();
setHasCameraPermissions(cameraPermissions.status === "granted");
};
// Check for permissions on load //
useEffect(() => {
handleCameraPermissions();
}, []);
if (hasCameraPermissions === undefined) {
return <Text>Permissions Required...</Text>;
} else if (!hasCameraPermissions) {
return <Text>Camera Permission Denied. Please change in settings.</Text>;
}
//Take Picture //
const handleTakePicture = async () => {
const options = { base64: true, exif: false, quality: 1 };
const newPicture = await cameraRef.current.takePictureAsync(options);
setPicture(newPicture);
};
if (picture) {
return (
<SafeAreaView style={styles.container}>
<Image
style={styles.preview}
source={{ uri: "data:image/jpg;base64," + picture.base64 }}
/>
<View style={styles.buttonContainer}>
<Button title="Add Picture" onPress={handleAddPicture} />
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container}>
<Camera style={styles.cameraContainer} ref={cameraRef}>
<View style={styles.buttonContainer}>
<Button title="Take Picture" onPress={handleTakePicture} />
</View>
</Camera>
</SafeAreaView>
);
}
export default Pictures;
setPicture returns the state back to undefined
my state in context
const [form, setForm] = useState({});
//[form ,setForm]
// make sure you object keys are camel case.
const handleAddPicture = () => {
setForm([...(form ?? []), { picture: picture.uri }]);
setPicture();
};
So i have a react navigation setup like this;
const WorldStackNavigator = createStackNavigator();
export const WorldNavigator = () => {
return (
<WorldStackNavigator.Navigator screenOptions={defaultNavOptions}>
<WorldStackNavigator.Screen
name='WorldCaseScreen'
component={WorldCaseScreen}
options={WorldCaseScreenOptions}
/>
</WorldStackNavigator.Navigator>
)
}
const FavouritesStackNavigator = createStackNavigator();
const FavouritesNavigator = () => {
return (
<FavouritesStackNavigator.Navigator screenOptions={defaultNavOptions} >
<FavouritesStackNavigator.Screen
name='FavouritesScreen'
component={FavouritesScreen}
options={FavouritesScreenOptions}
/>
<CountryStackNavigator.Screen
name='CountryDetailScreen'
component={CountryDetailScreen}
options={CountryDetailScreenOptions}
/>
</FavouritesStackNavigator.Navigator>
)
}
const CountryStackNavigator = createStackNavigator();
export const CountryNavigator = () => {
return (
<CountryStackNavigator.Navigator screenOptions={defaultNavOptions}>
<CountryStackNavigator.Screen
name='SearchbyContinentScreen'
component={SearchbyContinentScreen}
options={SearchbyContinentScreenOptions}
/>
<CountryStackNavigator.Screen
name='SearchIndividualCountryScreen'
component={SearchIndividualCountryScreen}
options={SearchIndividualScreenOptions}
/>
<CountryStackNavigator.Screen
name='CountryDetailScreen'
component={CountryDetailScreen}
options={CountryDetailScreenOptions}
/>
</CountryStackNavigator.Navigator>
)
}
And i group them all up in a tab navigator.
I'm using the 'CountryDetailScreen' in two different navigators.
So my problem is whenever 'CountryDetailScreen' is open in 'CountryNavigator' and i try to open 'CountryDetailScreen' in 'FavouritesNavigator' it navigates me back to 'CountryNavigator's 'CountryDetailScreen'. Then if i click on 'FavouritesNavigator' 'CountryDetailScreen' it comes with correct data.
Here is my CountryDetail code;
import { StyleSheet, Text, View, Image, ScrollView, Button, ActivityIndicator } from 'react-native'
import React, { useCallback, useEffect, useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { getIndividualCountryData, fetchFavourites, addToFav, removeFromFav } from '../store/actions/countryActions'
import Colors from '../constants/Colors';
import CaseInfoComp from '../components/CaseInfoComp';
import CustomHeaderButton from '../UI/CustomHeaderButton';
import { HeaderButtons, Item } from 'react-navigation-header-buttons';
const CountryDetailScreen = (props) => {
const data = props.route.params.data;
const dispatch = useDispatch();
const countriesReducer = useSelector(state => state.countryReducer)
const [error, setError] = useState(false);
const [isFav, setIsFav] = useState(false);
const [isFavLoading, setisFavLoading] = useState(true);
const [isInitial, setIsInitial] = useState(props.route.params.isInit);
const { singleCountry, favouriteCountries, loading } = countriesReducer;
let favHandlerDispatcher;
const loadIndividualCountryData = useCallback(async () => {
setError(false);
try {
await dispatch(getIndividualCountryData(data.Country, data.ThreeLetterSymbol));
} catch (error) {
setError(true)
}
}, [dispatch, setError])
const loadFavouriteCountries = useCallback(async () => {
setError(false);
try {
await dispatch(fetchFavourites());
} catch (error) {
setError(true)
}
}, [dispatch, setError])
useEffect(() => {
loadIndividualCountryData();
}, [loadIndividualCountryData])
useEffect(() => {
loadFavouriteCountries();
}, [])
useEffect(() => {
const favData = favouriteCountries.some((element) => element.countryCode === data.ThreeLetterSymbol)
setIsFav(favData)
favData ? favHandlerDispatcher = removeFromFav : favHandlerDispatcher = addToFav;
const favHandler = async (countryName, countryCode) => {
try {
setisFavLoading(true);
await dispatch(favHandlerDispatcher(countryName, countryCode));
await loadFavouriteCountries();
setisFavLoading(false);
} catch (error) {
throw error;
}
}
props.navigation.navigate('CountryDetailScreen', { data: data, favHandler: favHandler, isFav: favData, isFavLoading: isFavLoading })
}, [favouriteCountries])
if (loading || singleCountry.length === 0) {
return (
<View style={[styles.container, styles.horizontal]}>
<ActivityIndicator size="large" color={Colors.primary} />
</View>
)
} else if (error) {
return (
<View style={styles.container} >
<Text>Bir Hata Oluştu</Text>
<Button title='Tekrar deneyin' onPress={() => loadIndividualCountryData()} color={Colors.primary} />
</View>
)
} else {
return (
<ScrollView>
<View style={styles.imageContainer}>
<Image style={styles.image} source={{ uri: `https://countryflagsapi.com/png/${data.ThreeLetterSymbol}` }} />
</View>
{singleCountry.map((data) => {
return <CaseInfoComp header={data.header} content={data.content} color={data.color} key={data.header} />
})}
</ScrollView>
)
}
}
export default CountryDetailScreen
export const screenOptions = (navData) => {
const favHandler = navData.route.params.favHandler
const isFav = navData.route.params.isFav
const favLoading = navData.route.params.favLoading
return {
headerTitle: navData.route.params.data.Country,
headerRight: () => {
if (!favLoading) {
return (
<HeaderButtons HeaderButtonComponent={CustomHeaderButton} >
<Item title='Favourite' iconName={isFav ? 'star' : 'star-outline'} onPress={() => favHandler(navData.route.params.data.Country, navData.route.params.data.ThreeLetterSymbol)} />
</HeaderButtons>
)
}
}
}
}
const styles = StyleSheet.create({
imageContainer: {
flex: 1,
alignItems: 'center',
// borderBottomColor: Colors.primary,
// borderBottomWidth: 5
},
image: {
width: '100%',
height: 200,
borderBottomRightRadius: 30,
borderBottomLeftRadius: 30,
},
container: {
flex: 1,
justifyContent: "center",
alignItems: 'center'
}
})
How do i prevent it from going back to previous navigators screen.
I'm new to react native and I'm trying to upload multiple images using expo-image-picker-multiple but I'm having an error with the code so I'm trying to convert the class component to functional components so I can easily deal with it since I'm more familiar with functional components.
export default class ImageBrowse extends Component {
_getHeaderLoader = () => (
<ActivityIndicator size='small' color={'#0580FF'} />
);
imagesCallback = (callback) => {
const { navigation } = this.props;
this.props.navigation.setOptions({
headerRight: () => this._getHeaderLoader()
});
callback.then(async (photos) => {
const cPhotos = [];
for (let photo of photos) {
const pPhoto = await this._processImageAsync(photo.uri);
cPhotos.push({
uri: pPhoto.uri,
name: photo.filename,
type: 'image/jpg'
})
}
navigation.navigate('Main', { photos: cPhotos });
})
.catch((e) => console.log(e));
};
async _processImageAsync(uri) {
const file = await ImageManipulator.manipulateAsync(
uri,
[{ resize: { width: 1000 } }],
{ compress: 0.8, format: ImageManipulator.SaveFormat.JPEG }
);
return file;
};
_renderDoneButton = (count, onSubmit) => {
if (!count) return null;
return <TouchableOpacity title={'Done'} onPress={onSubmit}>
<Text onPress={onSubmit}>Done</Text>
</TouchableOpacity>
}
updateHandler = (count, onSubmit) => {
this.props.navigation.setOptions({
title: `Selected ${count} files`,
headerRight: () => this._renderDoneButton(count, onSubmit)
});
};
renderSelectedComponent = (number) => (
<View style={styles.countBadge}>
<Text style={styles.countBadgeText}>{number}</Text>
</View>
);
render() {
const emptyStayComponent = <Text style={styles.emptyStay}>Empty =(</Text>;
return (
<View style={[styles.flex, styles.container]}>
<ImageBrowser
max={4}
onChange={this.updateHandler}
callback={this.imagesCallback}
renderSelectedComponent={this.renderSelectedComponent}
emptyStayComponent={emptyStayComponent}
/>
</View>
);
}
}
Also this is related to the above code:
export default class MainScreen extends Component {
constructor(props) {
super(props)
this.state = {
photos: []
}
}
componentDidUpdate() {
const { params } = this.props.route;
if (params) {
const { photos } = params;
if (photos) this.setState({ photos });
delete params.photos;
}
}
renderImage(item, i) {
return (
<Image
style={{ height: 100, width: 100 }}
source={{ uri: item.uri }}
key={i}
/>
)
}
render() {
const { navigate } = this.props.navigation;
return (
<View style={{ flex: 1 }}>
<Button
title="Open image browser"
onPress={() => { navigate('ImageBrowser'); }}
/>
<ScrollView>
{this.state.photos.map((item, i) => this.renderImage(item, i))}
</ScrollView>
</View>
);
}
}
The error is when I browse the gallery to select images nothing is showing:
And this warning showing to me:
Also this is what i came up with when I tried to convert class to functional components:
const PropertiesInfo = ({ navigation }) => {
const [attachments, setAttachments] = useState([]);
const componentDidUpdate=()=> {
const { params } = attachments.route;
if (params) {
const { attachments } = params
if (attachments) setAttachments({ attachments })
delete params.attachments
}
};
const renderImage=(item, i) =>{
return (
<Image
style={{ height: 100, width: 100 }}
source={{ uri: item.uri }}
key={i}
/>
)
};
return (
<View style={{ flex: 1 }}>
<Button
title="Open image browser"
onPress={() => { navigate('ImageBrowser'); }}
/>
<ScrollView>
{attachments.map((item, i) => renderImage(item, i))}
</ScrollView>
</View>
);
};
export default PropertiesInfo;
And this also:
const ImageBrowse=({ navigation })=> {
_getHeaderLoader = () => (
<ActivityIndicator size='small' color={'#0580FF'} />
);
imagesCallback = (callback) => {
navigation.setOptions({
headerRight: () => _getHeaderLoader()
});
callback.then(async (attachments) => {
const cPhotos = [];
for (let photo of attachments) {
const pPhoto = await _processImageAsync(photo.uri);
cPhotos.push({
uri: pPhoto.uri,
name: photo.filename,
type: 'image/jpg'
})
}
navigation.navigate('Main', { attachments: cPhotos });
})
.catch((e) => console.log(e));
};
const _processImageAsync = async (uri) => {
const file = await ImageManipulator.manipulateAsync(
uri,
[{ resize: { width: 1000 } }],
{ compress: 0.8, format: ImageManipulator.SaveFormat.JPEG }
);
return file;
};
_renderDoneButton = (count, onSubmit) => {
if (!count) return null;
return <TouchableOpacity title={'Done'} onPress={onSubmit}>
<Text onPress={onSubmit}>Done</Text>
</TouchableOpacity>
}
updateHandler = (count, onSubmit) => {
navigation.setOptions({
title: `Selected ${count} files`,
headerRight: () => _renderDoneButton(count, onSubmit)
});
};
renderSelectedComponent = (number) => (
<View style={styles.countBadge}>
<Text style={styles.countBadgeText}>{number}</Text>
</View>
);
const emptyStayComponent = <Text style={styles.emptyStay}>Empty =(</Text>;
return (
<View style={[styles.flex, styles.container]}>
<ImageBrowser
max={4}
onChange={updateHandler}
callback={imagesCallback}
renderSelectedComponent={renderSelectedComponent}
emptyStayComponent={emptyStayComponent}
/>
</View>
);
};
export default ImageBrowse;
I'm refactoring to React Hooks but I can't get Infinite Scroll with FlatList working.
const [page, setPage] = useState(1);
This is my useEffect Hook:
useEffect(() => {
const loadProducts = async () => {
setIsLoading(true);
let response = await fetch(`${api}&page=${page}&perPage=5`);
let results = await response.json();
setProducts([...products, ...results.data]);
setIsLoading(false);
};
loadProducts();
}, [page]);
Offset is ${page}, limit is &perPage=5 (hardcoded to 5)
Flatlist:
<FlatList
data={products}
keyExtractor={(item) => item.id}
renderItem={renderGridItem}
onEndReached={loadMore}
onEndThreshold={0.3}
/>;
LoadMore:
const loadMore = () => {
setPage(page + 1);
};
In theory, this should work, shouldn't it?
Description
I was struggling a lot with this myself. Here's an example using a SectionList (basically the same as a Flatlist)
The header numbers indicates the request number send to the API. You can check that the request are in the correct order and that there are no duplicates, by clicking the "Check Numbers" button.
In this example we use reqres.in to simulate a fetch to some data.
The example also implements pull-to-refresh. Again, you can check that the length of the array is as expected after a pull-to-refresh by clicking the "Check length" button.
Expo snack
A snack of the example can be found here: https://snack.expo.io/BydyF9yRH
Make sure to change platform to iOS or Android in the snack (Web will not work)
Code
import * as React from 'react';
import { ActivityIndicator } from 'react-native'
var _ = require('lodash')
import {
StyleSheet,
Text,
View,
SafeAreaView,
SectionList,
Button,
RefreshControl
} from 'react-native';
function Item(item) {
return (
<View style={styles.item}>
<Text style={styles.title}>{item.title.first_name}</Text>
</View>
);
}
export default function testSectionList({ navigation }) {
const [data, setData] = React.useState()
const [loading, setLoading] = React.useState(true)
const [refreshing, setRefreshing] = React.useState(false);
const [showRefreshingIndicator, setShowRefreshingIndicator] = React.useState(false);
const dataIndex = React.useRef(0);
const totalHits = React.useRef(42); // In real example: Update this with first result from api
const fetchData = async (reset: boolean) => {
if (reset === true) dataIndex.current = 0;
// Make sure to return if no more data from API
if (dataIndex.current !== 0 && dataIndex.current >= totalHits.current) return []
// For example usage, select a random page
const fakepage = Math.round(Math.random()) * 2
const resultObject = await fetch(`https://reqres.in/api/users?page=${fakepage}`);
const result = await resultObject.json()
dataIndex.current++;
return {
title: `${dataIndex.current-1}`,
data: await result.data
}
}
const count = () => {
alert(data.length)
}
const checkPageNumbers = () => {
const numbers = data.map(item => parseInt(item.title))
const incremental = [...Array(data.length).keys()]
alert(_.isEqual(numbers, incremental))
}
const getInitialData = async () => {
const list = await fetchData(false)
if(!list) return
setData([list])
setLoading(false)
}
React.useEffect(() => {
getInitialData()
}, [])
const onEndReached = async () => {
const newItems = await fetchData(false)
if(!newItems.data.length) return
setData([...data, newItems])
}
const onRefresh = React.useCallback(async () => {
setShowRefreshingIndicator(true);
const newItems = await fetchData(true)
setData([newItems])
setShowRefreshingIndicator(false)
}, [refreshing]);
if (loading) return <Text>Loading...</Text>
return (
<SafeAreaView style={styles.container}>
<Button title={"Check numbers"} onPress={() => checkPageNumbers()} />
<Button title={"Check length"} onPress={() => count()} />
<SectionList
sections={data}
refreshing={refreshing}
refreshControl={
<RefreshControl refreshing={showRefreshingIndicator} onRefresh={onRefresh} />
}
onEndReached={() => {
if(refreshing) return;
setRefreshing(true)
onEndReached().then(() => {
setRefreshing(false)
})
}}
onEndReachedThreshold={1}
keyExtractor={(item, index) => item + index}
renderItem={({ item }) => <Item title={item} />}
renderSectionHeader={({ section: { title } }) => (
<Text style={styles.header}>{title}</Text>
)}
ListFooterComponent={<ActivityIndicator size={"large"} />}
stickySectionHeadersEnabled={false}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 40,
marginHorizontal: 16,
},
item: {
backgroundColor: '#f9c2ff',
padding: 2,
marginVertical: 2,
},
header: {
fontSize: 16,
},
title: {
fontSize: 12,
},
});
Try to use useCallback instead of useEffect on this case. Also, I've shown you how you can prevent spreading null result to setState.
const loadProducts = async () => {
setIsLoading(true);
let response = await fetch(`${api}&page=${page}&perPage=5`);
let results = await response.json();
if (result.data) {
setProducts([...products, ...results.data]);
}
setIsLoading(false);
};
useEffect(() => {
loadProducts();
}, [])
const onLoadMore = useCallback(() => {
loadProducts();
}
for more information about useCallback, please read this. https://reactjs.org/docs/hooks-reference.html#usecallback
I have a method,
const handleUpvote = (post, index) => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
console.log('mappedPosts', mappedPosts); // null
console.log('newPosts', newPosts); // null
if (post.userAction === "like") {
newPosts.userAction = null;
} else {
newPosts.userAction = "like";
}
setMappedPosts(newPosts);
upvote(user.id, post._id);
};
That is attached to a mapped element,
const mapped = userPosts.map((post, index) => (
<ListItem
rightIcon = {
onPress = {
() => handleUpvote(post, index)
}
......
And I have
const [mappedPosts, setMappedPosts] = useState(null);
When the component mounts, it takes userPosts from the redux state, maps them out to a ListItem and appropriately displays it. The problem is that whenever handleUpvote() is entered, it sees mappedPosts as null and therefore sets the whole List to null at setMappedPosts(newPosts);
What am I doing wrong here? mappedPosts is indeed not null at the point when handleUpvote() is clicked because.. well how can it be, if a mappedPosts element was what invoked the handleUpvote() method in the first place?
I tried something like
setMappedPosts({
...mappedPosts,
mappedPosts[index]: post
});
But that doesn't even compile. Not sure where to go from here
Edit
Whole component:
const Profile = ({
navigation,
posts: { userPosts, loading },
auth: { user, isAuthenticated },
fetchMedia,
checkAuth,
upvote,
downvote
}) => {
const { navigate, replace, popToTop } = navigation;
const [mappedPosts, setMappedPosts] = useState(null);
useEffect(() => {
if (userPosts) {
userPosts.forEach((post, index) => {
post.userAction = null;
post.likes.forEach(like => {
if (like._id.toString() === user.id) {
post.userAction = "liked";
}
});
post.dislikes.forEach(dislike => {
if (dislike._id.toString() === user.id) {
post.userAction = "disliked";
}
});
});
const mapped = userPosts.map((post, index) => (
<ListItem
Component={TouchableScale}
friction={100}
tension={100}
activeScale={0.95}
key={index}
title={post.title}
bottomDivider={true}
rightIcon={
<View>
<View style={{ flexDirection: "row", justifyContent: "center" }}>
<Icon
name="md-arrow-up"
type="ionicon"
color={post.userAction === "liked" ? "#a45151" : "#517fa4"}
onPress={() => handleUpvote(post, index)}
/>
<View style={{ marginLeft: 10, marginRight: 10 }}>
<Text>{post.likes.length - post.dislikes.length}</Text>
</View>
<Icon
name="md-arrow-down"
type="ionicon"
color={post.userAction === "disliked" ? "#8751a4" : "#517fa4"}
onPress={() => handleDownvote(post, index)}
/>
</View>
<View style={{ flexDirection: "row" }}>
<Text>{post.comments.length} comments</Text>
</View>
</View>
}
leftIcon={
<View style={{ height: 50, width: 50 }}>
<ImagePlaceholder
src={post.image.location}
placeholder={post.image.location}
duration={1000}
showActivityIndicator={true}
activityIndicatorProps={{
size: "large",
color: index % 2 === 0 ? "blue" : "red"
}}
/>
</View>
}
></ListItem>
));
setMappedPosts(mapped);
} else {
checkAuth();
fetchMedia();
}
}, [userPosts, mappedPosts]);
const handleDownvote = (post, index) => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
if (post.userAction === "dislike") {
newPosts.userAction = null;
} else {
newPosts.userAction = "dislike";
}
setMappedPosts(newPosts);
downvote(user.id, post._id);
};
const handleUpvote = post => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
console.log("mappedPosts", mappedPosts); // null
console.log("newPosts", newPosts); // null
if (post.userAction === "like") {
newPosts.userAction = null;
} else {
newPosts.userAction = "like";
}
setMappedPosts(newPosts);
upvote(user.id, post._id);
};
return mappedPosts === null ? (
<Spinner />
) : (
<ScrollView
refreshControl={
<RefreshControl
refreshing={false}
onRefresh={() => {
this.refreshing = true;
fetchMedia();
this.refreshing = false;
}}
/>
}
>
{mappedPosts}
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center"
}
});
Profile.propTypes = {
auth: PropTypes.object.isRequired,
posts: PropTypes.object.isRequired,
fetchMedia: PropTypes.func.isRequired,
checkAuth: PropTypes.func.isRequired,
upvote: PropTypes.func.isRequired,
downvote: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
auth: state.auth,
posts: state.posts
});
export default connect(
mapStateToProps,
{ fetchMedia, checkAuth, upvote, downvote }
)(Profile);
The reason why your current solution doesn't work is because you're rendering userPosts inside of the useEffect hook, which looks like it only runs once, ends up "caching" the initial state, and that's what you end up seeing in your handlers.
You will need to use multiple hooks to get this working properly:
const Profile = (props) => {
// ...
const [mappedPosts, setMappedPosts] = useState(null)
const [renderedPosts, setRenderedPosts] = useState(null)
useEffect(() => {
if (props.userPosts) {
const userPosts = props.userPosts.map(post => {
post.userAction = null;
// ...
})
setMappedPosts(userPosts)
} else {
checkAuth()
fetchMedia()
}
}, [props.userPosts])
const handleDownvote = (post, index) => {
// ...
setMappedPosts(newPosts)
}
const handleUpvote = (post) => {
// ...
setMappedPosts(newPosts)
}
useEffect(() => {
if (!mappedPosts) {
return
}
const renderedPosts = mappedPosts.map((post, index) => {
return (...)
})
setRenderedPosts(renderedPosts)
}, [mappedPosts])
return !renderedPosts ? null : (...)
}
Here's a simplified example that does what you're trying to do:
CodeSandbox
Also, one note, don't do this:
const Profile = (props) => {
const [mappedPosts, setMappedPosts] = useState(null)
useEffect(() => {
if (userPosts) {
setMappedPosts() // DON'T DO THIS!
} else {
// ...
}
}, [userPosts, mappedPosts])
}
Stay away from updating a piece of state inside of a hook that has it in its dependency array. You will run into an infinite loop which will cause your component to keep re-rendering until it crashes.
Let me use a simplified example to explain the problem:
const Example = props => {
const { components_raw } = props;
const [components, setComponents] = useState([]);
const logComponents = () => console.log(components);
useEffect(() => {
// At this point logComponents is equivalent to
// logComponents = () => console.log([])
const components_new = components_raw.map(_ => (
<div onClick={logComponents} />
));
setComponents(components_new);
}, [components_raw]);
return components;
};
As you can see the cycle in which setComponents is called, components is empty []. Once the state is assigned, it stays with the value logComponents had, it doesn't matter if it changes in a future cycle.
To solve it you could modify the necessary element from the received data, no components. Then add the onClick on the return in render.
const Example = props => {
const { data_raw } = props;
const [data, setData] = useState([]);
const logData = () => console.log(data);
useEffect(() => {
const data_new = data_raw.map(data_el => ({
...data_el // Any transformation you need to do to the raw data.
}));
setData(data_new);
}, [data_raw]);
return data.map(data_el => <div {...data_el} onClick={logData} />);
};