I have a component that looks like this:
<TestComponent refetch={fn} />
Within TestComponent, I have a save button that fires off a mutation:
const handleSaveClick = () => {
const reqBody: AddHotelRoomDescription = {...}
addHotelRoomDescriptionMutation(reqBody, {
onSuccess: () => {
closeDrawer();
handleRefetch();
}
})
}
Within my test file for this component, I am trying to ensure handleRefetch gets called:
it('successfully submits and calls the onSuccess method', async () => {
const mockId = '1234'
nock(API_URL)
.post(`/admin/${mockId}`)
.reply(200);
const user = userEvent.setup();
const editorEl = screen.getByTestId('editor-input');
await act( async () => { await user.type(editorEl, 'Updating description'); });
expect(await screen.findByText(/Updating description/)).toBeInTheDocument();
const saveButton = screen.getByText('SAVE');
await act( async () => { await user.click(saveButton) });
expect(mockedMutate).toBeCalledTimes(1);
await waitFor(() => {
expect(mockedHandleRefetch).toBeCalledTimes(1); <-- fails here
})
})
I am not sure how to proceed here. I know I can test useQuery calls by doing:
const { result } = renderHook(() => useAddHotelRoomDescription(), { wrapper }
But I think this is for a different situation.
Appreciate the guidance!
i have loading screen for call all the data function.i used async function for all function call.
//NOTE: this screen loads all the data and store it in store so user will have a smother experience
const LoadingScreen = (props) => {
const gotToHomeScreen = () => {
props.navigation.replace("Home", { screen: HOME_SCREEN });
};
//NOTE: loading data here for home screen journey
const getRequiredAPIDataInStore = async () => {
GetAllFieldProp();
GetAllSalaryAPIResponse();
GetSalaryAPIResponse();
let { spinnerStateForm101 } = GetForm101API();
let { spinnerStateForm106 } = GetForm106API();
GetMessagesCountAPI();
GetMessagesAPI(props);
GetAllFormAPIResponse();
GetAllSpecificSalaryAPIResponse();
let { spinnerStateMonthly } = GetMonthlyAbsenceAPI(props);
let { spinnerStateWeekly } = GetWeeklyAbsenceAPI(props);
if (
spinnerStateMonthly &&
spinnerStateWeekly &&
spinnerStateForm106 &&
spinnerStateForm101
) {
gotToHomeScreen();
}
};
getRequiredAPIDataInStore();
export default LoadingScreen;
but i am getting warning messages for this.
Warning: Cannot update a component from inside the function body of a different component.
at src/screens/loading-screen.js:19:26 in gotToHomeScreen
at src/screens/loading-screen.js:37:6 in getRequiredAPIDataInStore
How to solve this warning messsage?
Here's the approach I would take.
const Loading = () => {
const [spinnerStateMonthly, setSpinnerStatMonthly] = useState(null);
const [spinnerStateWeekly, setspinnerStateWeekly] = useState(null);
const [spinnerStateForm106, setspinnerStateForm106] = useState(null);
const [spinnerStateForm101, setSpinnerStateForm101] = useState(null);
const gotToHomeScreen = () => {
props.navigation.replace("Home", { screen: HOME_SCREEN });
};
useEffect(() => {
// async callback to get all the data and set state
(async () => {
await GetAllFieldProp();
await GetAllSalaryAPIResponse();
await GetSalaryAPIResponse();
const { spinnerStateForm101: local101 } = GetForm101API();
const { spinnerStateForm106: local106 } = GetForm106API();
setSpinnerStateForm101(local101);
setSpinnerStateForm106(local106);
await GetMessagesCountAPI();
await GetMessagesAPI(props);
await GetAllFormAPIResponse();
await GetAllSpecificSalaryAPIResponse();
const { spinnerStateMonthly: localMonthly } = GetMonthlyAbsenceAPI(props);
const { spinnerStateWeekly: localWeekly } = GetWeeklyAbsenceAPI(props);
setSpinnerStateMonthly(localMonthly);
setSpinnerStateWeekly(localWeekly);
})();
}, []);
// effect to check for what the state is and if all the states are satisfied
// then go to the home screen
useEffect(() => {
if (spinnerStateMonthly
&& spinnerStateWeekly
&& spinnerStateForm106
&& spinnerStateForm101) {
gotToHomeScreen();
}
}, [spinnerStateMonthly, spinnerStateWeekly, spinnerStateForm101,
spinnerStateForm106]);
};
So I wanted to add "whatsapp" like voice note feature in my app I am working on, where you record the voice note and click the "Send" button to send it. I have added the voice recorder code and its working fine in my Logs, but the problem is that when I press the "Send recording" button it sends an empty file in the chat box, and on pressing the same button the second time it then actually sends the recorded Voice note.
The Recorder code component "useRecorder"
import { useEffect, useState } from "react";
import Conversation_Logs from "../Logs/Conversation_Logs";
const useRecorder = () => {
const [audioURL, setAudioURL] = useState("");
const [audioBlob, setAudioBlob] = useState("");
const [isRecording, setIsRecording] = useState(false);
const [recorder, setRecorder] = useState(null);
useEffect(() => {
// Lazily obtain recorder first time we're recording.
if (recorder === null) {
if (isRecording) {
requestRecorder().then(setRecorder, console.error);
}
return;
}
// Manage recorder state.
if (isRecording) {
recorder.start();
} else {
recorder.stop();
}
// Obtain the audio when ready.
const handleData = e => {
setAudioURL(URL.createObjectURL(e.data));
let wavfromblob = new File([e.data], "incomingaudioclip.wav")
setAudioBlob(wavfromblob);
};
recorder.addEventListener("dataavailable", handleData);
return () => recorder.removeEventListener("dataavailable", handleData);
}, [recorder, isRecording]);
const startRecording = () => {
setIsRecording(true);
};
const stopRecording = () => {
setIsRecording(false);
};
return [audioURL,audioBlob, isRecording, startRecording, stopRecording];
};
async function requestRecorder() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
return new MediaRecorder(stream);
}
export default useRecorder;
The Code where send recording button is called component named "RecorderBox
import React, {useState,useEffect} from "react";
import { render } from "react-dom";
import useRecorder from "./useRecorder";
const RecorderBox = (props) =>{
const { log, handleUpdateLog,handleSubmitNewMessage ,selectedLog} = props
let [audioURL,audioBlob, isRecording, startRecording, stopRecording] = useRecorder();
const [ newMessage, setNewMessage ] = useState("");
const [ attachFile, setAttachFile ] = useState();
const submitNewMessage = () => {
setAttachFile(audioBlob)
const body = {
body: newMessage,
attachment: attachFile
}
handleUpdateLog(log,newMessage, attachFile)
console.log(attachFile)
// handleSubmitNewMessage(log.id,body)
}
useEffect(() => {
setNewMessage("")
setAttachFile(null)
!!document.getElementsByClassName("dzu-previewButton")[0] && document.getElementsByClassName("dzu-previewButton")[0].click()
}, [log])
return (
<div className="RecorderBox">
<audio src={audioURL} controls />
<button onClick={startRecording} disabled={isRecording}>
start recording
</button>
<button onClick={() => {
stopRecording();
}} disabled={!isRecording}>
stop recording
</button>
<button onClick={() => {
submitNewMessage();
}}>
send recording
</button>
<p>
<em>
</em>
</p>
</div>
);
}
export default RecorderBox;
TL;DR: submitNewMessage is running in the scope of the previous render, thus attachFile doesn't contain anything yet. Whatever audio you managed to send on the second click was actually the first recording you tried to send.
Solution: Skip attachFile completely.
A couple of hints: Don't have a useEffect depend on a state that the effect itself is updating, and dont use a useEffect for doing something that a click-handler can do.
Here's a safer version of your code. I haven't tested it, but any bugs should be easy to fix.
import { useCallback, useEffect, useRef, useState } from 'react';
// This is a nugget I use in many similar cases.
/**
* Similar to `useMemo`, but accepts a callback that return a promise.
* Very useful when working with states that depend on some async function.
*
* #link https://gist.github.com/mariusbrataas/ffca05c210ec540b4fb9f4324c4f2e68
*
* #example
* function MyComponent({url}:{url: string}) {
* const data = usePromiseMemo(
* () =>
* fetch(url)
* .then(r => r.json())
* .then(r => r.my.user.info),
* [url]
* );
*
* return <p>{info || "Loading info..."}</p>
* }
*/
export function usePromiseMemo<T>(
callback: () => Promise<T> | T,
deps?: any[]
) {
// Private states
const [state, setState] = useState<T>();
const isMounted = useRef(true);
// Register unmount
useEffect(() => {
if (!isMounted.current) isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
// Execute callback
useEffect(() => {
if (isMounted.current) {
if (state !== undefined) setState(undefined);
Promise.resolve()
.then(() => callback())
.then(r => {
if (isMounted.current) setState(r);
});
}
}, deps || []);
// Return state
return state;
}
function useRecorder() {
const [audioURL, setAudioURL] = useState<string>();
const [audioBlob, setAudioBlob] = useState<File>();
const [isRecording, setIsRecording] = useState(false);
// Handler for recorded data
const handleRecorderData = useCallback(
(e: BlobEvent) => {
setAudioURL(URL.createObjectURL(e.data));
setAudioBlob(new File([e.data], 'incomingaudioclip.wav'));
},
[setAudioURL, setAudioBlob]
);
// Create recorder
const recorder = usePromiseMemo(
() =>
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(stream => new MediaRecorder(stream)),
[]
);
// Attach event listener
useEffect(() => {
if (recorder)
recorder.addEventListener('dataavailable', handleRecorderData);
return () => {
if (recorder)
recorder.removeEventListener('dataavailable', handleRecorderData);
};
}, [recorder, handleRecorderData]);
// Handle start recording
const startRecording = useCallback(() => {
if (recorder) {
recorder.start();
setIsRecording(true);
}
}, [recorder, setIsRecording]);
// Handle stop recording
const stopRecording = useCallback(() => {
if (recorder) {
recorder.stop();
setIsRecording(false);
}
}, [recorder, setIsRecording]);
// Return
return {
audioURL,
audioBlob,
setAudioBlob,
isRecording,
startRecording,
stopRecording
};
}
export const RecorderBox = ({
log,
handleUpdateLog,
handleSubmitNewMessage,
selectedLog
}: any) => {
const {
audioURL,
audioBlob,
setAudioBlob,
isRecording,
startRecording,
stopRecording
} = useRecorder();
const [newMessage, setNewMessage] = useState('');
// Create a decorated submit function
const decoratedSubmitNewMessage = useCallback(() => {
const body = {
body: newMessage,
attachment: audioBlob
};
handleUpdateLog(log, newMessage, audioBlob);
// handleSubmitNewMessage(log.id, body); // You had this commented out, but it should work just fine.
}, [handleUpdateLog, handleSubmitNewMessage, audioBlob, newMessage]);
// Cleanup whenever receiving new object for `log`
useEffect(() => {
setNewMessage('');
setAudioBlob(undefined);
document.getElementsByClassName('dzu-previewButton')[0]?.click(); // No idea what this is supposed to do.
}, [log, setNewMessage, setAudioBlob]);
return (
<div className="RecorderBox">
<audio src={audioURL} controls />
<button onClick={startRecording} disabled={isRecording}>
start recording
</button>
<button onClick={stopRecording} disabled={!isRecording}>
stop recording
</button>
<button onClick={decoratedSubmitNewMessage}>send recording</button>
<p>
<em></em>
</p>
</div>
);
};
JavaScript version:
import { useCallback, useEffect, useRef, useState } from 'react';
// This is a nugget I use in many similar cases.
/**
* Similar to `useMemo`, but accepts a callback that return a promise.
* Very useful when working with states that depend on some async function.
*
* #link https://gist.github.com/mariusbrataas/ffca05c210ec540b4fb9f4324c4f2e68
*
* #example
* function MyComponent({url}:{url: string}) {
* const data = usePromiseMemo(
* () =>
* fetch(url)
* .then(r => r.json())
* .then(r => r.my.user.info),
* [url]
* );
*
* return <p>{info || "Loading info..."}</p>
* }
*/
export function usePromiseMemo(callback, deps) {
// Private states
const [state, setState] = useState();
const isMounted = useRef(true);
// Register unmount
useEffect(() => {
if (!isMounted.current) isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
// Execute callback
useEffect(() => {
if (isMounted.current) {
if (state !== undefined) setState(undefined);
Promise.resolve()
.then(() => callback())
.then(r => {
if (isMounted.current) setState(r);
});
}
}, deps || []);
// Return state
return state;
}
function useRecorder() {
const [audioURL, setAudioURL] = useState();
const [audioBlob, setAudioBlob] = useState();
const [isRecording, setIsRecording] = useState(false);
// Handler for recorded data
const handleRecorderData = useCallback(
e => {
setAudioURL(URL.createObjectURL(e.data));
setAudioBlob(new File([e.data], 'incomingaudioclip.wav'));
},
[setAudioURL, setAudioBlob]
);
// Create recorder
const recorder = usePromiseMemo(
() =>
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(stream => new MediaRecorder(stream)),
[]
);
// Attach event listener
useEffect(() => {
if (recorder)
recorder.addEventListener('dataavailable', handleRecorderData);
return () => {
if (recorder)
recorder.removeEventListener('dataavailable', handleRecorderData);
};
}, [recorder, handleRecorderData]);
// Handle start recording
const startRecording = useCallback(() => {
if (recorder) {
recorder.start();
setIsRecording(true);
}
}, [recorder, setIsRecording]);
// Handle stop recording
const stopRecording = useCallback(() => {
if (recorder) {
recorder.stop();
setIsRecording(false);
}
}, [recorder, setIsRecording]);
// Return
return {
audioURL,
audioBlob,
setAudioBlob,
isRecording,
startRecording,
stopRecording
};
}
export const RecorderBox = ({
log,
handleUpdateLog,
handleSubmitNewMessage,
selectedLog
}) => {
const {
audioURL,
audioBlob,
setAudioBlob,
isRecording,
startRecording,
stopRecording
} = useRecorder();
const [newMessage, setNewMessage] = useState('');
// Create a decorated submit function
const decoratedSubmitNewMessage = useCallback(() => {
const body = {
body: newMessage,
attachment: audioBlob
};
handleUpdateLog(log, newMessage, audioBlob);
// handleSubmitNewMessage(log.id, body); // You had this commented out, but it should work just fine.
}, [handleUpdateLog, handleSubmitNewMessage, audioBlob, newMessage]);
// Cleanup whenever receiving new object for `log`
useEffect(() => {
setNewMessage('');
setAudioBlob(undefined);
document.getElementsByClassName('dzu-previewButton')[0]?.click(); // No idea what this is supposed to do.
}, [log, setNewMessage, setAudioBlob]);
return (
<div className="RecorderBox">
<audio src={audioURL} controls />
<button onClick={startRecording} disabled={isRecording}>
start recording
</button>
<button onClick={stopRecording} disabled={!isRecording}>
stop recording
</button>
<button onClick={decoratedSubmitNewMessage}>send recording</button>
<p>
<em></em>
</p>
</div>
);
};
My goal is to record a video when holding down the camera button but also take a picture when tapping the camera button. Any idea what I'm doing wrong?
const [video, setVideo] = useState(null);
const [recording, setRecording] = useState(false);
const cameraRef = createRef();
const onLongPressButton = () => {
setRecording(true);
startRecord();
};
const startRecord = async () => {
setRecording(true);
console.log("RECORDING");
if (cameraRef.current) {
setRecording(true);
const recordedVideo = await cameraRef.current.recordAsync();
setVideo(recordedVideo);
}
};
const stopRecord = async () => {
await cameraRef.current.stopRecording();
console.log("STOP RECORDING");
setRecording(false);
};
const handlePhoto = async () => {
if (cameraRef.current && !recording) {
let photo = await cameraRef.current.takePictureAsync({});
console.log(photo.uri);
} else {
stopRecord();
}
};
And here is my camera button component:
<Circle
onPress={handlePhoto}
onLongPress={onLongPressButton}
onPressOut={async () => {
await cameraRef.current.stopRecording();
console.log("STOP RECORDING");
setRecording(false);
}}
delayLongPress={50}
/>
The issue appears to be not with the camera or touch handling, but with the use of createRef instead of useRef. Note that in your case, used within a function component, createRef will create a new ref on every render. Replace it with useRef so that the reference remains the same across renders:
const cameraRef = useRef();
The app is a quiz, and if user finishes the round he may send the points in firebase. If user is not connected to internet, I save the points in device memory, so when connection is established the points are send in firebase.
The best would be to let this happen automatically and show a message...
I'm trying to do this in App.js in a useEffect, but it checks only if I refresh the app. I tried withNavigationFocus and useFocusEffect but error: the App.js is unable to get access to navigation....
I could also move the code to WelcomeScreen.js and show a button if connection is established to add the points, but it's not that user friendly.
Any ideas would be appreciated.
Thanks!
useEffect(() => {
const getPoints = async () => {
let points = await AsyncStorage.getItem("savedPoints");
if (!!points) {
const getEmail = async () => {
const userData = await AsyncStorage.getItem("userData");
if (userData) {
const transformedData = JSON.parse(userData);
const { userEmail } = transformedData;
return userEmail;
}
};
const email = await getEmail();
// Give it some time to get the token and userId,
// because saveData needs them.
setTimeout(
async () => await dispatch(dataActions.saveData(email, +points)),
3000
);
await AsyncStorage.removeItem("savedPoints");
}
};
NetInfo.fetch().then(state => {
if (state.isConnected) {
console.log("isConnected");
getPoints();
}
});
}, []);
The solution
WelcomeScreen.js
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
console.log("useEffect welcome");
const unsub = NetInfo.addEventListener(state => {
setIsConnected(state.isConnected);
});
return () => unsub();
}, []);
const getPoints = async () => {
console.log("getPoints welcome");
let points = await AsyncStorage.getItem("savedPoints");
if (!!points) {
const getEmail = async () => {
const userData = await AsyncStorage.getItem("userData");
if (userData) {
// parse converts a string to an object or array
const transformedData = JSON.parse(userData);
const { userEmail } = transformedData;
return userEmail;
}
};
const email = await getEmail();
// Give it some time to get the token and userId,
// because saveData needs them.
setTimeout(
async () => await dispatch(dataActions.saveData(email, +points)),
3000
);
await AsyncStorage.removeItem("savedPoints");
}
};
if (isConnected) getPoints();
You can set up a listener to listen for an internet connection. Don't use any logic in app.js, use it in a separate screen component.
constructor(props) {
super(props);
this.state = {
isConnected: false
};
}
componentDidMount() {
this.listenForInternetConnection = NetInfo.addEventListener(state => {
// your logic is here or setState
this.setState({
isConnected: state.isConnected
});
});
}
componentWillUnmount() {
this.listenForInternetConnection();
}
You can use JS EventListeners
window.addEventListener('load', () => {
navigator.onLine ? showStatus(true) : showStatus(false);
window.addEventListener('online', () => {
showStatus(true);
});
window.addEventListener('offline', () => {
showStatus(false);
});
});