Possible unhandled promise rejection on hardware back press - react-native

I have set up a store function
export const storeData = async text => {
try {
await AsyncStorage.getItem("notes")
.then((notes) => {
const noteList = notes ? JSON.parse(notes) : [];
noteList.push(text);
AsyncStorage.setItem('notes', JSON.stringify(noteList));
});
} catch (error) {
console.log("error saving" + error);
}
};
When calling from the header back button it works as intended
navigation.setOptions({
headerLeft: () => (
<HeaderBackButton onPress={() => {
storeData(text).then(() => {
navigation.goBack();
}
}} />
)
});
But when using it from the hardware back button it gives me an "unhandled promise rejection, undefined is not an object. evaluating _this.navigation".
useEffect(() => {
const backHandler = BackHandler.addEventListener("hardwareBackPress", () => {
storeData(text).then(() => {
this.navigation.goBack();
});
});
return () => backHandler.remove();
}, [text]);
Can anyone see what might cause this behaviour?

replace this by props. thiskey word is used mainly in class components here i its a functional components so navigation is reached by props.navigation

The full code would look like
function EditNoteScreen({ navigation }) {
const [text, setText] = useState("");
const backAction = () => {
storeData(text).then(() => {
Keyboard.dismiss();
navigation.goBack();
});
}
useEffect(() => {
const backHandler = BackHandler.addEventListener("hardwareBackPress", () => {
backAction();
});
navigation.setOptions({
headerLeft: () => (
<HeaderBackButton onPress={() => {
backAction();
}} />
)
});
return () => backHandler.remove();
}, [text]);

If I simply have my storage function run with the hardware back press the code will work and the hardware back buttons default behavior will take me back, but then the new item will not show up until refreshed, which is why i want the back behavior delayed until saving is done.
One way to ignore this would simply be to update the flatlist again on state change, but I would rather have the information there from the refresh rather then popping in.

Related

Re render flat list when data change cause infinite loop React Native

I have two screens. Approve List and Approve Detail. When data approved in Approve Detail, page navigate to Approve List. Then approved data should disapear from FLatList. How to remove FlatList item when data approved? or how to re render FlatList when data change? Here is my code:
Approve List:
const Approve = ({ navigation }) => {
const [rekomendasi, setRekomendasi] = useState({})
// other code
const getRekomendasi = async (token, bagian) => {
try {
const response = await sippApi.get(`/penjaminan?bagian=${bagian}`, {
headers: {
Auth: token
}
});
setRekomendasi(response.data.data)
console.log(rekomendasi)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getToken();
getUserData()
getRekomendasi(token, userData.bagian);
}, [setToken, setUserData, rekomendasi]); // if I pass rekomendasi here, make infinite loop on api request
return (
<FlatList
onRefresh={() => onRefresh()}
refreshing={isFetching}
removeClippedSubviews
style={{ marginTop: 2 }}
data={rekomendasi}
keyExtractor={rekom => rekom.penjaminan.nomor_rekomendasi}
renderItem={({ item }) => {
return (
<TouchableOpacity onPress={() => navigation.navigate("ApproveDetail", { id: item.penjaminan.nomor_rekomendasi, bagian: userData.bagian })}>
<ApproveList
plafond={item.value}
kantor={item.nama_kantor}
nomor_rekomendasi={item.nomor_rekomendasi}
produk={item.skim}
/>
</TouchableOpacity>
)
}}
showsHorizontalScrollIndicator={false}
/>
)
}
If I pass value on second argument on UseEffect, it cause infinite loop on API request. If not, my FlatList cant re render when data change. What should I do?
Thanks for help
You have to remove the rekomendasi dependency in the useEffect to avoid infinite loop, it's only for init data :)
What is the purpose of onRefresh function in the FlatList ? Instead you could put the getRekomendasi function to trigger a new call and your data will be updated
try to separate the functions to two useEffects
useEffect(() => {
//<-- write your getToken() and getUserDate() here
getToken();
getUserData()
}, []);
useEffect(() => {
const getRekomendasi = async (token, bagian) => {
try {
const response = await sippApi.get(`/penjaminan?bagian=${bagian}`, {
headers: {
Auth: token
}
});
setRekomendasi(response.data.data)
console.log(rekomendasi)
} catch (error) {
console.log(error)
}
}
getRekomendasi(token, userData.bagian);
},[token,userData.bagian]);
Problem solved by using useFocusEffect
useFocusEffect(
React.useCallback(() => {
getRekomendasi(token, userData.bagian)
}, [token, userData.bagian])
);

React Native navigation with react hook form

In my React Native project I want to add edit, go back and save button in the header of my form screen.
To manage my form, I use react-hook-form.
The header come from react-navigation and I use the navigation.setOptions function to add my buttons.
This work well for the edit or go back button but save button don't fire handleSubmit function provide by react-hook-form.
If I put the same button in another place in my page, that work well.
const MemberScreen = (navigation: any) => {
const { control, handleSubmit, formState: { errors } } = useForm();
const [editMode, setEditMode] = useState(false);
useLayoutEffect(() => {
let title = "";
let headerRight: any;
let headerLeft: any;
if (editMode) {
title = "edit form"
headerRight = () => (<TouchableOpacity onPress={() => { save() }}><MaterialCommunityIcons name="content-save" color={AppConst.primaryColor} size={32} style={styles.iconItem} /></TouchableOpacity>)
headerLeft = () => (<TouchableOpacity onPress={() => { toggleEdit() }}><MaterialCommunityIcons name="close" color={AppConst.primaryColor} size={32} style={styles.iconItem} /></TouchableOpacity>)
} else {
headerRight = () => (<TouchableOpacity onPress={() => { toggleEdit() }}><MaterialCommunityIcons name="pencil" color={AppConst.primaryColor} size={32} style={styles.iconItem} /></TouchableOpacity>)
headerLeft = () => headerLeftWithBack(navigation);
}
navigation.navigation.setOptions({ title: title, headerRight: headerRight, headerLeft: headerLeft });
}, [navigation, editMode])
const toggleEdit = () => {
setEditMode(!editMode);
}
const save = () => {
handleSubmit((data) => {
onSubmit(data)
})
}
const onSubmit = async (data: any) => {
let body = { id: member.id, ...data }
// ...
}
return // ...
}
Do you have any idea or solution to fix this problem ?
This fix my problem because i miss parentheses :
const save = () => {
handleSubmit((data) => {
onSubmit(data)
})()
}

Flatlist inside tab navigator is scrolling to top on state change in react native

Here you can see the gif
Here is my whole Navigator functional component. I'm trying to implement two tabs using Tab Navigator. One to display the cryptos and the other to display the forex data.
The problem is, when I try to load more data on reaching the flatlist's end, the flatlist is scrolling to the top since I'm making a state change [page+1].
const Navigator = () => {
const Tab = createMaterialTopTabNavigator();
const renderItems = ({ item }) => (
<Text>{item.name}<Text>
);
const fetchMarketData = async () => {
console.log("Fetching");
const marketData = await getCryptoMarketData({ page });
if (marketData != "Network Error") {
const ids = data.map((item) => item.id);
let newData = marketData.filter((item) => !ids.includes(item.id));
setData([...data, ...newData]);
setFetching(false);
} else {
setFetching(false);
Alert.alert(marketData, "Sorry for the inconvenience");
}
};
useEffect(() => {
setFetching(true);
const data = async () => {
await fetchMarketData();
};
}, [page]);
const handleLoadMore = async () => {
setFetching(true);
setPage((page) => page + 1);
};
const ScreenA = () => (
<FlatList
data={data}
style={{ backgroundColor: "white" }}
keyExtractor={(item) => item.id}
renderItem={renderItems}
scrollEventThrottle={16}
onEndReached={handleLoadMore}
onEndReachedThreshold={0}
/>
);
return (
<Tab.Navigator
screenOptions={({ route }) => screenOptions(route)}
keyboardDismissMode="auto"
>
<Tab.Screen name="Crypto" component={ScreenA} />
<Tab.Screen name="Forex" component={ScreenC} />
</Tab.Navigator>
);
};
export default Navigator;
OnEndReached is firing the handleLoadMore function and after the state change on data, the Flatlist is scrolling to the top.
1st reason
you have typo in "fetchMarketData", how exactly u get "newData" because i cant see it anywhere, maybe it should be "marketData" if not then u adding SAME old data PLUS undefined[...data, ...undefined]
2nd reason
reason why is that u call setPage(page + 1) and then "fetchMarketData" this is bad why ? because setState is async and it can be changed instant or after 5 secound, so u dont know when its changed and this is why we have hooks, you can use "useEffect" to handle this
change your "handleLoadMore" for example like this
const handleLoadMore = () => {
setPage(page + 1);
};
add useEffect hook that runs when "page" state changes
React.useEffect(() => {
(async() => {
setFetching(true)
const marketData = await getCryptoMarketData({ page });
if (marketData != "Network Error") {
setData([...data, ...marketData]);
} else {
Alert.alert(marketData, "Sorry for the inconvenience");
}
setFetching(false)
})()
}, [page])

how to test component with setState hook inside async api call in useEffect(func, [])

I'm facing a problem unit-testing a component with react-native-testing-library.
I have a component like this:
// components/TestComponent.js
function TestComponent() {
const [data, setData] = useState();
useEffect(() => {
clientLibrary.getData()
.then((result) => { setData(result.data); } )
.catch((err) => { //handle error here } )
}, []);
render (
<ListComponent
testID={"comp"}
data={data})
renderItem={(item) => <ListItem testID={'item'} data={item} />}
/>
);
}
And I test it like this:
// components/TestComponent.test.js
it('should render 10 list item', async () => {
const data = new Array(10).fill({}).map((v, idx) => ({
id: `v_${idx}`,
}));
const req = jest.spyOn(clientLibrary, 'getData').mockImplementation(() => {
return Promise.resolve(data);
});
const {queryByTestId, queryAllByTestId} = render(
<TestComponent />,
);
expect(await queryByTestId('comp')).toBeTruthy(); // this will pass
expect(await queryAllByTestId('item').length).toEqual(10); // this will fail with result: 0 expected: 10
}); // this failed
The test will fail/pass with
Attempted to log "Warning: An update to TestComponent inside a test was not wrapped in act(...). pointing to setData in useEffect.
I've tried wrapping the render with act(), the assertion with act(), not mocking the api call, wrapping the whole test in act(), but the error won't go away.
I have tried looking at testing-library docs/git/q&a for this case, scoured stackoverflow questions too, but I still can't make this test works.
Can anyone point me to the right direction to solve this?
A note: I'm not trying to test implementation detail. I just want to test that given a fetch result X, the component would render as expected, which is rendering 10 list item.
Your component is performing an asynchronous state update during mounting inside useEffect so the act of rendering has an asynchronous side effect that needs to be wrapped in an await act(async()) call. See the testing recipes documentation on data fetching.
You can try something like this in your test:
it('should render 10 list item', async () => {
// Get these from `screen` now instead of `render`
const { queryByTestId, queryAllByTestId } = screen
const data = new Array(10).fill({}).map((v, idx) => ({
id: `v_${idx}`,
}));
const req = jest.spyOn(clientLibrary, 'getData').mockImplementation(() => {
return Promise.resolve(data);
});
await act(async () => {
render(
<TestComponent />
);
})
expect(await queryByTestId('comp')).toBeTruthy();
expect(await queryAllByTestId('item').length).toEqual(10);
});

Focusing on TextInput on focus react navigation

I have an OTP screen, I've added a ref to the first TextInput field so that when the screen loads, it gets focused:
// ref
const pin1 = useRef();
// navigation listener
navigation.addListener('focus', () => {
pin1.current.focus();
})
But it returns an error:
TypeError: undefined is not an object (evaluating 'pin1.current.focus')
This code works pretty well:
navigation.addListener('focus', () => {
alert('Hello')
})
I think you should use autoFocus={true} with your TextInput you when you open the screen your TextInput automatically focused.
Check autoFocus
You should check the current object before calling any function.
navigation.addListener('focus', () => {
if (pin1.current) {
pin1.current.focus();
}
});
Embed the listener inside a useEffect
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
navigation.addListener('focus', () => {
pin1.current.focus();
})
}
return unsubscribe;
}, [navigation, pin1]);
also
<TextInput ref={pin1} />