I have useContext which save all loggedUsers in an array. Each user has few properties, one of them is messages array, which stores objects with two properties. I am using this context through custom hook inside of component, like so: const { loggedUsers, setLoggedUsers } = useUsers();. The context looks like this:
export const UsersProvider = ({ children }: Props) => {
const [loggedUsers, setLoggedUsers] = useState<any[]>([]);
const socket = useSocket();
const addUser = (user: any) => {
console.log(typeof loggedUsers);
const users: any[] = loggedUsers;
users.push(user);
setLoggedUsers(users);
};
const sortUsers = (users: any) => {
users.forEach((user: any) => {
user.self = user.userID === socket?.id;
});
let sorted = users.sort((a: any, b: any) => {
if (a.self) return -1;
if (b.self) return 1;
if (a.username < b.username) return -1;
return a.username > b.username ? 1 : 0;
});
setLoggedUsers(sorted);
};
return (
<UsersContext.Provider
value={{ loggedUsers, setLoggedUsers, addUser, sortUsers }}
>
{children}
</UsersContext.Provider>
);
};
User in array looks like this:
{userID: 'Cm6vG0udcV6vl7MEAAAB', userName: 'test123', messages: [{message: 'dsada', fromSelf: true}], self: false, connected: true}
I am updating the context/state like so, but it doesn't force rerender, so my messages between users are not shown. What could be the problem? If I console.log loggedUsers the messages are there, but they do not show up on DOM. If you need more code, let me know.
socket.on('private message', listenerMessage);
const listenerMessage = async (msgInfo: any) => {
console.log(msgInfo);
console.log(msgInfo.from);
const usersList = loggedUsers;
for (let i = 0; i < usersList.length; i++) {
const user = usersList[i];
if (user.userID === msgInfo.from) {
user.messages.push({
message: msgInfo.message,
fromSelf: false,
});
if (user !== state.userID) {
user.hasNewMessages = true;
}
console.log(loggedUsers, 'onprivatemessage');
setLoggedUsers(usersList);
break;
}
}
};
const onMessage = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (state.userName) {
socket?.emit('private-message', {
message: mess,
to: state.userID,
});
const usersList = loggedUsers;
for (let i = 0; i < usersList.length; i++) {
const user = usersList[i];
if (user.userID === state.userID) {
user.messages.push({ message: mess, fromSelf: true });
console.log(loggedUsers, 'onmessage');
}
}
setLoggedUsers(usersList);
}
setMess('');
};
You shouldn't mutate useState's state directly.
Clone it before:
const usersList = [...loggedUsers]
const usersList = loggedUsers.slice()
Here :
setLoggedUsers(usersList);
You are passing the same state array to setLoggedUsers. Cloning the state will solve this.
What you can do is this :
setLoggedUsers(prev=>{
const prevUserList = prev.slice()
//your for loop or anything
return prevUserList;
});
Also here :
const usersList = loggedUsers;
for (let i = 0; i < usersList.length; i++) {
const user = usersList[i];
if (user.userID === state.userID) {
user.messages.push({ message: mess, fromSelf: true });
console.log(loggedUsers, 'onmessage');
}
}
You never update usersList , i don't know if that is wanted.
you using const while you updating the objects.
try using let and not const.
const objects should not be changed.
Related
The problem I'm having is, that I have a useContext in which I provide all logged users. On the initial run of the app or when the users' log in the array gets populated with all the users that are currently on the server... Which works as expected. But I have also the functionality, that whenever the server "user-connected" event runs, the front-end should just push the user to the end of this array. And there lays the problem. From the backend, the right user is sent, but when I access the connectedUsers array, the array is empty... but it should be already populated.
UsersProvider.tsx
export const inState = {
connectedUsers: [],
addUser: (user: any) => {},
sortUsers: (user: any, socketID: string) => {},
setLoggedUsers: () => {},
};
export interface initState {
connectedUsers: any[];
addUser(user: any): void;
sortUsers(users: any, socketID: string): void;
setLoggedUsers: React.Dispatch < React.SetStateAction < any[] >> ;
}
const UsersContext = createContext < initState > (inState);
export const useUsers = () => {
return useContext(UsersContext);
};
const initUserProps = (user: any) => {
user.messages = [];
user.hasNewMessages = false;
};
export const UsersProvider = ({
children
}: Props) => {
const [connectedUsers, setLoggedUsers] = useState < any[] > ([]);
const addUser = (user: any) => {
console.log('add', connectedUsers);
// This is empty, but it should be already populated when next user connected.
};
const sortUsers = (users: any, socketUserID: string) => {
const usersCopy = users;
usersCopy.forEach((u: any) => {
for (let i = 0; i < usersCopy.length; i++) {
const existingUser = usersCopy[i];
if (existingUser.userID === u.userID) {
existingUser.connected = u.connected;
break;
}
}
u.self = u.userID === socketUserID;
initUserProps(u);
});
// put the current user first, and sort by username
let sorted = usersCopy.sort((a: any, b: any) => {
if (a.self) return -1;
if (b.self) return 1;
if (a.username < b.username) return -1;
return a.username > b.username ? 1 : 0;
});
setLoggedUsers([...sorted]);
};
return ( <
UsersContext.Provider value = {
{
connectedUsers,
setLoggedUsers,
addUser,
sortUsers
}
} >
{
children
} <
/UsersContext.Provider>
);
};
And the part of ChatBoard.tsx, you can find addUser function initiated whenever user-connected happens. I really don't know why the would array be empty, if it is populated on the first run with users event.
const ChatBoard = (props: Props) => {
const socket = useSocket();
const {
connectedUsers,
setLoggedUsers,
addUser,
sortUsers
} = useUsers();
useEffect(() => {
if (socket == null) return;
socket.on('users', (users) => {
console.log(users);
if (socket.userID) {
const socketID: string = socket ? .userID;
sortUsers(users, socketID);
}
});
socket.on('user-connected', (user: any) => {
console.log(user, 'this user connected!');
const connectingUser = user;
addUser(connectingUser);
});
socket.on('user-disconnected', (userID) => {
console.log('disconnected user');
const users = [...connectedUsers];
users.forEach((u) => {
if (u.userID === userID) {
u.connected = false;
setLoggedUsers([...users]);
}
});
});
return () => {
socket.off('users');
socket.off('user-connected');
};
}, [socket]);
CodeSandbox
So I have found the problem... so with React hooks sometimes a problem occurs called "Stale Closures", which means that React was picking up the old state (empty one, the one that was not yet populated and always returning that one.).
The solution to this problem, in my case is that when you use setState you use it with a callback. Like so, so you always get the latest state.
const addUser = (user: any) => {
setLoggedUsers((oldUsers) => {
const newUsers: any[] = [...oldUsers];
console.log(newUsers);
for (let i = 0; i < newUsers.length; i++) {
const existingUser = newUsers[i];
if (existingUser.userID === user.userID) {
existingUser.connected = true;
return newUsers;
}
}
initReactiveProperties(user);
newUsers.push(user);
return newUsers;
});
};
I want to perform same functionality on hardware button press as on doneClick. My app has some tags. There are tags on the screen whose state is maintained in the backend. Upon clicking them, their state toggles. When I change the state of one tag, I can see that it's really changed. But when I don't change the state the below line makes the tags array to empty
When I use this
const [allData, changeAllData] = useState([]);
My state is changed on single action. But when I don't select or unselect a tag and come back to previous screen, I see that no tags are selected and allData is just an empty array.
But when I use this
const [allData, changeAllData] = useState(props?.selectedLifeStyle)
My state is not changed on single action. I have to either select two tags, or unselect two tags, select or unselect same or different tags, only then my state changes.
Here is full piece of code:
const LifestyleScreen = (props) => {
const [searchUniqueValue, setSearchUniqueValue] = useState("");
const [searchValue, setSearchValue] = useState("");
const [choice, changeChoice] = useState([]);
// const [allData, changeAllData] = useState([]);
const [allData, changeAllData] = useState(props?.selectedLifeStyle);
const [limit, changeLimit] = useState(10000);
const [offset, changeOffset] = useState(0);
const [fetching, changeFetchingStatus] = useState(false);
const [searchArr, newSearchArr] = useState("");
useEffect(() => {
if (props.user !== null) {
changeFetchingStatus(true);
props.loginChange({ selectedLifeStyle: allData });
debugger;
changeAllData(props.selectedLifeStyle);
let params = {
limit,
offset,
};
getData(
getLifeStyleTags,
params,
(res) => {
console.log(res, "res in lifestyle tag");
changeFetchingStatus(false);
changeChoice(res.lifestyle_tags);
newSearchArr(res.lifestyle_tags);
},
(err) => {
changeFetchingStatus(false);
console.log(JSON.stringify(err, null, 2));
},
props.user ? props.user.api_key : masterKey
);
}
}, []);
const doneClick = () => {
if (allData.length === 0) {
showMessage({
message: "Select at least one lifestyle",
type: "warning",
});
} else if (allData.length > 10) {
showMessage({
message: "Select at most ten lifestyles",
type: "warning",
});
} else {
props.navigation.goBack(null);
props.loginChange({ selectedLifeStyle: allData });
}
};
const saveLifestyles = (item) => {
let counter = 0;
let arr = allData;
if (allData.length > 9) {
for (let i = 0; i < allData.length; i++) {
if (allData[i].id === item.id) {
arr.splice(i, 1);
changeAllData([...arr]);
counter = 1;
break;
}
props.loginChange({ selectedLifeStyle: allData });
if (allData.length == i + 1) {
showMessage({
message: "Only 10 lifestyle tags can be added",
type: "warning",
});
}
}
} else {
for (let i = 0; i < allData.length; i++) {
if (allData[i].id === item.id) {
arr.splice(i, 1);
changeAllData([...arr]);
counter = 1;
props.loginChange({ selectedLifeStyle: allData });
}
}
props.loginChange({ selectedLifeStyle: allData });
if (counter === 0) {
arr.push(item);
changeAllData([...arr]);
props.loginChange({ selectedLifeStyle: allData });
}
}
};
}
Need help passing data "locationpos"= index of my Locations[] from function to class. I'm very new to React and I'm not sure what I'm doing wrong.
ERROR
Failed to compile
./src/components/data.js
Line 20:30: 'locationpos' is not defined no-undef
Search for the keywords to learn more about each error.
This error occurred during the build time and cannot be dismissed.
class Data {
constructor(locationpos) {
this.locationpos=locationpos;
this.updateData();
}
getTimes(date = null) {
date = date === null ? moment().format('DD/MM/YYYY') : date;
var data = this.getData();
return data ? data[date] : [];
}
getSpeadsheetUrl() {
return config.myData[locationpos];
}
function Daily({ locationProps = 1, root }) {
const context = useContext(ThemeContext);
const localization = useCallback(() => {
if (root && cookies.get("location") !== undefined) {
return cookies.get("location");
}
return locationProps;
}, [locationProps, root]);
const [locationState] = useState(localization());
const handleClick = event => {
window.focus();
notification.close(event.target.tag);
};
const openNav = () => {
document.getElementById("sidenav").style.width = "100%";
};
const closeNav = e => {
e.preventDefault();
document.getElementById("sidenav").style.width = "0";
};
// eslint-disable-next-line
const locationpos = locations.indexOf(locations[locationState]);
const _data = useRef(new Data(locationpos));
const getTimes = () => _data.current.getTimes();
Inside your data class, you need to use the instance variable as this.locationPos
getSpeadsheetUrl() {
return config.myData[this.locationpos];
}
I have a component that's state I'm trying to use in other components, it has to be used in multiple components so i'm switching it to redux. Right now using the same function that works in my component, I am getting the error 'newUsers.push is not a function'
Below is my redux action:
import { FETCH_USERS_TO_ADD } from './types';
import axios from 'axios'
export const fetchUsersToAdd = () => dispatch => {
var userBox = [];
var newUserBox = [];
let newUserIds = {};
let newUsers = [];
axios.all([
axios.get('/updateUserList'),
axios.get('/userInformation')
])
.then(axios.spread((newUsers, currentUsers) => {
userBox.push(newUsers.data)
newUserBox.push(currentUsers.data)
for (let newUser of newUserBox){
newUserIds[newUser.id] = newUser.id
}
for (let user of userBox){
if(!newUserIds[user.staffID]){
console.log(user)
**newUsers.push(user)**
}
}
})).then(dispatch({
type: FETCH_USERS_TO_ADD,
payload: newUsers
}))
}
The Code Below in My class component doesnt give me this error:
let newUserIds = {};
let newUsers = [];
this.state = {
userBox : [],
newUserBox : [],
usersToBeAdded:[],
}
componentDidMount(){
axios.all([
axios.get('/updateUserList'),
axios.get('/userInformation')
])
.then(axios.spread((newUsers, currentUsers) => {
this.setState({
userBox : newUsers.data,
newUserBox : currentUsers.data,
})
}))
}
checkForNewUsers = (e) => {
for (let newUser of this.state.newUserBox){
newUserIds[newUser.id] = newUser.id
}
for (let user of this.state.userBox){
if(!newUserIds[user.staffID]){
newUsers.push(user)
}
}
this.setState({
usersToBeAdded:newUsers
})
console.log(newUsers)
}
<UpdateUsersButton onClick={this.checkForNewUsers}/>
When user is consoled it returns an array like this:
Array(10)
0: {staffID: 1, dept: "Junior Web Developer", email: "manasaN#tpionline.com", name: "manasa", password: "$2y$10$/zYS7OhzwdLOi6Slzc3xxxxxiY0y1J6MjnLCN24GmZ3rMHWUS"}
1: {staffID: 2, dept: "Web Developer", email: "juliof#tpionline.net", name: "Julio Fajardo", password: "$2y$10$MphAC8aRY2uzs1Zxxxxxnd7t0KukEkvGbx5Y4van.Da6"}
I think it's because newUsers gets shadowed in the arrow function and isn't what you think it is anymore. Don't reuse variable names.
Try this:
import { FETCH_USERS_TO_ADD } from './types';
import axios from 'axios'
export const fetchUsersToAdd = () => dispatch => {
var userBox = [];
var newUserBox = [];
let newUserIds = {};
let newUsersArr = []; // Make unique
axios.all([
axios.get('/updateUserList'),
axios.get('/userInformation')
])
.then(axios.spread((newUsers, currentUsers) => { // Was shadowed here
userBox.push(newUsers.data)
newUserBox.push(currentUsers.data)
for (let newUser of newUserBox){
newUserIds[newUser.id] = newUser.id
}
for (let user of userBox){
if(!newUserIds[user.staffID]){
console.log(user)
newUsersArr.push(user)
}
}
})).then(dispatch({
type: FETCH_USERS_TO_ADD,
payload: newUsersArr
}))
}
Here I validate if my users status is true, and if they are, I put them in an array. The thing here is that next time it will validate, all those who already was true will be added to the same array. Can it be solved by filter instead of push, or should I take the validation in any other way?
import {
UPDATE_LIST_SUCCESS
} from './types'
var arr = []
export const fetchList = () => {
return (dispatch) => {
firebaseRef.database().ref().child('users')
.on('value', snapshot => {
snapshot.forEach(function (child) {
var data = child.val()
if (child.val().profile.status === true) {
arr.push(data)
}
})
dispatch({ type: UPDATE_LIST_SUCCESS, payload: arr })
})
}
}
You can do it like this:
import {
UPDATE_LIST_SUCCESS
} from './types'
export const fetchList = () => {
return (dispatch) => {
firebaseRef.database().ref().child('users')
.on('value', snapshot => {
var arr = snapshot.filter(function (child) {
return child.val().profile.status === true
}).map(function (child) {
return child.val();
});
dispatch({ type: UPDATE_LIST_SUCCESS, payload: arr })
})
}
}
So here is my not so pretty way of solving it, but it works.
import {firebaseRef} from '../firebase/firebase'
import {
UPDATE_LIST_SUCCESS
} from './types'
export const fetchList = () => {
return (dispatch) => {
const arrayToFilter = []
firebaseRef.database().ref().child('users')
.on('value', snapshot => {
let snap = snapshot.val()
// Get acces to the keys in the object i got from firebase
let keys = Object.keys(snap)
// iterate the keys and put them in an User object
for (var i = 0; i < keys.length; i++) {
let k = keys[i]
let name = snap[k].profile.name
let age = snap[k].profile.age
let status = snap[k].profile.status
let profile_picture = snap[k].profile.profile_picture
let users = {name: '', age: '', status: Boolean, profile_picture: ''}
users.name = name
users.age = age
users.status = status
users.profile_picture = profile_picture
// adding the user object to an array
arrayToFilter.push(users)
}
// filter and creates a new array with users depending if their status is true
let arr = arrayToFilter.filter(child => child.status === true)
dispatch({ type: UPDATE_LIST_SUCCESS, payload: arr })
})
}
}