How to cleanup an async funtion on componentDidUpdate using hooks - reactjs

I've a component upon whose mounting, I'm subscribing to a socket(async task) and I've to unsubscribe(async) in upon unmounting. When the component updates, I've to unsubscribe to the old socket and subscribe to the new one. I'm not sure how to do this using react hooks. Attaching a sample codesandbox for reference.
https://codesandbox.io/s/clever-robinson-h0gq5

I'm not sure how you would like to implement the code within your sandbox but I think to call and async task within an effect you can do it like this:
useEffect(() => {
const timeout = async (msg, time) => {
await setTimeout(() => {
console.log(msg);
}, time);
}
timeout("subscribed to websocket", 2000);
return () => {
timeout("unsubscribed to websocket", 3000);
};
}, [match]);
To use an async await within useEffect you need to declare it within the function and call it from there.

Related

Using useEffect properly when making reqs to a server

I have a handleRating function which sets some state as so:
const handleRating = (value) => {
setCompanyClone({
...companyClone,
prevRating: [...companyClone.prevRating, { user, rating: value }]
});
setTimeout(() => {
handleClickOpen();
}, 600);
};
I think also have a function which patches a server with the new companyClone values as such:
const updateServer = async () => {
const res = await axios.put(
`http://localhost:3000/companies/${companyClone.id}`,
companyClone
);
console.log("RES", res.data);
};
my updateServer function gets called in a useEffect. But I only want the function to run after the state has been updated. I am seeing my res.data console.log when I load my page. Which i dont want to be making reqs to my server until the comapanyClone.prevRating array updates.
my useEffect :
useEffect(() => {
updateServer();
}, [companyClone.prevRating]);
how can I not run this function on pageload. but only when companyClone.prevRating updates?
For preventing function call on first render, you can use useRef hook, which persists data through rerender.
Note: useEffect does not provide the leverage to check the current updated data with the previous data like didComponentMount do, so used this way
Here is the code example.
https://codesandbox.io/s/strange-matan-k5i3c?file=/src/App.js

How to use context values in useEffect, that only runs once

i've got an interesting problem here. I am building a react application using web socket communication with the server. I create this websocket in a useEffect hook, which therefore cannot run multiple times, otherwise i'd end up with multiple connections. In this useEffect, however i intend to use some variables,which are actually in a context (useContext) hook. And when the context values change, the values in useEffect , understandably, don't update. I've tried useRef, but didn't work. Do you have any ideas?
const ws = useRef<WebSocket>();
useEffect(() => {
ws.current = new WebSocket("ws://localhost:5000");
ws.current.addEventListener("open", () => {
console.log("opened connection");
});
ws.current.addEventListener("message", (message) => {
const messageData: ResponseData = JSON.parse(message.data);
const { response, reload } = messageData;
if (typeof response === "string") {
const event = new CustomEvent<ResponseData>(response, {
detail: messageData,
});
ws.current?.dispatchEvent(event);
} else {
if (reload !== undefined) {
console.log("general info should reload now");
GeneralInfoContext.reload(reload);
}
console.log(messageData);
}
});
});
The web socket is stored as a ref for better use in different functions outside of this useEffect block
Note: the context value to be used is actually a function, GeneralInfoContext.reload()
Solution with split useEffect
You can split the logic that opens the websocket connection vs. the one that adds the message handler into separate useEffects - the first can run once, while the second can re-attach the event every time a dependency changes:
useEffect(() => {
ws.current = new WebSocket("ws://localhost:5000");
ws.current.addEventListener("open", () => {
console.log("opened connection");
});
}, []);
useEffect(() => {
const socket = ws.current;
if(!socket) throw new Error("Expected to have a websocket instance");
const handler = (message) => {
/*...*/
}
socket.addEventListener("message", handler);
// cleanup
return () => socket.removeEventListener("message", handler);
}, [/* deps here*/])
The effects will run in order so the second effect will run after the first effect has already set ws.current.
Solution with callback ref
Alternatively you could put the handler into a ref and update it as necessary, and reference the ref when calling the event:
const handlerRef = useRef(() => {})
useEffect(() => {
handlerRef.current = (message) => {
/*...*/
}
// No deps here, can update the function on every render
});
useEffect(() => {
ws.current = new WebSocket("ws://localhost:5000");
ws.current.addEventListener("open", () => {
console.log("opened connection");
});
const handlerFunc = (message) => handlerRef.current(message);
ws.current.addEventListener("message", handlerFunc);
return () => ws.current.removeEventListener("message", handlerFunc);
}, []);
It's important that you don't do addEventListener("message", handlerRef.current) as that will only attach the original version of the function - the extra (message) => handlerRef.current(message) wrapper is necessary so that every message gets passed to the latest version of the handler func.
This approach still requires two useEffect as it's best to not put handlerRef.current = /* func */ directly in the render logic, as rendering shouldn't have side-effects.
Which to use?
I like the first one personally, detaching and reattaching event handlers should be harmless (and basically 'free') and feels less complicated than adding an additional ref.
But the second one avoids the need for an explicit dependency list, which is nice too, especially if you aren't using the eslint rule to ensure exhaustive deps. (Though you definitely should be)
You can provide useEffect with a list of variables and useEffect will re-run when these variables change.
This is a little example:
const [exampleState, setExampleState] = useState<boolean>(false);
useEffect(() => {
console.log("exampleState was updated.");
}, [exampleState]);
An example from reactjs website:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes
You should pass an empty array as the second parameter to the useEffect, so it this case it becomes akin to the componentDidMount() logic of react
useEffect(() => {
...your websocket code here
}, [])

