Reset useLazyQuery after called once - react-native

I'm using useLazyQuery to trigger a query on a button click. After the query is called once, the results (data, error, etc) are passed to the component render on each render. This is problematic for example when the user enters new text input to change what caused the error: the error message keeps reapearing. So I would like to "clear" the query (eg. when user types new data into TextInput) so the query results return to there inital state (everything undefined) and the error message goes away.
I can't find any clear way to do this in the Apollo docs, so how could I do that?
(I though of putting the query in the parent component so it does not update on each rerender, but I'd rather not do that)
This is how I have my component currently setup:
import { useLazyQuery } from 'react-apollo'
// ...
const [inputValue, setInputValue] = useState('')
const [getUserIdFromToken, { called, loading, data, error }] = useLazyQuery(deliveryTokenQuery, {
variables: {
id: inputValue.toUpperCase(),
},
})
useEffect(() => {
if (data && data.deliveryToken) {
onSuccess({
userId: data.deliveryToken.vytal_user_id,
token: inputValue,
})
}
}, [data, inputValue, onSuccess])
// this is called on button tap
const submitToken = async () => {
Keyboard.dismiss()
getUserIdFromToken()
}
// later in the render...
<TextInput
onChangeText={(val) => {
setInputValue(val)
if (called) {
// clean/reset query here? <----------------------
}
})
/>

Thanks #xadm for pointing out the solution: I had to give onCompleted and onError callbacks in useLazyQuery options, and pass the variables to the call function, not in useLazyQuery options. In the end the working code looks like this:
const [inputValue, setInputValue] = useState('')
const [codeError, setCodeError] = useState<string | undefined>()
const [getUserIdFromToken, { loading }] = useLazyQuery(deliveryTokenQuery, {
onCompleted: ({ deliveryToken }) => {
onSuccess({
userId: deliveryToken.vytal_user_id,
token: inputValue,
})
},
onError: (e) => {
if (e.graphQLErrors && e.graphQLErrors[0] === 'DELIVERY_TOKEN_NOT_FOUND') {
return setCodeError('DELIVERY_TOKEN_NOT_FOUND')
}
return setCodeError('UNKNOWN')
},
})
const submitToken = () => {
Keyboard.dismiss()
getUserIdFromToken({
variables: {
id: inputValue
},
})
}

Related

How to clean up React-Native useEffect with axios

Currently I have defined in a functional component a useEffect as below
useEffect(() => {
(async function () {
posts.current = await BlogConsumer.getBlogPosts();
setLoading(false);
})();
return () => {
BlogConsumer.call_controller.abort();
};
}, []);
where this BlogConsumer is defined as below
class BlogConsumer {
static posts = {};
static call_controller = new AbortController();
static async getBlogPosts() {
await axios
.get('https://nice.api', {
signal: this.call_controller.signal,
})
.then(response => {
// treatment for success
})
.catch(error => {
// treatment for erros
});
return this.posts;
}
}
export default BlogConsumer;
The overral ideia is that in the render of the component I'll be calling a static method from my consumer and will retrieve the necessary data. For the pourpuse of not having memory leaks, I have my callback function in my useEffect that will abort my call whenever I unmount the component, but this is not working. React's message of Warning: Can't perform a React state update on an unmounted component. still appears if I enter the component and leave the screen before the API call is finished. I don't know where I am wrong, so I'd like a little help.
Thanks in advance.
You could just cancel the request on unmount. Like this:
export const fetchData = async (signal) => {
try {
const res = await instance.get("/pages/home", {
signal,
});
return res.data;
} catch (error) {
return Promise.reject(error);
}
};
useEffect(() => {
const controller = new AbortController();
fetchData(controller.signal);
return () => {
controller.abort()
};
}, []);

State not updating from useEffect hook - Cannot read property 'title' of undefined

I am trying to update the state in my component using useEffect
useEffect(() => {
async function fetchData() {
let response = await getAccountCoverTypes();
setData(response);
}
fetchData();
}, []);
I am setting my default value:
const [data, setData] = useState({
payload: { title: "", accountCoverTypes: [] },
And trying to map through the data
const {
payload: { title, accountCoverTypes },
} = data;
{accountCoverTypes.map((button, index) => (
...
))}
When I try and use the data - I get the error, Cannot read property title of undefined. How can update the state from useEffect?
My service call:
const call = async () => {
try {
const response = await getItem(xxx);
return response
}
json returned:
{
"payload": {
"title": "text here",
"accountCoverTypes": [
{
"title": "sample text",
"details": "sample text",
"link": "test"
},..
]
}
}
Here you go with a solution
const [data, setData] = useState(null);
useEffect(() => {
async function fetchData() {
let response = await getAccountCoverTypes();
setData(response);
}
fetchData();
}, []);
{
data &&
data.accountCoverTypes &&
data.accountCoverTypes.length &&
data.accountCoverTypes.map((button, index) => (
...
))}
Before looping or iterating through the data variable, you should check the presence of data.
Ok, when your function first time executed data value will be undefined, as you do not set any initial value for that state const [data, setData] = useState()//initial value is not set;
Your approach is absolutely correct in sense of updating data using setData function invoked from your useEffect hook. Once your async function executed - data will be updated and component will be rerendered
React relies on the order in which Hooks are called.https://reactjs.org/docs/hooks-rules.html
That means that after the useEffect runs, the useState reinitializes the data
So move the useState before the useEffect in your code
const [data, setData] = useState();
useEffect(() => {
async function fetchData() {
let response = await getAccountCoverTypes();
setData(response);
}
fetchData();
}, []);

state not updated inside specified function

So i'm trying to change my state like this
export default function RegisterScreen({navigation}) {
const {register, control, handleSubmit, errors} = useForm({
resolver: yupResolver(schema),
});
const [state, setState] = useState({current_state: 'initial'});
const onSubmit = (data) => {
console.log(state.current_state);
setState({current_state: 'login_process'});
console.log(state.current_state);
});
}
inside on submit. but the state is not changing it is stuck on initial
then i'm trying to do this
console.log(this.state.current_state);
this.setState({current_state: 'login_process'}, () => {
console.log(this.state.current_state);
});
but i get this error
Possible Unhandled Promise Rejection (id: 0): TypeError: undefined is
not an object (evaluating '_this.state.current_state')
how can i fix it ? and is it the proper way to do state management in react native ?
You can't do async actions with useState hook. If you want to trigger changes, try something like this:
export default function RegisterScreen({navigation}) {
const {register, control, handleSubmit, errors} = useForm({
resolver: yupResolver(schema),
});
const [state, setState] = useState({current_state: 'initial'});
useEffect(() => {
console.log(`state.current_state has been changed to: ${state.current_state}`)
}, [state.current_state])
const onSubmit = (data) => {
console.log(state.current_state);
setState({current_state: 'login_process'});
console.log(state.current_state);
});
}
The error you are receiving is that you can't use this in functional components.
You can go further with your useEffect hook and do something like this:
useEffect(() => {
if(state.current_state === 'login_process') {
// add some logic that should happen when login is processing
}
}, [state.current_state])
I think the problem is not in the code but in not putting
e.preventDefault()
try the following:
const onSubmit = (e,data) => {
e.preventDefault()
console.log(state.current_state);
setState({current_state: 'login_process'});
console.log(state.current_state);
});
}

