if action.payload.number exist increase state.points with action.payload.points - react-native

In React-Native I´ve two TextInputs and an add-button
My add function works i.e it creates a list. But each time I fill out the form, it adds a new listpost. I want to check if action.payload.number exist, and if true, increase state.points with action.payload.points.
as yet everything I tried with have failed i.e it didn't recognize state.number === action.payload.number and adds a new row in the list
Please help me solving this issue
Thanks in advance
Pierre
const InputScreen = () => {
const [number, setNumber] = useState("");
const [points, setPoints] = useState("");
const {addScorer} = useContext(Context);
const onPress = () => {
addScorer(number, points);
setnumber("");
setPoints("");
};
return (
<View>
<Text style={styles.label}>enter Scorer Number:</Text>
<TextInput style={styles.input} value={number} onChangeText={setNumber} />
<Text style={styles.label}>enter Points Scored:</Text>
<TextInput style={styles.input} value={points} onChangeText={setPoints} />
<TouchableOpacity onPress={onPress}>
<FontAwesome5 name="plus-circle" size={44} color="coral" />
</TouchableOpacity>
</View>
);
};
export default InputScreen;
const scorerReducer = (state, action) => {
const id = Date.now().toString().slice(-4);
switch (action.type) {
case "add_scorer":
// if (state.number === action.payload.nummer) {
if (state.find((scorer) => scorer.id === action.payload.id)) {
return [
...state,
{
points: state.points + +action.payload.points,
},
];
} else {
return [
...state,
{
id: id,
number: action.payload.number,
points: +action.payload.points,
},
];
}
default:
return state;
}
};
const addScorer = (dispatch) => {
return (number, points, id) => {
dispatch({type: "add_scorer", payload: {number, points, id}});
};
};
export const {Context, Provider} = createDataContext(
scorerReducer,
{addScorer},
[]
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

I solved it without using Reducer:)
export const ScoredProvider = ({children}) => {
const [playerList, setPlayerList] = useState([]);
const [number, setNumber] = useState("");
const [points, setPoints] = useState("");
const addScorer = () => {
const players = [...playerList];
if (number.trim().length === 0) {
return;
}
const posit = players.map((player) => player.number).indexOf(number);
if (posit !== -1) {
setPlayerList((playerList) =>
playerList.map((scorer, index) =>
index === posit
? {
...scorer,
points: scorer.points + +points,
}
: scorer
)
);
} else {
const newScorer = {
id: Date.now(),
number: number,
points: +points,
};
setPlayerList([...playerList, newScorer]);
setPoints(points);
}
};
return (
<ScoredContext.Provider
value={{number, setNumber, points, setPoints, playerList, addScorer}}
>
{children}
</ScoredContext.Provider>
);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Related

FlatList in ReactNative does not update/re-render when its data changes

Hej, I advanced my FlatList in React Native with
a) inbox/archive views
and b) with standard filter functionalities.
It's working somehow, but is not production ready!
Can someone please check this (I think well-organized) code and tell me where I do what wrong?
What is not working:
a) FlatList does not always re-render/update when the stream state, which is its data prop, changes
b) FlatList does not remove an item immediately when I archive/unarchive via swipe functionality. I have to manually change the view to see ...
c) FlatList does not directly apply the filter on state, I have to click twice to make it happen ...
//React
import { View, StyleSheet, Pressable, Animated, FlatList } from "react-native";
import { useCallback, useContext, useEffect, useState, useMemo } from "react";
//Internal
import SelectBtn from "./SelectBtn";
import SessionComponent from "./SessionComponent";
import LoadingOverlay from "../notification/LoadingOverlay";
import { SessionsContext } from "../../store/context-reducer/sessionsContext";
//External
import { Ionicons } from "#expo/vector-icons";
import { useNavigation } from "#react-navigation/native";
import { database, auth } from "../../firebase";
import { ref, onValue, remove, update } from "firebase/database";
function SessionStream() {
const navigation = useNavigation();
const sessionsCtx = useContext(SessionsContext);
const currentSessions = sessionsCtx.sessions;
const [isFetching, setIsFetching] = useState(true);
const [stream, setStream] = useState([]);
const [inbox, setInbox] = useState([]);
const [archive, setArchive] = useState([]);
const [filter, setFilter] = useState([]);
const sessionList = ["Sessions", "Archive"];
const sortList = ["ABC", "CBA", "Latest Date", "Earliest Date"];
useEffect(() => {
//Fetches all sessions from the database
async function getSessions() {
setIsFetching(true);
const uid = auth.currentUser.uid;
const sessionsRef = ref(database, "users/" + uid + "/sessions/");
try {
onValue(sessionsRef, async (snapshot) => {
const response = await snapshot.val();
if (response !== null) {
const responseObj = Object.entries(response);
const sessionsData = responseObj.map((item) => {
return {
id: item[1].id,
title: item[1].title,
laps: item[1].laps,
endTime: item[1].endTime,
note: item[1].note,
identifier: item[1].identifier,
date: item[1].date,
smed: item[1].smed,
externalRatio: item[1].externalRatio,
internalRatio: item[1].internalRatio,
untrackedRatio: item[1].untrackedRatio,
archived: item[1].archived,
};
});
sessionsCtx.setSession(sessionsData);
setIsFetching(false);
} else {
sessionsCtx.setSession([]);
setIsFetching(false);
}
});
} catch (err) {
alert(err.message);
setIsFetching(false);
}
}
getSessions();
}, []);
useEffect(() => {
//Sorts sessions into archived and unarchived
setInbox(
currentSessions.filter((session) => {
return session.archived === false || session.archived === undefined;
})
);
setArchive(
currentSessions.filter((session) => {
return session.archived === true;
})
);
}, [currentSessions, archiveHandler, unArchiveHandler, sessionsCtx, stream]);
if (isFetching) {
setTimeout(() => {
return <LoadingOverlay />;
}, 5000);
}
const onPressHandler = useCallback(
//Callback to open the session
(item) => {
navigation.navigate("Detail", {
sessionID: item.id,
});
},
[onPressHandler]
);
const rightSwipeActions = useCallback(
//Swipe actions for the session list
(item, swipeAnimatedValue) => {
return (
<View
style={{
flexDirection: "row",
width: 168,
height: 132,
}}
>
{item.archived === false ? (
<Pressable
onPress={archiveHandler.bind(this, item)}
style={({ pressed }) => pressed && styles.swipePressed}
>
<View style={styles.archive}>
<Animated.View
style={[
styles.archive,
{
transform: [
{
scale: swipeAnimatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolate: "clamp",
}),
},
],
},
]}
>
<Ionicons
name="ios-archive-outline"
size={24}
color="white"
/>
</Animated.View>
</View>
</Pressable>
) : (
<Pressable
onPress={unArchiveHandler.bind(this, item)}
style={({ pressed }) => pressed && styles.swipePressed}
>
<View style={styles.unArchive}>
<Animated.View
style={[
styles.unArchive,
{
transform: [
{
scale: swipeAnimatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolate: "clamp",
}),
},
],
},
]}
>
<Ionicons
name="md-duplicate-outline"
size={24}
color="white"
/>
</Animated.View>
</View>
</Pressable>
)}
<Pressable
onPress={deleteHandler.bind(this, item)}
style={({ pressed }) => pressed && styles.pressed}
>
<View style={styles.trash}>
<Animated.View
style={[
styles.trash,
{
transform: [
{
scale: swipeAnimatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolate: "clamp",
}),
},
],
},
]}
>
<Ionicons name="trash-outline" size={24} color="white" />
</Animated.View>
</View>
</Pressable>
</View>
);
},
[rightSwipeActions]
);
const deleteHandler = useCallback(
(item) => {
try {
sessionsCtx.deleteSession(item.id); // delete from local context
const uid = auth.currentUser.uid;
const sessionRef = ref(
database,
"users/" + uid + "/sessions/" + item.id
);
remove(sessionRef); // delete from firebase
} catch (error) {
alert(error.message);
}
},
[deleteHandler]
);
const archiveHandler = (item) => {
try {
const id = item.id;
const updatedSession = {
...item, // copy current session
archived: true,
};
const uid = auth.currentUser.uid;
const sessionRef = ref(database, "users/" + uid + "/sessions/" + id);
update(sessionRef, updatedSession);
/* sessionsCtx.updateSession(id, updatedSession); */
//update inbox state
setInbox(
currentSessions.filter((session) => {
const updatedData = session.archived === false;
return updatedData;
})
);
//update archive state
setArchive(
currentSessions.filter((session) => {
const updatedData = session.archived === true;
return updatedData;
})
);
} catch (error) {
alert(error.message);
}
};
const unArchiveHandler = (item) => {
try {
const id = item.id;
const updatedSession = {
...item, // copy current session
archived: false,
};
const uid = auth.currentUser.uid;
const sessionRef = ref(database, "users/" + uid + "/sessions/" + id);
update(sessionRef, updatedSession);
/* sessionsCtx.updateSession(id, updatedSession); */
//update unarchived session list
setArchive((preState) => {
//remove the item from archived list
preState.filter((session) => session.id !== item.id);
return [...preState];
});
} catch (error) {
alert(error.message);
}
};
const selectSessionHandler = useCallback(
(selectedItem) => {
switch (selectedItem) {
case "Sessions":
setStream(inbox);
break;
case "Archive":
setStream(archive);
break;
}
},
[selectSessionHandler, inbox, archive]
);
const selectFilterHandler = (selectedItem) => {
//filter the session list
switch (selectedItem) {
case "ABC":
// Use the Array.sort() method to sort the list alphabetically in ascending order
const sortedList = stream.sort((a, b) => {
return a.title.localeCompare(b.title);
});
setStream((preState) => {
return [...sortedList];
});
break;
case "CBA":
// Use the Array.sort() method to sort the list alphabetically in descending order
const sortedList2 = stream.sort((a, b) => {
return b.title.localeCompare(a.title);
});
setStream((preState) => {
return [...sortedList2];
});
break;
case "Latest Date":
// Use the Array.sort() method to sort the list by date in descending order
const sortedList3 = stream.sort((a, b) => {
return b.date.localeCompare(a.date);
});
setStream((preState) => {
return [...sortedList3];
});
break;
case "Earliest Date":
// Use the Array.sort() method to sort the list by date in ascending order
const sortedList4 = stream.sort((a, b) => {
return a.date.localeCompare(b.date);
});
setStream((preState) => {
return [...sortedList4];
});
break;
}
};
const renderSessionItem = useCallback(({ item }) => {
return (
<Pressable
/* style={({ pressed }) => pressed && styles.pressed} */
onPress={onPressHandler.bind(null, item)}
key={item.id}
>
<SessionComponent
key={item.id}
title={item.title}
identifier={item.identifier}
date={item.date}
rightSwipeActions={rightSwipeActions.bind(null, item)}
smed={item.smed}
endTime={item.endTime}
/>
</Pressable>
);
}, []);
return (
<View style={styles.container}>
<View style={styles.menuRow}>
<SelectBtn
data={sortList}
onSelect={(item) => selectFilterHandler(item)}
/>
<SelectBtn
data={sessionList}
onSelect={(item) => selectSessionHandler(item)}
/>
</View>
<FlatList
data={stream}
renderItem={renderSessionItem}
keyExtractor={(item) => item.id}
extraData={stream}
/>
</View>
);
}
export default SessionStream;
What I already tried:
I tried ChatGPT the whole day yesterday ... ;-)
I tried updating the global state for sessions to trigger re-renders ...
I tried updating the local state as obj or via the spread operator to ...
I tried extraData prop at FlatList
I removed useCallback to make sure it doesnt somehow block ...
can you do this when you are updating your stream state
const oldStream = stream;
const newStream = newStream; // put the value that you have updated
const returnedTarget= Object.assign(stream, newStream);
setStream(returnedTarget);
The problem might be you are mutating the copy obj
ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
c) in your selectFilterHandler method you are updating filter and in the same method you are trying to use updated filter value. which is not possible as useState does not update value immediately.
b) in your archiveHandler i think you are not updating the setStream state. if you are trying to run
const selectSessionHandler = useCallback(
(selectedItem) => {
switch (selectedItem) {
case "Sessions":
setStream(inbox);
break;
case "Archive":
setStream(archive);
break;
}
},
[selectSessionHandler, inbox, archive]
);
this method whenever inbox/archive changes it will not work it will be work only when you call this method and any of the value in dependency array has changed. (you can use useEffect and pass and deps arr [inbox,archive] which will run every time and can update your state.

making several api calls slows down react native app

So I am calling getUserProducts() (to show the updated list) whenever a product is added to the list and whenever a product is deleted from the list. But I've noticed when I add several items to the list through the dropdown, some times the product doesn't show in the list/getUserProducts isn't called (and then if I add another product it'll then show the previous added product) I'm assuming its because I'm calling it every time I add and that's making it slow? Is there a way I can work around this to optimize it?
const App = () => {
const [products, setProducts] = useState<ProductType[] | []>([]);
const [userProducts, setUserProducts] = useState<ProductType[] | []>([]);
const [toggleCheckBox, setToggleCheckBox] = useState(false);
const [value, setValue] = useState(' ');
const [isFocus, setIsFocus] = useState(false);
const [visible, setVisible] = useState(false);
const [productId, setProductId] = useState('');
const [product, setProduct] = useState('');
const [num, setNum] = useState('');
const [amount, setAmount] = useState('');
const submitForm = async () => {
let body;
body = {
product_id: productId,
product: product,
num: num,
amount: amount,
};
const response = await postProduct(body);
if (response == undefined) {
return;
}
};
const getProducts = async () => {
try {
const response = await axios.get('http://192.168.1.32:3000/api/products');
setProducts(response.data);
} catch (error) {
// handle error
alert('no');
}
};
const getUserProducts = async () => {
try {
const response = await axios.get(
'http://192.168.1.32:3000/api/user_products',
);
setUserProducts(response.data);
} catch (error) {
// handle error
alert('no');
}
};
React.useEffect(() => {
getProducts();
getUserProducts();
console.log(userProducts);
}, []);
return (
<>
<Provider>
<Dialog visible={visible} onDismiss={() => setVisible(false)}>
<DialogHeader title="Add to your list" />
<DialogContent>
<Dropdown
style={[styles.dropdown, isFocus && {borderColor: 'blue'}]}
data={products}
search
maxHeight={300}
labelField="product"
valueField="num"
placeholder={!isFocus ? 'Select item' : '...'}
searchPlaceholder="Search..."
value={value}
onFocus={() => setIsFocus(true)}
onBlur={() => setIsFocus(false)}
onChange={item => {
setValue(item.num);
setProductId(item.product_id);
setProduct(item.product);
setNum(item.num);
setIsFocus(false);
}}
/>
<TextInput
label="quantity"
variant="standard"
onChangeText={text => {
setAmount(text);
console.log(text);
}}
/>
</DialogContent>
<DialogActions>
<Button
title="Cancel"
compact
variant="text"
onPress={() => setVisible(false)}
/>
<Button
title="Add"
compact
variant="text"
onPress={() => {
setVisible(false);
submitForm();
console.log('added');
getUserProducts();
}}
/>
</DialogActions>
</Dialog>
{userProducts.length > 0 ? (
userProducts.map(userProduct => (
<ListItem
title={
userProduct.product +
' x' +
userProduct.amount +
' num: ' +
userProduct.num
}
onPress={async () => {
await deleteProduct(userProduct.product_id);
console.log('deleted');
getUserProducts();
ToastAndroid.show('Done', ToastAndroid.SHORT);
}}
trailing={
<CheckBox
disabled={false}
value={toggleCheckBox}
onValueChange={newValue => setToggleCheckBox(newValue)}
/>
}
/>
))
) : (
<Text>Nothing in your list yet</Text>
)}
</Provider>
</>
);
};
export default App;
I'm pretty certain that you aren't experiencing "lag" but race conditions.
See, when you create an item, you call submitForm() and getuserProducts() and both are async functions. Depending on how long the individual requests take, or how their execution gets scheduled getuserProducts() may very well finish before submitForm(). The new data then only reaches the server after you fetched the (not so) new data.
Consider the following code (it's just a simplified version of your app):
import React, { useState } from 'react';
interface ProductType {
id: number;
name: string;
}
export default function NotWorking() {
const [products, setProducts] = useState<ProductType[]>([]);
const createProduct = async () => {
await serverCreateProduct(`product: ${products.length}`);
console.log('product created');
};
const getProducts = async () => {
setProducts(await serverGetProducts());
console.log('products loaded...');
};
return (
<div>
<button
onClick={() => {
createProduct();
getProducts();
}}
>
Add
</button>
<h2>List:</h2>
<ul>
{products.map((product) => (
<li key={String(product.id)}>{product.name}</li>
))}
</ul>
</div>
);
}
const _userProducts: ProductType[] = [];
async function serverGetProducts() {
return new Promise<ProductType[]>((resolve) => {
setTimeout(() => {
resolve([..._userProducts]);
}, 300);
});
}
async function serverCreateProduct(name: string) {
return new Promise<void>((resolve) => {
setTimeout(() => {
_userProducts.push({ id: Math.random(), name });
resolve();
}, 500);
});
}
If you execute it, you will see that getProducts() finishes before createProduct(), so that result cannot include the new data.
You should await both of them, in order to get what you want, for example:
const createProduct = async () => {
await serverCreateProduct(`product: ${products.length}`);
console.log('product created');
setProducts(await serverGetProducts());
console.log('products loaded');
};
// ...
<button onClick={() => createProduct()}>Add</button>
See the code working here.

How to select one item of the array, when the user selects the contact?

What im trying to do is to select one or more phone numbers from the user contact list.
function ContactList() {
const [checked, setChecked] = useState(false);
const [contacts, setContacts] = useState([]);
const [filter, setFilter] = useState([]);
const [search, setSearch] = useState('');
const [data, setData] = useState(contacts)
useEffect(() => {
(async () => {
const { status } = await Contacts.requestPermissionsAsync();
if (status === 'granted') {
const { data } = await Contacts.getContactsAsync({
fields: [Contacts.Fields.PhoneNumbers],
});
if (data.length > 0) {
setContacts(data);
setFilter(data);
}
}
})();
}, []);
const searchFilter = (text) => {
if (text) {
const newData = contacts.filter((item) => {
const itemData = item.name ? item.name.toUpperCase() : ''.toUpperCase();
const textData = text.toUpperCase();
return itemData.indexOf(textData) > -1;
});
setFilter(newData);
setSearch(text);
} else {
setFilter(contacts);
setSearch(text);
}
};
const onChangeValue = () => {
setChecked(!checked)
};
useEffect(() => {
checked &&
setData((previous) => [...previous, {phone: contacts}])
}, [checked]
return(
<View>
<CheckBox
style={{ width: 15, height: 15 }}
right={true}
checked={checked}
onPress={onChangeValue}
/>
</View>
);
export default ContactList;
So far, when the user selects one phone number, it will select all phone numbers on his contact list.
I think I should get with the index only one contact from the list but I don't know how to get there.
How can I solve this error?
You can maintain a Map for checkboxes.
const [checked, setChecked] = useState(new Map());
on change use index to set values in Map.
const onChangeValue = (index) => {
checked.set(index, true)
};
in JSX use like this
<CheckBox
style={{ width: 15, height: 15 }}
right={true}
checked={checked.get(index)}
onPress={()=>onChangeValue(index)}
/>

React Native Jest - How to test Functional component with multiple hooks? Unable to stub AccessiblityInfo module

I'm trying to write unit tests for a functional component I've recently written. This component makes use of multiple hooks, including, useState, useEffect and useSelector. I'm finding it very difficult to write tests for said component since I've read that it's not good practice to alter the state but only test for outcomes.
Right now I'm stuck writing pretty simple unit tests that I just can't seem to get working. My goal for the first test is to stub AccessibilityInfo isScreenReaderEnabled to return true so that I can verify the existence of a component that should appear when we have screen reader enabled. I'm using sinon to stub AccessibilityInfo but when I mount my component the child component I'm looking for doesn't exist and the test fails. I don't understand why it's failing because I thought I had stubbed everything properly, but it looks like I'm doing something wrong.
I'll add both my component and test files below. Both have been stripped down to the most relevant code.
Home-Area Component:
const MAP_MARKER_LIMIT = 3;
const MAP_DELTA = 0.002;
const ACCESSIBILITY_MAP_DELTA = 0.0002;
type HomeAreaProps = {
onDismiss: () => void;
onBack: () => void;
onCompleted: (region: Region) => void;
getHomeFence: (deviceId: string) => void;
setHomeFence: (deviceId: string, location: LatLng) => void;
initialRegion: LatLng | undefined;
deviceId: string;
};
const HomeArea = (props: HomeAreaProps) => {
// reference to map view
const mapRef = useRef<MapView | null>(null);
// current app state
let previousAppState = useRef(RNAppState.currentState).current;
const initialRegion = {
latitude: parseFloat((props.initialRegion?.latitude ?? 0).toFixed(6)),
longitude: parseFloat((props.initialRegion?.longitude ?? 0).toFixed(6)),
latitudeDelta: MAP_DELTA,
longitudeDelta: MAP_DELTA,
};
// modified region of senior
const [region, setRegion] = useState(initialRegion);
// is accessibility screen reader enabled
const [isScreenReaderEnabled, setIsScreenReaderEnabled] = useState(false);
// state for floating modal
const [showFloatingModal, setShowFloatingModal] = useState(false);
// state for center the zone alert screen
const [showAlertScreen, setShowAlertScreen] = useState(false);
// state for center the zone error screen
const [showErrorScreen, setShowErrorScreen] = useState(false);
// To query error status after a request is made, default to false incase
// error cannot be queried from store
const requestError = useSelector<AppState, boolean>((state) => {
if (state.homeFence[props.deviceId]) {
return state.homeZoneFence[props.deviceId].error;
} else {
return false;
}
});
// To access device data from redux store, same as above if device data
// can't be queried then set to null
const deviceData = useSelector<AppState, HomeDeviceData | null | undefined>(
(state) => {
if (state.homeFence[props.deviceId]) {
return state.homeFence[props.deviceId].deviceData;
} else {
return null;
}
}
);
const [initialHomeData] = useState<HomeDeviceData | null | undefined>(
deviceData
);
// didTap on [x] button
const onDismiss = () => {
setShowFloatingModal(true);
};
// didTap on 'save' button
const onSave = () => {
if (
didHomeLocationMovePastLimit(
region.latitude,
region.longitude,
MAP_MARKER_LIMIT
)
) {
setShowAlertScreen(true);
} else {
updateHomeFence();
}
};
const onDismissFloatingModal = () => {
setShowFloatingModal(false);
props.getHomeFence(props.deviceId);
props.onDismiss();
};
const onSaveFloatingModal = () => {
setShowFloatingModal(false);
if (
didHomeLocationMovePastLimit(
region.latitude,
region.longitude,
MAP_MARKER_LIMIT
)
) {
setShowFloatingModal(false);
setShowAlertScreen(true);
} else {
updateHomeFence();
}
};
const onDismissModal = () => {
setShowFloatingModal(false);
};
// Center the Zone Alert Screen
const onBackAlert = () => {
// Go back to center the zone screen
setShowAlertScreen(false);
};
const onNextAlert = () => {
updateHomeFence();
setShowAlertScreen(false);
};
// Center the Zone Error Screen
const onBackError = () => {
setShowErrorScreen(false);
};
const onNextError = () => {
updateHomeFence();
};
const didHomeLocationMovePastLimit = (
lat: number,
lon: number,
limit: number
) => {
if (
lat !== undefined &&
lat !== null &&
lon !== undefined &&
lon !== null
) {
const haversineDistance = haversineFormula(
lat,
lon,
initialRegion.latitude,
initialRegion.longitude,
"M"
);
return haversineDistance > limit;
}
return false;
};
// didTap on 'reset' button
const onReset = () => {
// animate to initial region
if (initialRegion && mapRef) {
mapRef.current?.animateToRegion(initialRegion, 1000);
}
};
// did update region by manually moving map
const onRegionChange = (region: Region) => {
setRegion({
...initialRegion,
latitude: parseFloat(region.latitude.toFixed(6)),
longitude: parseFloat(region.longitude.toFixed(6)),
});
};
// didTap 'left' map control
const onLeft = () => {
let adjustedRegion: Region = {
...region,
longitude: region.longitude - ACCESSIBILITY_MAP_DELTA,
};
// animate to adjusted region
if (mapRef) {
mapRef.current?.animateToRegion(adjustedRegion, 1000);
}
};
// didTap 'right' map control
const onRight = () => {
let adjustedRegion: Region = {
...region,
longitude: region.longitude + ACCESSIBILITY_MAP_DELTA,
};
// animate to adjusted region
if (mapRef) {
mapRef.current?.animateToRegion(adjustedRegion, 1000);
}
};
// didTap 'up' map control
const onUp = () => {
let adjustedRegion: Region = {
...region,
latitude: region.latitude + ACCESSIBILITY_MAP_DELTA,
};
// animate to adjusted region
if (mapRef) {
mapRef.current?.animateToRegion(adjustedRegion, 1000);
}
};
// didTap 'down' map control
const onDown = () => {
let adjustedRegion: Region = {
...region,
latitude: region.latitude - ACCESSIBILITY_MAP_DELTA,
};
// animate to adjusted region
if (mapRef) {
mapRef.current?.animateToRegion(adjustedRegion, 1000);
}
};
const updateHomeFence = () => {
const lat = region.latitude;
const lon = region.longitude;
const location: LatLng = {
latitude: lat,
longitude: lon,
};
props.setHomeFence(props.deviceId, location);
};
// gets accessibility status info
const getAccessibilityStatus = () => {
AccessibilityInfo.isScreenReaderEnabled()
.then((isEnabled) => setIsScreenReaderEnabled(isEnabled))
.catch((error) => console.log(error));
};
// listener for when the app changes app state
const onAppStateChange = (nextAppState: AppStateStatus) => {
if (nextAppState === "active" && previousAppState === "background") {
// when we come to the foreground from the background we should
// check the accessibility status again
getAccessibilityStatus();
}
previousAppState = nextAppState;
};
useEffect(() => {
getAccessibilityStatus();
RNAppState.addEventListener("change", onAppStateChange);
return () => RNAppState.removeEventListener("change", onAppStateChange);
}, []);
useEffect(() => {
// exit screen if real update has occurred, i.e. data changed on backend
// AND if there is no request error
if (initialHomeData !== deviceData && initialHomeData && deviceData) {
if (!requestError) {
props.onCompleted(region);
}
}
setShowErrorScreen(requestError);
}, [requestError, deviceData]);
return (
<DualPane>
<TopPane>
<View style={styles.mapContainer}>
<MapView
accessible={false}
importantForAccessibility={"no-hide-descendants"}
style={styles.mapView}
provider={PROVIDER_GOOGLE}
showsUserLocation={false}
zoomControlEnabled={!isScreenReaderEnabled}
pitchEnabled={false}
zoomEnabled={!isScreenReaderEnabled}
scrollEnabled={!isScreenReaderEnabled}
rotateEnabled={!isScreenReaderEnabled}
showsPointsOfInterest={false}
initialRegion={initialRegion}
ref={mapRef}
onRegionChange={onRegionChange}
/>
<ScrollingHand />
{isScreenReaderEnabled && (
<MapControls
onLeft={onLeft}
onRight={onRight}
onUp={onUp}
onDown={onDown}
/>
)}
{region && <PulsingMarker />}
{JSON.stringify(region) !== JSON.stringify(initialRegion) && (
<Button
style={[btn, overrideButtonStyle]}
label={i18n.t("homeZone.homeZoneArea.buttonTitle.reset")}
icon={reset}
onTap={onReset}
accessibilityLabel={i18n.t(
"homeZone.homeZoneArea.buttonTitle.reset"
)}
/>
)}
</View>
</TopPane>
<OneButtonBottomPane
onPress={onSave}
buttonLabel={i18n.t("homeZone.homeZoneArea.buttonTitle.save")}
>
<View style={styles.bottomPaneContainer}>
<BottomPaneText
title={i18n.t("homeZone.homeZoneArea.title")}
content={i18n.t("homeZone.homeZoneArea.description")}
/>
</View>
</OneButtonBottomPane>
<TouchableOpacity
style={styles.closeIconContainer}
onPress={onDismiss}
accessibilityLabel={i18n.t("homeZone.homeZoneArea.buttonTitle.close")}
accessibilityRole={"button"}
>
<Image
style={styles.cancelIcon}
source={require("../../../assets/home-zone/close.png")}
/>
</TouchableOpacity>
<HomeFloatingModal
showFloatingModal={showFloatingModal}
onDismiss={onDismissModal}
onDiscard={onDismissFloatingModal}
onSave={onSaveFloatingModal}
/>
<HomeAlert
isVisible={showAlertScreen}
modalTitle={i18n.t("home.feedbackCenter.title.confirmZoneCenter")}
modalDescription={i18n.t(
"home.feedbackCenter.description.confirmZoneCenter"
)}
onBackButtonTitle={i18n.t("home.feedback.buttonTitle.back")}
onNextButtonTitle={i18n.t("home.feedback.buttonTitle.okay")}
onBack={onBackAlert}
onNext={onNextAlert}
/>
<HomeAlert
isVisible={showErrorScreen}
sentimentType={SentimentType.alert}
showWarningIcon={false}
modalTitle={i18n.t("home.errorScreen.title")}
modalDescription={i18n.t("home.errorScreen.description")}
onBackButtonTitle={i18n.t("home.errorScreen.buttonTitle.cancel")}
onNextButtonTitle={i18n.t("home.errorScreen.buttonTitle.tryAgain")}
onBack={onBackError}
onNext={onNextError}
/>
</DualPane>
);
};
export default HomeArea;
Home-Area-Tests:
import "jsdom-global/register";
import React from "react";
import { AccessibilityInfo } from "react-native";
import HomeArea from "../../../src/home/components/home-area";
import HomeAlert from "../../../src/home/components/home-alert";
import MapControls from "../../../src/home/components/map-controls";
import { mount } from "enzyme";
import { Provider } from "react-redux";
import configureStore from "redux-mock-store";
import sinon from "sinon";
jest.useFakeTimers();
const mockStore = configureStore();
const initialState = {
homeFence: {
"c9035f03-b562-4670-86c6-748b56f02aef": {
deviceData: {
eTag: "964665368A4BD68CF86B525385BA507A3D7F5335",
fences: [
{
pointsOfInterest: [
{
latitude: 32.8463898,
longitude: -117.2776381,
radius: 100,
uncertainty: 0,
poiSource: 2,
},
],
id: "5e1e0bc0-880d-4b0c-a0fa-268975f3046b",
timeZoneId: "America/Los_Angeles",
type: 7,
name: "Children's Pool",
},
{
pointsOfInterest: [
{
latitude: 32.9148887,
longitude: -117.228307,
radius: 100,
uncertainty: 0,
poiSource: 2,
},
],
id: "782d8fcd-242d-47c0-872b-f669e7ca81c7",
timeZoneId: "America/Los_Angeles",
type: 1,
name: "Home",
},
],
},
error: false,
},
},
};
const initialStateWithError = {
homeFence: {
"c9035f03-b562-4670-86c6-748b56f02aef": {
deviceData: {
eTag: "964665368A4BD68CF86B525385BA507A3D7F5335",
fences: [],
},
error: true,
},
},
};
const store = mockStore(initialState);
const props = {
onDismiss: jest.fn(),
onBack: jest.fn(),
onCompleted: jest.fn(),
getHomeZoneFence: jest.fn(),
setHomeZoneFence: jest.fn(),
initialRegion: { latitude: 47.6299, longitude: -122.3537 },
deviceId: "c9035f03-b562-4670-86c6-748b56f02aef",
};
// https://github.com/react-native-maps/react-native-maps/issues/2918#issuecomment-510795210
jest.mock("react-native-maps", () => {
const { View } = require("react-native");
const MockMapView = (props: any) => {
return <View>{props.children}</View>;
};
const MockMarker = (props: any) => {
return <View>{props.children}</View>;
};
return {
__esModule: true,
default: MockMapView,
Marker: MockMarker,
};
});
describe("<HomeArea />", () => {
describe("accessibility", () => {
it("should return true and we should have map control present", async () => {
sinon.stub(AccessibilityInfo, "isScreenReaderEnabled").callsFake(() => {
return new Promise((res, _) => {
res(true);
});
});
const wrapper = mount(
<Provider store={store}>
<HomeArea {...props} />
</Provider>
);
expect(wrapper).not.toBeUndefined(){jest.fn()} onRight={jest.fn()} onUp={jest.fn()} onDown={jest.fn()} />).instance()).not.toBeUndefined();
expect(wrapper.find(MapControls).length).toEqual(1);
});
});
describe("requestError modal", () => {
it("should render requestErrorModal", async () => {
const store = mockStore(initialStateWithError);
const wrapper = mount(
<Provider store={store}>
<HomeArea {...props} />
</Provider>
);
expect(wrapper).not.toBeUndefined();
expect(
wrapper.contains(
<HomeAlert
isVisible={false}
modalTitle={""}
modalDescription={""}
onBackButtonTitle={""}
onNextButtonTitle={""}
onBack={jest.fn()}
onNext={jest.fn()}
/>
)
).toBe(true);
});
});
});
One thought I had was to stub getAccessibilityStatus in my component but haven't had any luck doing so. I've been reading online functional components are a bit of a "black box" and stubbing functions doesn't seem possible, is this true? I'm starting to wonder how I can successfully test my component if the multiple hooks and the fact that it's a functional component make it very difficult to do so.
Any help is greatly appreciated.
It probably is because the promise is not resolving before you check that the component exists. You can read more about it here https://www.benmvp.com/blog/asynchronous-testing-with-enzyme-react-jest/
try it like this
const runAllPromises = () => new Promise(setImmediate)
...
describe("accessibility", () => {
it("should return true and we should have map control present", async () => {
sinon.stub(AccessibilityInfo, "isScreenReaderEnabled").callsFake(() => {
return new Promise((res, _) => {
res(true);
});
});
const wrapper = mount(
<Provider store={store}>
<HomeArea {...props} />
</Provider>
);
await runAllPromises()
// after waiting for all the promises to be exhausted
// we can do our UI check
component.update()
expect(wrapper).not.toBeUndefined();
expect(wrapper.find(MapControls).length).toEqual(1);
});
});
...

React Apollo fetch once

Im working on simply COVID-19 tracker and i have a problem.
Is there any option in Apollo for React to fetch graphql data once per button press?
Now i have TextInput and Button but when i fetch data once i can't type another country in input because i have immediately error.
const Tile = () => {
const [country, setCountry] = useState('Poland');
const [cases, setCases] = useState(0);
const MY_QUERY = gql`
query getCountryStats($country: String!) {
country(name: $country) {
todayCases
}
}
`;
const [getCountryStats, {data, loading, error}] = useLazyQuery(MY_QUERY, {
variables: {
country: country,
},
onCompleted: (data) => {
setCases(data.country.todayCases);
},
});
if (loading) return <Text>LOADING...</Text>;
if (error) return <Text>Error!</Text>;
return (
<View>
<CasesNumber>{cases}</CasesNumber>
<FinderWrapper>
<FinderInput
onChangeText={(text) => {
setCountry(text);
}}
/>
<FinderButton
onPress={() => {
getCountryStats();
}}>
<Text>FIND</Text>
</FinderButton>
</FinderWrapper>
</View>
);
};
export default Tile;
Try using this
const Tile = () => {
const [country, setCountry] = useState('Poland');
const [cases, setCases] = useState(0);
let inputValue = ‘’;
const MY_QUERY = gql`
query getCountryStats($country: String!) {
country(name: $country) {
todayCases
}
}
`;
const [getCountryStats, {data, loading, error}] = useLazyQuery(MY_QUERY, {
variables: {
country: country,
},
onCompleted: (data) => {
setCases(data.country.todayCases);
},
});
const onGetCountryStats = () => {
setCountry(inputValue);
getCountryStats();
}
if (loading) return <Text>LOADING...</Text>;
if (error) return <Text>Error!</Text>;
return (
<View>
<CasesNumber>{cases}</CasesNumber>
<FinderWrapper>
<FinderInput
onChangeText={(text) => {
inputValue = text;
}}
/>
<FinderButton
onPress={() => {
onGetCountryStats();
}}>
<Text>FIND</Text>
</FinderButton>
</FinderWrapper>
</View>
);
};
export default Tile;