Updating useState array from callback - react-native

I am building a React Native (Expo) app that scans for Bluetooth devices. The Bluetooth API exposes a callback for when devices are detected, which I use to put non-duplicate devices into an array:
const DeviceListView = () => {
const [deviceList, setDeviceList] = useState([]);
const startScanning = () => {
manager.startDeviceScan(null, null, (error, device) => {
// Add to device list if not already in list
if(!deviceList.some(d => d.device.id == device.id)){
console.log(`Adding ${device.id} to list`);
const newDevice = {
device: device,
...etc...
};
setDeviceList(old => [...old, newDevice]);
}
});
}
// map deviceList to components
componentList = deviceList.map(...);
return <View> {componentList} </View>
}
The problem is that the callback is called many many times faster than setDeviceList updates, so the duplicate checking doesn't work (if I log deviceList, it's just empty).
If I use an additional, separate regular (non-useState) array, the duplicate checking works, but the state doesn't update consistently:
const DeviceListView = () => {
const [deviceList, setDeviceList] = useState([]);
var deviceList2 = [];
const startScanning = () => {
manager.startDeviceScan(null, null, (error, device) => {
// Add to device list if not already in list
if(!deviceList2.some(d => d.device.id == device.id)){
console.log(`Adding ${device.id} to list`);
const newDevice = {
device: device,
...etc...
};
deviceList2.push(newDevice);
setDeviceList(old => [...old, newDevice]);
}
});
}
// map deviceList to components
componentList = deviceList.map(...);
return <View> {componentList} </View>
}
This code almost works, but the deviceList state doesn't update correctly: it shows the first couple of devices but then doesn't update again unless some other component causes a re-render.
What do I need to do to make this work as expected?

I would suggest wrap your duplicate check within the state set function itself, and then return the same device list if no new devices have been found. This offloads race condition handling to the underlying react implementation itself, which I've found to be good enough for most cases.
Thus it would look something like this:
const DeviceListView = () => {
const [deviceList, setDeviceList] = useState([]);
const startScanning = () => {
manager.startDeviceScan(null, null, (error, device) => {
// Add to device list if not already in list
setDeviceList(old => {
if(!old.some(d => d.device.id == device.id)){
console.log(`Adding ${device.id} to list`);
const newDevice = {
device: device,
// ...etc...
};
return [...old, newDevice]
}
return old
});
});
}
// map deviceList to components
componentList = deviceList.map(...);
return <View> {componentList} </View>
}
Since old is unchanged if no new unique devices are found it will also skip next re-render according to the docs ( which is a neat optimisation :) )
This is the preferred way to implement state updates that are dependant on previous state according to the docs
https://reactjs.org/docs/hooks-reference.html#functional-updates

convert your callback to promise so that until you get completed device list, checkout below code (PS. not tested, please change as you need)
const [deviceList, setDeviceList] = useState([]);
const [scanning, setScanning] = useState(false);
useEffect(() => {
if(scanning) {
setDeviceList([]);
startScanning();
}
}, [scanning]);
const subscription = manager.onStateChange(state => {
if (state === "PoweredOn" && scanning === false) {
setCanScan(true);
subscription.remove();
}
}, true);
const fetchScannedDevices = () => {
return new Promise((resolve, reject) => {
manager.startDeviceScan(null, null, (error, device) => {
// Add to device list if not already in list
if (!deviceList.some(d => d.device.id == device.id)) {
console.log(`Adding ${device.id} to list`);
const newDevice = {
device: device,
// ...etc...
};
resolve(newDevice);
}
if (error) {
reject({});
}
});
});
};
const startScanning = async () => {
try {
const newDevice = await fetchScannedDevices();
setDeviceList(old => [...old, newDevice]);
} catch (e) {
//
}
};
const handleScan = () => {
setScanning(true);
};
// map deviceList to components
componentList = deviceList.map(() => {});
return (
<View>
<Button
onPress={() => handleScan()}>
Scan
</Button>
<View>{componentList}</View>
</View>
);
};

Related

Best practice handling API calls and subsequent actions based on response data with Redux

