React Frontend is not receiving backend Socket.io emitted signals - reactjs

I learned socket.io a few days ago and since I have been trying to build a global chat app. I have perfectly set up the backend as well as frontend but for some reason my frontend code isn't receiving the emitted signals from the backend, I checked the backend and yes the Signals are being passed over to the backend. Here's the Code, I will share the server index.js, redux slice, & also the frontend code which sends the request.
import express from 'express';
import cors from 'cors';
import http from 'http';
import dt from 'dotenv';
import {Server} from 'socket.io';
import userRoutes from './routes/userRoutes.js';
import {dbConnect} from './config/db.js'
import {errorHandler} from './middlewares/errorMiddleware.js'
const dotenv = dt.config();
import MSG from './models/messageModel.js';
const PORT = process.env.NODE_PORT || 5000;
const app = express();
app.use(express.urlencoded({extended : false}));
app.use(express.json());
app.use(cors());
const server = http.createServer(app);
dbConnect();
const io = new Server(server, {
cors : {
origin : "http://localhost:5173",
methods : ["GET","POST"]
}
});
io.on('connection', (socket) => {
socket.on('conn_made', async() => {
const messages = await MSG.find();
socket.emit('initload', {msg : messages});
})
socket.on('post', async(data) => {
const message = await MSG.create(data);
socket.emit('updatemsg', message);
})
});
app.use('/api/user', userRoutes);
app.use(errorHandler);
server.listen(PORT, () => {
console.log(`The Server is running on ${PORT}`);
});
This is the Index.js.
import { createSlice } from "#reduxjs/toolkit";
import io from 'socket.io-client';
const socket = io.connect('http://localhost:8000/');
const initState = {
messages : [],
isLoading : false
}
export const messageSlice = createSlice({
name : 'message',
initialState : initState,
reducers : {
resetmsg : (state) => {
state.messages = [];
state.isLoading = false;
},
addInit : (state,action) => {
state.isLoading = false;
console.log(action.payload);
},
connect : (state) => {
socket.emit('conn_made');
state.isLoading = true;
},
post : (state,action) => {
socket.emit('post', action.payload);
},
receive : (state,action) => {
state.messages.push(action.payload);
}
}
});
export const {resetmsg,addInit,connect,post,receive} = messageSlice.actions;
export default messageSlice.reducer;
This is the redux slice that emits some signals.
import React, { useState, useEffect } from 'react';
import {FaSignOutAlt, FaShare} from 'react-icons/fa';
import {logout} from '../features/auth/authSlice';
import { useNavigate } from 'react-router-dom';
import {useSelector, useDispatch} from 'react-redux';
import io from 'socket.io-client';
const socket = io.connect('http://localhost:8000/');
import {resetmsg , addInit , connect , post , receive } from '../features/messages/messageSlice';
const Mainchat = () => {
const [connected, setConnected] = useState(false);
const [text, setText] = useState("");
const {user} = useSelector((state) => state.auth);
const dispatch = useDispatch();
const navigate = useNavigate();
socket.on('initload', (data) => {
const {msg} = data;
dispatch(addInit(msg));
})
socket.on('updatemsg', (data) => {
dispatch(receive(data));
})
const handleConnect = () => {
dispatch(connect());
setConnected(true);
}
const handleLogout = async () => {
await dispatch(logout());
dispatch(resetmsg());
navigate('/login');
}
const onSend = () => {
const msg = {user : user._id , text : text};
dispatch(post(msg))
setText("");
}
return (
<section className='container flex flex-col p-4 mt-16 h-full w-[40vw] border-2 border-gray-300 rounded-lg'>
<div className='flex flex-row justify-between mb-5'>
<h1 className='text-3xl font-bold'>{connected ? "Connected." : "Disconnected."}</h1>
<button onClick={handleLogout}><FaSignOutAlt /></button>
</div>
{connected ? (
<div className='flex flex-col space-y-2'>
<div className='h-[400px] border-y-2 overflow-y-scroll'>
</div>
<div className='flex flex-row justify-between'>
<input value={text} onChange={(e) => setText(e.target.value)} className='w-96 border-2 border-black py-1 px-2 rounded-lg' type='text' placeholder='Type your Message...' />
<button onClick={onSend}><FaShare/></button>
</div>
</div>
) : (
<button onClick={handleConnect}>Connect</button>
)}
</section>
)
}
export default Mainchat
This is the frontend Code. I can't seem to find the issue anyhow, When I click 'Connect', it actually does emit the signal to the backend but the signal emitted from the backend is not read.
I can't seem to find the issue anyhow, When I click 'Connect', it actually does emit the signal to the backend but the signal emitted from the backend is not read.

