Where to set up notifications/status updates for app? - node.js

I am implementing status updates/notifications, but I do not know where to make the calls from. Right now I am making them in my container app AsyncApp.js that holds all the navigation bars and my components(except login/logout etc...) the issue is that when ever I go to a new component my notifications start from fresh, which is wrong because I want it stay continuous throughout all pages.
AsyncApp.js
class AsyncApp extends Component {
constructor(props){
super(props)
this.startTimer = this.startTimer.bind(this)
this.handleEvent = this.handleEvent.bind(this)
this.handleClose = this.handleClose.bind(this)
this.state = {
redirect: false,
maxSessionInactivity: null,
showAlert: false,
sinceLastCheck: ''
}
}
async componentDidMount() {
this.show = null
let self = this
let messages;
const { dispatch } = this.props
await document.body.addEventListener("keypress", this.handleEvent);
await document.body.addEventListener("click", this.handleEvent);
await fetch('/api/getStatus').then(res => res.json()).then(function(res){
// if(!res.data.is_active){
// self.setState({redirect: true})
// }
console.log("IN GET STATUS ", res)
})
.catch(err => self.setState({redirect: true}))
await fetch('/api/getFirstNotification')
.then(res => res.json())
.then(function(res){
// if(res.status.errorOccured){
// self.setState({redirect: true})
// }
messages = res.data.messages
dispatch(updateMessages(res.data.messages))
self.setState({sinceLastCheck: res.data.since_last_check})
})
.catch(err => self.setState({redirect: true}))
//await fetch('/api/getStatus').then(res => res.json()).then(res => this.setState({maxSessionInactivity: res.data.session_inactivity_minutes - 1 * 1000}));
await this.startTimer()
await console.log("STATE J", this.state)
await this.interval(messages)
await this.notifications()
}
startTimer() {
this.firstTimer = setTimeout(function() {
this.setState({showAlert: true})
}.bind(this), 100000);
this.lastTimer = setTimeout(function() {
this.setState({redirect: true})
}.bind(this), 600000)
}
handleEvent(e){
console.log("event", e)
clearTimeout(this.firstTimer)
clearTimeout(this.lastTimer)
this.startTimer()
}
async interval(messages){
this.intervalStatus = await setInterval(async () => {
await this.notify(messages)
}, 15000)
};
async notifications(){
const { dispatch } = this.props
this.newNotifications = await setInterval( async () => {
let data = { since_last_checked : this.state.sinceLastCheck }
let res1 = await fetch('/api/getNotifications', {
method:'POST',
headers: {
'Content-type': 'application/json',
'accept': 'application/json'
},
body:JSON.stringify(data)
})
.then(res => res.json())
.catch(err => console.log(err))
console.log("NOTIFICATIONS NEXTTT", res1)
if(res1 === undefined || res1.data === undefined || res1.data === null){
this.setState({redirect: true})
}
if(res1 != undefined && res1.data != null) dispatch(updateMessages(res1.data.messages))
let res2 = await fetch('/api/getStatus')
.then(res => res.json())
.catch(err => console.log(err))
console.log("STATUSS", res2)
if(res2 === undefined || res2.data === undefined || res2.data === null || res2.data.is_active === 'N' || res2.data.status === 'closed'){
this.setState({redirect: true})
}
}, 5000)
}
handleClose(event){
this.setState({showAlert: false})
}
componentWillUnmount(){
console.log("componentWillUnmount!!!!")
clearInterval(this.newNotifications)
clearInterval(this.intervalStatus)
clearTimeout(this.firstTimer)
clearTimeout(this.lastTimer)
document.body.removeEventListener("keypress", this.handleEvent);
document.body.removeEventListener("click", this.handleEvent);
}
notify(arr){
if(arr === undefined) return null
if(typeof arr === 'string'){
return toast.success(`${arr}`)
}
if(arr.length < 4){
let messages = arr.map(message => toast.success(`${message.message_text}`))
return messages
} else {
return toast.success(`You have ${arr.length} new Notifications!`)
}
};
render() {
const { classes } = this.props
if (this.state.redirect) return <Redirect to="/logout" />
return (
<div>
<ToastContainer />
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={this.state.showAlert}
autoHideDuration={6000}
onClose={this.handleClose}
>
<MySnackbarContentWrapper
onClose={this.handleClose}
variant="warning"
message="Your session will expire in one minute!"
/>
</Snackbar>
<ThemeProvider theme={theme}>
<div className={classes.root}>
<CssBaseline />
<nav className={classes.drawer}>
<Hidden xsDown implementation="css">
<Navigator PaperProps={{ style: { width: drawerWidth } }} />
</Hidden>
</nav>
<div className={classes.appContent}>
<Header onDrawerToggle={this.handleDrawerToggle} />
<main className={classes.mainContent}>
<div>
<Switch>
<Route exact path="/EditContracts/:contractId/sections/:section" component={EditSection} />
<Route exact path="/EditContracts/:contractId" component={EditContract} />
<Route exact path="/EditUsers/:userId" component={EditUser} />
<Route exact path="/EditEndpoints/:epId" component={EditEndpoint} />
<Route exact path="/EditContracts/:contractId/addSection" component={CreateSection} />
<Route exact path="/Contracts/List" component={Contracts} />
<Route exact path="/Contracts/Create" component={CreateContract} />
<Route exact path="/Contracts/Import" component={ImportContract} />
<Route exact path="/Users/List" component={Users} />
<Route exact path="/Users/Create" component={CreateUser} />
<Route exact path="/Endpoints/Create" component={CreateEndpoint} />
<Route exact path="/Endpoints/List" component={Endpoints} />
<Route exact path="/Pug_Community" component={PugCommunity} />
<Redirect exact from="/Users" to="/Users/List" />
<Redirect exact from="/Endpoints" to="/Endpoints/List" />
<Redirect exact from="/Contracts" to="/Contracts/List" />
</Switch>
</div>
</main>
</div>
</div>
</ThemeProvider>
</div>
)
}
}
App.js
export default class App extends Component {
render() {
return (
<Switch>
<Route exact path="/signin" component={SignIn} />
<Route exact path="/changePassword" component={ChangePassword} />
<Route exact path="/logout" component={Logout} />
<Redirect exact from="/" to="/signin" />
<Route path="/" component={AsyncApp} />
</Switch>
)
}
}
Root.js
const store = configureStore()
export default class Root extends Component {
render() {
return (
<Provider store={store}>
<Router>
<App />
</Router>
</Provider>
)
}
}

