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

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();
}, []);

Related

How to test a component that renders asynchronously after a call

Suppose I have a component that loads its content when an asynchronous call returns succesfuly:
const MyScreen = () => {
let userData: userDataResponse;
const [email, setEmail] = useState("");
const [firstTime, setFirstTime] = useState(true);
async function localGetUserData() {
userData = await getUserData();
setEmail(userData.email);
setFirstTime(false);
}
useEffect(() => {
localGetUserData();
}, []);
if (firstTime) {
return <Text>Cargando...</Text>;
}
return (
<SafeAreaView style={styles.formStyling}>
When the data is available, it sets a state variable so the real content renders
If I want to test it, I think I should mock the getUserData so the mocked function returns a mocked email, say {email: a#b.c}
What would be a good approach to achieve this?
Assuming following component setup (as I cannot see whole component):
myScreenUtils.js
export const getUserData = async () => {
return Promise.resolve('original implementation')
}
MyScreen.jsx
import { useState, useEffect } from "react";
import { getUserData } from './myScreenUtils.js'
const MyScreen = () => {
let userData;
const [email, setEmail] = useState("");
const [firstTime, setFirstTime] = useState(true);
async function localGetUserData() {
userData = await getUserData();
setEmail(userData.email);
setFirstTime(false);
}
useEffect(() => {
localGetUserData();
}, []);
if (firstTime) {
return <div>Cargando...</div>;
}
return (
<div>{email}</div>
)
};
export default MyScreen;
You can write following tests:
import { screen, render, waitFor, waitForElementToBeRemoved } from '#testing-library/react';
import MyScreen from "../MyScreen";
import * as utils from '../myScreenUtils';
describe('MyScreen', () => {
it('the text is displayed and then removed', async () => {
jest.spyOn(utils, 'getUserData').mockResolvedValue({ email: 'mocked value' });
render(<MyScreen />);
expect(screen.getByText('Cargando...')).toBeInTheDocument();
await waitForElementToBeRemoved(() => screen.queryByText('Cargando...'))
})
it('the text email is fetched and displayed', async () => {
jest.spyOn(utils, 'getUserData').mockResolvedValue({ email: 'mocked value' });
render(<MyScreen />);
await waitFor(() => {
expect(screen.getByText('mocked value')).toBeInTheDocument()
})
})
})

How to re-run useQuery and FlatList?

I use FlatList with useState.
const [state, setState] = useState(route);
<FlatList
keyboardDismissMode={true}
showsVerticalScrollIndicator={false}
data={state}
keyExtractor={(comment) => "" + comment.id}
renderItem={renderComment}
/>
When I change the datㅁ which is contained in state, I want to re-run Flatlist with new data.
So after I mutate my data, I try to rerun useQuery first in order to change state. I put refetch module here.
1)
const { data: updatePhoto, refetch } = useQuery(SEE_PHOTO_QUERY, {
variables: {
id: route?.params?.photoId,
},
});
If I put button, this onValid function will executed.
<ConfirmButton onPress={handleSubmit(onValid)}>
onValid function changes data and after all finished, as you can see I put refetch().
=> all this process is for that if I add comment and press confirm button, UI (flatlist) should be changed.
const onValid = async ({ comments }) => {
await createCommentMutation({
variables: {
photoId: route?.params?.photoId,
payload: comments,
},
});
await refetch();
console.log(updatePhoto);
};
But when I console.log data after all, it doesnt' contain added data..
what is the problem here?
If you need more explanation, I can answer in real time.
please help me.
add full code
export default function Comments({ route }) {
const { data: userData } = useMe();
const { register, handleSubmit, setValue, getValues } = useForm();
const [state, setState] = useState(route);
const [update, setUpdate] = useState(false);
const navigation = useNavigation();
useEffect(() => {
setState(route?.params?.comments);
}, [state, route]);
const renderComment = ({ item: comments }) => {
return <CommentRow comments={comments} photoId={route?.params?.photoId} />;
};
const { data: updatePhoto, refetch } = useQuery(SEE_PHOTO_QUERY, {
variables: {
id: route?.params?.photoId,
},
});
const createCommentUpdate = (cache, result) => {
const { comments } = getValues();
const {
data: {
createComment: { ok, id, error },
},
} = result;
if (ok) {
const newComment = {
__typename: "Comment",
createdAt: Date.now() + "",
id,
isMine: true,
payload: comments,
user: {
__typename: "User",
avatar: userData?.me?.avatar,
username: userData?.me?.username,
},
};
const newCacheComment = cache.writeFragment({
data: newComment,
fragment: gql`
fragment BSName on Comment {
id
createdAt
isMine
payload
user {
username
avatar
}
}
`,
});
cache.modify({
id: `Photo:${route?.params?.photoId}`,
fields: {
comments(prev) {
return [...prev, newCacheComment];
},
commentNumber(prev) {
return prev + 1;
},
},
});
}
};
const [createCommentMutation] = useMutation(CREATE_COMMENT_MUTATION, {
update: createCommentUpdate,
});
const onValid = async ({ comments }) => {
await createCommentMutation({
variables: {
photoId: route?.params?.photoId,
payload: comments,
},
});
await refetch();
console.log(updatePhoto);
};

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

Reset useLazyQuery after called once

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

function with second argument react hooks

all i want is, after changing the state, i want to run the second argument..
without hooks.. this is what it looks like
state = {
gasTypeFrom: '',
}
setModal = item => {
setState({ gasType: item }, () => {
renderFrom();
});
};
this is what i tried with hooks
const [froms, setFroms] = useState({
gasType: 'Select Value Here',
displayModal: false,
});
function setModalFrom(item) {
useEffect(
() => {
setFroms({...froms, gasType: item});
},
() => {
renderModalFrom();
}
);
console.log('setModalFrom()', froms.gasType);
}
how do i do it in hooks with a second argument?
useEffect takes a function callback and a dependency array, so when a value in the dependency array is updated the effect is fired.
const [froms, setFroms] = useState({
gasType: 'Select Value Here',
displayModal: false,
});
useEffect(() => {
renderModalFrom();
}, [froms]); // when a value here updates, effect is run
...somewhere in your code
setFroms({...froms, gasType: item}); // this updates the value
useEffect documentation