Screen redirection takes time to execute after asynchronous call in React-native

I am developing a small app with Expo, React-native-router-flux, firebase and react-redux. I am trying to implement a launch screen that appears after the splash screen and checks if the user is loaded or not. The launch screen calls the following action inside componentDIdMount function:
export const tryToSignInSilently = user => {
return () => {
console.log(user);
console.log(Actions);
setTimeout(() => {
if (user != null) Actions.tabbar();
else Actions.LoginScreen();
}, 1000);
};
};
I had to add that setTimeout to be able to redirect the screen otherwise, it would not change screen. 1) Is that the recommended solution to the problem?
After It redirects to the login screen and the submit button is pressed, another action is created:
export const login = (email, password) => {
return dispatch => {
dispatch({ type: LOGIN });
console.log("This executes");
FirebaseService.signIn(email, password)
.then(user => {
console.log("This takes almost a minute to execute");
dispatch({ type: LOGIN_SUCCESS, payload: user });
Actions.tabbar();
})
.catch(error => {
dispatch({ type: LOGIN_FAIL });
if (error) {
Alert.alert(
i18n.t("app.attention"),
i18n.t("login.enter.message"),
[{ text: i18n.t("app.ok") }],
{ cancelable: true }
);
}
}); };};
FirebaseService.signIn function =>
static async signIn(email, password) {
return await firebase.auth().signInWithEmailAndPassword(email, password); }
The interesting note is: If I press the submit button in the login screen, and save the code (causing the live reload), the firebase function is executed immediately and the page is correctly redirected to the home screen.
2) What could be causing that behavior?
Thank you very much!
Try to encapsulate your component with a using useContext hook approach.
Do all the login inside the context component by using useEffect hook with the Firebase function onAuthStateChanged. See sample code below:
const AuthProvider = ({ children }) => {
const [userObject, setUserObject] = useState(null);
const [loggedIn, setLoggedIn] = useState(null);
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if(user){
setLoggedIn(true);
setUserObject(user);
}
else {
setLoggedIn(false);
setUserObject(null);
}
});
// Cleanup subscription on unmount
return () => unsubscribe();
}, []);
const [state, dispatch] = useReducer(reducer, []);
return(
<AuthContext.Provider value={{ loggedIn, userObject }}>{ children }</AuthContext.Provider>
);
}
export { AuthProvider, AuthContext };
Then on the launch screen use the context variable 'loggedIn' to detect if the user is already loggedin or not.

