Cannot Prevent Async on GraphQL Mutation and Recoil State updating process - react-native

I am working on a Chat Room portion of a larger React Native app and am facing issues with the updating of the page after a text has been sent. Currently, when the user compiles a text in a TextInput and hits the send button, it triggers a mutation that is supposed to add a message object to the chatroom model, which is linked to all of the users that are currently in the chatroom. It is then supposed to take the result from this mutation, which is the updated chatroom connected to all the users (the current user included obviously) and render its contents. It is intended to rerender the page after the activeThread atom is updated, since the page used the contents of activeThread to render everything on the page, new messages included. However, this occurs asyncronously and it tries to render a promise.... which you can't do. I've tried everything I'm capable of, using thens and awaits everywhere I could but JavaScript's giving me the middle finger pretty hard on this one. My code is below...
const handleSendMessage = async () => {
console.log(activeThread.id)
if (newMessage.length > 0){
return sendMessage({
variables: {
chatroomId: activeThread.id,
content: newMessage
}
}).then( async (newMessageThread) => {
await setUpdating(true)
await setNewMessage("")
await setKeyboardVisible(false);
await setActiveThread(newMessageThread)
}).then( async() => {
await console.log(activeThread)
await setUpdating(false)
})
}
else{
}
}
setUpdating is part of a useState. This is defaulted to false and when true the main page is not set to render. It is intended as a guard against attempting to render the promise. Didn't work, obviously
setNewMessage is defaulted to "" and is responsible for keeping track of the text the user has entered into the TextInput. Pretty irrelevant here.
setKeyBoardVisible is pretty self explanatory and also not necessary
setActiveThread is the heavy lifter here. Pretty much all of the contents rendered are going to be pulling data from activeThread, which is, again; a recoil state. For example, everything below looks essentially something like
<View>
<Text> {activeThread.someMethodOrValue} </Text>
</View>
I can only assume this has something to do with the async-ing. I have a console.log(error) statement in my backend GraphQL mutation resolver that would catch any errors there, and it's not triggering anything. The error I get everytime is the following...
TypeError: undefined is not an object (evaluating 'activeThread.chatroomName.split')
This error is located at:
in MessageThread (created by SceneView)
in StaticContainer
in EnsureSingleNavigator (created by SceneView)
in SceneView (created by SceneView)
in {/* keeps going down the stack you get the idea */}
[Unhandled promise rejection: TypeError: undefined is not an object (evaluating 'activeThread.chatroomName.split')]
at Pages/CommunicationPage/MessageThread.js:210:37 in MessageThread
Any solutions?

While the code I had still looks like it should work to me, we all know how finnicky code can be sometimes. What ended up working was separating the handleSendMessage function and the mutation, creating a whole new function for the mutation.
My new code looks like this...
const handleSendMessage = () => {
if (newMessage.length > 0){
handleMutation().then( (resolved) => { // This line fixed all the promise issues
setNewMessage("") // clears current message input
let newActiveThread = resolved.data.driverSendMessage.chatroom // creates new thread JSON from mutation data
console.log(newActiveThread) // testing out a different bug now lolllll
setActiveThread(newActiveThread) // Sets current thread to match the new one
// changes the entire user state, leaving all over threads untouched but updating the current one
let updatedThreads = [newActiveThread]
user.chatrooms.forEach( (chat) => {
if (chat.id == newActiveThread.id){
console.log("Skipping " + newActiveThread.chatroomName)
}
else {
updatedThreads.push(chat)
}
})
// changes the main recoil state
setUser({...user, chatrooms: updatedThreads})
})
}
else{
// Throw Error Handling for no input or just do nothing, we'll see
}
}
const handleMutation = async () => {
return sendMessage({
variables: {
chatroomId: activeThread.id,
content: newMessage
}
})
}

Related

React-Native - useEffect causes infinite loop