Related

Uncaught (in promise) IntegrationError: Invalid value for stripe.confirmCardPayment intent secret: value should be a client_secret string

I'm working on a project where I am trying to integrate stripe. I'm trying to get payments to go through with one click of the placeOrderHandler button. Currently, when I click this button I get an error stating: Uncaught (in promise) IntegrationError: Invalid value for stripe.confirmCardPayment intent secret: value should be a client_secret string, and then if I click this button again it goes through and console.log(client_secret) provides the string associated with it. I'm unsure why this is happening, and I would really appreciate any help or advice on how to fix this. Thank you!
PlaceOrderScreen.js
import React, { useState, useEffect} from 'react';
import { useDispatch, useSelector, createSelector } from 'react-redux';
import {createOrder} from '../actions/orderActions';
import {listProducts} from '../actions/productActions';
import { ORDER_CREATE_RESET } from '../constants/orderConstants';
import { PAYMENT_SUCCESS } from '../constants/paymentConstants';
import LoadingBox from '../components/LoadingBox';
import MessageBox from '../components/MessageBox';
import { loadStripe } from "#stripe/stripe-js";
import { Elements } from "#stripe/react-stripe-js";
import {CardElement, PaymentElement, CardNumberElement, CardExpiryElement, CardCvcElement, useStripe, useElements} from '#stripe/react-stripe-js';
import {payment, paymentInfo} from '../actions/paymentActions';
import LoadingSpinner from "../components/LoadingSpinner";
export default function PlaceOrderScreen(props) {
const cart = useSelector((state) => state.cart);
const userSignin = useSelector((state) => state.userSignin);
const { userInfo } = userSignin;
const paymentCreate = useSelector((state) => state.paymentCreate);
const { client_secret } = paymentCreate;
const paymentInformation = useSelector((state) => state.paymentInformation);
const {loadingPayment} = paymentInformation;
const userId = userInfo._id;
const orderCreate = useSelector((state) => state.orderCreate);
const { loading, success, error, order } = orderCreate;
const [loadedPay, setLoadedPay] = useState('');
const dispatch = useDispatch();
const productList = useSelector((state) => state.productList);
const { products } = productList;
useEffect(() =>{
dispatch(listProducts({}));
}, [dispatch]);
const elements = useElements();
const stripe = useStripe();
const placeOrderHandler = async (e) => {
e.preventDefault();
if (!stripe || !elements) {
console.log('Stripe.js has not yet loaded.')
return;
}
const paymentMethodType = 'card';
const currency = 'usd';
const {error: backendError} = dispatch(payment(paymentMethodType, currency, cart, userId, ));
if (backendError) {
//addMessage(backendError.message);
console.log(backendError)
return;
}
console.log('Client secret returned')
console.log(client_secret)
const {error: stripeError, paymentIntent} = await stripe.confirmCardPayment(client_secret,
{
payment_method: {
card: elements.getElement(CardNumberElement),
billing_details: {
name: cart.billingAddress.fullName,
},
},
},
)
if (stripeError) {
console.log('stripeError')
return;
}
cart.paymentId = paymentIntent.id
dispatch(createOrder({ ...cart, orderItems: cart.cartItems }))
const orderId = order._id;
setLoadedPay(false)
.then(dispatch(paymentInfo(orderId)))
setLoadedPay(true)
};
const [isLoading, setIsLoading] = useState(false);
var loaded = false;
useEffect(() => {
if (success ) {
props.history.push(`/order/${order._id}`)
dispatch({ type: ORDER_CREATE_RESET })
}
}, [dispatch, order, success]);
return (
<div>
{loadedPay === false ? (<LoadingBox></LoadingBox>) : (
<div>
{loadingPayment & loadingShipping ? (
<LoadingBox></LoadingBox>
) : (
<div>
{isLoading ? <LoadingSpinner /> :
<div>
<div className="row top">
<div className="col-2">
<ul>
<li>
<div className="card card-body">
<h2>Payment</h2>
<div>
<h1>Card</h1>
<form id="payment-form" >
<label htmlFor="card">Card</label>
{/*<CardElement id="card" />*/}
<CardNumberElement id="card"/>
<CardExpiryElement id="card"/>
<CardCvcElement id="card"/>
</form>
</li>
<li>
<button
type="button"
onClick={placeOrderHandler}
disabled={isLoading && cart.cartItems.length === 0}
className="primary block"
>
Place Order
</button>
</li>
{loading && <LoadingBox></LoadingBox>}
{error && <MessageBox variant="danger">{error}</MessageBox>}
</ul>
</div>
</div>
</div>
</div>
}
</div>
)}
</div>
)}
</div>
);
}
paymentReducer.js
export const paymentCreateReducer = (state = {client_secret:[]}, action) => {
switch (action.type) {
case PAYMENT_REQUEST:
return { ...state, loading: true };
case PAYMENT_SUCCESS:
return { ...state, loading: false, client_secret: action.payload};
case PAYMENT_FAIL:
return { ...state, loading: false, error: action.payload };
default:
return state;
}
};
Backend
stripe.js
....
stripeRouter.get(
'/config',
expressAsyncHandler((req, res) => {
res.send({
publishableKey: '',
});
}));
stripeRouter.post(
'/pay',
expressAsyncHandler(async (req, res) => {
const userId = req.body.userId
const user = await User.findById(userId);
stripeClient.paymentIntents.create({
payment_method_types: [req.body.paymentMethodType],
amount: amount,
currency: req.body.currency,
})
.catch(function(err) {
console.log("There was an error"})
.then(async function(paymentIntents) {
const clientSecret = paymentIntents.client_secret
const client_secret = clientSecret.toString()
res.status(201).send(client_secret)
})
}));

How to prevent react is rendering 2 times

I am facing a problem where the react is rendering 2 times but I do not see any problem in the code or it is come from the socket? I noticed this problem when consoling and when displaying data, the data duplicate. Below I put my client react code with the server socket code.
React client
import "./App.css";
import io from "socket.io-client";
import { useEffect, useState } from "react";
const socket = io.connect("http://localhost:3001");
function App() {
const [text, setText] = useState("");
const [room, setRoom] = useState("");
const [data, setData] = useState([]);
const [action, setAction] = useState(true);
console.log("data", data);
console.log(text);
console.log(room);
const sendMessage = () => {
socket.emit("send_message", { text, room });
};
const joinRoom = () => {
if (room !== "") {
socket.emit("join_room", room);
}
};
useEffect(() => {
socket.on("receive_message", (msg) => {
setData((prev) => [
...prev,
{
message: msg,
},
]);
});
}, [action]);
return (
<div className="App">
{data.map((da) => (
<p>{da.message}</p>
))}
<input
type="text"
placeholder="room no"
onChange={(e) => {
setRoom(e.target.value);
}}
></input>
<button onClick={joinRoom}>Join</button>
<br />
<input
type="text"
placeholder="message .."
onChange={(e) => {
setText(e.target.value);
}}
></input>
<button onClick={sendMessage}>Send Message</button>
</div>
);
}
export default App;
Socket server
const express = require("express");
const app = express();
const http = require("http");
const { Server } = require("socket.io");
const cors = require("cors");
app.use(cors());
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: "http://localhost:3000",
methods: ["GET", "POST"],
},
});
io.on("connection", (socket) => {
// console.log(`User Connected ${socket.id}`);
socket.on("join_room", (data) =>{
socket.join(data);
})
socket.on("send_message", (data) => {
socket.to(data.room).emit("receive_message", data.text);
})
});
server.listen(3001, () => {
console.log("SERVER OK");
});