At a first glance, it looks like you are doing more work than you need to in your AsyncApp using timers. I can think of two possible solutions that could simplify your workflow:
Since you are already using redux, I would take your fetch() calls out of the AsyncApp component entirely and store them in async redux action creators using redux-thunk.
Set only one timer on AsyncApp, call your async action methods like updateNotifications() and updateStatus() every interval, and then check whether the returned data is new in a redux reducer.
Your reducer can then also be the place that determines whether notifications have been "read". If you have control over the server and database, you will want a server side attribute to also store whether notifications have been read so that a total cache refresh would not redisplay old notifications as new.
While the first suggestion will likely be more intuitive with your current app design, I would argue a more elegant solution would use some kind of websocket implementation. Subscribing to a socket channel using socket.io or PusherJS. Implementing this kind of change requires server changes too, so it may not be feasible for you, but it would remove the need for timers. Pusher, for example, can be configured to subscribe to a notification channel and kick off a redux action with the new data anytime a new notification is received. See: pusher-redux.
Here are some ideas for how to organize the code for the first suggestion:
AsyncApp.js
import { updateNotifications, updateStatus } from './actionCreators.js'
class AsyncApp extends Component {
// ... other necessary code
public interval: any;
public getData = () => {
const { dispatch } = this.props
dispatch(updateNotifications())
dispatch(updateStatus())
}
public componentWillMount(){
//Grab data when the component loads the first time
this.getData()
// Then set a single interval timer to handle both data updates.
this.interval = setInterval(()=>{
this.getData()
},30000)
}
public componentWillUnMount(){
// clear the interval when the component unmounts.
clearInterval(this.interval)
}
}
actionCreators.js
// action type constants
export const RECEIVE_NOTIFICATIONS = 'RECEIVE_NOTIFICATIONS'
function receiveNotifications(notifications){
return({
type: RECEIVE_NOTIFICATIONS,
notifications,
})
}
// Here's the part that uses thunk:
export function updateNotifications() {
return function(dispatch) {
return fetch(`api/getNotifications`)
.then(
response => response.json(),
error => console.log('An error occurred.', error)
)
.then(json =>
dispatch(receiveNotifications(json))
)
}
}
// ... use the same pattern for updateStatus()
reducers.js
import { RECEIVE_NOTIFICATIONS } from './actionCreators.js'
const notifications = (state=[], action) => {
switch(action.type){
case RECEIVE_NOTIFICATIONS:
/* Depending on the structure of your notifications object, you can filter
or loop through the list to determine whether or not any items in the
list are new. This is also where you can toast if there are N number of
new notifications. */
return action.notifications
default:
return state
}
}

