I have a screen that represents a schedule which lets the user give each week a name. Once the user finishes editing all the weeks he will presses the checkmark and the app should update the backend.
I am getting a very weird bug where the first time I click the checkmark edit data gets updated (the log says "editData changed" and the ui changes) but when I print the state inside updateTheDB it has not been updated. If I try to enter edit mode again and save without making any new changes the previous changes are updated inside updateTheDB.
I thought this was a copy by reference not but value problem but I am using JSON.parse(JSON.stringify to copy so that can't be it.
The call to setState (setEditData) is inside onSavePressed which is in a modal that opens when the user tries to name a week.
Does anyone know what could have caused this?
EDIT
I want the to cause a render when setEditToServer is called
This is my code:
const Schedule = (props: IPorps) => {
const { week_names_props, navigation } = props;
const days = ['s', 'm', 't', 'w', 't', 's', 'w'];
//bottom Modal
const [active_modal, setActiveModal] = useState<MProps>(null)
//data
const [serverData, setServerData] = useState<IData>({ weekNames: week_names_props, weekEvents: {} })
//edit_mode data
const [editData, setEditData] = useState<IData>(null)
//other
const [isLoading, setIsLoading] = useState<boolean>(false)
const [editable, setEditable] = useState<boolean>(false)
const doTheLoadingThingy = (): void => {
Axios.get(`api/weeks/getWeekNameByCompanyId`, {
params: {
company_id: 1,
},
}).then((response) => {
setServerData({ ...serverData, weekNames: response.data })
//useEffect will hide the loading and the modal
})
.catch(() => { setIsLoading(false); errorToast("get") })
}
const setEditToServer = () => {
console.log("setEditToServer"
setEditData({
weekNames: JSON.parse(JSON.stringify(serverData.weekNames)),
weekEvents: {}
})
}}
useEffect(() => {
setEditToServer()
setIsLoading(false)
}, [serverData])
useEffect(() => {
console.log("editData changed")
}, [editData])
const updateTheDB = () => {
setIsLoading(true)
console.log(editData)
//send to backend
}
useEffect(() => {
navigation.setOptions({
headerLeft: () => (
<View style={{ flexDirection: "row", justifyContent: "space-around", alignItems: "center", paddingHorizontal: 30 }}>
{editable ? (
<>
<Icon
name={"check"}
onPress={() => {
updateTheDB()
setEditable(false)
}}/>
<Icon
name={"cancel"}
onPress={() => {
console.log("cancel")
setEditToServer()
setEditable(false)
}}/>
</>
) : (
<Icon
name={'edit'}
onPress={() => setEditable(true)}/>
)}
</View>
)
})
}, [editable])
return (
<>
{isLoading ?
(<ActivityIndicator size="large" />) :
(
<>
<BottomModal
innerComponent={active_modal ? active_modal.modal : null}
innerComponentProps={active_modal ? active_modal.props : null}
modalStyle={active_modal ? active_modal.style : null}
modalVisible={(active_modal != null)}
onClose={() => {
setActiveModal(null);
}}
/>
<WeekDaysHeader />
{editData ?
(<FlatList
data={editData.weekNames}
keyExtractor={(item) => item.week.toString()}
style={styles.flatListStyle}
// extraData={editData?editData.weekEvents:null}
renderItem={({ item }) => {
return (
<Week
days={days}
events={editData.weekEvents[item.week.toString()]}
dayComponents={ScheduleScreenDay}
week_name={item.week_name ? item.week_name : null}
week_number={Number(item.week)}
onHeaderPressed={editable ?
(week_number, week_title) => {
console.log("Pressed", week_number, week_title)
setActiveModal({
props: {
week_number_props: week_number,
week_title_props: week_title,
onSavePressedProps: (new_name) => {
if (new_name) {
let tmp = JSON.parse(JSON.stringify(editData.weekNames))
const i = tmp.findIndex((item) => item.week.toString() === week_number.toString())
tmp[i].week_name = new_name
setEditData((prev) => ({ weekNames: tmp, weekEvents: prev.weekEvents }))
}
}
}, modal: NameWeekModalComponet,
style: styles.weekNameModalStyle
});
} : undefined}
/>
);
}}
/>) : null}
</>
)
}
</>)
};
export default Schedule;
I thought this was a copy by reference not but value problem but I am using JSON.parse(JSON.stringify) to copy so that can't be it.
Thats exactly the problem:
// Always true
JSON.parse(JSON.stringify(['a'])) !== JSON.parse(JSON.stringify(['a']))
Therefore whenever setEditToServer is called the component will re-render, that's because React makes a shallow comparison with the previous state when deciding for render.
const setEditToServer = () => {
// Always re-render
setEditData({
weekNames: JSON.parse(JSON.stringify(serverData.weekNames)),
weekEvents: {}
})
}}
The problem was in this useEffect:
useEffect(() => {
navigation.setOptions({
headerLeft: () => (
//...
)
})
}, [editable])
The functions inside it used the editData state but the useEffect didn't have it in the deps list so when the onPress was clicked it got the old state of editData. The solution was to add editData to the deps list. Like this:
useEffect(() => {
navigation.setOptions({
headerLeft: () => (
//...
)
})
}, [editable, editData])
Related
I got an error :
index.js:1 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 the componentWillUnmount method.
And I have been trying to find what makes that error and I found the thing that makes an error.
so I tried to search how to avoid this error in this case. but I couldn't find it.
so The problem is when I upload the csv file and then the file contains info state.
so I show this file information on my website.
And when the file is uploaded then the component is changing
So I used it with the ternary operator. So I tried to remove the ternary operator then the error had disappeared I assumed that it made the error .
So I'm trying to fix it but I can't figure it out
here is my code :
const CsvShowData = ({ info, setInfo }) => {
return (
//
<>
{info.length !== 0 ? (
<DataTable>
{info.slice(0, 1).map(inf => (
<MainRow key={inf}>
{inf.map((d, index) => (
<Row key={index}>
<div className="titleRow">
<h3>{d}</h3>
</div>
</Row>
))}
</MainRow>
))}
{info.slice(1, 10).map((a, key) => (
<MainRow key={key}>
{a.map((b, idx) => (
<Row key={idx}>
<div className="sideRow">
<p>{b}</p>
</div>
</Row>
))}
</MainRow>
))}
</DataTable>
) : (
<CsvTable>
<CsvFileReader info={info} setInfo={setInfo} />
</CsvTable>
)}
</>
);
};
Thank you in advance!
CsvFileReader Component
const CsvFileReader = ({ setInfo }) => {
const handleOnDrop = data => {
const infos = data.map(item => item.data);
setTimeout(() => setInfo([...infos]), 1000); // save timeout ref
};
const handleOnError = (err, file, inputElem, reason) => {
console.log(err);
};
const handleOnRemoveFile = data => {
console.log(data);
};
return (
<>
<MainReader>
<CSVReader
onDrop={handleOnDrop}
onError={handleOnError}
config={
(({ fastMode: true }, { chunk: "LocalChunkSize" }),
{ header: false })
}
addRemoveButton
onRemoveFile={handleOnRemoveFile}
>
You should use a ref to save setTimeout and remove setInfo when component is unmounted.
const ref = useRef();
const handleOnDrop = (data) => {
const infos = data.map((item) => item.data);
ref.current = setTimeout(() => setInfo([...infos]), 1000); // save timeout ref
};
useEffect(() => {
return () => {
if (ref.current) {
clearTimeout(ref.current);
}
};
});
Still with my react project, now I'm learning the hooks, perhaps I have an issue with the 'infinite loop' (Maximum update depth exceeded) and I can't figure out how to handle this. I have redux to handle the states. I used useEffect, because when I clicked on a div, it was showing, or did what I wanted, always one step late
function OffersWhatTypeOfWebsiteComponent(props) {
const dispatch = useDispatch()
const [active, setActive] = useState(1)
const [typeWebSite, setTypeWebSite] = useState('withCMS')
const updateTypeWebSite = () => {
dispatch({
type: "TYPE_WEBSITE",
payload: typeWebSite
})
}
useEffect(() => {
updateTypeWebSite();
}, [updateTypeWebSite()]);
const renderElements = (props) => {
switch (active) {
case 1 :
return (
<>
<OffersChooseYourPackageCMSComponent
/>
</>
);
break
case 2 :
return (
<>
<OffersChooseYourPackageLikeAProComponent/>
</>
)
default:
return 'error'
}
}
return (
<div>
<OffersTitleCardComponent
titleNumber='2'
titleSection='What type of Website'
/>
<div className='chooseYourProject'>
<OffersCardsWithCheckComponent
titleCard='With CMS'
subtitleCard='xxxx'
active={active === 1 ? 'listing-active' : 'listing'}
onClick={() => {
setActive(1);
setTypeWebSite('withCMS');
updateTypeWebSite()
}}
/>
<OffersCardsWithCheckComponent
titleCard='Like a pro'
subtitleCard='xxx'
active={active === 2 ? 'listing-active' : 'listing'}
onClick={() => {
setActive(2);
setTypeWebSite('like a pro');
updateTypeWebSite()
}}
/>
</div>
{
renderElements({})
}
</div>
);
}
export default OffersWhatTypeOfWebsiteComponent;
This is the sub-component :
function OffersChooseYourPackageCMSComponent(props) {
const dispatch = useDispatch()
const [active, setActive] = useState(1)
const [packageWebSite, setPackageWebSite] = useState('Shopify')
const [pricePackageWebSite, setPricePackageWebSite] = useState(300)
const updatePackageWebSite = () => {
dispatch({
type: "PACKAGE_WEBSITE",
payload: {packageWebSite, pricePackageWebSite}
})
}
useEffect(() => {
updatePackageWebSite();
}, [updatePackageWebSite()]);
const renderElements = () => {
switch (active) {
case 1 :
return (
<>
<OffersNumbersOfProductsComponent/>
</>
);
break
case 2 :
return (
<>
<OffersNumbersOfPagesComponent/>
<OffersWoocommerceComponent/>
</>
);
break
default :
return 'error'
}
}
return (
<div>
<OffersTitleCardComponent
titleNumber='3'
titleSection='Choose your package'
/>
<div className="shopify">
<OffersCardsWithCheckComponent
onClick={() => {
setActive(1);
setPackageWebSite('Shopify');
setPricePackageWebSite(300);
updatePackageWebSite()
}}
active={active === 1 ? "listing-active" : "listing"}
titleCard='Shopify'
subtitleCard='xxx'
pricePackage='$54349'
detailPrice='(1 landing page + up to 5 products)'
/>
<OffersCardsWithCheckComponent
onClick={() => {
setActive(2);
setPackageWebSite('WordPress');
setPricePackageWebSite(900);
updatePackageWebSite()
}}
active={active === 2 ? "listing-active" : "listing"}
titleCard='WordPress'
subtitleCard='xxxx'
pricePackage='$23349'
detailPrice='(1 landing page)'
/>
</div>
{renderElements()}
</div>
);
}
export default OffersChooseYourPackageCMSComponent;
Don't hesitate to tell me some good practices too, on what I should arrange also if needed.
Thanks for your help
You should replicate this into your sub-component as well
const updateTypeWebSite = useCallback(() => {
dispatch({
type: "TYPE_WEBSITE",
payload: typeWebSite
})
}, [typeWebSite])
useEffect(() => updateTypeWebSite(), [updateTypeWebSite]);
Read this at reactjs documentation
Found something that worked, don't know if it's the best solutuion :
const [typeWebSite, setTypeWebSite] = useState('withCMS')
const updateTypeWebSite = () => {
dispatch({
type: "TYPE_WEBSITE",
payload: typeWebSite
})
}
useEffect(() => {
updateTypeWebSite()
},[typeWebSite,setTypeWebSite]);
so I am trying to update my parent component whenever I scroll to the bottom. When I do scroll to the bottom, I am calling fetchData, which is in the parent component and is a dependency in the useEffect, however, the component does not update, leaving me with data from the first component render.
The first 7 sets of data is fetched successfully, and when I scroll to the bottom, fetchData is called again, however the component does not update, however when I then click save on the parent component, then the next sets of data is successfully loaded and displayed to the screen as it should. I feel I am doing something wrong i'm just clueless as to what it is. Any help would be very much appreciated
This is the parent component:
function Following({route, navigation}) {
const [followingData, setfollowingData] = useState();
const [loading, setLoading] = useState(true);
const [lastVisible, setLastVisible] = useState(0);
const fetchData = useCallback(() => {
const dataRef = firestore().collection('usernames');
dataRef
.doc(route.params.username.toLowerCase())
.collection('Following')
.orderBy('followedAt')
.startAfter(lastVisible)
.limit(7)
.onSnapshot((snapshot) => {
const last = snapshot.docs[snapshot.docs.length - 1];
setLastVisible(last.data().followedAt);
let promises = [];
snapshot.forEach((doc) => {
const data = doc.data();
promises.push(
data.path.get().then((res) => {
const userData = res.data();
return {
profileName: doc.id ? doc.id : null,
displayName: userData.displayName ? userData.displayName : null,
followerCount:
userData.followers !== undefined ? userData.followers : 0,
followingCount:
userData.following !== undefined ? userData.following : 0,
imageUrl: userData.imageUrl ? userData.imageUrl : null,
};
}),
);
});
Promise.all(promises).then((res) => {
setfollowingData(res);
});
setLoading(false);
});
}, []);
useEffect(() => {
const dataRef = firestore().collection('usernames');
const cleanup = dataRef
.doc(route.params.username.toLowerCase())
.collection('Following')
.onSnapshot(fetchData);
return cleanup;
}, [route.params.username, fetchData]);
return (
<>
{loading ? (
<ActivityIndicator size="large" color="black" />
) : (
<>
<FolloweringScreens
data={followingData}
screen={'Following'}
username={route.params.username}
navigation={navigation}
fetchData={fetchData}
/>
</>
)}
</>
);
}
export default Following;
And the child component is:
function FolloweringScreens({
data,
screen,
username,
navigation,
fetchData
}) {
return (
<>
<FlatList
scrollEnabled={true}
onEndReachedThreshold={0}
onEndReached={fetchData}
data={data}
keyExtractor={(i, index) => index.toString()}
renderItem={({item, index}) => {
return (
<>
(`All my data being used here`)
</>
);
}}
/>
</>
);
}
export default FolloweringScreens;
I'm getting the data from the database and show it in a FlatList. Whenever I add or remove something from the data the data isn't showing correctly in the FlatList.
Whenever I remove something it shows an empty list.
Whenever I add something it only shows the newly added data - nothing else.
I'm using firebase realtime database and use the data I get as follows:
firebase.database().ref(`/wordlists/${editKey}`).on('value', snap => {
if (snap.val() !== null) {
setIsLoading(false);
const val = snap.val().words;
const data = [];
Object.keys(val).forEach(key => {
data.push({ key, word: val[key].word });
})
setWords(data);
// setWords([...data]) doesn't work either.
}
})
My Flatlist looks like this:
<FlatList
data={words}
renderItem={renderItem}
keyExtractor={item => item.key}
extraData={words}
/>
When I console.log() the data I always get the data I want to show but the FlatList just won't show it correctly.
It also doesn't work when I use the spread-operator and/or extraData.
Because someone asked for it here is the entire file (I left out the styling and the imports)
const EditList = ({ editKey }) => {
const [wordlist, setWordlist] = useState(0);
const [refresh, setRefresh] = useState(false);
const [words, setWords] = useState([]);
const [wordLoading, setWordLoading] = useState({ loading: false });
const [loading, setIsLoading] = useState(false);
const [btnLoading, setBtnLoading] = useState(false);
const [word, setWord] = useState('');
useEffect(() => {
if (editKey !== 0) {
setIsLoading(true);
firebase.database().ref(`/wordlists/${editKey}`).on('value', snap => {
if (snap.val() !== null) {
setIsLoading(false);
setWordlist({...snap.val()});
const val = snap.val().words;
const data = [];
Object.keys(val).forEach(key => {
data.push({ key, word: val[key].word });
})
setWords([...data]);
setRefresh(!refresh);
console.log(data, 'DATA');
}
})
}
}, [editKey])
const onAdd = () => {
setBtnLoading(true);
firebase.database().ref(`/wordlists/${editKey}/words`).push({ word })
.then(() => {
setBtnLoading(false);
setWord('');
setRefresh(!refresh);
})
}
const onDelete = (key) => {
setWordLoading({ key, loading: true });
firebase.database().ref(`/wordlists/${editKey}/words/${key}`).remove().then(() => {
setWordLoading({ loading: false });
setRefresh(!refresh);
});
}
const renderItem = ({ item }) => (
<ItemWrapper>
<ItemWord>{ item.word }</ItemWord>
<DeleteView onPress={() => onDelete(item.key)}>
{ wordLoading.loading && wordLoading.key === item.key ?
<ActivityIndicator size="small" /> :
<DIcon name="trash-2" size={24} />
}
</DeleteView>
</ItemWrapper>
)
const createData = (words) => {
const data = [];
if (typeof words !== 'undefined') {
Object.keys(words).forEach(key => {
const obj = { key, word: words[key].word };
data.push(obj);
})
}
console.log(data, 'DATADATADATA');
return data;
}
if (editKey === 0) {
return (
<NokeyWrapper>
<NoKeyText>No list selected...</NoKeyText>
</NokeyWrapper>
)
}
if (loading) {
return (
<NokeyWrapper>
<ActivityIndicator size="large" />
</NokeyWrapper>
)
}
return (
<Wrapper
behavior={Platform.OS == "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === 'ios' && 180}
>
<WordListName>{wordlist.listName}</WordListName>
<FlatListWrapper>
<FlatList
data={words}
renderItem={renderItem}
keyExtractor={item => item.key}
//extraData={refresh}
extraData={words}
/>
</FlatListWrapper>
<AddWordWrapper>
<SInput value={word} onChangeText={(text) => setWord(text)} />
<Button onPress={() => onAdd()} loading={btnLoading}>
<Feather name="plus" size={24} color="black" />
</Button>
</AddWordWrapper>
</Wrapper>
)
};
export default EditList;
u need to useRef for this instance because the new 'words' is not inside the .on('value') call.
const [words, _setWords] = useState([]);
const wordRef = useRef(words)
//function to update both wordRef and words state
const setWords = (word) => {
wordRef = word
_setWords(word)
}
useEffect(() => {
if (editKey !== 0) {
setIsLoading(true);
let data = wordRef //create a temp data variable
firebase.database().ref(`/wordlists/${editKey}`).on('value', snap => {
if (snap.val() !== null) {
setIsLoading(false);
setWordlist({...snap.val()});
const val = snap.val().words;
Object.keys(val).forEach(key => {
data.push({ key, word: val[key].word });
})
setWords(data);
setRefresh(!refresh);
console.log(data, 'DATA');
}
})
return () => firebase.database().ref(`/wordlists/${editKey}`).off('value') // <-- need to turn it off.
}
}, [editKey, wordRef])
You probably need to change setRefresh etc with the same method if they are not refreshing.
After a lot more tries I found out the problem was somewhere else. Somehow using 'flex: 1' on my in my renderItem() was causing this issue. I actually found this issue also on github: GitHub React Native issues
So after removing 'flex: 1' from the element everything was showing as expected.
// before
const renderItem = ({ item }) => (
<ItemWrapper style={{ flex: 1, flexDirection: row }}>
<ItemWord>{ item.word }</ItemWord>
</ItemWrapper>
)
// after
const renderItem = ({ item }) => (
<ItemWrapper style={{ width: '100%' }}>
<ItemWord>{ item.word }</ItemWord>
</ItemWrapper>
)
So I have a problem, I've been stuck on for a couple of hours. My state doesn't get updated inside a function. As you can see in my example I have a useState hook which is responsible for keeping the value of the text input. Let's say I type in 'abcd', if i console log the state in the handleChange and outside of it just before return, the state shows correctly, however on the handleHeaderRightButtonPress which is responsible for the saving functionality basically, it doesn't update, it's always my default value, in this case randomVal. Any ideeas why this behaviour could happen or how could i troubleshoot it? Thanks in advance:
My example(I stripped out unnecessary code so it's easier)
const TextAreaScreen = ({ navigation, route }) => {
const placeholder = route.params?.placeholder;
const [value, setValue] = useState('randomval');
useEffect(() => {
navigation.setOptions({
title: route.params?.title,
headerRight: () =>
<NavigationHeader.TextButton
label={t('general.done')}
onPress={handleHeaderRightButtonPress}
/>
});
}, []);
const handleChange = (value: string) => {
console.log('here', value); //the updated value shows up correctly
setValue(value);
};
const handleHeaderRightButtonPress = () => {
const onFinish = route.params?.onFinish;
console.log('value in handleFunc', value); // the updated values does NOT work here
onFinish(value);
navigation.goBack();
};
console.log('state val::', value); // updated value shows up correctly
return (
<TextArea
value={value}
placeholder={placeholder}
onChangeText={handleChange}
/>
);
};
export default TextAreaScreen;
Pass value to useEffect like :
useEffect(() => {
navigation.setOptions({
title: route.params?.title,
headerRight: () =>
<NavigationHeader.TextButton
label={t('general.done')}
onPress={handleHeaderRightButtonPress}
/>
});
}, [value]);
Just noticed I wasn't updating my useEffect with the value. Fixed by adding it as a dependency in the array:
useEffect(() => {
navigation.setOptions({
title: route.params?.title,
headerLeft: () => {
const onBackPress = () => {
navigation.goBack();
};
return Platform.select({
ios: (
<NavigationHeader.TextButton
label={t('general.cancel')}
onPress={onBackPress}
/>
),
android: (
<NavigationHeader.IconButton
iconName="times"
label={t('general.cancel')}
onPress={onBackPress}
/>
)
});
},
headerRight: () =>
Platform.select({
ios: (
<NavigationHeader.TextButton
label={t('general.done')}
onPress={handleHeaderRightButtonPress}
/>
),
android: (
<NavigationHeader.IconButton
iconName="check"
label={t('general.done')}
onPress={handleHeaderRightButtonPress}
/>
)
})
});
}, [value]); // here