React Native/Jest TypeError: Cannot read property 'params' of undefined - testing with jest

I'm trying to create a test in an application with jest and this is some lines of my code:
import React, { Component } from 'react';
import {...} from 'react-native';
import jwt_decode from 'jwt-decode';
class CreateProduct extends Component {
constructor(props) {
super(props);
this.keyboardHeight = new Animated.Value(0);
this.imageHeight = new Animated.Value(199);
this.state = {
isButtonsHidden: false,
title: '',
price: '',
description: '',
isDialogVisible: false,
messageError: '',
};
}
_goBack = async () => {
const {state} = this.props.navigation;
var token = state.params ? state.params.token : undefined;
this.props.navigation.navigate('MyProducts', {token:token});
}
I want to test the navigation:
this.props.navigation.navigate('MyProducts', {token:token});
Now this is the attempt to test:
describe('Testing navigation', () =>{
let wrapper = null
const spyNavigate = jest.fn()
const props = {
navigation:{
navigate: spyNavigate
}
}
const params = {
token: 'randomToken'
}
beforeEach(() => {
wrapper = shallow(<CreateProduct {...props}/>)
wrapper.setState({params: params})
})
it('should test navigation', () => {
wrapper.instance()._goBack(params)
expect(spyNavigate).toHaveBeenCalled()
})
})
But I'm receiving this error.
I'm assuming that there is an error with the way I'm passing the const params. Can you help me telling what's the best way I can do this to simulate a token and that way I can navigate in the screen?
Thanks.
Rootcause is your _goBack is async. But you don't await till it ends before running expect. Even more: jest also does not wait _goBack to finish so you don't even see an error
Cannot read property 'params' of undefined
that happens because you don't mock state in navigation.params.
To work with async code there are 2 different approaches in Jest: either returning Promise from the it() or running done() callback manually(it's passed as 1st argument in it()).
I'll picking 2nd since it allows us also await until goBack is finished before running expect:
describe('Testing navigation', () => {
let wrapper = null
const spyNavigate = jest.fn()
const props = {
navigation: {
navigate: spyNavigate,
state: {}
}
}
const params = {
token: 'randomToken'
}
beforeEach(() => {
wrapper = shallow(<CreateProduct {...props} />)
wrapper.setState({ params: params })
})
it('should test navigation', async () => {
await wrapper.instance()._goBack(params)
expect(spyNavigate).toHaveBeenCalled()
})
})
Or without using async/await it would look like
it('should test navigation', () => {
return wrapper.
instance()._goBack(params).
then(() => expect(spyNavigate).toHaveBeenCalled());
})
that looks messy
Or using done() callback
it('should test navigation', (done) => {
wrapper.
instance()._goBack(params).
then(() => expect(spyNavigate).toHaveBeenCalled()).
then(done);
})