Related

How can I avoid creating a new session when a user enters the same url and instead lets the new user join the host session?

I am trying to make an application which lets the first person to enter the url navigate to his specific hostId. The next person who comes to the same url is supposed to join the "hosts" session. This works, but it simultaneously creates a new session with the new user while also pushing the user to the hosts session. I do think it has something to do with the code where I am saying: "If (!host) { socket.emit("host:joined") }." And since the second user's host is "null" at the first render it also calls for emit("host:joined"), instead of only the "socket.emit("user:joined") (which is in the StartPage component).
How can the second user ONLY join the host session, without creating a new session?
const socket = socketio.connect(import.meta.env.VITE_APP_SOCKET_URL)
function App() {
const [host, setHost] = useState(null)
const [loading, setLoading] = useState(true)
const [showModal, setShowModal] = useState(false)
const [queueList, setQueueList] = useState([])
useEffect(() => {
if (!host) {
socket.emit("host:joined")
}
console.log("socket.id", socket.id)
socket.on("host:joined", (hostId) => {
setHost(hostId)
setLoading(false)
})
socket.on("user:joined", (id) => {
console.log("user joined session:", id)
})
socket.on("playlist", (data) => {
setQueueList(data)
})
if (host === undefined) {
socket.emit("disconnect")
}
console.log("HOST", host)
return () => {
console.log("Cleaning up")
socket.off("host:joined")
socket.off("disconnect")
socket.off("playlist")
}
}, [host, queueList])
return (
<div className="App">
{loading && <LoadingSpinner />}
<Routes>
<Route
path="/:id"
element={
<StartPage
socket={socket}
host={host}
queueList={queueList}
setShowModal={setShowModal}
showModal={showModal}
/>
}
/>
{host && <Route path="/" element={<Navigate to={`/${host}`} />} />}
</Routes>
</div>
)
}
export default App
Startpage component:
const StartPage = ({ socket, host, queueList, setShowModal, showModal }) => {
const { id } = useParams()
useEffect(() => {
if (id !== socket.id) {
socket.emit("user:joined", id)
}
}, [id])
return (
<>
</>
)
}
export default StartPage
Socket_controller:
let io = null
let sessions = []
// Create a session when host enters the site
const handleConnect = function (id) {
console.log("ID", id)
if (!id) {
console.log("Host joined with id:", this.id)
let session = {
id: this.id,
playlist: [],
users: [],
}
if (sessions.find((session) => session.id === this.id)) {
return
} else {
sessions.push(session)
this.join(session)
}
io.to(session).emit("host:joined", this.id)
} else {
console.log("User joined with id:", this.id)
const session = sessions.find((session) => session.id === id)
console.log("session", session)
session.users.push(this.id)
this.join(session)
io.to(session).emit("host:joined", id)
console.log("sessions", sessions)
}
}
/**
* Export controller and attach handlers to events
*/
module.exports = function (socket, _io) {
io = _io
// handle host connect
socket.on("host:joined", handleConnect)
socket.on("user:joined", handleConnect)
}
Backend and frontend consoles:
I have tried many different things, such as using a loadingspinner while host i loading. I've tried refactoring both in the controller and on the frontend. The positive thing is that it works as intended visually, which means that the two users are on the same session and can share information. However, I can see in the code that it is not corrent as new sessions are created, and also the second user is joining twice.