Suppose we have this scenario: we need to make a request to an API to get data on number of books available. If books > 0, we must trigger
some sort of popup with a function which is provided to us. The books need to be stored into the redux store for other components to use. The codebase already uses redux-thunk and redux-thunk-middleware.
What would be the best implementation using a hook and why? (displayPopUp is the function that we must use to trigger the pop-up)
1)
const useBooksNotification = () => {
const dispatch = useDispatch();
const [shouldShowNotification, setShouldShowNotification] = useState(false);
const books = useSelector(selectAvailableBooks);
useEffect(() => {
if (shouldShowNotification) {
setShouldShowNotification(false);
if (books.length > 0) {
displayPopUp();
}
}
}, [shouldShowNotification, books]);
const showNotification = async () => {
if (!shouldShowNotification) {
await dispatch(fetchBooks);
setShouldShowNotification(true);
}
};
return {
showNotification,
};
};
or 2)
const useBooksNotification2 = () => {
const dispatch = useDispatch();
const showNotification = async () => {
{
const response = await dispatch(fetchBooks);
const books = response.value;
if (books.length > 0) {
displayPopUp();
}
}
};
return {
showNotification,
};
};
Personally, I prefer 2 since to me it is much more readable but someone told me 1 is preferable i.e listening to the selector for the books instead of getting the books directly from the action/API response. I am very curious as to why this is? Or if there is an even better implementation.
I would understand using a selector if there was no displayPopUp function given to us and instead there was some implementation like so:
const BooksNotification = () => {
const dispatch = useDispatch();
const books = useSelector(selectAvailableBooks);
const showNotification = () => {
dispatch(fetchBooks);
};
return books.length > 0 ? <h1>Pretend this is a pop-up</h1> : null;
};
const SomeComponent = () => {
<div>
<h1>Component</h1>
<BooksNotification />
</div>
}

Testing custom hook - not wrapped in act warning

I' trying to test a custom hook but I receive this warning message
console.error node_modules/#testing-library/react-hooks/lib/core/console.js:19
Warning: An update to TestComponent inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser.
This is my custom hook
import { useState, useEffect } from 'react'
import io from 'socket.io-client'
import config from './../../../../config'
const useNotificationsSocket = (user) => {
const [socket, setSocket] = useState(null)
const [numUnreadMessages, setNumUnreadMessages] = useState(0)
const configureSocket = socket => {
socket.on('connect', () => {
const data = {
user: user,
}
socket.emit('user joined', data)
})
socket && socket.on('messages updated', (data) => {
//console.log(data)
setNumUnreadMessages(data.numUnreadMessages)
})
}
useEffect(() => {
const fetchSocket = async () => {
const s = await io(config.nSocket.url, {transports: ['websocket']})
configureSocket(s)
setSocket(s)
}
// Check that user is not an empty object as this causes a crash.
user && user.Id && fetchSocket()
}, [user])
return [socket, numUnreadMessages]
}
export { useNotificationsSocket }
and this is the test
import { renderHook, act } from '#testing-library/react-hooks'
import { useNotificationsSocket } from './../hooks/useNotificationsSocket'
jest.mock('socket.io-client')
describe('useNotificationsSocket', () => {
it('returns a socket and numUnreadMessages', async () => {
const user = { Id: '1' }
const { result } = renderHook(() => useNotificationsSocket(user))
expect(result).not.toBeNull()
})
})
I've tried importing act and wrapping the code in a call to act but however I try to wrap the code I still get a warning and can't figure out how I should use act in this case.
Your hook is asynchronous, so you need to await its response:
describe('useNotificationsSocket', () => {
it('returns a socket and numUnreadMessages', async () => {
const user = { Id: '1' }
const { result } = renderHook(() => useNotificationsSocket(user))
await waitFor(() => expect(result).not.toBeNull())
})
})
Additionally, if you define multiple tests, you may encounter your original error if you fail to unmount the hook. At least this appears to be the behaviour in #testing-library/react v13.3.0. You can solve this by unmounting the hook when your test completes:
describe('useNotificationsSocket', () => {
it('returns a socket and numUnreadMessages', async () => {
const user = { Id: '1' }
const { result, unmount } = renderHook(() => useNotificationsSocket(user))
await waitFor(() => expect(result).not.toBeNull())
unmount()
})
})

