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.
Related
Hi I am having an issue in my react native expo app when I updated the APK version from 28 to 29 - for google plays new update.
Heres my error after dependancy updates in the component specified in the error log below:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in %s.%s, a useEffect cleanup function
I not sure exactly where the problem lies in the code
Code with useEffect cleanup attempt:
//React Deps
import React, { useState, useEffect, useCallback } from "react";
import {
View,
StyleSheet,
Image,
Text,
Platform,
Button,
ActivityIndicator,
} from "react-native";
import { SearchBar } from "react-native-elements";
import { HeaderButtons, Item } from "react-navigation-header-buttons";
import { useSelector, useDispatch } from "react-redux";
//Components
import HeaderButton from "../components/HeaderButton";
import JokesList from "../components/JokesList";
//Misc
import Colors from "../constants/Colors";
//Store
import * as jokeAction from "../store/actions/jokes-action";
const HomeScreen = (props) => {
const jokes = useSelector((state) => state.jokes.jokes);
const [search, setIsSearch] = useState(""); //Search
const [newSearch, setIsNewSearch] = useState([]); //New Search
const [isFetching, setIsFetchingFav] = useState(false); //Fav
const [isLoading, setIsLoading] = useState(); //Loading
const [isRefreshing, setIsRefreshing] = useState(false); //Refreshing
const [error, setError] = useState(""); //Errors
const dispatch = useDispatch();
//Load Jokes
useEffect(() => {
let isCancelled = false; //attempt
console.log("load jokes");
setIsLoading(true);
loadJokes().then(() => {
if (!isCancelled) {
setIsLoading(false);
console.log("set is loading to false");
}
});
//Cleanup ?
return () => {
isCancelled = true;
};
}, [dispatch, loadJokes]); //Deps
//Loading All Jokes
const loadJokes = useCallback(async () => {
console.log("enter load jokes func");
setError(null);
setIsRefreshing(true);
try {
await dispatch(jokeAction.fetchJokes());
} catch (err) {
setError(err.message);
console.log("error");
}
setIsRefreshing(false);
}, [dispatch, setIsLoading, setError]);
//Update after render
useEffect(() => {
console.log("update after render");
const willFocusSub = props.navigation.addListener("willFocus", loadJokes);
return () => {
willFocusSub.remove();
};
}, [loadJokes]);
//Load Fav Jokes
useEffect(() => {
console.log("load fav jokes");
fetchingFavJokes();
}, [dispatch, fetchingFavJokes]);
//Favs
const fetchingFavJokes = useCallback(async () => {
setError(null);
try {
await dispatch(jokeAction.loadFavJokes());
} catch (err) {
setError(err.message);
}
setIsFetchingFav(false);
}, [dispatch, setIsFetchingFav, setError]);
//Events
const updateSearch = (text) => {
setIsSearch(text.toUpperCase());
const newData = jokes.filter((joke) => {
/* When no search text present, do not apply filtering */
if (!text) {
return true;
}
return (
typeof joke.title.toUpperCase() === "string" &&
joke.title.toUpperCase().includes(text.toUpperCase())
);
});
setIsNewSearch(newData);
};
//Loading Spinner
if (isLoading) {
return (
<View style={{ ...styles.screen, ...styles.centered }}>
<ActivityIndicator size="large" color="white" />
</View>
);
}
//Error
if (error) {
return (
<View style={{ ...styles.screen, ...styles.centered }}>
<Text style={{ color: "white" }}>An Error Occurred!</Text>
<Button
title="Try Again"
onPress={loadJokes}
color={Colors.accentColor}
/>
</View>
);
}
//Fall Back Text
if (!isLoading && jokes.length === 0) {
return (
<View style={styles.centered}>
<Text>No Jokes Found</Text>
</View>
);
}
return (
//Logo
<View style={styles.screen}>
<View style={styles.logoContainer}>
<Image style={styles.logo} source={require("../assets/logo.png")} />
</View>
<View style={styles.searchBar}>
<SearchBar
containerStyle={{
backgroundColor: "white",
borderTopColor: "white",
borderBottomColor: "white",
height: 50,
alignItems: "center",
justifyContent: "center",
}}
inputContainerStyle={{ backgroundColor: "white", height: 40 }}
inputStyle={{
backgroundColor: "white",
color: "#232323",
fontSize: 16,
fontFamily: "luckiest-guy",
}}
lightTheme
leftIconContainerStyle={{ backgroundColor: "white" }}
searchIcon={{ size: 24 }}
onChangeText={(text) => {
updateSearch(text);
}}
onClear={(text) => setIsSearch("")}
placeholder="Keyword or Topic..."
value={search}
/>
</View>
<JokesList
listData={!search ? jokes : newSearch}
onRefreshing={loadJokes}
isRefreshing={isRefreshing}
navigation={props.navigation}
/>
</View>
);
};
...Navigation/Styles etc
export default HomeScreen;
I was trying to remove a view from the screen when the keyboard appears so everything can fit on the screen.
After the keyboard is initially dismissed the first time - with the keyboardDidHide code commented out - it will not automatically dismiss it again.
Here is the code part of the code. I am using Formik for the form.
What am I doing wrong?
const [keyboardUp, setKeyboardUp] = React.useState(false);
Keyboard.addListener("keyboardDidShow", _keyboardDidShow);
Keyboard.addListener("keyboardDidHide", _keyboardDidHide);
React.useEffect(() => {
Keyboard.addListener("keyboardDidShow", _keyboardDidShow);
Keyboard.addListener("keyboardDidHide", _keyboardDidHide);
// cleanup function
return () => {
Keyboard.removeListener("keyboardDidShow", _keyboardDidShow);
Keyboard.removeListener("keyboardDidHide", _keyboardDidHide);
};
}, []);
const _keyboardDidShow = () => {
setKeyboardUp(true);
};
const _keyboardDidHide = () => {
setKeyboardUp(false);
};
const LoginForm=()=>{
let input_height=40;
return (
<Formik
style={{ backgroundColor:"blue" }}
initialValues={{email:'',password:''}}
onSubmit={(values)=> {
let username = values.email;
let p = values.password;
signIn({ username,p })
}}
>
{(props)=>(
<View style={{width:"100%", height:"45%", justifyContent:"center",alignItems:"center"}}>
<TextInput style={{width:"75%", borderColor: "#1d3557", borderWidth:1, height:input_height, margin:"2%", paddingLeft:"3%", backgroundColor:"white", borderColor:"grey", borderRadius:8}}
placeholder='email' onChangeText={props.handleChange('email')} value={props.values.title} />
<TextInput style={{width:"75%", height:input_height, borderColor: "#1d3557", borderWidth:1, margin:"2%", paddingLeft:"3%", backgroundColor:"white", borderColor:"grey", borderRadius:8}}
placeholder='password' onChangeText={props.handleChange('password')} value={props.values.password} />
<TouchableOpacity style={{backgroundColor:"#008b9a", justifyContent:"center", marginTop:"8%", alignItems:"center", width:"75%", height:40, borderRadius:16}}
onPress={props.handleSubmit}>
<Text style={{fontSize:16, fontWeight:"bold"}}>LOGIN</Text>
</TouchableOpacity>
</View>
)}
</Formik>
);
}
I'm seeing on the docs that they add the listeners inside of useEffect from react. Can u try this way?
import React, { useEffect } from "react";
import { Keyboard, TextInput, StyleSheet } from "react-native";
const Example = () => {
useEffect(() => {
Keyboard.addListener("keyboardDidShow", _keyboardDidShow);
Keyboard.addListener("keyboardDidHide", _keyboardDidHide);
// cleanup function
return () => {
Keyboard.removeListener("keyboardDidShow", _keyboardDidShow);
Keyboard.removeListener("keyboardDidHide", _keyboardDidHide);
};
}, []);
const _keyboardDidShow = () => {
alert("Keyboard Shown");
};
const _keyboardDidHide = () => {
alert("Keyboard Hidden");
};
return <TextInput style={s.input} placeholder='Click here ...' onSubmitEditing={Keyboard.dismiss} />;
}
const s = StyleSheet.create({
input:{
margin:60,
padding: 10,
borderWidth: 0.5,
borderRadius: 4,
backgroundColor: "#fff"
}
})
export default Example;
If this doesn't work,
Keyboard.removeListener("keyboardDidShow", _keyboardDidShow);
Keyboard.removeListener("keyboardDidHide", _keyboardDidHide);
Do this
const a = Keyboard.removeListener("keyboardDidShow", _keyboardDidShow);
const b = Keyboard.removeListener("keyboardDidHide", _keyboardDidHide);
return a.remove(),b.remove();
I using expo-camera to record video and save it in my device, but even on stop recording it has video uri, the .MediaLibrary.createAssetAsync is giving the above error, also the recording gets stopped from the first even if I have used setTimeout to stop the recording, this is my code for the camera:
import React, { useState, useEffect, useRef } from 'react';
import { Text, View } from 'react-native';
import { Camera } from 'expo-camera';
import { MediaLibrary } from 'expo-media-library';
import { Audio } from 'expo-av';
export default function App() {
const [hasPermission, setHasPermission] = useState(null);
const [camera, setCamera] = useState(null);
const [recording, setRecording] = useState(false);
const [video, setVideo] = useState(null);
const [stop, setStop] = useState(false);
const recordingVideo = async () => {
const video = await camera.recordAsync();
console.log(video);
setVideo(video);
}
const saveVideo = async () => {
console.log('uri', video.uri);
const asset = await MediaLibrary.createAssetAsync(video.uri);
if (asset) {
console.log('asset', asset);
setVideo(null);
}
};
useEffect(() => {
console.log('recoring', recording);
if (recording && camera) recordingVideo();
}, [recording, camera]);
useEffect(() => {
console.log('stop', stop);
if (stop) {
setRecording(false);
camera.stopRecording();
saveVideo();
}
}, [stop]);
useEffect(() => {
(async () => {
const { status } = await Camera.requestPermissionsAsync();
const audioPerm = await Audio.requestPermissionsAsync();
setHasPermission(status === 'granted' && audioPerm.status === 'granted');
})();
}, []);
useEffect(() => {
if(camera) {
console.log('ref');
setRecording(true);
setTimeout(() => {
setStop(true);
}, 10000);
}
}, [camera]);
if (hasPermission === null) {
return <View />;
}
if (hasPermission === false) {
return <Text>No access to camera or audio</Text>;
}
return (
<View style={{ flex: 1 }}>
<Camera style={{ flex: 1 }} type={Camera.Constants.Type.front} ref={ref => setCamera(ref)}>
<View style={{ flex: 1, backgroundColor: '#00000000', justifyContent: 'center' }}>
<Text style={{ marginHorizontal: 40 }}>{recording ? 'Recording' : !stop ? 'Waiting for recording' : 'recording finished' }</Text>
</View>
</Camera>
</View>
);
}
I searched in expo-media-library docs and logged the video.uri that is exactly some matching parameter but cannot understand why it is working like that.
I was having the same problem in my Camera App. I solved by changing my import
// Change from this
import { MediaLibrary } from 'expo-media-library';
// To this
import * as MediaLibrary from 'expo-media-library';
I have a component that fetch data first. then I manipulate the data but it doesn't change value until I change the screen and return back. Here I change the data based on the categories and doctors which i fetch using redux. but transformed data remains empty until i back to the page for the second time. Thanks for your help
import React, { useState, useEffect, useCallback } from "react";
import {
View,
Text,
StyleSheet,
ActivityIndicator,
FlatList,
Dimensions,
} from "react-native";
import { useSelector, useDispatch } from "react-redux";
import Colors from "../../constants/Colors";
import * as DoctorsActions from "../../store/actions/Doctors";
const { height } = Dimensions.get("window");
const ConcultationMainScreen = (props) => {
const dispatch = useDispatch();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const categories = useSelector((state) => state.categories.categories);
const doctors = useSelector((state) => state.doctors.doctors);
const loadAllDoctors = useCallback(async () => {
try {
await dispatch(DoctorsActions.getDoctors());
} catch (err) {
setError(err);
console.log(error);
}
}, [dispatch, setError]);
useEffect(() => {
setIsLoading(true);
loadAllDoctors().then(() => {
setIsLoading(false);
});
}, [dispatch, loadAllDoctors]);
if (isLoading) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color={Colors.blue} />
</View>
);
}
let transformedData = [];
for (const cat in categories) {
let doctorsOfCategory = [];
for (const doc in doctors) {
if (doctors[doc].categories[0] === categories[cat].name) {
doctorsOfCategory.push({
doctorName: doctors[doc].name,
});
} else {
continue;
}
}
transformedData.push({
categoryName: categories[cat].name,
doctorsOfCategory: doctorsOfCategory,
});
}
console.log(transformedData);
const renderConsultCategories = (ItemData) => {
return (
<View style={styles.item}>
<View style={styles.titleContainer}>
<Text>
گفتگو و مشاوره با متخصصین <Text>{ItemData.item.categoryName}</Text>
</Text>
</View>
</View>
);
};
return (
<View style={styles.screen}>
<FlatList
data={transformedData}
keyExtractor={(item) => item.categoryName}
renderItem={renderConsultCategories}
/>
</View>
);
};
export const screenOptions = (navData) => {
return {
headerTitle: "صفحه مشاوره",
headerTitleAlign: "center",
};
};
const styles = StyleSheet.create({
screen: {
flex: 1,
},
centered: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
item: {
flexDirection: "row-reverse",
width: "100%",
height: height / 8,
marginVertical: 10,
borderBottomWidth: 1,
},
});
export default ConcultationMainScreen;
Use this.forceUpdate() to re-render without calling setState().
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} />);
};