ReactJS Error when using map function, cannot read properties of undefined

I'm trying to make a sport/tinder like app for a school project from a friend of mine. It came together well on my localhost, but for him it was a requirement to host it online. Not really a professional in hosting, but I was a bit familiar with Heroku. I used a client and a server side for my application, so I build the client side and put it into the server side folder. This server side is hosted on the Heroku page. But whenever I try to login, it won't work and I get this error message in my console.
TypeError: Cannot read properties of undefined (reading 'map')
The error says it is caused by this line of code.
const matchedUserIds = matches.map(({user_id}) => user_id)
This is the whole MatchDisplay file that is used in my Dashboard. I'm using a MongoDB for the storage of my users.
import axios from "axios";
import { useEffect, useState } from "react";
import { useCookies } from "react-cookie";
const MatchesDisplay = ({ matches, setClickedUser }) => {
const [matchedProfiles, setMatchedProfiles] = useState(null);
const [cookies, setCookie, removeCookie] = useCookies(null);
const [matched, setMatched] = useState(null);
const matchedUserIds = matches.map(({ user_id }) => user_id);
const userId = cookies.UserId;
const getMatches = async () => {
try {
const response = await axios.get(
"https://[app].herokuapp.com/users",
{
params: { userIds: JSON.stringify(matched()) },
}
);
setMatchedProfiles(response.data);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
getMatches();
}, [matches]);
const filteredMatchedProfiles = matchedProfiles?.filter(
(matchedProfile) =>
matchedProfile.matches.filter(
(profile) => profile.user_id === userId
).length > 0
);
return (
<div className="matches-display">
{filteredMatchedProfiles?.map((match) => (
<div
key={match.user_id}
className="match-card"
onClick={() => setClickedUser(match)}
>
<div className="img-container">
<img
src={match?.url}
alt={match?.first_name + "profile"}
/>
</div>
<h3>{match?.first_name}</h3>
</div>
))}
</div>
);
};
export default MatchesDisplay;
Any help is welcome. If you need more code examples, please reply ;)
EDIT
The ChatContainer that passes the user to the MatchesDisplay.
import ChatHeader from "./ChatHeader";
import MatchesDisplay from "./MatchesDisplay";
import ChatDisplay from "./ChatDisplay";
import { useState } from 'react';
const ChatContainer = ({user}) => {
const [ clickedUser, setClickedUser] = useState(null)
return (
<div className="chat-container">
<ChatHeader user={user}/>
<div>
<button className="option" onClick={() => setClickedUser(null)}>Matches</button>
<button className="option" disabled={!clickedUser}>Chat</button>
<button className="option" >Prices</button>
</div>
{!clickedUser && <MatchesDisplay matches={user.matches} setClickedUser={setClickedUser}/>}
{clickedUser && <ChatDisplay user={user} clickedUser={clickedUser}/>}
</div>
)
}
export default ChatContainer
The Dashboard that passes the user to the Chatcontainer.
import TinderCard from 'react-tinder-card';
import {useEffect, useState} from 'react';
import {useCookies} from 'react-cookie';
import ChatContainer from '../components/ChatContainer'
import axios from "axios";
const Dashboard = () => {
const [user, setUser] = useState(null)
const [genderedUsers, setGenderedUsers] = useState(null)
const [lastDirection, setLastDirection] = useState(null)
const [cookies, setCookie, removeCookie] = useCookies(['user'])
const [matchedUserIds, setMatchedUserIds] = useState(null)
const [filteredGenderedUsers, setFilteredGenderedUsers] = useState(null)
const userId = cookies.UserId
const getUser = async () => {
try {
const response = await axios.get('https://funfit-webpage.herokuapp.com/user', {
params: {userId}
})
return setUser(response.data)
} catch (error) {
console.log(error)
}
}
const getGenderedUsers = async () => {
try {
const response = await axios.get('https://funfit-webpage.herokuapp.com/gendered-users', {
params: {gender: user?.gender_interest}
})
return setGenderedUsers(response.data)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getUser()
}, [])
useEffect(() => {
setMatchedUserIds(user?.matches.map(({user_id}) => user_id).concat(userId))
if (user) return getGenderedUsers()
}, [user])
useEffect(() => {
if (genderedUsers) {
return setFilteredGenderedUsers(genderedUsers?.filter(
genderedUser => !matchedUserIds.includes(genderedUser.user_id)
))
}
}, [genderedUsers])
const updateMatches = async (matchedUserId) => {
try {
await axios.put('https://funfit-webpage.herokuapp.com/addmatch', {
userId,
matchedUserId
})
return getUser()
} catch (error) {
console.log(error)
}
}
const swiped = (direction, swipedUserId) => {
console.log(direction, swipedUserId)
if (direction === 'right') {
updateMatches(swipedUserId)
}
return setLastDirection(direction)
}
const outOfFrame = (name) => {
console.log(name + ' left the screen!')
}
return (<>
{user && <div className="dashboard">
<ChatContainer user={user}/>
<div className="swipe-container">
<div className="card-container">
{filteredGenderedUsers?.map((genderedUser) =>
<TinderCard
className='swipe'
key={genderedUser.user_id}
onSwipe={(dir) => swiped(dir, genderedUser.user_id)}
onCardLeftScreen={() => outOfFrame(genderedUser.first_name)}>
<div style={{backgroundImage: 'url(' + genderedUser.url + ')'}} className='card'>
<h3>{'Name: ' + genderedUser.first_name} <br/> {'Sport: ' + genderedUser.about}</h3>
</div>
</TinderCard>)}
<div className="swipe-info">
{lastDirection ? <p>You swiped {lastDirection}</p> : <p/>}
</div>
</div>
</div>
</div>}
</>)
}
export default Dashboard

