How can I set up a connection inside useEffect
and at the same time decorate that connection with a custom hook? Custom hooks are not allowed to run inside useEffect
and ref.current
is not permitted during rendering. What is the proper way to wrap a connection with a React-friendly interface?
I believe the right way to set up a WebSocket
connection in React, is with a useEffect
hook, along the lines of
const connection = useRef(null);
useEffect(() => {
const ws = new WebSocket(socketUrl);
connection.current = ws
return () => {
ws.close();
connection.current = null;
}
}
In a similar way I envisage creating a RTCDataConnection
, which follows the same connection interface with .send('...')
and open
, close
, error
and message
events.
I am looking for a way to provide a React-like interface to these connections. I was thinking something like the following custom useConnection
hook, which takes a JavaScript connection object and in return provides two React state variables lastMessage
and readyState
, and a method sendMessage
:
export function useConnection(connection) {
const [lastMessage, setLastMessage] = useState(null);
const [readyState, setReadyState] = useState(null);
const onReadyStateChange = useCallback(
(e) => {
console.info(e);
setReadyState(connection.readyState);
},
[setReadyState]
);
const onMessage = useCallback(
({ data }) => {
const message = JSON.parse(data);
setLastMessage(message);
},
[setLastMessage]
);
function sendMessage(message) {
const msgJSON = JSON.stringify(message);
connection.send(msgJSON);
}
useEffect(() => {
connection.addEventListener('open', onReadyStateChange);
connection.addEventListener('close', onReadyStateChange);
connection.addEventListener('error', onReadyStateChange);
connection.addEventListener('message', onMessage);
return () => {
connection.removeEventListener('open', onReadyStateChange);
connection.removeEventListener('close', onReadyStateChange);
connection.removeEventListener('error', onReadyStateChange);
connection.removeEventListener('message', onMessage);
};
}, [connection]);
return { lastMessage, readyState, sendMessage };
}
I can’t find out, however, how to properly apply the hook to the connection.
- By the rules of hooks, I can’t call
useConnection
insideuseEffect
. It throws "Invalid hook call" at me. - I can’t reference
connection.current
outside theuseEffect
either, because that violates the rules ofuseRef
: It is not allowed to access the.current
property during rendering.
So my question is, how do I bring the connection and the hook together? Or should I use a different approach altogether?
2
Answers
What you would probably want is to call your hook at the top level of your component and then use it’s returned parts inside the
useEffect()
Your approach looks fantastic!
If you want to do this i.e. call the hook
from inside
useEffect
because it is the only place where you have access tosomething
so this is not possible.hooks should be called in the body of your function component not conditionally and not inside a loop you can think of them like an import.
solution:
create a state
connection
and use its value asuseConnection
function paramNote: you may need to make some work in your hook to handle
null
connection recived because this will happen when the hook is called when the component first mountNow from
useEffect
instead of call the hook, you just updateconnection
so the component re-renders and your hook function will be recalled each timeconnection
receives a new value. also note that theuseEffect
hook of your custom hook will fire each time receivedconnection
is updated, since it is included in its dependency array.