React native with axios fetch first post delayed

I am using react native, and axios.
I have two parts.
The exercice list that is rendered with useEffect in a div. Inside, there is a Input form which once pressed the Add set button, the set is added to the database and the exercices are fetched again with the passed function.
The main problem is that when I first add an exercice, the exercice s not rendering. I must go back and come again in the page to render the first one. after doing this process I can add as many exercices... And with delete is same. I can delete any exercice but when deleting the last one, it persist and I must leave the page to see the changes...
THIS IS THE FUNCTION THAT ADD THE exercices. It executes once the alert button is pressed
const NewExercice = ({dayID, getAllEx}) => {
// States and ontext change functions
const [exName, setexName] = useState('');
const [comment, setcomment] = useState('');
const handleExname = text => setexName(text);
const handleComments = text => setcomment(text);
// Add new exercices
const handleNewExercice = async () => {
try
{
const status = await data.post('/api/create-exercice', {dayID, exName, comments: comment});
Alert.alert(
'Exercice created',
'Please add new sets to existing exercices',
[
{
text: 'Ok!',
// Fetch again for all the exercices
onPress: getAllEx
}
]
)
}
catch (error)
{
console.log(error);
}
}
Bellow is the component that adds map over the array state
<View>
{error ? (<Text>No exercices created yet.</Text>) :
exArray.map(obj => (
<ExerciceWrapper getAllEx={getAllExercices} navigation={navigation} key={obj.exID} object={obj} />
))}
</View>
Bellow is the function that fetch the data from the DB and set the state to be able to be rendered in the component above
const getAllExercices = async () => {
try
{
const response = await data.get('/api/get-all-ex/' + dayID);
setExArray(response.data);
}
catch (error)
{
if (error.response.status === 404) return setError(true);
else return console.log(error);
}
}
useEffect(() => {
getAllExercices();
}, []);
You need to toggle the error value when you have successful fetch as well
update code to this
const getAllExercices = async () => {
try
{
const response = await data.get('/api/get-all-ex/' + dayID);
setExArray(response.data);
setError(response.data.length < 1)
}
catch (error)
{
if (error.response.status === 404) return setError(true);
else return console.log(error);
}
}

wait for onSnapshot fetching data

I'm currently learning React Native (Expo).
I want to use redux and react-native-firebase.
When I subscribe to firebase (onSnapshot) at startup of my app, it returns the data from firebase. But since onSnapchot doesn't return a promise, I can't use it for my app-loading component.
Therefore, I also need to fetch the data from firebase to prevent the app from flicker.
The result is that at startup of my app I fetch the data twice.
So my question is:
How can I wait for onSnapshot loading my data from firebase?
Thanks
const Manager = (props) => {
//STATE
const [init, setInit] = useState(false);
//HOOKS
const fetchData = useFetchData();
useInitFirebaseSubscriptions();
//FUNCTIONS
async function onInit() {
console.log('[MANAGER]: loading app...');
await Promise.all([fetchData()]);
}
function onFinishedInit() {
console.log('[MANAGER]: ...app loading successfull!');
setInit(true);
}
//RETURN
if (!init) {
return <AppLoading startAsync={onInit} onFinish={onFinishedInit} onError={console.warn} />;
} else {
return props.children;
}
};
export default Manager;
//INITIAL FETCH BEFORE RENDERING
export function useFetchData() {
const dispatch = useDispatch();
return async function () {
try {
await firestore()
.collection('users')
.get()
.then((querySnapshot) => dispatch(actions.fetch(querySnapshot)));
} catch (err) {
console.log(err.message);
}
};
}
//INIT SUBSCRIPTIONS TO FIREBASE
export function useInitFirebaseSubscriptions() {
const dispatch = useDispatch();
useEffect(() => {
console.log('[CONTROLLER]: subscribed to Firebase');
const unsubscribe = firestore()
.collection('users')
.onSnapshot(
(querySnapshot) => dispatch(action.fetch(querySnapshot)),
(error) => console.log(error)
);
return () => {
unsubscribe();
console.log('[CONTROLLER]: unsubscribed from Firebase');
};
}, []);
}
[MANAGER]: loading app...
[MANAGER]: subscribed to Firebase
[USER_REDUCER]: fetched data
[USER_REDUCER]: fetched data
[MANAGER]: ...app loading successfull!
I think you can accomplish your goal by adding some "loading" state in redux for when you are actively fetching data from firebase. Add the state and reducer cases specific to this data fetching/loading.
Example code:
export function useInitFirebaseSubscriptions() {
const dispatch = useDispatch();
useEffect(() => {
console.log('[CONTROLLER]: subscribed to Firebase');
dispatch(action.startFetch()); // <-- dispatch starting data fetch
const unsubscribe = firestore()
.collection('users')
.onSnapshot(
(querySnapshot) => {
dispatch(action.fetch(querySnapshot));
dispatch(action.completedFetch()); // <-- done fetching
},
(error) => {
console.log(error);
dispatch(action.completedFetch()); // <-- done fetching
},
);
return () => {
unsubscribe();
console.log('[CONTROLLER]: unsubscribed from Firebase');
};
}, []);
};
Select the loading state from the redux store and conditionally render the loading UI, otherwise render the passed children.
const Manager = (props) => {
const isFetchingData = useSelector(state => state.isFetchingData);
if (isFetchingData) {
return <AppLoadingIndicator />;
}
return props.children; // *
};
* Generally you may use some additional conditional rendering here depending on if data was actually fetched/returned and is just empty, or if there was an error, etc... basically provide a bit of a result status.

how to handle the return values from reducer, inside a functional component

This may be a basic question, but I'm new to React Native and stuck here.
My code pasted below. reducer and functional component. I want to capture the response returned from reducer.
reducer.js
export const ActivationCenterReducer = (
state = INIT_KIT_STATE,
{ type, payload = {} }
) => {
switch (type) {
case 'KIT_ACTIVATION_SUCCESS_DATA': {
const { message, response_code, apiLoading, apiError } = payload;
return {
...state,
apiLoading: apiLoading,
apiError: apiError,
message: message,
response_code: response_code
};
}
// ...
}
// ...
};
Functional Component class:
const kitActivationCenter = ({ route, navigation }) => {
const response_code = useSelector(
store => store.kitActivationCenter.response_code
);
const handleKitActivation = () => {
/*This will call the validation() inside action.js and that follows the reducer.js file. where reducer.js file returning the values on success response. but I am not able to access that response_code returned from reducer.
How to save the response_code from the below dispatch function.*/
dispatch(Validation(locator, pin));
if (response_code === 200) {
// should navigate to the next screen
}
};
};
My question is how to capture the returned response_code from reducer.
I'm able to navigate to the next screen on clicking the submit button couple of times.I notice that first time when the dispatch function is called, the state of the response_code is not updating , hence the response_code != 200.
I want a way to capture the response and assign to variable.
Thanks in advance.
You are probably looking at the old value of response_code in your handleKitActivation.
const kitActivationCenter = ({ route, navigation }) => {
const response_code = useSelector(
store => store.kitActivationCenter.response_code
);
const handleKitActivation = () => {
dispatch(Validation(locator, pin));
// HERE the response_code does not have result value
// of your calling dispatch(Validation(locator, pin)) above yet
if (response_code === 200) {
// should navigate to the next screen
}
};
};
I suggest to move your response_code hanfling to the useEffect:
const kitActivationCenter = ({ route, navigation }) => {
const response_code = useSelector(
store => store.kitActivationCenter.response_code
);
// this effect will run whenever your response_code changes
useEffect(() => {
if (response_code === 200) {
// should navigate to the next screen
}
}, [response_code]);
const handleKitActivation = () => {
dispatch(Validation(locator, pin));
};
};