I am trying to show some dynamic content in my component but somehow useEffect causes a infinite loop.
What can be the problem?
useEffect(() => {
retrieveLocalData('following').then((contacts) => {
setLocalData(JSON.parse(contacts));
});
}, [getLocalData]);
async function retrieveLocalData(key) {
try {
return await AsyncStorage.getItem(key);
} catch (error) {
console.log(error);
}
}
console.log('test'); // infinite
Code: https://codepen.io/eneskul/pen/OJWEgmw
Updated Answer
The infinite loop is a result of the useEffect hook updating the same value that is triggering the hook to run in the first place.
Here's a simple example to illustrate the problem:
const [value, setValue] = useState({ foo: 'bar' });
useEffect(() => {
Promise.resolve('{"foo":"bar"}').then((result) => {
const newValue = JSON.parse(result);
// `newValue` is a new object, even if its content is identical to `value`.
setValue(newValue);
});
}, [value]);
In this example, when value is set, it causes the useEffect hook to execute, which will asynchronously update value with a new object, which will cause the useEffect hook to execute again, and so on. Even though the contents of the objects are identical, the JSON.parse call creates a new object with a new reference.
You can prevent the infinite loop by doing a deep equality check of the two objects before updating the state. Using something like Lodash's isEqual function makes this pretty easy.
useEffect(() => {
Promise.resolve('{"foo":"bar"}').then((result) => {
setValue((prev) => {
const newValue = JSON.parse(result);
// Do a deep comparison and only update state with new object if content is different.
return isEqual(prev, newValue) ? prev : newValue;
});
});
}, [value]);
In this example, the reference to value will only change if the contents of the objects are different.
However, this only explains what the problem is. I'm not sure what the right solution is for your problem, since it's not clear why the component only needs to load data from local storage into state when the state changes, but the state is only updated when it loads from local storage. There seems to be a "chicken or the egg" problem here. It feels like there should be something else that should trigger loading data from local storage into state, other than the data that was just loaded from local storage into state.
Previous Answer
The likely culprit here is getLocalData in the dependency list of the useEffect hook. If that is not a stable reference (i.e. the reference changes on each render), then it will cause the useEffect hook to execute, which will then trigger a state update, which will trigger a render, which will cause useEffect to execute again, which starts the whole thing over again.
In the sample code, it's not clear where getLocalData comes from. Wherever it comes from, you might consider wrapping it with the useCallback hook to create a stable reference. If it's just a typo and meant to be retrieveLocalData, then that is definitely the issue. Because retrieveLocalData is declared inside the component's render function, it will create a new instance of the function (with a new reference) on each render.
I would just move it inside the useEffect hook and eliminate the dependencies.
useEffect(() => {
AsyncStorage.getItem('following')
.then((contacts) => {
setLocalData(JSON.parse(contacts));
})
.catch((error) => {
console.log(error);
});
}, []);

watchEffect with side effect but without infinite loop

what I am trying to do is:
construct an URL based on props
initially and whenever the URL changes, fetch some data
Since this is asynchronous and I also want to indicate loading, I use this construct:
const pageUrl = computed(() => `/api/${props.foo}/${props.bar}`)
const state = reactive({
page: null,
error: null,
loading: false
})
watchEffect(async () => {
state.loading = true
try {
const resp = await axios.get(pageUrl.value)
state.page = resp.data
} catch (err) {
state.error = err
console.log(err)
}
state.loading = false
})
// return page, loading, error for the component to use
The problem is that this seems to run in an infinite loop because in the body, I am not only reacting to the pageUrl, but also to state which itself is modified in the function body.
Alternatively, I can use watch(pageUrl, async pageUrl => { ... }), but this seems only to be triggered when pageUrl changes (in my case: I modify the URL because the props are updated via vue-router, but not when I initially visit the URL).
What should I do here, is my idea of signalling the loading state not appropriate here?
From a logical point of view, the page is a computed value, the only reason I use watch here is that it's asynchronous and might yield an error as well.
Thanks to Husam I got aware of the bug, and it seems like changing a piece of state twice introduces this behaviour - in my case setting loading to true and then again to false.
This behaviour is not apparent in Vue3, and a workaround (and in general maybe the much cleaner method) could be to directly use watch instead of watchEffect.
The source code shows different overloads of the funciton, and there is an options argument that is not directly documented in the Vue3 API. There, I found that my call above needs to read like watch(value, async value => { effect }, { immediate: true }).

Initializing a map in firestore

