todos not loading while using AsyncStorage - javascript

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);
}
};

Related

Close accordions on radio button click

I have an accordion component in my React Native app which is from galio framework . I have populated it with api data. The accordion closes if you click in the title but I want it to close when I select a radio button. Here is my code:
const Step3 = () => {
const [questions, setQuestions] = useState([]);
const [answers, setAnswers] = useState([]);
const [icon, setIcons] = useState([]);
const [iconColor, setIconsColor] = useState([])
const [refreshing, setRefreshing] = useState(true);
const getQuestions = async () => {
const locale = i18next.language; // TODO: get current locale
const response = await apiStandarts.get(`/questions?locale=${locale}`, {
params: { active: 1, _sortId: [1,2] , _sort: "sortId:ASC"},
});
setRefreshing(false)
setQuestions(response.data);
};
const isOptionSelected = (option) => {
const answer = answers[option.question];
if (answer) {
return option.id == answer.id;
}
return false;
};
const questionIcon = async () => {
const response = await apiStandarts.get(`/commitments-icons`);
setIcons(response.data)
}
const questionIconColor = async () => {
const response = await apiStandarts.get(`/commitments`);
setIconsColor(response.data)
}
const objectMap = (obj, fn) =>
Object.fromEntries(
Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)])
);
const newAnswers = objectMap(answers, (item) => {
return [item.id, item.description];
});
// useEffect(() => {
// questionIcon();
// }, []);
useEffect(() => {
questionIcon();
getQuestions();
}, []);
const OptionList = (groupOption) => {
return (
groupOption.options.map((item, index) => {
const clickedRadio = () => {
const selectedOption = { [item.question]: { ...item } };
setAnswers({ ...answers, ...selectedOption });
};
let status = isOptionSelected(item) ? true : false;
return (
<Radio
initialValue={status}
label={item.description}
onChange={() => clickedRadio()}
color="rgba(0,0,0,.54)"
radioInnerStyle={{backgroundColor: "#3671a6"}}
labelStyle={ styles.label}
containerStyle={{ width: 300, padding: 5 }}
/>
);
})
);
};
return (
<View style={styles.container}>
<Text style={{ fontWeight: "bold", fontSize: 12, color: "#6B24" }}>
{t("Choose an option/Scroll for more questions")}
</Text>
<FlatList
data={questions}
keyExtractor={(result) => result.id.toString()}
contentContainerStyle={{ padding: 5, paddingBottom: 5 }}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={getQuestions} />}
renderItem={({ item, index }) => {
const arr = [item.commitments[0].commitment_icon];
const questIcon = arr.filter(i => Boolean(i)).map(id => icon.find(o => o.id === id)?.image?.url);
const imgUrl = APIURL + questIcon;
function iconBgColor(){
let bgColor
switch (item.commitments[0].commitment_icon) {
case 1:
bgColor="#78bad3"
break;
case 3:
bgColor = "#027a95"
break;
case 6:
bgColor = "#027a95"
break;
case 4:
bgColor = '#1fc191'
break;
case 5:
bgColor = '#78bad3'
break;
case 2:
bgColor = "#e4da4d"
break;
case 7:
bgColor = "#1fc191"
break;
default:
bgColor= "#fff"
break;
}
return bgColor;
}
const backgroundColor = iconBgColor(item.commitments[0].commitment_icon)
const data = [
{
title: (<>
<View style={[styles.iconWrapper,{backgroundColor: backgroundColor}]}>
{
imgUrl.indexOf('.svg') > 0 ? <SvgUri uri={APIURL + questIcon} height={20} width={20} style={styles.iconColor}/> : null
}
</View>
<Text style={styles.text}>{item.sortId}.</Text>
<Text style={styles.text} key={item.description}>{item.description}</Text>
</>),
content:<View><OptionList key={item?.question_options.id} options={item?.question_options}></OptionList></View>
},
];
return (
<View style={styles.groupOptions} key={index}>
<Accordion style={styles.accordion} headerStyle={styles.headerStyle} contentStyle={styles.contentStyle} dataArray={data} opened={index} />
</View>
);
}}
/>
</View>
);
};
Any ideas how to achieve what I want? Any answer would be appreciated, thanks.
you have a next code:
<Accordion
style={styles.accordion}
headerStyle={styles.headerStyle}
contentStyle={styles.contentStyle}
dataArray={data}
opened={index} // here you should have "isAccordionOpen"
/>
and handle a variable in state your Step3 component.
In this function you should to change this variable "isAccordionOpen"
const clickedRadio = () => {
const selectedOption = { [item.question]: { ...item } };
setAnswers({ ...answers, ...selectedOption });
}
As I have checked in the docs and Galio Library there seems no prop to manage Accordion after the first render cycle.
We can only manage the Accordion initially on the first render.
If you have to manage it then you have to make changes in the Galio library code.
Here I am attaching the Sample code base, Hoping that it might help you.
Sample Code:
import React, { useState } from 'react'
import { View } from 'react-native'
import { Accordion, Block, Checkbox } from 'galio-framework';
const App = () => {
const [openIndex, setOpenIndex] = useState(-1)
const radioClickHandler = (id, status) => {
setOpenIndex(-1)
}
const data = [
{
title: "First Chapter",
content: (
<Checkbox
onChange={radioClickHandler.bind(null, 'first')}
color="primary"
label="Primary Checkbox"
/>
)
},
{
title: "Second Chapter",
content: (
<Checkbox
onChange={radioClickHandler.bind(null, 'second')}
color="primary"
label="Secondary Checkbox"
/>
)
}
]
const onOpen = (prop) => {
setOpenIndex(prop?.title === "First Chapter" ? 0 : 1)
}
return (
<View style={{ flex: 1, justifyContent: 'center' }}>
<Block style={{ height: 200 }}>
<Accordion
dataArray={data}
opened={openIndex}
onAccordionOpen={onOpen}
/>
</Block>
</View>
)
}
export default App
Here are the changes in the library code to manage the collapse Accordion Component.
file Path: 'node_modules/galio-framework-src-Accordion.js'
Add Below Code
const [selected, setSelected] = useState(opened);
useEffect(() => {
setSelected(opened)
}, [opened])

