How to trigger callback using useState synchronously if callback was external - reactjs

I have the following hook:
const MessageStorage = () => {
const [messages, setMessages] = useState([]);
const addMessage = (message) => { ... }
const reset = (callback) => {
setMessages([])
callback(); // wrong since setMessages([]) is asynchronous
}
return { addMessage, reset };
}
The problem is with reset function. The standard approach to triggering callback is to use useEffect to track the change of state. But the problem is that callback is defined by the consumer of MessageStorage hook, and it must be defined as a parameter of reset function.
What's the best way to tackle this?

One approach is to put the callback into state:
const [callback, setCallback] = useState();
// ...
const reset = (callback) => {
setMessages([])
setCallback(callback);
}
useEffect(() => {
callback?.();
setCallback(); // make sure callback only gets called once
}, [messages]);

Related

Memoize function argument in custom hook

I'm building a custom hook that accepts a function. If the function will not change between re-renders/updates, where should I memoize the function?
OPTION 1
const useCustomHook = (callback) => {
const callbackRef = useRef();
callbackRef.current = callback;
// No effect, this will be re-created every time this hook is called
// because a new instance of callback is being passed
const callbackWrapper = useCallback(() => {
if (callbackRef.current) {
callbackRef.current();
}
}, [callbackRef]);
// use callbackWrapper
}
const Component = () => {
// New instance of passed callback will be created each time this component re-renders
useCustomHook(() => {
console.log(`I'm being passed to the hook`);
});
// ...
// ...
return <div></div>;
}
OPTION 2
const useCustomHook = (callback) => {
// Callback is already memoized
const callbackRef = useRef();
callbackRef.current = callback;
// use callbackRef
}
const Component = () => {
// Memoized function passed, but
// 1. Is this allowed?
// 2. Requires more effort by users of the hook
useCustomHook(useCallback(() => {
console.log(`I'm being passed to the hook`);
}, []));
// ...
// ...
return <div></div>;
}
Option 2 seems more valid but it requires users of the custom hook to first enclose their function in a useCallback() hook. Is there an alternative way where users don't need to enclose the passed function in useCallback()?
I would probably use the useEffect hook to "memoize" the callback to the React ref.
const useCustomHook = (callback) => {
const callbackRef = useRef();
useEffect(() => {
// only update callback saved in ref if callback updates
callbackRef.current = callback;
}, [callback]);
// provide stable callback that always calls latest
// callback stored in ref
const callbackWrapper = useCallback(() => {
if (callbackRef.current) {
callbackRef.current();
}
}, []);
// use callbackWrapper
}
Or use useMemo or useCallback directly, but at this point you are essentially just redefining useMemo or useCallback in your custom hook. In this case, pass an additional dependency array argument.
const useCustomHook = (callback, deps) => {
const callbackWrapper = useMemo(() => callback, deps);
// use callbackWrapper
}
This being said, you will likely want to be in the habit of memoizing callbacks that are being passed down before they are passed, in order to guaranteed provided stable references. This way you won't need to go through the rigamarole of the weirdness of your custom hook.
Another other way is to declare the callback outside of the component body ( but I don't recommend that, It can create some edge cases )
function cb () {
console.log(`I'm being passed to the hook`);
}
const Component = () => {
useCustomHook(cb);
// ...
// ...
return <div></div>;
}
memoize the function in you want on top level component body and then pass it to your custom hook like this:
const Component = () => {
// hooks should be called on top level function body
const cb = useCallback(() => {
console.log(`I'm being passed to the hook`);
}, []);
useCustomHook(cb);
// ...
// ...
return <div></div>;
}

wait for state to change in the same function?

How can I wait for the url hook to not be empty? handleViewSheet is an on click function and I do not want to use useEffect because of the initial render. Can this be done without useEffect?
const [url, setUrl] = useState('')
const handleViewSheet = () => {
GetSheet(id).then((res) => {
if (res) {
setUrl(res);
}
});
const task = getDocument(url); // url is still empty at this point
}
It seems like a job for useEffect to be honest, so I would recommand to consider using it (You can handle the first render within the useEffect if you want).
But if you still want to avoid using useEffect, you can use async/await to wait for the promise to finish before using url, along with the setUrl call. Something like this:
const [url, setUrl] = useState('')
const handleViewSheet = async () => {
let resUrl = await GetSheet(id).then((res) => {
if (res) {
setUrl(res);
return res;
}
return '';
});
const task = getDocument(resUrl );
}

React infinity loop when making HTTP calls using useEffect

I am trying to make 2 HTTTP calls inside a React component that will then call the setters for 2 properties that are defined using useState. I have followed what I thought was the correct way of doing so in order to prevent inifinite rerendering but this is still happening. Here is my code:
function Dashboard({ history = [] }) {
const [teamInfo, setTeamInfo] = useState(null);
const [survey, setSurvey] = useState(null);
const [open, setOpen] = useState(false);
const user = getUser();
const getSurveyHandler = async () => {
const surveyResponse = await getSurveys('standard');
setSurvey(surveyResponse.data);
};
const getTeamInfoHandler = async () => {
const teamInfoResponse = await getTeamInfo(user.teamId);
setTeamInfo(teamInfoResponse);
};
useEffect(() => {
document.body.style.backgroundColor = '#f9fafb';
getSurveyHandler();
getTeamInfoHandler();
}, [survey, teamInfo]);
As you can see, I have defined the functions outside of the useEffect and passed in the two state variables into the dependency array that will be checked to prevent infinite rerendering.
Can anyone see why this is still happening?
Thanks
You are setting survey and teamInfo in your functions with a dependency on them in your useEffect.
useEffect runs everytime a dependency changes. You are setting them, causing a rerender. Since they changed, the useEffect runs again, setting them again. The cycle continues.
You need to remove those.
useEffect(() => {
document.body.style.backgroundColor = '#f9fafb';
getSurveyHandler();
getTeamInfoHandler();
}, []);
The only other thing recommended is to move async functions inside the useEffect unless you need to call them from other places in the component.
useEffect(() => {
const getSurveyHandler = async () => {
const surveyResponse = await getSurveys('standard');
setSurvey(surveyResponse.data);
};
const getTeamInfoHandler = async () => {
const teamInfoResponse = await getTeamInfo(user.teamId);
setTeamInfo(teamInfoResponse);
};
document.body.style.backgroundColor = '#f9fafb';
getSurveyHandler();
getTeamInfoHandler();
}, []);

ReactJS 16.13.1 Hook incoherent state in listener

I don't understand which magic operate here, I try everything is come up in my mind, I can't fix that problem.
I want to use an array which is in the react state of my component in a websocket listener, when the listener is triggered my state is an empty array, however I set a value in an useEffect.
Here my code :
function MyComponent() {
const [myData, setMyData] = useState([]);
const [sortedData, setSortedData] = useState([]);
useEffect(() => {
someAxiosCallWrapped((response) => {
const infos = response.data;
setMyData(infos.data);
const socket = getSocket(info.socketNamespace); // wrap in socket namespace
handleEvents(socket);
});
}, []);
useEffect(() => {
setSortedData(sortTheArray(myData));
}, [myData]);
const handleEvents = (socket) => {
socket.on('EVENT_NAME', handleThisEvent);
};
const handleThisEvent = payload => {
const myDataCloned = [...myData]; //<=== my probleme is here, whatever I've tried myData is always an empty array, I don't understand why
/**
* onHandleEvent is an external function, just push one new object in the array no problem in the function
*/
onHandleEvent(myDataCloned, payload);
setMyData(myDataCloned);
};
return (
<div>
// display sortedData no problem here
</div>
);
}
Probably missed something obvious, if someone see what.
Thanks !
From the docs:
Any function inside a component, including event handlers and effects, “sees” the props and state from the render it was created in.
Here handleEvents is called from useEffect on mount and hence it sees only the initial data ([]). To catch this error better, we can move the functions inside useEffect (unless absolutely necessary outside)
useEffect(() => {
const handleEvents = (socket) => {
socket.on('EVENT_NAME', handleThisEvent);
};
const handleThisEvent = payload => {
const myDataCloned = [...myData];
onHandleEvent(myDataCloned, payload);
};
someAxiosCallWrapped((response) => {
const infos = response.data;
setMyData(infos.data);
const socket = getSocket(info.socketNamespace); // wrap in socket namespace
handleEvents(socket);
});
return () => {
socket.off('EVENT_NAME', handleThisEvent);
}
}, [myData, onHandleEvent]);
Now, you can see that the useEffect has dependencies on myData and onHandleEvent. We did not introduce this dependency now, it already had these, we are just seeing them more clearly now.
Also note that we are removing the listener on change of useEffect. If onHandleEvent changes on every render, you would to wrap that with useCallback in parent component.
Is it safe to omit a function from dependencies - Docs
you need to use useMemo hook to update the function once the array value changed unless Hooks Component will always use initial state value.
change the problem part to this and try
const handleThisEvent = useMemo(payload => {
const myDataCloned = [...myData];
onHandleEvent(myDataCloned, payload);
setMyData(myDataCloned);
},[the values you need to look for(In this case myData)]);
I finaly come up with a code like that.
useEffect(() => {
let socket;
someAxiosCallWrapped((response) => {
const infos = response.data;
setMyData(infos.data);
socket = getSocket(info.socketNamespace); // wrap in socket namespace
setSocket(socket)
});
return () => {
if(socket) {
socket.disconnect();
}
}
}, []);
useEffect(() => {
if(socket) {
const handleEvents = (socket) => {
socket.off('EVENT_NAME').on('EVENT_NAME', handleThisEvent);
};
const handleThisEvent = payload => {
const myDataCloned = [...myData];
onHandleEvent(myDataCloned, payload);
};
}
}, [socket, myData, onHandleEvent]);
Thanks to Agney !

React UseEffect and Unsubscribe promise with conditional listener ! (Optimize Firestore onsnapshot)

I never find a solution for my useEffect probleme :
I'm useing firebase and create a listener (onSnapshot) on my database to get the last state of my object "Player" I can get when the states currentGroup and currentUser are available
const [currentGroup, setCurrentGroup] = useState(null)
const [currentUser, setCurrentUser] = useState(null)
const [currentPlayer, setCurrentPlayer] = useState(null)
const IDefineMyListener = () =>{
return firebase.doc(`group/${currentGroup.id}${users/${currentUser.id}/`)
.onSnpashot(snap =>{
//I get my snap because it changed
setCurrentPlayer(snap.data())
})
}
Juste above, i call a useEffect when the currentGroup and currentUser are available and (IMPORTANT) if I didn't already set the currentPlayer
useEffect(() => {
if (!currentGroup || !currentUser) return
if (!currentPlayer) {
let unsubscribe = IDefineMyListener()
return (() => {
unsubscribe()
})
}
},[currentGroup,currentUser])
As you can think, unsubscribe() is called even if the IDefineMyListener() is not redefined. In other words, when currentGroup or currentUser changes, this useEffect deleted my listener whereas I NEED IT.
How can i figure out ?!
PS :if I remove if (!currentPlayer), of course it works but will unlessly get my data
PS2 : If I remove the unsubscribe, my listener is called twice each time.
You can use useCallback hook to work around this.
First we'll define your listener using useCallback and give the dependency array the arguments as currentGroup and currentUser.
const IDefineMyListener = useCallback(event => {
return firebase.doc(`group/${currentGroup.id}${users/${currentUser.id}/`)
.onSnpashot(snap =>{
//I get my snap because it changed
setCurrentPlayer(snap.data())
})
}, [currentGroup, currentUser]);
And we will only use useEffect to register and deregister your listener.
useEffect(() => {
//subscribe the listener
IDefineMyListener()
return (() => {
//unsubscribe the listener here
unsubscribe()
})
}
},[])
Since we passed an [] to useEffect, it will only run once when the component is mounted. But we have already registered the callback. So your callback will run everytime the currentGroup or currentUser changes without deregistering your listener.
The problem was about my bad understanding of the unsubscribe() .
I didn't return anything in my useEffect, but save my unsusbcribe function to call it when i need.
let unsubscribe = null //I will save the unsubscription listener inside it
const myComponent = () => {
const [currentGroup, setCurrentGroup] = useState(null)
const [currentUser, setCurrentUser] = useState(null)
const [currentPlayer, setCurrentPlayer] = useState(null)
useEffect(() => {
if (!currentGroup || !currentUser) {
if(unsubscribe){
unsubscribe() // 2 - when I dont need of my listener, I call the unsubscription function
}
return
}
if (!currentPlayer && !unsubscribe) {
unsubscribe = IDefineMyListener() // 1 - I create my listener and save the unsubscription in a persistant variable
}
},[currentGroup,currentUser])
const IDefineMyListener = () =>{
return firebase.doc(`group/${currentGroup.id}${users/${currentUser.id}/`)
.onSnpashot(snap =>{
//I get my snap because it changed
setCurrentPlayer(snap.data())
})
}
...

Resources