I'm trying to build an app using react native with a firestore database. I'm fairly new to the react native framework (as well as working with firestore), so it's possible I might be trying to solve this problem the wrong way.
I have a database that works well and is already populated. For each user of this app, I'd like to add a map to their entry. I want to use this map to store some data about the user which they can fill out later.
Here's some code:
componentDidMount() {
this.readProfile(this.props.uid);
}
readProfile = (uid) => {
this.props.getProfile(uid).then((profile) =>
{
if(!profile.userMap)
{
profile.userMap = generateUserMap();
}
...
}
export const generateUserMap = function () {
var map = new Map();
SomeEnum.forEach((key, value) => {
map.set(key, false);
});
AnotherEnum.forEach((key, value) => {
map.set(key, false);
});
OneMoreEnum.forEach((key, value) => {
map.set(key, false);
});
return map;
};
...
<Input
value={this.state.profile.userMap[SomeEnum.Foo]}
onChangeText={(foo) => this.updateUserMap({ foo })}
/>
What I want this code to be doing is to read in the user's profile when I load the page. That part seems to be working fine. My next concern is to properly initialize the map object. The code doesn't seem to be properly initializing the map, but I'm not sure why. Here's why I say that:
TypeError: Cannot read property 'Foo' of undefined
With the stack trace pointing to my component's Connect() method.
Any help would be greatly appreciated.
EDIT: Apologies for the oversight, here is the updateUserMap function:
updateUserMap = (property) => {
const profile = Object.assign({}, this.state.profile, property);
this.setState({ profile });
}
So, as anyone who looks over this question can probably tell, I was doing a few things pretty wrong.
The error I'm getting referred specifically to that input block in my render method - this.state.profile.userMap was undefined at that point. I can guarantee that it won't be undefined if I do my check within the render method but before I'm accessing the userMap. Because of how the lifecycle methods work in react native, ComponentDidMount wouldn't be called before my render method would.
My enum code also wouldn't work. I changed that to a simple for loop and it works like a charm.
Here's my updated code:
render() {
if(!this.state.profile.userMap)
{
this.state.profile.userMap = generateUserMap();
}

React Native Expo: Cannot add child that doesn't have a YogaNode - Error on Android

I am stuck with this error:
Cannot add a child that doesn't have a YogaNode to a parent without a measure function!
(Trying to add a 'ReactRawTextShadowNode' to a 'LayoutShadowNode')
The app is on expo and works fine in iOS
but on Android I always get this error when pressing the button for authentication.
Earlier it was working fine, I tried to reset my commits to track the error but for no help.
I think, whenever this function is executed, the error arises:
onButtonPress = async () => {
const { code } = this.props;
await this.props.loginUser({ code });
if (this.props.error) {
await AsyncStorage.removeItem('code');
this.props.navigation.goBack();
} else {
await AsyncStorage.setItem('code', code);
await this.props.orderUpdate();
await this.props.menuFetch();
this.props.navigation.navigate('main');
}
};
Note that the props are accessing redux state and calling redux actions.
This issue having a different reason:
Might be the comments inside the render method of component so try to remove comments inside render method of component component.
Might be because of that you have not closed a tag correctly
Might be using of && operator inside render method so remove '&&'
operator and use ternary operator.
Instead { foobar && <View/> }
Use this { foobar ? <View/> : null }

Why can't I access state in ComponentDidMount?

I have this code for React Native:
componentWillMount() {
shiftKeys = []; // need to clear previously set data or we will get dupicate array key errors
// get the current user from firebase
const userData = firebaseApp.auth().currentUser;
const profileRef = firebaseApp.database().ref('userdata').child(userData.uid);
profileRef.on('value', snapshot => {
if (snapshot.val().hasOwnProperty('licenseType') && snapshot.val().hasOwnProperty('licenseState') && snapshot.val().hasOwnProperty('distance')) {
this.setState({
licenseType: snapshot.val().licenseType,
licenseState: snapshot.val().licenseState,
distancePref: snapshot.val().distance,
});
console.log('State1', this.state.distancePref)
} else {
// redirect back to profile screens because we need three values above to search.
this.props.navigation.navigate('Onboarding1')
}
});
}
componentDidMount() {
console.log('State2', this.state.distancePref)
var geoQuery = geoFire.query({
center: [45.616422, -122.580453],
radius: 1000// need to set dynamically
});
I think this is some kind of scope issue?
When I look at the console log, State 1 is set correctly, but State 2 prints nothing.
In my app I need to look up a users distance preference, then use that to run a query.
How do I pass the value from componentWillMount to componentDidMount?
https://facebook.github.io/react/docs/react-component.html#the-component-lifecycle
setState in componentWillMount - bad way. You do not solve the problem this way, because state will not be updated until componentDidMount (see lifecycle). Check your condition when creating the state, in the constructor.
Or you can solve the problem using redux.
The root issue with this problem had to do with my not understanding how react and react native render the code.
I was trying to get users info from firebase, then set preferences, then use those preferences to run a search.
I added redux and thunk to handle the getting and saving of the users preferences separately from (and before) the user has a chance to search.