How should I pass data from react app.js to profile.js and set id in url?

Edited the post!!!
id coming trough login request, at the else branch at handleChangeId! it gets the correct id! i try to push at the top!
export function Login({handleChangeId}) {
const login = () => {
//node port egyezés szükséges
Axios.post('http://localhost:3001/login', {
LoginUsername: usernameLog,
LoginPassword: passwordLog,
}).then((response) => {
if (response.data.message) {
setLoginCorrect(response.data.message)
}
else {
handleChangeId(response.data[0].id);
navigate("/App");
}
});
};
}
than at App.js i try to get the id from the login Route, and push trough profile Route
let id = null;
function changeID(newId) {
id= newId;
}
ReactDOM.render(
<BrowserRouter>
<Routes>
<Route exact path="/" element={<Login handleChangeId={changeID} />}
<Route exact path="/profile" element={<Profile id={id} />} />
</Routes>
</BrowserRouter>,
document.getElementById('root')
); />
at Profile.js, i also try to get the id this way, but the value=null every way that i tried! And this is what im looking for - how to read and set the id and get their personal datas, when the users hit profile on the menubar!
export function Profile({ id }) {
const [customers, setCustomers] = useState([]);
useEffect(() => {
Axios.get(`http://localhost:3001/profile?userId=${id}`)
.then((response) => {
if (response) {
setCustomers(response.data);
}
else {
alert("Data currently unavailable!")
}
});
}, []);
}
Issue
let id = null;
function changeID(newId) {
id= newId;
}
Doesn't work because id is declared each render cycle with a null value, and simply mutating it doesn't trigger a component rerender with the updated value to be passed to the Profile component.
Solution
Make id part of the component state so updating it triggers a rerender with the updated value closed over in scope.
Example:
const [id, setId] = React.useState(null);
...
const changeID = (newId) => {
setId(newId);
}
...
<Routes>
<Route path="/" element={<Login handleChangeId={changeID} />}
<Route
path="/profile"
element={<Profile id={id} />} // <-- updated id state value passed here
/>
</Routes>

How to use useEffect and the Context API to check if a user is logged in and protect a route?

I am trying to protect routes based on whether a user is logged in or not but I cannot get this to work properly since it seems that the information stored in my context provider is not available on the initial component load.
I am checking whether the user is authenticated within my App.js file by making a request to my node server through the useEffect hook. It tries to store this info within the context api which it successfully does but it appears that rendering other components will not wait for the context api to "catch up" or load first.
I am sure there is something simple I am missing or maybe I am using bad convention with checking if a user is authenticated. Any help would be appreciated!
App.js
import React, { useState, useEffect } from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import Axios from 'axios';
import Header from './components/layout/Header';
import Home from './components/pages/Home';
import HiddenPage from './components/pages/HiddenPage';
import Login from './components/auth/Login';
import Register from './components/auth/Register';
import UserContext from './context/UserContext';
import ProtectedRoute from './components/auth/ProtectedRoute';
import './style.scss';
export default function App() {
const [userData, setUserData] = useState({
token: undefined,
user: undefined,
});
useEffect(() => {
const checkLoggedIn = async () => {
let token = localStorage.getItem('auth-token');
if (token === null) {
localStorage.setItem('auth-token', '');
token = '';
}
const tokenResponse = await Axios.post(
'http://localhost:5000/users/tokenIsValid',
null,
{ headers: { 'x-auth-token': token } }
);
if (tokenResponse.data) {
const userResponse = await Axios.get('http://localhost:5000/users/', {
headers: { 'x-auth-token': token },
});
setUserData({
token,
user: userResponse.data,
});
}
};
checkLoggedIn();
}, []);
return (
<>
<BrowserRouter>
<UserContext.Provider value={{ userData, setUserData }}>
<Header />
<div className="container">
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/login" component={Login} />
<Route exact path="/register" component={Register} />
<ProtectedRoute path="/hidden" component={HiddenPage} />
</Switch>
</div>
</UserContext.Provider>
</BrowserRouter>
</>
);
}
ProtectedRoute.js
import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';
import UserContext from '../../context/UserContext';
export default function ProtectedRoute(props) {
const { userData } = useContext(UserContext);
const Component = props.component;
const isAuthenticated = !!userData.user;
console.log(isAuthenticated);
return isAuthenticated ? <Component /> : <Redirect to={{ pathname: '/' }} />;
}
add a loading state...
const [loading, setLoading] = useState(false)
after you check local storage and make axios call, update the state
if (loading) return null;
// else render the routes
return (
// the regular routes...
)
You basically want to account for a 3rd state.
Either:
there is a user
there is NO user
the effect has not yet completed
At whatever stage in your useEffect you can confirm that there is no user (unauthenticated) set the user to false.
Since undefined !== false, in your return you can test for both...
if (userData.user === undefined) return 'loading...'
Then you can use your ternary after this line with the knowledge that user has some value. Either false or some user object...

