I'm making an app that have notes, and when develop the delete function, i faced this error, the useState do not update when use alongside with redux dispatch function ( even the redux function run, the useState do not run ) , i tried to create the same issue on codesandbox, but weird is it WORKING TOTALLY FINE ON WEB?!
Here is the code:
NoteList.tsx
function NoteList(props: noteListI) {
const { title, note, id, date, selectStatus } = props; //they are props
const nav = useNavigation(); //for navigation
const [isDeleteChecked, setDeleteChecked] = useState(false);
const dispatch = useDispatch();
const data = useSelector((state: RootState) => state.persistedReducer.note); // note item from redux
const toggleSelectedButton = useSelector(
(state: RootState) => state.toggle.enableSelectedButton
); // to show selected button icon
const onNavDetail = () => {
nav.navigate(RouteName.EDIT_NOTE, {
date: date,
note: note,
header: title,
id: id,
});
};
const toggleSelectButton = () => {
dispatch(switchToggle());
}; // toggle delete button function
const setDeleteItem = () => {
setDeleteChecked(!isDeleteChecked);
dispatch(toggleSelect({ id: id }));
}; ////==>>> the issue here the 'setDeleteChecked' not even work
return (
<TouchableOpacity
onLongPress={() => {
toggleSelectButton();
}}
style={CONTAINER}
onPress={() => (!toggleSelectedButton ? onNavDetail() : setDeleteItem())}
>
<View style={NOTE_ITEM_CONTAINER}>
<Text>{isDeleteChecked?.toString()}</Text> ==>always false, why????!
<View>
<View row centerV style={HEADER_CONTAINER}>
<View>
<AppText bold style={HEADER_TEXT}>
{title}
</AppText>
</View>
{toggleSelectedButton && (
<View>
{selectStatus ? ( ===> this is from redux and work but slow
<CheckIcon name="checkcircle" size={size.iconSize} />
) : (
<CheckIcon name="checkcircleo" size={size.iconSize} />
)}
</View>
)}
</View>
<View style={NOTE_CONTAINER}>
<AppText numberOfLines={7}>{note}</AppText>
</View>
</View>
<View
style={{
alignSelf: "flex-end",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
>
<AppText>{moment(date).format("h:mmA MMM Do YY")}</AppText>
</View>
</View>
</TouchableOpacity>
);
}
export default memo(NoteList);
I use these from flatlist, here is the main flatlist code:
export default function NoteListScreen() {
const [user, setUser] = useState<any>();
const nav = useNavigation();
// useEffect(() => {
// dispatch(loadDefault());
// }, []);
const dispatch: AppDispatch = useDispatch();
const data = useSelector((state: RootState) => state.persistedReducer.note);
const userInfo: user = useSelector(
(state: RootState) => state.persistedReducer.firebase.userInfomation
);
useEffect(() => {
dispatch(fetchNote(userInfo.email)); //fetch note from firebase
}, []);
return (
<SafeAreaView style={CONTAINER}>
{data.length === 0 ? (
<>
<ScrollView>
<HeaderNote />
<AppText style={EMPTY_NOTE}>
Hmm, so don't have any secret yet
</AppText>
</ScrollView>
<FooterNote />
</>
) : (
<View style={CONTAINER}>
<FlatList
removeClippedSubviews
data={data}
style={{
marginBottom:
Platform.OS === "ios"
? onePercentHeight * 15
: onePercentHeight * 12,
}}
keyExtractor={() => {
return (
new Date().getTime().toString() +
Math.floor(
Math.random() * Math.floor(new Date().getTime())
).toString()
);
}}
ListHeaderComponent={() => <HeaderNote />}
renderItem={({ item, index }) => {
return (
<NoteList ==> here , the note list that faced error
note={item.note}
title={item.header}
date={item.date}
id={item.id}
selectStatus={item.selectStatus}
/>
);
}}
/>
<FooterNote />
</View>
)}
</SafeAreaView>
);
}
Here is the reducer code:
const noteReducer = createSlice({
name: "note",
initialState: NoteList,
reducers: {
addNote: (state, action: PayloadAction<NoteI>) => {
const newNote: NoteI = {
id:
new Date().getTime().toString() +
Math.floor(
Math.random() * Math.floor(new Date().getTime())
).toString(),
header: action.payload.header,
note: action.payload.note,
date: new Date(),
selectStatus: false,
};
state.push(newNote);
},
toggleSelect: (state, action: PayloadAction<NoteI>) => {
return state.map((item) => {
if (item.id === action.payload.id) {
return { ...item, selectStatus: !item.selectStatus };
}
return item;
});
}, ///========>This is the reducer using in the note function
loadDefault: (state) => {
return state.map((item) => {
return { ...item, selectStatus: false };
});
},
resetNote: (state) => {
return (state = []);
},
editNote: (state, action: PayloadAction<NoteI>) => {
return state.map((item) => {
if (item.id === action.payload.id) {
return {
...item,
note: action.payload.note,
header: action.payload.header,
date: action.payload.date,
};
}
return item;
});
},
},
extraReducers: (builder) => {
builder.addCase(fetchNote.fulfilled, (state, action) => {
state = [];
return state.concat(action.payload);
});
},
});
Here is the image of what i'm talking about, the code in image from noteList.tsx, the first piece of code i post here
Here is the quick gif:
In above gif, the false must return true then false everytime i click ( as above code ) but i don't why it never change value, the black dot also change color because it use value using in the same function using with this value, but when setDeleteItem fire, it NOT fire the setDeleteChecked(!isDeleteChecked)
Here is the demo that i made, but it WORK TOTALLY FINE, but in my app, it make weird error https://codesandbox.io/s/nostalgic-neumann-0497v?file=/redux/some-redux.tsx
Please help, i'm trying to provide must as i can, i stuck for days for this, thank you so much, if you need any detail, just tell me
Related
I am fetching an array of two objects. there are "title" and "iqtiboslar" array inside objects. and show it in SectionList but it is giving an error: "can not read properties of undefined(reading 'length')". Here is my code. Any ideas will be highly appreciated
const Item = ({iqtiboslar}) => (
<View>
<Text>{iqtiboslar}</Text>
</View>
);
const HomeScreen = ({navigation}) => { const [quote, setQuote] = useState();
useEffect(() => {
fetchQuotes();
return () => {
setQuote();
};
}, []);
const fetchQuotes = async () => {
try {
const quoteCollection = await firestore().collection('iqtiboslar').get(); // get(:field) to get specific doc
quoteCollection._docs.map(doc => setQuote(doc.data().items));
// quoteCollection._docs.map(doc => console.log(doc));
} catch (error) {
console.log(error);
}
};
return (
<View style={styles.container}>
{quote ? (
<SectionList
sections={quote}
keyExtractor={(item, index) => item + index}
renderItem={({item}) => <Item title={item.title} />}
renderSectionHeader={({section}) => <Text>{section.title}</Text>}
/>
) : (
<ActivityIndicator />
)} </View>
);
};
export default HomeScreen;
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
text: {
color: 'red',
},
});
Try this code
<SectionList
sections={quote}
keyExtractor={(item, index) => item + index}
renderItem={({item}) => <Text>{item}</Text>}
renderSectionHeader={({section: {title}}) => (
<Text style={styles.text}>{title}</Text>
)}
I am trying to use AsyncStorage to fetch my todos from inside the useEffect hook. If there are no todos(Meaning todos === []) Then a Text Component shows saying "Add a todo".
App image in expo
Initially the todos are set to "[]" inside the useState hook. When the addItem() method is called onPress the todos are not loading.
I do not know why this is happening...
export default function App() {
const [todo, setTodo] = useState('');
const [todos, setTodos] = useState([]);
useEffect(() => {
_retrieveData();
}, [todos]);
const addItem = (newTodo) => {
if (newTodo.length === 0) {
Alert.alert(
'Enter a String',
'You have entered a string with 0 characters',
[{ text: 'Okay', style: 'default' }]
);
} else {
console.log(newTodo);
let newTodos = [newTodo, ...todos];
setTodo('');
_storeData(JSON.stringify(newTodos));
}
};
const deleteTodo = (idx) => {
setTodos(todos.filter((todo, id) => id !== idx));
};
const _storeData = async (value) => {
try {
await AsyncStorage.setItem('TASKS', value);
} catch (error) {
// Error saving data
console.log(e);
}
};
const _retrieveData = async () => {
try {
const value = await AsyncStorage.getItem('TASKS');
if (value !== null) {
// We have data!!
setTodos(JSON.parse(value));
console.log(value);
}
} catch (error) {
// Error retrieving data
console.log(error);
}
};
return (
<TouchableWithoutFeedback
onPress={() => {
Keyboard.dismiss();
}}
>
<View style={styles.outerContainer}>
<Text style={styles.header}>TODO</Text>
<View style={styles.container}>
<TextInput
placeholder='new todo'
style={styles.input}
value={todo}
onChangeText={(text) => {
setTodo(text);
}}
></TextInput>
<Button title='Add' onPress={() => addItem(todo)}></Button>
</View>
<ScrollView style={styles.scrollView}>
{todos === [] ? (
<View>
<Text>Add a todo!</Text>
</View>
) : (
todos.map((todo, idx) => (
<View style={styles.todo} key={idx}>
<Text style={styles.todoText}>{todo}</Text>
<View style={styles.delete}>
<Button
color='red'
title='Delete'
onPress={() => deleteTodo(idx)}
></Button>
</View>
</View>
))
)}
</ScrollView>
</View>
</TouchableWithoutFeedback>
);
}
Dont use passed todo value newTodo, as setState is async dont get executed immediately, so you can use current setted todo value instead passed old value,
const addItem = (newTodo) => {
if (todo.length === 0) {
Alert.alert(
'Enter a String',
'You have entered a string with 0 characters',
[{ text: 'Okay', style: 'default' }]
);
} else {
console.log(todo);
let newTodos = [todo, ...todos];
setTodo('');
_storeData(JSON.stringify(newTodos));
setTodos(newTodos);
}
};
I have two components the first where user can add a place to the favorites and the second is favorites component where user may see all his favorite places. When the user for the first time opens the favorites component everything works as expected: all the favorite places that user has already added to the favorites rendered. But if user go to the first component and add one more place and then go to the second component new place will not appear because component has already rendered and the state didn't changed because useEffect not triggered. Help me please what should I use in my FavouritePlaces component instead of useEffect to rerender this component every time when user open FavouritePlaces?
Component where user can add to favorites:
const ModalWindow = ({navigateToPlace, sendDataToParent, visible, marker}: HomeNavigationProps<"ModalWindow">) => {
const regex = /(<([^>]+)>)|( )|(&nbps)/ig;
const result = marker.description.replace(regex, '');
const [favKeys, setFavKeys] = useState([]);
const onDismiss = () => {
sendDataToParent(false)
}
const onNavigationTap = () => {
onDismiss();
navigateToPlace(true, marker.coordinates);
}
const getFavourites = async () => {
let keys = []
keys = await AsyncStorage.getAllKeys()
setFavKeys(keys);
}
const onHeartPress = async () => {
const jsonValue = JSON.stringify(marker)
try {
if (favKeys.includes(marker.id.toString())){
await AsyncStorage.removeItem(marker.id.toString())
await getFavourites();
} else {
await AsyncStorage.setItem(marker.id.toString(), jsonValue)
await getFavourites();
}
} catch (e) {
console.log('error in onHeartPress', e)
}
console.log('Done.')
//remove after test
try {
await AsyncStorage.removeItem('__react_native_storage_test')
} catch(e) {
// remove error
}
console.log('Done.')
}
return (
<Modal visible={visible} onDismiss={onDismiss} contentContainerStyle={styles.container}>
<IconButton
style={
styles.iconButton
}
icon="close"
color={Colors.black}
size={30}
onPress={() => onDismiss()}
/>
<Text
style={{fontStyle: "italic", fontSize: 20, alignSelf: "center", maxWidth: '75%'}}>{marker.title}
</Text>
<CustomCarousel {...{marker}} />
<ScrollView showsVerticalScrollIndicator={false} style={{marginTop: '3%', marginLeft: '3%', marginRight: '3%'}}>
<Text>{result}</Text>
</ScrollView>
<View style={{flexDirection: "row", justifyContent: "space-around", marginLeft: "3%", marginRight: "3%", marginBottom: "15%"}}>
<TouchableOpacity onPress={() => onNavigationTap()}>
<View style={{flexDirection: "row", alignItems: "center"}}>
<Ionicons size={height/20} name={'navigate-circle-outline'} />
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => onHeartPress()}>
{marker.id ?
<View style={{flexDirection: "row", alignItems: "center"}}>
{favKeys.includes(marker.id.toString()) ? <Ionicons size={height/20} name={'heart-dislike'} /> : <Ionicons size={height/20} name={'heart'} />}
</View> : undefined}
</TouchableOpacity>
</View>
</Modal>
);
}
export default ModalWindow;
My Favorite Places component:
const FavouritePlaces = ({navigation}: HomeNavigationProps<"FavouritePlaces">) => {
const [markers, setMarkers] = useState([]);
useEffect(() => {
const getFavourites = async () => {
let keys = []
try {
keys = await AsyncStorage.getAllKeys()
} catch (e) {
// read key error
}
let values
try {
let forDeletion = ['__react_native_storage_test', 'NAVIGATION_STATE_KEY-40.0.0'];
keys = keys.filter(item => !forDeletion.includes(item))
values = await AsyncStorage.multiGet(keys)
setMarkers(values)
} catch (e) {
// read error
}
}
getFavourites();
}, [])
const transition = (
<Transition.Together>
<Transition.Out type='fade'/>
<Transition.In type='fade'/>
</Transition.Together>
);
const list = useRef<TransitioningView>(null);
const theme = useTheme()
const width = (wWidth - theme.spacing.m * 3) / 2;
const [footerHeight, setFooterHeight] = useState(0);
return (
<Box flex={1} backgroundColor="background">
<StatusBar style="black" />
<Header
title="Избранные места"
left={{icon: 'menu', onPress: () => navigation.openDrawer()}}
right={{icon: 'shopping-bag', onPress: () => true}}
/>
<Box flex={1}>
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{
paddingBottom: footerHeight,
}}>
<Transitioning.View ref={list} transition={transition} style={{}}>
{markers ?
<Box flexDirection='row' style={{justifyContent: "space-around"}}>
<Box>
{markers
.filter((_, i) => i % 2 === 0).map((currentMarker) => <Picture
key={currentMarker}
place={currentMarker}
width={width}
height={height}
/>)}
</Box>
<Box>
{markers
.filter((_, i) => i % 2 !== 0).map((currentMarker) => <Picture
key={currentMarker}
place={currentMarker}
width={width}
height={height}/>)}
</Box>
</Box> : undefined}
</Transitioning.View>
</ScrollView>
{/*<TopCurve footerHeight={footerHeight}/>*/}
<Box position='absolute' bottom={0} left={0} right={0} onLayout={({
nativeEvent: {
layout: {height},
}
}) => setFooterHeight(height)}>
</Box>
</Box>
</Box>
)
}
export default FavouritePlaces
Try this
useEffect(() => {
// ... Your code goes here
}, [navigation]);
this will render whenever update in navigate
I've found the solution. React navigation has hook useIsFocused, so what can we do is:
import { useIsFocused } from "#react-navigation/native";
const isFocused = useIsFocused();
useEffect(() => {
// ... Your code goes here
}, [isFocused]);
You can use React Context API to share the state across the screens.
Check out this Expo Snack I created.
import {
CompositeNavigationProp,
NavigationContainer,
NavigatorScreenParams,
} from '#react-navigation/native';
import {
createStackNavigator,
StackNavigationProp,
} from '#react-navigation/stack';
import * as React from 'react';
import {
Button,
FlatList,
ListRenderItem,
Text,
TextInput,
View,
} from 'react-native';
type MainStackParamsList = {
FavoritePlacesScreen: undefined;
};
type ModalStackParamsList = {
MainStack: NavigatorScreenParams<MainStackParamsList>;
AddFavoritePlacesModal: undefined;
};
type FavoritePlace = {
id: number;
name: string;
};
type FavoritePlacesContextValue = {
favoritePlaces: FavoritePlace[];
addNewFavoritePlace: (favoritePlace: FavoritePlace) => void;
removeFavoritePlace: (id: number) => void;
};
const FavoritePlacesContext = React.createContext<FavoritePlacesContextValue>({
favoritePlaces: [],
addNewFavoritePlace: () => {},
removeFavoritePlace: () => {},
});
const MainStack = createStackNavigator<MainStackParamsList>();
type FavoritePlacesScreenProps = {
navigation: CompositeNavigationProp<
StackNavigationProp<MainStackParamsList, 'FavoritePlacesScreen'>,
StackNavigationProp<ModalStackParamsList>
>;
};
const FavoritePlacesScreen = ({navigation}: FavoritePlacesScreenProps) => {
const {favoritePlaces, removeFavoritePlace} = React.useContext(
FavoritePlacesContext,
);
const renderItem = React.useCallback<ListRenderItem<FavoritePlace>>(
({item}) => {
return (
<View style={{height: 50, padding: 10, flexDirection: 'row'}}>
<Text style={{fontSize: 16}}>{item.name}</Text>
<Button onPress={() => removeFavoritePlace(item.id)} title="Remove" />
</View>
);
},
[removeFavoritePlace],
);
return (
<View style={{flex: 1}}>
<FlatList
data={favoritePlaces}
keyExtractor={(item) => String(item.id)}
renderItem={renderItem}
/>
<Button
onPress={() => {
navigation.navigate('AddFavoritePlacesModal');
}}
title="Add new favorite"
/>
</View>
);
};
const MainStackNavigator = () => {
return (
<MainStack.Navigator>
<MainStack.Screen
component={FavoritePlacesScreen}
name="FavoritePlacesScreen"
/>
</MainStack.Navigator>
);
};
type AddFavoritePlacesModalProps = {
navigation: StackNavigationProp<
ModalStackParamsList,
'AddFavoritePlacesModal'
>;
};
const AddFavoritePlacesModal = ({navigation}: AddFavoritePlacesModalProps) => {
const {addNewFavoritePlace} = React.useContext(FavoritePlacesContext);
const [favoritePlaceName, setFavoritePlaceName] = React.useState('');
const handleOnSave = React.useCallback(() => {
addNewFavoritePlace({
id: Date.now(),
name: favoritePlaceName,
});
navigation.goBack();
}, [addNewFavoritePlace, favoritePlaceName, navigation]);
return (
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<View style={{borderRadius: 6, borderWidth: 1, borderColor: '#333'}}>
<TextInput
onChangeText={setFavoritePlaceName}
placeholder="Name your favorite place"
/>
</View>
<Button onPress={handleOnSave} title="Save" />
</View>
);
};
// Put the favorite places list screen and the add favorite place modal here.
// Then use FavoritePlacesContext.Provider to wrap ModalStack.Navigator in order
// for the context to be available on MainStack
const ModalStack = createStackNavigator<ModalStackParamsList>();
const ModalNavigator = () => {
const [favoritePlaces, setFavoritePlaces] = React.useState<FavoritePlace[]>(
[],
);
const addNewFavoritePlace = React.useCallback(
(favoritePlace: FavoritePlace) => {
setFavoritePlaces((prev) => [...prev, favoritePlace]);
},
[],
);
const removeFavoritePlace = React.useCallback((id: number) => {
setFavoritePlaces((prev) =>
prev.filter((favoritePlace) => favoritePlace.id !== id),
);
}, []);
return (
<FavoritePlacesContext.Provider
value={{
favoritePlaces,
addNewFavoritePlace,
removeFavoritePlace,
}}
>
<ModalStack.Navigator headerMode="none">
<ModalStack.Screen component={MainStackNavigator} name="MainStack" />
<ModalStack.Screen
component={AddFavoritePlacesModal}
name="AddFavoritePlacesModal"
options={{headerShown: false}}
/>
</ModalStack.Navigator>
</FavoritePlacesContext.Provider>
);
};
const App = () => {
return (
<NavigationContainer>
<ModalNavigator />
</NavigationContainer>
);
};
export default App;
I am new to react native and my JS is a bit rusty. I need to be able to change the value of my collection for the firestore. I have two buttons that will change the value of typeOfPost by setting the state. Component1 can successfully get "this.state.typeOfPost". However, when I click one of the buttons and update the state my log inside of the async function is not being called. It is only called when the app initially renders. What I find weird is that my log on the top of Component1 will display as expected. Is there any better way of doing this?
class Forum extends Component {
state = {
typeOfPost: ' '
}
onPressSitter = () => {
this.setState({
typeOfPost: 'sitterPosts'
})
}
onPressNeedSitter = () => {
this.setState({
typeOfPost: 'needPosts'
})
}
render() {
return (
<View style={styles.container}>
<View style={styles.row}>
<TouchableOpacity
style={styles.button}
onPress={this.onPressSitter}
>
<Text>I am a sitter</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.button}
onPress={this.onPressNeedSitter}
>
<Text>Need a sitter</Text>
</TouchableOpacity>
</View>
<View>
<Component1 typeOfPost = {this.state.typeOfPost}> </Component1>
</View>
</View>
)
}
}
const Component1 = (props) => {
console.log("type of post " + props.typeOfPost);
const [loading, setLoading] = useState(true); // Set loading to true on component mount
const [data, setData] = useState([]); // Initial empty array of data
const getData = async () => {
console.log("type of post inside async " + props.typeOfPost);
const subscriber = firestore()
.collection(props.typeOfPost) // need to be able to update this
.onSnapshot(querySnapshot => {
const data = [];
querySnapshot.forEach(documentSnapshot => {
data.push({
...documentSnapshot.data(),
key: documentSnapshot.id,
});
});
setData(data);
setLoading(false);
});
// Unsubscribe from events when no longer in use
return () => subscriber();
}
useEffect(() => {
getData();
}, [])
if (loading) {
return <ActivityIndicator />;
}
return (
<FlatList
data={data}
ListEmptyComponent={
<View style={styles.flatListEmpty}>
<Text style={{ fontWeight: 'bold' }}>No Data</Text>
</View>
}
renderItem={({ item }) => (
<View>
<Text>User ID: {item.fullName}</Text>
</View>
)}
/>
)
}
There is a difference between mount and render. I see no problem with your code except the few remarks I have made. The thing is that when you change typeOfPost, the component is rerendered, but the useEffect is not called again, since you said, it's just called when it was first mounted:
useEffect(() => {
}, []) // ---> [] says to run only when first mounted
However here, you want it to run whenever typeOfPost changes. So here is how you can do this:
useEffect(() => {
getData();
}, [typeofPost])
class Forum extends Component {
state = {
typeOfPost: ' '
}
onPressSitter = () => {
this.setState({
typeOfPost: 'sitterPosts'
})
}
onPressNeedSitter = () => {
this.setState({
typeOfPost: 'needPosts'
})
}
render() {
return (
<View style={styles.container}>
<View style={styles.row}>
<TouchableOpacity
style={styles.button}
onPress={this.onPressSitter}
>
<Text>I am a sitter</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.button}
onPress={this.onPressNeedSitter}
>
<Text>Need a sitter</Text>
</TouchableOpacity>
</View>
<View>
<Component1 typeOfPost = {this.state.typeOfPost}> </Component1>
</View>
</View>
)
}
}
const Component1 = (props) => {
const { typeOfPost } = props
console.log("type of post " + props.typeOfPost);
const [loading, setLoading] = useState(true); // Set loading to true on component mount
const [data, setData] = useState([]); // Initial empty array of data
const getData = () => {
setLoading(true)
console.log("type of post inside async " + props.typeOfPost);
const subscriber = firestore()
.collection(props.typeOfPost) // need to be able to update this
.onSnapshot(querySnapshot => {
const data = [];
querySnapshot.forEach(documentSnapshot => {
data.push({
...documentSnapshot.data(),
key: documentSnapshot.id,
});
});
setData(data);
setLoading(false);
});
// Unsubscribe from events when no longer in use
return () => subscriber();
}
useEffect(() => {
getData();
}, [typeofPost])
if (loading) {
return <ActivityIndicator />;
}
return (
<FlatList
data={data}
ListEmptyComponent={
<View style={styles.flatListEmpty}>
<Text style={{ fontWeight: 'bold' }}>No Data</Text>
</View>
}
renderItem={({ item }) => (
<View>
<Text>User ID: {item.fullName}</Text>
</View>
)}
/>
)
}
you are using a class based component to access react hook which is a bad practice, i will advice you use a functional component and you have access to react useCallback hook which will handle your request easily
const ButtonPressed = useCallback(() => {
setLoading(true);
getData()
}).then(() => setLoading(false));
}, [loading]);
I've been trying to toggle favorite products and store their id and an isFav prop on a Firebase database. Then I use them to show the favorites on the FavoritesScreen.
If I go to the ProductDetailsScreen (where I toggle the favorite) I toggle it true/false with no problem.
Further if I then use the Bottom Tab Navigation to check the FavoritesScreen or the OrdersScreen etc and then go back to ProductDetailsScreen, nothing is changed.
But if (from the ProductDetailsScreen) I go back (to ProductsOverviewScreen) and then come back again on ProductDetailsScreen the state of isFav
snaps back to false! Nevertheless the id and isFav are saved on Firebase, but isFav is saved as false.
Note: I use a useState() hook...
One more thing that I don't understand happens when I try to log isFav.
I have two logs, one inside the toggleFavoriteHandler and one outside. When I first run the toggleFavoriteHandler, where I also have setIsFav(prevState => !prevState); I get:
Output:
outside: false
inside: false
outside: true
So I guess the first two false are from the initial state and then the true is from the above state-toggling. But why it gets it only outside true? Why actually the first two are false? I change the state to true before the log. I would expect it to immediately change to true and have them all true!
Then if I go back to ProductsOverviewScreen and then again to ProductDetailsScreen I get two logs from outside:
Output:
outside: true
outside: false
So it snaps back to its initial state! ?
I really do not understand how the work-flow goes. Are these logs normal?
Can anybody give some hints where the bug from going back and forth could be, please?
Thanks!
Here is the code:
ProductDetailsScreen.js
...
const ProductDetailScreen = (props) => {
const [ isFav, setIsFav ] = useState(false);
const dispatch = useDispatch();
const productId = props.navigation.getParam('productId');
const selectedProduct = useSelector((state) =>
state.products.availableProducts.find((prod) => prod.id === productId)
);
const toggleFavoriteHandler = useCallback(
async () => {
setError(null);
setIsFav((prevState) => !prevState);
console.log('isFav inside:', isFav); // On first click I get: false
try {
await dispatch(
productsActions.toggleFavorite(
productId,
isFav,
)
);
} catch (err) {
setError(err.message);
}
},
[ dispatch, productId, isFav setIsFav ]
);
console.log('isFav outside: ', isFav); // On first click I get: false true
return (
<ScrollView>
<View style={styles.icon}>
<TouchableOpacity style={styles.itemData} onPress={toggleFavoriteHandler}>
<MaterialIcons name={isFav ? 'favorite' : 'favorite-border'} size={23} color="red" />
</TouchableOpacity>
</View>
<Image style={styles.image} source={{ uri: selectedProduct.imageUrl }} />
{Platform.OS === 'android' ? (
<View style={styles.button}>
<CustomButton
title="Add to Cart"
onPress={() => dispatch(cartActions.addToCard(selectedProduct))}
/>
</View>
) : (
<View style={styles.button}>
<Button
color={Colours.gr_brown_light}
title="Add to Cart"
onPress={() => dispatch(cartActions.addToCard(selectedProduct))}
/>
</View>
)}
<Text style={styles.price}>€ {selectedProduct.price.toFixed(2)}</Text>
<Text style={styles.description}>{selectedProduct.description}</Text>
</ScrollView>
);
};
ProductDetailScreen.navigationOptions = ({ navigation }) => {
return {
headerTitle: navigation.getParam('productTitle'),
headerLeft: (
<HeaderButtons HeaderButtonComponent={CustomHeaderButton}>
<Item
title="goBack"
iconName={Platform.OS === 'android' ? 'md-arrow-back' : 'ios-arrow-back'}
onPress={() => navigation.goBack()}
/>
</HeaderButtons>
),
headerRight: (
<HeaderButtons HeaderButtonComponent={CustomHeaderButton}>
<Item
title="cart"
iconName={Platform.OS === 'android' ? 'md-cart' : 'ios-cart'}
onPress={() => navigation.navigate({ routeName: 'Cart' })}
/>
</HeaderButtons>
)
};
};
...styles
products.js/actions
export const toggleFavorite = (id, isFav) => {
return async (dispatch) => {
try {
// If it is a favorite, post it.
// Note it is initially false...
if (!isFav) {
const response = await fetch('https://ekthesi-7767c.firebaseio.com/favorites.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id,
isFav
})
});
if (!response.ok) {
throw new Error(
'Something went wrong.'
);
}
const resData = await response.json();
// Note: No `name` property, that's why we use a `for_in` loop
// console.log('POST', JSON.stringify(resData));
dispatch({ type: TOGGLE_FAVORITE, productId: id });
} else if (isFav) {
// First get the key in order to delete it in second fetch(...).
const response = await fetch(`https://ekthesi-7767c.firebaseio.com/favorites.json`);
if (!response.ok) {
throw new Error(
'Something went wrong.'
);
}
const resData = await response.json();
// Note: No `name` property, that's why we use a `for_in` loop
// console.log('fetch', JSON.stringify(resData));
for (const key in resData) {
console.log('resData[key].id', resData[key].id === id);
if (resData[key].id === id) {
await fetch(`https:app.firebaseio.com/favorites/${key}.json`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(
'Something went wrong.'
);
}
// console.log('fetch', JSON.stringify(resData));
dispatch({ type: TOGGLE_FAVORITE, productId: id });
}
}
}
} catch (err) {
// send to custom analytics server
throw err;
}
};
};
ProductsOverviewScreen.js
...
const ProductsOverviewScreen = (props) => {
const [ isLoading, setIsLoading ] = useState(false);
const [ error, setError ] = useState(); // error initially is undefined!
const [ isRefresing, setIsRefresing ] = useState(false);
const dispatch = useDispatch();
const categoryId = props.navigation.getParam('categoryId');
const products = useSelector((state) =>
state.products.availableProducts.filter((prod) => prod.categoryIds.indexOf(categoryId) >= 0)
);
const productId = props.navigation.getParam('productId');
const isFav = useSelector((state) => state.products.favoriteProducts.some((product) => product.id === productId));
const loadProducts = useCallback(
async () => {
setError(null);
setIsRefresing(true);
try {
await dispatch(productsActions.fetchProducts());
} catch (err) {
setError(err.message);
}
setIsRefresing(false);
},
[ dispatch, setIsLoading, setError ]
);
// loadProducts after focusing
useEffect(
() => {
const willFocusEvent = props.navigation.addListener('willFocus', loadProducts);
return () => willFocusEvent.remove();
},
[ loadProducts ]
);
// loadProducts initially...
useEffect(
() => {
setIsLoading(true);
loadProducts();
setIsLoading(false);
},
[ dispatch, loadProducts ]
);
const selectItemHandler = (id, title) => {
props.navigation.navigate('DetailScreen', {
productId: id,
productTitle: title,
isFav: isFav
});
};
if (error) {
return (
<View style={styles.centered}>
<Text>Something went wrong!</Text>
<Button title="Try again" onPress={loadProducts} color={Colours.chocolate} />
</View>
);
}
if (isLoading) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color={Colours.chocolate} />
</View>
);
}
if (!isLoading && products.length === 0) {
return (
<View style={styles.centered}>
<Text>No products yet!</Text>
</View>
);
}
return (
<FlatList
onRefresh={loadProducts}
refreshing={isRefresing}
data={products}
keyExtractor={(item) => item.id}
renderItem={(itemData) => (
<ProductItem
title={itemData.item.title}
image={itemData.item.imageUrl}
onSelect={() => selectItemHandler(itemData.item.id, itemData.item.title)}
>
{Platform.OS === 'android' ? (
<View style={styles.actions}>
<View>
<CustomButton
title="Details"
onPress={() => selectItemHandler(itemData.item.id, itemData.item.title)}
/>
</View>
<BoldText style={styles.price}>€ {itemData.item.price.toFixed(2)}</BoldText>
<View>
<CustomButton
title="Add to Cart"
onPress={() => dispatch(cartActions.addToCard(itemData.item))}
/>
</View>
</View>
) : (
<View style={styles.actions}>
<View style={styles.button}>
<Button
color={Colours.gr_brown_light}
title="Details"
onPress={() => selectItemHandler(itemData.item.id, itemData.item.title)}
/>
</View>
<BoldText style={styles.price}>€ {itemData.item.price.toFixed(2)}</BoldText>
<View style={styles.button}>
<Button
color={Colours.gr_brown_light}
title="Add to Cart"
onPress={() => dispatch(cartActions.addToCard(itemData.item))}
/>
</View>
</View>
)}
</ProductItem>
)}
/>
);
};
ProductsOverviewScreen.navigationOptions = (navData) => {
return {
headerTitle: navData.navigation.getParam('categoryTitle'),
headerRight: (
<HeaderButtons HeaderButtonComponent={CustomHeaderButton}>
<Item
title="cart"
iconName={Platform.OS === 'android' ? 'md-cart' : 'ios-cart'}
onPress={() => navData.navigation.navigate({ routeName: 'Cart' })}
/>
</HeaderButtons>
)
};
};
...styles
State updates are not synchronous. Considering the following:
const [isFav, setIsFav] = React.useState(true);
setIsFav(false); // state update here
console.log(isFav); // isFav hasn't updated yet and won't be `false` until next render
To get the latest state, you need to put your log in useEffect/useLayoutEffect.
From React docs,
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may batch or defer the update until later.
https://reactjs.org/docs/react-component.html#setstate
After the comment of #satya I gave it another try.
Now I get the state of isFav from the redux state.
Namely, I check if the current product is in the favoriteProducts array.
...imports
const ProductDetailScreen = (props) => {
const [ error, setError ] = useState(); // error initially is undefined!
const dispatch = useDispatch();
const productId = props.navigation.getParam('productId');
const selectedProduct = useSelector((state) =>
state.products.availableProducts.find((prod) => prod.id === productId)
);
// HERE !!! I get to see if current product is favorite!
const currentProductIsFavorite = useSelector((state) => state.products.favoriteProducts.some((product) => product.id === productId));
const toggleFavoriteHandler = useCallback(
async () => {
setError(null);
try {
await dispatch(productsActions.toggleFavorite(productId, currentProductIsFavorite));
} catch (err) {
setError(err.message);
}
},
[ dispatch, productId, currentProductIsFavorite, setIsFav ]
);
...
return (
<ScrollView>
<View style={styles.icon}>
<TouchableOpacity style={styles.itemData} onPress={toggleFavoriteHandler}>
<MaterialIcons name={currentProductIsFavorite ? 'favorite' : 'favorite-border'} size={23} color="red" />
</TouchableOpacity>
</View>
<Image style={styles.image} source={{ uri: selectedProduct.imageUrl }} />
...