Call external fetch function in react component

I have outsourced a fetch function into lib/cityData.js
const fetch = require('cross-fetch');
const dev = process.env.NODE_ENV !== 'production';
const server = dev ? 'http://localhost:3000' : 'https://your_deployment.server.com';
const fetchCityData = (city) => {
const options = {
method: `POST`,
};
fetch(`${server}/api/weather?city=${city}`, options)
.then((response) => {
if(response.ok){
return response.json().then(data => console.log(data))
}
throw new Error('Api is not available')
})
.catch(error => {
console.error('Error fetching data in city data: ', error)
})
}
//fetchCityData('London')
module.exports.fetchCityData = fetchCityData;
Data is an object, so fetchCityData('London') returns
{
location: {
name: 'London',
region: 'City of London, Greater London',
country: 'United Kingdom',
lat: 51.52,
lon: -0.11,
tz_id: 'Europe/London',
localtime_epoch: 1632394483,
localtime: '2021-09-23 11:54'
},
current: {
last_updated_epoch: 1632393900,
last_updated: '2021-09-23 11:45',
temp_c: 18,
temp_f: 64.4,
is_day: 1,
condition: {
text: 'Partly cloudy',
icon: '//cdn.weatherapi.com/weather/64x64/day/116.png',
code: 1003
},
wind_mph: 13.6,
wind_kph: 22,
wind_degree: 250,
wind_dir: 'WSW',
pressure_mb: 1020,
pressure_in: 30.12,
precip_mm: 0,
precip_in: 0,
humidity: 73,
cloud: 75,
feelslike_c: 18,
feelslike_f: 64.4,
vis_km: 10,
vis_miles: 6,
uv: 4,
gust_mph: 11.2,
gust_kph: 18
}
}
So, now I have a component and this needs this data. The process is as follows
In input user types city
Auto select gets fired
onSelect city is set (setCity(city))
This happens in the component SearchBar.js. Then city is being passed to the component ForecastButtons.js
This component takes the city and then onClick it should call my function above fetchCityData.js and return current temperature for the selected city. Before my fetchCityData function was part of ForecastButtons component, but I needed to outsource it, so now, of course the code is broken:
import React, { useState } from 'react';
import fetchCityData from '../lib/cityData'
export const ForecastButtons = ({ city }) => {
const [payload, setPayload] = useState(null)
const [error, setError] = useState(null)
const [loading, setLoading] = useState(true)
const location = payload?.location?.name;
const currentTemp = payload?.current?.temp_c;
return(
<div className="sm:col-span-2">
<p className="block text-sm font-medium text-gray-700">Select forecast</p>
<button onClick={fetchCityData} className="mt-1 bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded" type='button'>
Today
</button>
<p key={city?.location?.id} className='my-5'>
{ location ? `Current weather in ${location} is ${currentTemp} degrees ` : 'Please search for city to see current weather'}
</p>
</div>
)
}
setPayload in this component was called after fetch returned json. Now the payload is basically my data. Should I import the function somehow and setPayload(data)? I am new to react, this is way to complex for me. How do I use fetchCityData, setPayload and onClick in the button of my component still get the weather?
So I did just quick sketch how it could be. As I sad I transformed fetchCityData to hook usefetchCityData which return [data, loading, error]. We call that hook providing city. Inside hook when city changing useEffect calls server, updates all states, and returns [data, loading, error]. I am using my fake data and timeout to imitate network connection, also you can get fake error from server.
hooks.js:
// import axios from "axios";
import React from "react";
const useFetchCityData = (city) => {
const [data, setData] = React.useState();
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState(false);
React.useEffect(() => {
const dev = process.env.NODE_ENV !== "production";
const server = dev
? "http://localhost:3000"
: "https://your_deployment.server.com";
setData(undefined);
setLoading(true);
setError(false);
const options = {
method: `POST`,
};
// axios(`${server}/api/weather?city=${city}`, options)
new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.2) {
resolve(`data from server for ${city}`);
}
reject();
}, 2000);
})
.then((data) => {
setData(data);
setLoading(false);
})
.catch((error) => {
console.error("Error fetching data in city data: ", error);
setLoading(false);
setError(true);
});
}, [city]);
return [data, loading, error];
};
export { useFetchCityData };
App.js:
import React from "react";
import { useFetchCityData } from "./hooks";
const App = () => {
const [city, setCity] = React.useState("London");
const [data, loading, error] = useFetchCityData(city);
return (
<div>
<div>data: {data}</div>
<div>loading: {loading.toString()}</div>
<div>error: {error.toString()}</div>
</div>
);
};
export default App;
Another solution would simply be:
cityData.js
const fetch = require('cross-fetch');
const dev = process.env.NODE_ENV !== 'production';
const server = dev ? 'http://localhost:3000' : 'https://your_deployment.server.com';
const fetchCityData = (city) => {
const options = {
method: `POST`,
};
return fetch(`${server}/api/weather?city=${city}`, options)
.then((response) => {
if(response.ok){
return response.json()
}
throw new Error('Api is not available')
})
.catch(error => {
console.error('Error fetching data in city data: ', error)
})
}
ForecasButtons.js
import React, { useState, useEffect } from 'react';
import { fetchCityData } from '../lib/cityData'
export const ForecastButtons = ({ city }) => {
const [payload, setPayload] = useState(null)
const getData = () => {
fetchCityData(city).then((payload) => setPayload(payload));
}
const location = payload?.location?.name;
const currentTemp = payload?.current?.temp_c;
return(
<div className="sm:col-span-2">
<p className="block text-sm font-medium text-gray-700">Select forecast</p>
<button onClick={getData} className="mt-1 bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded" type='button'>
Today
</button>
<p key={city?.location?.id} className='my-5'>
{ location ? `Current weather in ${location} is ${currentTemp} degrees ` : 'Please search for city to see current weather'}
</p>
</div>
)
}
It works, but as a beginner, I can't tell if this solution has any downsides.

