I have this component:
export const Widget: FC<IWidgetProps> = ({ appid, flavor, showWidgetField, updateSpeed }) => {
const { state: geocodingState } = useContext(GeocodingContext);
const { dispatch: weatherDispatch } = useContext(WeatherContext);
const fetchCb = useCallback(() => {
if (geocodingState.coords) getWeatherInfo(weatherDispatch)(appid, geocodingState.coords.lon, geocodingState.coords.lat);
}, [geocodingState.coords]);
useEffect(() => {
let interval: NodeJS.Timeout | null = null;
if (!interval) {
interval = setInterval(fetchCb, updateSpeed);
} else {
if (!geocodingState.coords) clearInterval(interval);
}
return () => {
if (interval) clearInterval(interval);
}
}, [geocodingState.coords]);
return (
<>
</>
);
};
Unfortunately, the interval does not get cleared when the geocodingState.coords is nullified nor in the useEffect return.
Because everytime you change geocodingState.coords, you creating new interval.... and the previous one is still running. Best solution for this is to save interval in useState, because othervise, your code doesnt know ID of interval, that needs to be cleared.
export const Widget: FC<IWidgetProps> = ({ appid, flavor, showWidgetField, updateSpeed }) => {
const { state: geocodingState } = useContext(GeocodingContext);
const { dispatch: weatherDispatch } = useContext(WeatherContext);
const [intervalId, setIntervalId] = useState();
const fetchCb = useCallback(() => {
if (geocodingState.coords) getWeatherInfo(weatherDispatch)(appid, geocodingState.coords.lon, geocodingState.coords.lat);
}, [geocodingState.coords]);
useEffect(() => {
if (!intervalId) {
interval = setInterval(fetchCb, updateSpeed);
setIntervalId(interval)
} else {
if (!geocodingState.coords) clearInterval(intervalId);
}
return () => {
if (intervalId){
clearInterval(interval);
setIntervalId(undefined);
}
}, [geocodingState.coords]);
return (
<>
</>
);
};
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>
);
};
I have fetch method in useEffect hook:
export const CardDetails = () => {
const [ card, getCardDetails ] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => getCardDetails(data))
}, [id])
return (
<DetailsRow data={card} />
)
}
But then inside DetailsRow component this data is not defined, which means that I render this component before data is fetched. How to solve it properly?
Just don't render it when the data is undefined:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data));
}, [id]);
if (card === undefined) {
return <>Still loading...</>;
}
return <DetailsRow data={card} />;
};
There are 3 ways to not render component if there aren't any data yet.
{data && <Component data={data} />}
Check if(!data) { return null } before render. This method will prevent All component render until there aren't any data.
Use some <Loading /> component and ternar operator inside JSX. In this case you will be able to render all another parts of component which are not needed data -> {data ? <Component data={data} /> : <Loading>}
If you want to display some default data for user instead of a loading spinner while waiting for server data. Here is a code of a react hook which can fetch data before redering.
import { useEffect, useState } from "react"
var receivedData: any = null
type Listener = (state: boolean, data: any) => void
export type Fetcher = () => Promise<any>
type TopFetch = [
loadingStatus: boolean,
data: any,
]
type AddListener = (cb: Listener) => number
type RemoveListener = (id: number) => void
interface ReturnFromTopFetch {
addListener: AddListener,
removeListener: RemoveListener
}
type StartTopFetch = (fetcher: Fetcher) => ReturnFromTopFetch
export const startTopFetch = function (fetcher: Fetcher) {
let receivedData: any = null
let listener: Listener[] = []
function addListener(cb: Listener): number {
if (receivedData) {
cb(false, receivedData)
return 0
}
else {
listener.push(cb)
console.log("listenre:", listener)
return listener.length - 1
}
}
function removeListener(id: number) {
console.log("before remove listener: ", id)
if (id && id >= 0 && id < listener.length) {
listener.splice(id, 1)
}
}
let res = fetcher()
if (typeof res.then === "undefined") {
receivedData = res
}
else {
fetcher().then(
(data: any) => {
receivedData = data
},
).finally(() => {
listener.forEach((cb) => cb(false, receivedData))
})
}
return { addListener, removeListener }
} as StartTopFetch
export const useTopFetch = (listener: ReturnFromTopFetch): TopFetch => {
const [loadingStatus, setLoadingStatus] = useState(true)
useEffect(() => {
const id = listener.addListener((v: boolean, data: any) => {
setLoadingStatus(v)
receivedData = data
})
console.log("add listener")
return () => listener.removeListener(id)
}, [listener])
return [loadingStatus, receivedData]
}
This is what myself needed and couldn't find some simple library so I took some time to code one. it works great and here is a demo:
import { startTopFetch, useTopFetch } from "./topFetch";
// a fakeFetch
const fakeFetch = async () => {
const p = new Promise<object>((resolve, reject) => {
setTimeout(() => {
resolve({ value: "Data from the server" })
}, 1000)
})
return p
}
//Usage: call startTopFetch before your component function and pass a callback function, callback function type: ()=>Promise<any>
const myTopFetch = startTopFetch(fakeFetch)
export const Demo = () => {
const defaultData = { value: "Default Data" }
//In your component , call useTopFetch and pass the return value from startTopFetch.
const [isloading, dataFromServer] = useTopFetch(myTopFetch)
return <>
{isloading ? (
<div>{defaultData.value}</div>
) : (
<div>{dataFromServer.value}</div>
)}
</>
}
Try this:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
if (!data) {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data))
}
}, [id, data]);
return (
<div>
{data && <DetailsRow data={card} />}
{!data && <p>loading...</p>}
</div>
);
};
I am trying to clean up my code an separate into functions that only have one task.
In v1 joinDailyCo(url); was defined inside fetchUrl(). Now I tried to move it out with
const url = fetchUrl();
joinDailyCo(url);
However, as soon I do that, I get the error message:
Unhandled Rejection (TypeError): Cannot read property 'join' of
undefined
const Daily = ({ eventSlug, tableId }) => {
const classes = useStyles();
const iframeRef = useRef();
const dailyRef = useRef();
const joinedRef = useRef();
useEffect(() => {
// Join call
const joinDailyCo = async (url) => {
if (joinedRef.current) {
// This is needed due to it never returning if there wasn't a meeting joined first.
await dailyRef.current.leave();
}
await dailyRef.current.join({ url });
};
// Retrieve dailySessionId and meetingToken.
const fetchUrl = async () => {
try {
const {
data: { dailySessionId, meetingToken },
} = await api.get(
`events/${eventSlug}/space/tables/${tableId}/daily-auth/`
);
const url = `${DAILY_URL}/${dailySessionId}?t=${meetingToken}`;
return url;
// joinDailyCo(url);
} catch (error) {
Sentry.captureException(error);
}
};
const url = fetchUrl();
url && joinDailyCo(url);
}, [eventSlug, tableId]);
useEffect(() => {
dailyRef.current = DailyIframe.wrap(iframeRef.current, {
// showLeaveButton: true,
});
dailyRef.current.on(eventTypes.LEFT_MEETING, () => {
joinedRef.current = false;
});
dailyRef.current.on(eventTypes.JONING_MEETING, () => {
joinedRef.current = true;
});
return () => {
dailyRef.current.destroy();
};
}, []);
return (
<iframe
ref={iframeRef}
className={classes.root}
title="Video Meeting"
allow="camera; microphone; display-capture; fullscreen"
/>
);
};
export default Daily;
I'm learning to React Hooks.
And I'm struggling initialize data that I fetched from a server using a custom hook.
I think I'm using hooks wrong.
My code is below.
const useFetchLocation = () => {
const [currentLocation, setCurrentLocation] = useState([]);
const getCurrentLocation = (ignore) => {
...
};
useEffect(() => {
let ignore = false;
getCurrentLocation(ignore);
return () => { ignore = true; }
}, []);
return {currentLocation};
};
const useFetch = (coords) => {
console.log(coords);
const [stores, setStores] = useState([]);
const fetchData = (coords, ignore) => {
axios.get(`${URL}`)
.then(res => {
if (!ignore) {
setStores(res.data.results);
}
})
.catch(e => {
console.log(e);
});
};
useEffect(() => {
let ignore = false;
fetchData(ignore);
return () => {
ignore = true;
};
}, [coords]);
return {stores};
}
const App = () => {
const {currentLocation} = useFetchLocation();
const {stores} = useFetch(currentLocation); // it doesn't know what currentLocation is.
...
Obviously, it doesn't work synchronously.
However, I believe there's the correct way to do so.
In this case, what should I do?
I would appreciate if you give me any ideas.
Thank you.
Not sure what all the ignore variables are about, but you can just check in your effect if coords is set. Only when coords is set you should make the axios request.
const useFetchLocation = () => {
// Start out with null instead of an empty array, this makes is easier to check later on
const [currentLocation, setCurrentLocation] = useState(null);
const getCurrentLocation = () => {
// Somehow figure out the current location and store it in the state
setTimeout(() => {
setCurrentLocation({ lat: 1, lng: 2 });
}, 500);
};
useEffect(() => {
getCurrentLocation();
}, []);
return { currentLocation };
};
const useFetch = coords => {
const [stores, setStores] = useState([]);
const fetchData = coords => {
console.log("make some HTTP request using coords:", coords);
setTimeout(() => {
console.log("pretending to receive data");
setStores([{ id: 1, name: "Store 1" }]);
}, 500);
};
useEffect(() => {
/*
* When the location is set from useFetchLocation the useFetch code is
* also triggered again. The first time coords is null so the fetchData code
* will not be executed. Then, when the coords is set to an actual object
* containing coordinates, the fetchData code will execute.
*/
if (coords) {
fetchData(coords);
}
}, [coords]);
return { stores };
};
function App() {
const { currentLocation } = useFetchLocation();
const { stores } = useFetch(currentLocation);
return (
<div className="App">
<ul>
{stores.map(store => (
<li key={store.id}>{store.name}</li>
))}
</ul>
</div>
);
}
Working sandbox (without the comments) https://codesandbox.io/embed/eager-elion-0ki0v