Reputation: 1432
I am building a Chat application using Reactjs and Redux. I have 2 components called ChatHeads and ChatBox which get mounted side-by-side at the same time.
In the ChatHeads component, the selection of User (to whom you want to chat with) is possible and this selection is stored in the redux store as chatInfo
.
ChatHeads Component:
function ChatHeads(props) {
const {
dispatch,
userInfo,
userId
} = props;
const [chatHeads, setChatHeads] = useState([]);
const handleChatHeadSelect = (chatHead, newChat = false) => {
dispatch(
chatActions.selectChat({
isNewChat: newChat,
chatId: chatHead.chat._id,
chatUser: chatHead.user
})
);
};
const loadChatHeads = async () => {
const response = await services.getRecentChats(userId, userInfo);
setChatHeads(response.chats);
};
useEffect(() => loadChatHeads(), [userInfo]);
return (
// LOOPING THOUGH ChatHeads AND RENDERING EACH ITEM
// ON SELECT OF AN ITEM, handleChatHeadSelect WILL BE CALLED
);
}
export default connect(
(state) => {
return {
userInfo: state.userInfo,
userId: (state.userInfo && state.userInfo.user && state.userInfo.user._id) || null,
selectedChat: (state.chatInfo && state.chatInfo.chat && state.chatInfo.chat._id) || null
};
},
null,
)(ChatHeads);
Chat Actions & Reducers:
const initialState = {
isNewChat: false,
chatId: '',
chatUser: {},
};
const chatReducer = (state = initialState, action) => {
let newState;
switch (action.type) {
case actions.CHAT_SELECT:
newState = { ...action.payload };
break;
default:
newState = state;
break;
}
return newState;
};
export const selectChat = (payload) => ({
type: actions.CHAT_SELECT,
payload,
});
In the ChatBox component, I am establishing a socket connection to the server and based on chatInfo
object from the global store & ws events, I perform some operations.
ChatBox Component:
let socket;
function ChatBox(props) {
const { chatInfo } = props;
const onWSMessageEvent = (event) => {
console.log('onWSMessageEvent => chatInfo', chatInfo);
// handling event
};
useEffect(() => {
socket = services.establishSocketConnection(userId);
socket.addEventListener('message', onWSMessageEvent);
return () => {
socket.close();
};
}, []);
return (
// IF selectedChatId
// THEN RENDER CHAT
// ELSE
// BLANK SCREEN
);
}
export default connect((state) => {
return {
chatInfo: state.chatInfo
};
}, null)(ChatBox);
Steps:
chatInfo
object has been populated properly.chatInfo: {
isNewChat: false,
chatId: '603326f141ee33ee7cac02f4',
chatUser: {
_id: '602a9e589abf272613f36925',
email: '[email protected]',
firstName: 'user',
lastName: '2',
createdOn: '2021-02-15T16:16:24.100Z',
updatedOn: '2021-02-15T16:16:24.100Z'
}
}
chatInfo
property should have the latest values. But, I am always getting the initialState instead of the updated ones.chatInfo: {
isNewChat: false,
chatId: '',
chatUser: {}
}
What am I missing here? Please suggest...
Upvotes: 3
Views: 1156
Reputation: 1133
using store.getState()
to get the latest states
I had the same issue where I had latest value in redux store but getting previous/stale value inside socket
above answer with refs did not worked for me
So I use store.getState()
to get the current/latest states;
socket.on("message", async (data: any) => {
const state = store.getState(); // using this
console.log(state.chatInfo) // I get latest value here
}
Upvotes: 0
Reputation: 4079
The reason for this behaviour is that when you declare your callback
const { chatInfo } = props;
const onWSMessageEvent = (event) => {
console.log('onWSMessageEvent => chatInfo', chatInfo);
// handling event
};
it remembers what chatInfo
is right at this moment of declaration (which is the initial render). It doesn't matter to the callback that the value is updated inside the store and inside the component render scope, what matters is the callback scope and what chatInfo
is referring to when you declare the callback.
If you want to create a callback that can always read the latest state/props, you can instead keep the chatInfo
inside a mutable reference.
const { chatInfo } = props;
// 1. create the ref, set the initial value
const chatInfoRef = useRef(chatInfo);
// 2. update the current ref value when your prop is updated
useEffect(() => chatInfoRef.current = chatInfo, [chatInfo]);
// 3. define your callback that can now access the current prop value
const onWSMessageEvent = (event) => {
console.log('onWSMessageEvent => chatInfo', chatInfoRef.current);
};
You can check this codesandbox to see the difference between using ref and using the prop directly.
You can consult the docs about stale props and useRef docs
Broadly speaking, the issue is that you're trying to manage a global subscription (socket connection) inside a much more narrow-scope component.
Another solution without useRef
would look like
useEffect(() => {
socket = services.establishSocketConnection(userId);
socket.addEventListener('message', (message) => handleMessage(message, chatInfo));
return () => {
socket.close();
};
}, [chatInfo]);
In this case the message event handler is passed the necessary information through arguments, and the useEffect
hook re-runs every time we get a new chatInfo
.
However, this probably doesn't align with your goals unless you want to open a separate socket for each chat and close the socket every time you switch to a different chat.
Thus, the "proper" solution would entail moving the socket interaction up in your project. One hint is that you are using userId
to open the socket, which means that it's supposed to run once you know your userId
, not once the user selects a chat.
To move the interaction up, you could store incoming messages in a redux store and pass the messages to the ChatBox
component through props. Or you could create connect to the socket in ChatHeads
component and pass the messages down to the ChatBox
. Something like
function ChatHeads(props) {
const {
dispatch,
userInfo,
userId
} = props;
const [chatHeads, setChatHeads] = useState([]);
const loadChatHeads = async () => {
const response = await services.getRecentChats(userId, userInfo);
setChatHeads(response.chats);
};
useEffect(() => loadChatHeads(), [userInfo]);
const [messages, setMessages] = useState([]);
useEffect(() => {
socket = services.establishSocketConnection(userId);
socket.addEventListener('message', (msg) => setMessages(messages.concat(msg)));
}, [userId]);
return () => socket.close();
}
return (
// render your current chat and pass the messages as props
)
Or you could create a reducer and dispatch a chatActions.newMessage
event and then the messages get to the current chat using redux.
The main point is that if you need chatInfo
to open the socket, then every time chatInfo
changes, you might have to open a new socket, so it makes sense to add the dependency to the useEffect
hook. If it only depends on userId
, then move it up to where you get the userId
and connect to the socket there.
Upvotes: 5