useEffect keep getting executed every time even dependency not changed

We have UserContext which sets user object which we can use throughout application. Our UserContext keep executing every time and unnecessary making api call even though dependency hasn't changed.
import React, { useState, useEffect } from 'react'
import APIService from './utils/APIService';
import { getCookies } from './utils/Helper';
const UserContext = React.createContext();
const UserContextProvider = (props) => {
const [token, setToken] = useState(getCookies('UserToken'));
const [user, setUser] = useState(null);
useEffect(() => {
console.log('Inside userContext calling as token ', token)
fetchUserInfo();
}, [token]);
const fetchUserInfo = async() => {
if (token) {
let userRes = await APIService.get(`/user?token=${token}`);
console.log('User route called')
setUser(userRes.data);
}
}
/*
If user logoff or login, update token from child component
*/
const refreshToken = (newToken) => {
//token = newToken;
setToken(newToken);
fetchUserInfo()
}
return (
<UserContext.Provider value={{user, token, refreshToken}}>
{props.children}
</UserContext.Provider>
);
}
export { UserContextProvider, UserContext }
Whenever we navigate to different page in our react app, we are seeing "User" route being called every time even though token isn't updated. Our token changes only when user log off.
Our AppRouter looks like following;
import React from 'react';
import AppRouter from "./AppRouter";
import { Container } from 'react-bootstrap';
import Header from './components/Header';
import { ToastProvider, DefaultToastContainer } from 'react-toast-notifications';
import 'bootstrap/dist/css/bootstrap.css';
import './scss/styles.scss';
import { UserContextProvider } from './UserContextProvider';
export default function App() {
const ToastContainer = (props) => (
<DefaultToastContainer
className="toast-container"
style={{ zIndex:100,top:50 }}
{...props}
/>
);
return (
<UserContextProvider>
<ToastProvider autoDismiss={true} autoDismissTimeout={3000} components={{ ToastContainer }}>
<Container fluid>
<Header />
<AppRouter />
</Container>
</ToastProvider>
</UserContextProvider>
)
}
This is our internal app so we want user to be logged in for 30 days and they don't have to keep login every time. So when user login first time, we create a token for them and keep that token in cookies. So if user close the browser and come back again, we check token in cookies. If token exists, we make API call to fetch user information and setUser in our context. This is the part which isn't working and it keep calling our user api during navigation to each route in application.
Here is our login.js
import React, { useState, useContext } from 'react';
import { setCookies } from '../../utils/Helper';
import APIService from '../../utils/RestApiService';
import { UserContext } from '../../UserContextProvider';
import queryString from 'query-string';
import './_login.scss';
const Login = (props) => {
const [email, setEmail] = useState(null);
const [password, setPassword] = useState(null);
const [error, setError] = useState(null);
const { siteId } = props;
const { refreshToken} = useContext(UserContext);
const onKeyPress = (e) => {
if (e.which === 13) {
attemptLogin()
}
}
let params = queryString.parse(props.location.search)
let redirectTo = "/"
if (params && params.redirect)
redirectTo = params.redirect
const attemptLogin = async () => {
const payload = {
email: email,
password: password,
siteid: siteId
};
let response = await APIService.post('/login', payload);
console.log('response - ', response)
if (response.status === 200) {
const { data } = response;
setCookies('UserToken', data.token);
refreshToken(data.token)
window.location.replace(redirectTo);
}
else {
const { error } = response.data;
setError(error);
}
}
const renderErrors = () => {
return (
<div className="text-center login-error">
{error}
</div>
)
}
return (
<div className="login-parent">
<div className="container">
<div className="login-row row justify-content-center align-items-center">
<div className="login-column">
<div className="login-box">
<form className="login-form form">
<h3 className="login-form-header text-center">Login</h3>
<div className="form-group">
<label>Email:</label>
<br/>
<input
onChange={e => setEmail(e.target.value)}
placeholder="enter email address"
type="text"
onKeyPress={onKeyPress}
className="form-control"/>
</div>
<div className="form-group">
<label>Password:</label>
<br/>
<input
onChange={e => setPassword(e.target.value)}
placeholder="enter password"
type="password"
className="form-control"/>
</div>
<div className="form-group">
<button
className="btn btn-secondary btn-block"
onClick={attemptLogin}
type="button">
Login
</button>
</div>
{error ? renderErrors() : null}
</form>
</div>
</div>
</div>
</div>
</div>
)
}
export default Login;
Our userContext looks like below
import React, { useState, useEffect } from 'react'
import APIService from './utils/APIService';
import { getCookies } from './utils/Helper';
const UserContext = React.createContext();
const UserContextProvider = (props) => {
const [token, setToken] = useState(getCookies('UserToken'));
const [user, setUser] = useState(null);
useEffect(() => {
if (!token) return;
console.log('Inside userContext calling as token ', token)
fetchUserInfo();
}, [token]);
const fetchUserInfo = async() => {
if (token) {
let userRes = await APIService.get(`/user?token=${token}`);
console.log('User route called')
setUser(userRes.data);
}
}
/*
If user logoff or login, update token from child component
*/
const refreshToken = (newToken) => {
//token = newToken;
setToken(newToken);
fetchUserInfo()
}
return (
<UserContext.Provider value={{user, token, refreshToken}}>
{props.children}
</UserContext.Provider>
);
}
export { UserContextProvider, UserContext }
Our getCookies function which simply read cookies using universal-cookies package
export const getCookies = (name) => {
return cookies.get(name);
};
So I tried to replicate your issue using a CodeSandbox, and these are my findings based on your code:
Context:
Your context has a useEffect which depend on token. When you call refreshToken, you update the token which automatically triggers the useEffect and makes a call to fetchUserInfo. So you don't need to call fetchUserInfo after setToken in refreshToken. Your context would look like:
const UserContext = React.createContext();
const UserContextProvider = (props) => {
const [token, setToken] = useState(getCookies("UserToken"));
const [user, setUser] = useState(null);
useEffect(() => {
console.log("Inside userContext calling as token ", token);
fetchUserInfo();
}, [token]);
const fetchUserInfo = async () => {
if (token) {
let userRes = await APIService.get(`/user?token=${token}`);
console.log('User route called')
setUser(userRes.data);
}
};
const refreshToken = (newToken) => {
setToken(newToken);
};
return (
<UserContext.Provider value={{ user, token, refreshToken }}>
{props.children}
</UserContext.Provider>
);
};
export { UserContextProvider, UserContext };
Route:
Now coming to your routing, since you've not included code of AppRouter I had to make an assumption that you use react-router with Switch component. (As shown in CodeSandbox).
I see a line in your Login component which is window.location.replace(redirectTo);. When you do this, the entire page gets refreshed (reloaded?) and React triggers a re-render, which is why I suppose your context methods fire again.
Instead use the history API from react-router (Again, my assumption) like so,
let history = useHistory();
history.push(redirectTo);
Here's the sandbox if you want to play around:

Resources