React Component does not update until refresh

I have the following code:
In App.js:
class App extends React.Component {
constructor() {
super();
this.state = {
loggedStatus: {
username: undefined,
isLoggedIn: undefined,
}
}
}
componentDidMount() {
let username = undefined;
let isLoggedIn = undefined;
if (localStorage.getItem("token")) {
fetch("https://localhost:8000/user", {
method: "POST",
headers: {
"Authorization": `Bearer ${localStorage.getItem("token")}`
}
}).then(response => response.json()).then(response => {
if (response.success) {
username = response.username;
isLoggedIn = true;
} else {
username = undefined;
isLoggedIn = false;
localStorage.removeItem("token");
}
this.setState({
loggedStatus: {
username: username,
isLoggedIn: isLoggedIn
}
})
})
}
}
render() {
return (
<div className="App">
<Router>
<Navbar loggedStatus={this.state.loggedStatus}/>
<Switch>
<Route path="/register">
<RegisterForm />
</Route>
<Route path="/login">
<LoginForm />
</Route>
</Switch>
</Router>
</div>
)
}
}
and in Navbar.js:
logout = () => {
localStorage.removeItem("token");
}
render() {
return (
<nav className="Navbar">
{this.props.loggedStatus.isLoggedIn ?
<>
<ul className="Navbar-list">
<li className="Navbar-item">
<span className="Navbar-greeting">Hello, {this.props.loggedStatus.username}</span>
</li>
<li className="Navbar-item">
<button className="Navbar-logoutBtn" onClick={this.logout}>Sign Out</button>
</li>
</ul>
</>
:
<>
<ul className="Navbar-list">
<li className="Navbar-item">
<Link to="/register" className="Navbar-link">Sign Up</Link>
</li>
<li className="Navbar-item">
<Link to="/login" className="Navbar-link">Log In</Link>
</li>
</ul>
</>
}
</nav>
)
}
The problem I'm having is I would like my navbar component to update when either the user logs in, or logs out. With my current code, I have to refresh the page in order for it to update. I've been messing around with things with no luck. I understand that componentDidMount is only called once through the entire process, which is why setState is only called upon refresh.
Edit: Login.
fetch(`${this.apiURL}/user/login`, {
method: "POST",
body: JSON.stringify(user),
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
}
})
.then(response => response.json())
.then(response => {
if (response.success) {
status = {
statusMsg: <p className="LoginForm-statusMsg">{response.statusMsg}</p>
}
localStorage.setItem("token", response.token);
this.setState({
status: status
}, () => setTimeout(() => {
this.props.history.push("/");
}, 5000));
} else {
status = {
statusMsg: <p className="LoginForm-statusMsg">{response.statusMsg}</p>
}
this.setState({
status : status
})
}
});
}
}
setState({}) always forces to re-render. (unless you return false in: shouldComponentUpdate(nextProps, nextState)) You can check this by putting a console log in
componentDidUpdate(prevProps, prevState) {
console.log("Component did update")
}
It's not clear what your JobsScreenTabs component consists of but make sure that for changes you expect to happen inside the JobsScreenTabs component it actually changes its state. Pass properties from your WorkshopJobsScreen component or make changes directly in the JobsScreenTabs component.
Also important:
Using State Correctly
There are three things you should know about setState().
Do Not Modify State Directly
For example, this will not re-render a component:
// Wrong
this.state.comment = 'Hello';
Instead, use setState():
// Correct
this.setState({comment: 'Hello'});
React may batch multiple setState() calls into a single update for performance.
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
Neither parent nor child components can know if a certain component is stateful or stateless, and they shouldn’t care whether it is defined as a function or a class.
This is why state is often called local or encapsulated. It is not accessible to any component other than the one that owns and sets it.
So if you wish to make changes in your component make sure to manipulate the state of the correct component.
Read more about React lifecycles at: https://reactjs.org/docs/state-and-lifecycle.html
So just some general info. Answer starts here:
You can pass your state in props from app js to navbar component through the route:
let loggedStatus = {
username: undefined,
isLoggedIn: false
}
<Route path="/" render={(props) => <NavBar props={loggedStatus} {...props} /> } exact />
In NavBar you can access it:
export class NavBar extends Component {
constructor(props) {
super(props);
console.log(props)
this.state = {
isLoggedIn: this.props.loggedStatus.isLoggedIn
As I'm not aware of the login flow you are following. But as far as what I understood. The code below should match your requirement.
In NavBar.js
logout = () => {
this.props.handleLogout()
}
In app.js
handleLogout = () => {
localStorage.removeItem("token");
this.setState({
loggedStatus: {
username: undefined,
isLoggedIn: false
}
})
}
render(){
return(
..
..
<Navbar loggedStatus={this.state.loggedStatus} handleLogout={this.handleLogout}/>
)
}
The state changes so it will re-render the component.
Hope this helps you.

ComponentDidMount infinite loop

first of all I'd like to say I have read about the subject and I still can't understand what's happening, so sorry in advance if it's a duplicate. component that fetches data from a server on componentDidMount, but somehow it enters an infinite loop. I am using react router & redux saga and the parent of said component is also connected to the redux store (I think this is the problem).
App.js render method:
render() {
console.log(this.props.isAuthenticated);
let routes = (
<Switch>
<Route path="/login" component={Login} />
<Route path="/" exact component={Home} />
<Redirect to="/" />
</Switch>
);
if (this.props.isAuthenticated) {
routes = (
<Switch>
<Route path='/logout' component={Logout} />
<Route path='/data/people' component={People} />
<Route path="/" exact component={Home} />
<Redirect to="/" />
</Switch>
)
}
return (
<div>
<Layout>
{routes}
</Layout>
</div>
);
}
Call to fetch data (Called in People.js):
componentDidMount() {
this.props.fetchPeople()
}
App.js redux connection:
const mapStateToProps = state => {
return {
isAuthenticated: state.login.token !== null,
username: state.login.username
};
};
const mapDispatchToProps = dispatch => {
return {
autoLogin: () => dispatch(actions.checkLoginState())
}
};`
People.js redux connection:
const mapStateToProps = state => {
return {
people: state.data.people,
loading: state.data.loading
};
};
const mapDispatchToProps = dispatch => {
return {
fetchPeople: () => dispatch(actions.fetchPeople())
};
};
actions.fetchPeople calls this saga:
export function* fetchPeopleSaga () {
try {
yield put(actions.fetchPeopleStart());
const people = yield axios.get('/data/people');
yield put(actions.fetchPeopleSuccess(people));
} catch (error) {
yield put(actions.fetchPeopleFail(error));
}
};
And here's the relevant reducer:
const initialState = {
people: null,
error: null,
loading: false
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.FETCH_PEOPLE_START:
return {
...state,
loading: true
}
case actionTypes.FETCH_PEOPLE_SUCCESS:
return {
...state,
people: action.people,
loading: false
}
case actionTypes.FETCH_PEOPLE_FAIL:
return {
...state,
people: null,
error: action.error,
loading: false
}
default:
return state
}
};
As you can see they aren't even accessing the same props, so I don't understand why it enters infinite rendering.
Help would be much appreciated!
You can add another state loaded and set it to true in case of either FETCH_PEOPLE_SUCCESS or FETCH_PEOPLE_FAIL. Then, add a check for both loaded and loading before you fetch people data, something like:
if (!loaded && !loading) {
this.props.fetchPeople()
}
ComponentDidMount get called once unless you redirect to that page again.
Please check, you must be getting redirecting on that page/component again and again.

Resources