UseState not update when using alongside with redux dispatch in arrow function

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

Re rendering a component with an async function inside

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]);

How to change input value using setState

i'm building a simple todo app, and i need to change the input value ( to edit tasks ). I tried to make it like in the react native docs :
export default function UselessTextInput() {
const [value, onChangeText] = React.useState('Useless Placeholder');
return (
<TextInput
style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
onChangeText={text => onChangeText(text)}
value={value}
/>
);
}
But in my code i have my input inside map function and it displays an error : "undefined is not a function ( near '...todos.tasks.map...')
Can anyone explain me why do I get this error and how to solve it ?
My code :
const App = () => {
const[todos,setTodos] = useState({
tasks: [],
task: '',
key: ''
})
const addItem = () => {
if(todos.task != '' && todos.task != null){
setTodos({
tasks: todos.tasks.concat(todos.task),
task: ''
})
console.log(todos.tasks)
}
else {
Alert.alert('OOPS!', 'You need to fill in input' , [{
text: 'Understood'
}])
}
}
const removeItem = arg => {
const list = todos.tasks;
list.splice(arg,1)
setTodos({tasks: list})
}
const handleInputTextChange = (newText) => {
setTodos({
tasks: newText
})
}
return (
<ScrollView keyboardShouldPersistTaps='handled'>
<View style={styles.container}>
<View style = {styles.header}>
<Text style = {styles.title}>Todos</Text>
</View>
<View style={styles.content}>
<TextInput
style = {styles.input}
placeholder = "Type new item"
value = {todos.task}
onChangeText = {e => setTodos({...todos, task: e, key: Date.now()})}
/>
<ButtonSubmit text = 'Submit' onPress = {addItem}/>
{
todos.tasks.map((item, index) => {
return(
<TouchableOpacity>
<View style = {styles.Wrapper} key = {todos.key}>
<View style = {styles.taskWrapper}>
<TextInput style = {styles.task} id = {todos.key} value = {item} onChangeText={handleInputTextChange} />
</View>
<ButtonRemove onPress = {() => removeItem(index)} />
</View>
</TouchableOpacity>
)
})
}
</View>
</View>
</ScrollView>
);
}
You are overwriting your tasks array with the input value and then you get an error when trying to map tasks that is a string and not an array anymore.
Try this:
const App = () => {
const[todos,setTodos] = useState({
tasks: [],
task: '',
key: ''
})
const addItem = () => {
if(todos.task != '' && todos.task != null){
setTodos({
tasks: todos.tasks.concat(todos.task),
task: ''
})
console.log(todos.tasks)
}
else {
Alert.alert('OOPS!', 'You need to fill in input' , [{
text: 'Understood'
}])
}
}
const removeItem = arg => {
const list = todos.tasks;
list.splice(arg,1)
setTodos({tasks: list})
}
const handleInputTextChange = (newText, index) => {
setTodos((s) => {
...s,
tasks: s.tasks.map((t, i) => i === index ? newText : t)
})
}
return (
<ScrollView keyboardShouldPersistTaps='handled'>
<View style={styles.container}>
<View style = {styles.header}>
<Text style = {styles.title}>Todos</Text>
</View>
<View style={styles.content}>
<TextInput
style = {styles.input}
placeholder = "Type new item"
value = {todos.task}
onChangeText = {e => setTodos({...todos, task: e, key: Date.now()})}
/>
<ButtonSubmit text = 'Submit' onPress = {addItem}/>
{
todos.tasks.map((item, index) => {
return(
<TouchableOpacity>
<View style = {styles.Wrapper} key = {todos.key}>
<View style = {styles.taskWrapper}>
<TextInput style = {styles.task} id = {todos.key} value = {item} onChangeText={value => handleInputTextChange(value, index)} />
</View>
<ButtonRemove onPress = {() => removeItem(index)} />
</View>
</TouchableOpacity>
)
})
}
</View>
</View>
</ScrollView>
);
}
Also, check your code for using key prop as it appears to be problematic. You should never use Date.now() as a key. Check React docs:
https://reactjs.org/docs/lists-and-keys.html

Why the state of a useState snaps back to false if I go back and forth on the Screen?

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 }} />
...

Categories

Resources