How to fix the unmounted component in react hooks

How to fix the error: Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
here' the code:
useEffect(() => {
let ignore = false;
setTimeout(() => {
const fetchData = async () => {
try {
setLoading(true);
setError({});
const response = await getLatest();
if (!ignore) setData(response['data']);
} catch (err) {
setError(err);
}
setLoading(false);
};
fetchData();
});
return () => {
ignore = true;
};
}, []);
The problem here is when I click the home page while loading the data and click the room page. then the error will appeared. How to fix on it?
You need to clean up your timeout on unmount, otherwise, it tries to execute your hooks to update the internal state of the component after it goes out of scope (I assume functions like setLoading and setError are from a React hook).
To do so, put the output of setTimeout in a variable and then call clearTimeout in the function you return from useEffect. That's the clean up function and it's run when the component gets unmounted. So you need something like this:
React.useEffect(() => {
...
const timeout = setTimeout(...)
return () => {
...
clearTimeout(timeout);
}
}, [])
See the docs for more info: https://reactjs.org/docs/hooks-reference.html#cleaning-up-an-effect

Set subscription once at time of mounting and do not refresh it on every update?

I have the following firebase subscription being created with useEffect in a simple functional component:
const [messages, setMessages] = useState([]);
useEffect(() => {
const unsubscribe = db.collection('chatChannels').doc('general')
.onSnapshot(snapshot => {
if (snapshot.data()) {
setMessages(snapshot.data().messages);
} else {
console.log('it was empty');
}
})
return () => {
unsubscribe();
};
});
This is intended to retrieve the content of the messages field off the general document in the chatChannels collection. I'm trying to set up a simple instant messaging public channel, in other words.
The issue is I find this is problematic for performance. Anytime a message is sent, state is updated, the subscription is canceled, useEffect triggers, and the subscription is recreated. It's a lot of unnecessary network roundtrips. I would like to avoid that. But I can't think of a way to set this up such that a new message updates state without remaking the subscription every time.
I do want to unsubscribe when the component is entirely unmounted, but not every time it updates. It's unclear to me whether that's possible.
Pass an empty dependency [] to the useEffect hook and it'll behave componentDidMount and componentWillUnmount.
useEffect(() => {
const unsubscribe = db.collection('chatChannels').doc('general')
.onSnapshot(snapshot => {
if (snapshot.data()) {
setMessages(snapshot.data().messages);
} else {
console.log('it was empty');
}
})
return () => {
unsubscribe();
};
}, []);
Here is a link to the official useEffect documentation: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

Using React State Hook, call function after setting multiple states

I'm trying to make a function that will reset multiple states and then make an API call, however I'm having trouble making the API call happen AFTER the three states have been set. My function looks like this:
const resetFilters = () => {
setYearFilter("");
setProgressFilter("");
setSearchFilter("");
callAPI();
};
I've tried using Promise.resolve().then(), and tried using async await, but it seems the useState setter function doesn't return a promise. Is there a way to make this all happen synchronously?
You could use useEffect to listen on changes and do each task sequentially
const resetFilters = () => {
setYearFilter("")
}
useEffect(() => {
setProgressFilter("")
}, [yearFilter])
useEffect(() => {
setSearchFilter("")
}, [progressFilter])
useEffect(() => {
callAPI()
}, [searchFilter])

Resources