socket starts a connection in other component - reactjs

I have an incomprehensible problem.
I have 2 React components Signin and Chat, connection to the socket should only occur in second component Chat.
const url = 'http://localhost:4000';
const socket = openSocket(url);
const Chat = () => {
useEffect(() => {
subscribeToChanges();
}, []);
const subscribeToChanges = () => {
socket.on('getAllMessages', message => {
const {userMessage, userId} = message;
setState(prevState => ({
...state,
messages: [...prevState.messages, {userMessage, userId}]
}));
});
};
return (
<div>
</div>
);
};
export default Chat;
But when I update both components in the browser, I see at my backend
logs and they show the same connection to the socket in both.
But as I understand it, this should happen only when i update a `Chat
Maybe this is somehow influenced by the routes, or i don't understand sockets enough
<Switch>
<Route exact path="/">
<SignIn/>
</Route>
<Route exact path="/chat">
<Chat/>
</Route>
</Switch>

You must create your socket instance inside of the component and not in the file otherwise the instance will created when you import the file and not when you render it
Also you must clean ip your socket instance when you unmount the component
const url = 'http://localhost:4000';
const Chat = () => {
const socket = useRef(null);
useEffect(() => {
socket.current = openSocket(url); // create socket here and pass it on
subscribeToChanges();
return () => {
// close socket on unmount
socket.current.close();
}
}, []);
const subscribeToChanges = () => {
socket.current.on('getAllMessages', message => {
const {userMessage, userId} = message;
setState(prevState => ({
...state,
messages: [...prevState.messages, {userMessage, userId}]
}));
});
};
return (
<div>
</div>
);
};
export default Chat;

Related

SocketIO does not work in components of other routes in REACT

I have a component called NAV this component is static in the application, in that component I handle the socket.on listener to store the notification information, if there is a path that does not contain the socket it works normal, but when I put the socket in a component Inside of a route in react, the socket that was inside the NAV component stops working, I swear I must repeat the socket.on code of the NAV component so that the information can arrive again, this is a bad practice because the code should not be repeated so much This happens to me with 5 components in different routes, but I don't know how to prevent this from happening
I tried to make a component that covers all the components inside the routes and manages the sockets information there but it didn't work for me.
THIS IS THE CODE I HAVE IN THE SOCKET ON THE SERVER
socket.on("received event", async (id, productID) => {
if (id || productID) {
let product;
if (productID !== undefined)
product = await Product.findById(productID);
const user = getUser(id ? id : product.owner);
if (user.length > 0)
user.map((user) => io.to(user.socketId).emit("received event"));
}
});
THIS IS THE CODE THAT I HAVE IN THE NAV BUT I HAVE TO REPEAT IT IN EACH ROUTE THAT INTEGRATES THE SOCKET
useEffect(() => {
socket.on("received event", async () => {
await searchNotifications();
});
return () => socket.off();
});
THIS IS THE MAIN APP COMPONENT
function App() {
return (
<LoadingZone>
<Nav /> // HERE IS THE "SOCKET" THE ABOVE CODE THAT IS REPEATED
<Routes>
<Route path="/" element={<Home />}/>
<Route path="/report" element={<PrivateRoute><Report /></PrivateRoute>}/>
<Route path="/messages" element={<PrivateRoute><Messages /></PrivateRoute>}/>
<Route path="/notifications" element={<PrivateRoute><Notifications /></PrivateRoute>}/>
<Route path="/help/*" element={<Help />} />
<Route path="/post/information/*" element={<Post />} /> // THE SOCKET THAT IS IN THE NAV DOES NOT AFFECT IT
</Routes>
<Footer />
<PopupWindow/>
</LoadingZone>
);
}
export default App;
It's supposed to keep taking the socket from the NAV since it doesn't depend on any route but it doesn't, I have the main socket in a separate file where I can access the different components. But this is my problem
This is the nav component
import { socket } from "../api";
function Nav() {
const dispatch = useDispatch();
const searchNotifications = useCallback(async () => {
const briefNotifications = await getNotifications(cookies.get("id"));
const currentNotification = [];
let count = 0;
for (let i = 0; i < 3; i++) {
if (briefNotifications[i] !== undefined)
currentNotification.push(briefNotifications[i]);
}
for (let i = 0; i < briefNotifications.length; i++) {
if (!briefNotifications[i].view) count += 1;
}
dispatch(set(count));
dispatch(changeNotifications(currentNotification));
}, [dispatch]);
useEffect(() => {
socket.on("received event", async () => { // This works until I go to the Post route
await searchNotifications();
});
return () => socket.off();
});
Return <Content/>
}
This is the Post component
function Post() {
... // Code that has nothing to do with the problem
Return <Content/>
}
It's supposed to keep taking the socket from the NAV since it doesn't depend on any route but it doesn't
You can create global hooks to handle repeated useEffect in many components. For example like this:
export const useSocket = () => {
const [data, setData] = useState();
useEffect(() => {
socket.on("received event", async () => {
const res = await searchNotifications();
setData(res);
});
return () => socket.off();
});
return data;
}
And in any component that needs to use socket:
import { useSocket } from 'yourJsFile';
const AnyComponent = props => {
const data = useSocket();
}
Not a lot of context provided for what exactly isn't working, but it appears the useEffect hook is missing a dependency array. Without it the component(s) are constantly opening/closing (subscribing/unsubscribing) the socket connection.
Since it doesn't appear there are any external dependencies try adding an empty dependency array so the effect is run once when the component mounts to connect the socket event, and unsubscribed from the event when the component unmounts.
Example:
useEffect(() => {
socket.on("received event", async () => {
await searchNotifications();
});
return () => socket.off();
}, []); // <-- add empty dependency array

Component shows for a brief moment before redirecting in react router v6

I have a small issue I'm not able to fix. In my react app I use react-router v6 and I have to following routes:
<Route path="/" element={<App />} />
<Route path=":id" element={<CharacterDetails/>} />
<Route path="*" element={<Navigate replace to="/" />} />
As you can see, I have a route that needs an id. This works fine as long as I provide an existing Id so that CharacterDetails component fetches some data successfully. However, if I pass some random text in the URL like "localhost:3000/randomText" the CharacterDetails component still shows for a brief second till the useEffect fires to determine that the request is a 404 and only after that it redirects me to the App component.
How can I check if the URL provided should indeed return some data before rendering the component ? and redirect to the App component directly (without the flickering of the CharacterDetails component) when it is not a valid URL
Thanks!
EDIT: I'm not sure if this is a router issue or should I do it at the component level, I'm waiting for suggestions
EDIT2: Component code
const CharacterDetails = () => {
const { id } = useParams<string>();
const navigate = useNavigate();
const [state, dispatch] = useReducer(characterReducer, initialState);
const { data, episodes, loading } = state;
useEffect(() => {
const fetchData = async (id: string) => {
dispatch({ type: "LOADING_START" })
try {
let response = await getSingleCharacterData(id);
let URLs = response.data.episode;
let listOfEpisodes = await getEpisodeName(URLs);
dispatch({
type: "FETCH_SUCCESS",
payload: { data: response.data, episodeList: listOfEpisodes },
});
dispatch({ type: "LOADING_OVER" })
} catch (error) {
dispatch({ type: "LOADING_OVER" })
navigate("/");
}
};
if (id) fetchData(id);
}, [id, navigate]);
return (
<CharacterDetailsContainer>
{loading ? <Loading /> :
data && (
<div> </div>
)}
</CharacterDetailsContainer>
}
You can use the useParams hook in the child.
const acceptableIDs = ["dog", "cat"];
function CharacterDetails() {
let { id } = useParams();
return acceptableIDs.includes(id) ? (
<div>
<h3>ID: {id}</h3>
</div>
) : null; // render nothing or redirect
}
If it takes too long to check if the ID is valid, you could show a transition.
Note this is business logic and should probably not bleed into the router.
This isn't an issue with the router/routes, it's something that routed components need to handle.
In the CharacterDetails component use some "loading" state to conditionally render null or some loading indicator while the id path param is validated. Note that "loading" needs to be the initial state so the code isn't leaking any initial non-loading UI, waiting until the useEffect hook runs at the end of the initial render to dispatch({ type: "LOADING_START" }) is too late unless the initial redux state has it set true.
Example:
const CharacterDetails = () => {
const { id } = useParams();
const navigate = useNavigate();
const [isLoading, setIsLoading] = React.useState(true);
useEffect(() => {
setIsLoading(true);
// logic to validate id param
if (is404) {
navigate("/404", { replace: true }); // redirect
} else {
setIsLoading(false); // clear loading state so page content renders
}
}, [id]);
if (isLoading) {
return null; // or loading spinner/etc...
}
return page content
};
Your code:
const CharacterDetails = () => {
const { id } = useParams<string>();
const navigate = useNavigate();
const [isLoading, setIsLoading] = React.useState<boolean>(true); // <-- initially true
const [state, dispatch] = useReducer(characterReducer, initialState);
const { data, episodes } = state;
useEffect(() => {
const fetchData = async (id: string) => {
setIsLoading(true);
dispatch({ type: "LOADING_START" });
try {
let response = await getSingleCharacterData(id);
let URLs = response.data.episode;
let listOfEpisodes = await getEpisodeName(URLs);
dispatch({
type: "FETCH_SUCCESS",
payload: { data: response.data, episodeList: listOfEpisodes },
});
setIsLoading(false);
} catch (error) {
// handle any errors, etc...
// redirect home
navigate("/", { replace: true });
} finally {
dispatch({ type: "LOADING_OVER" });
}
};
if (id) fetchData(id);
}, [id, navigate]);
if (isLoading) {
return null; // or loading spinner/etc...
}
return ( <some JSX> )
}

How to keep authenticated state on refresh?

I'm using firebase authentication for my app. I used useAuth hook from here. Integrate with react-router guide about redirect (Auth).
SignIn,SignOut function is working as expected. But when I try to refresh the page. It redirects to /login again.
My expected: Redirect to / route when authenticated.
I tried to add this code in PrivateRoute.js
if (auth.loading) {
return <div>authenticating...</div>;
}
So I can refresh the page without redirect to /login but it only show authenticating... when click the log out button.
Here is my code: https://codesandbox.io/s/frosty-jennings-j1m1f?file=/src/PrivateRoute.js
What I missed? Thanks!
Issue
Seems you weren't rendering the "authenticating" loading state quite enough.
I think namely you weren't clearing the loading state correctly in the useEffect in useAuth when the initial auth check was resolving.
Solution
Set loading true whenever initiating an auth check or action, and clear when the check or action completes.
useAuth
function useProvideAuth() {
const [loading, setLoading] = useState(true); // <-- initial true for initial mount render
const [user, setUser] = useState(null);
// Wrap any Firebase methods we want to use making sure ...
// ... to save the user to state.
const signin = (email, password) => {
setLoading(true); // <-- loading true when signing in
return firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then((response) => {
setUser(response.user);
return response.user;
})
.finally(() => setLoading(false)); // <-- clear
};
const signout = () => {
setLoading(true); // <-- loading true when signing out
return firebase
.auth()
.signOut()
.then(() => {
setUser(false);
})
.finally(() => setLoading(false)); // <-- clear
};
// Subscribe to user on mount
// Because this sets state in the callback it will cause any ...
// ... component that utilizes this hook to re-render with the ...
// ... latest auth object.
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if (user) {
setUser(user);
} else {
setUser(false);
}
setLoading(false); // <-- clear
});
// Cleanup subscription on unmount
return () => unsubscribe();
}, []);
// Return the user object and auth methods
return {
loading,
user,
signin,
signout
};
}
Check the loading state in PrivateRoute as you were
function PrivateRoute({ children, ...rest }) {
const auth = useAuth();
if (auth.loading) return "authenticating";
return (
<Route
{...rest}
render={({ location }) =>
auth.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)
}
/>
);
}
Demo
Try this approach, it works for me :
const mapStateToProps = state => ({
...state
});
function ConnectedApp() {
const [auth, profile] = useAuth()
const [isLoggedIn, setIsLoggedIn] = useState(false)
useEffect(() => {
if (auth && auth.uid) {
setIsLoggedIn(true)
} else {
setIsLoggedIn(false)
}
}, [auth, profile]);
return (<Router>
<Redirect to="/app/home"/>
<div className="App">
<Switch>
<Route path="/home"><Home/></Route>
<Route path="/login"><Login styles={currentStyles}/></Route>
<Route path="/logout"><Logout styles={currentStyles}/></Route>
<Route path="/signup" render={isLoggedIn
? () => <Redirect to="/app/home"/>
: () => <Signup styles={currentStyles}/>}/>
<Route path="/profile" render={isLoggedIn
? () => <Profile styles={currentStyles}/>
: () => <Redirect to="/login"/>}/>
</Switch>
</div>
</Router>);
}
const App = connect(mapStateToProps)(ConnectedApp)
export default App;

How to cancelled previous api from components when users changed route in reactjs?

I have got two components. For example,
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
Home.js
const Home = ()=>{
const [count, setCount] = useState([]);
useEffect(() => {
axios.get("https://test.com/home").then(res=>console.log(res))
});
return(
<div>test-1</div>
)
}
About.js
const About = ()=>{
const [count, setCount] = useState([]);
useEffect(() => {
axios.get("https://test.com/about").then(res=>console.log(res))
});
return(
<div>test-2</div>
)
}
Problem:
Here are two components Home.js and about.js When I go to Home component then it's fetched https://test.com/home in the browser and when I go to about component, then it should fetch only this API https://test.com/about.
However, I saw a previous API also called from the browser. Here is two endpoints see currently in the browser.
https://test.com/home
https://test.com/about
How can I cancelled the previous API from the browser when I change the route?
Something like this should work:
const createAxiosGroup = () => {
//last.source is a source to cancel last request
// initial value is a dummy
const last = { source: { cancel: () => 0 } };
return (...args) => {
//cancel last request
last.source.cancel();
//set last source. To be used in current request
last.source = axios.CancelToken.source();
const argCopy = [...args];
argCopy[1] = {
...argCopy[1],
cancelToken: last.source.token,
};
//pass args with cancelToken in second argument
return axios.get(...argCopy);
};
};
//axios.get grouped so it'll cancel any previously
// active requests
const grouped = createAxiosGroup();
const Home = () => {
const [count, setCount] = useState([]);
useEffect(() => {
grouped('https://test.com/home').then((res) =>
console.log(res)
);
});
return <div>test-1</div>;
};
const About = () => {
const [count, setCount] = useState([]);
useEffect(() => {
grouped('https://test.com/about').then((res) =>
console.log(res)
);
});
return <div>test-2</div>;
};

useEffect and SocketIO Infinite Rerendering, why is it happening?

I am getting data, using useEffect Hook from sockets. Every time I get the response, one particular component rerenders, but I don't pass any socket data to that component.
Code:
const App = () => {
const isMounted = React.useRef(false);
const [getSocketData, setSocketData] = useState([]);
useEffect(() => {
console.log('[App.js] Mounted!');
isMounted.current = true;
const socket = socketIOClient(process.env.REACT_APP_BOARD);
socket.on('ServersInfo', (data) => {
if (isMounted.current) {
setSocketData([...data]);
}
});
socket.on('connect_error', () => {
if (isMounted.current) {
setSocketData([]);
}
console.log('Connection error!');
});
return (() => {
isMounted.current = false;
socket.disconnect();
console.log('[App.js] unmounted');
});
}, []);
const routes = (
<Switch>
<Route path={['/', '/users/:id']} exact component={() => <MainPage axiosInstance={axiosInstance} />} />
<Route path='/servers' render={(spProps) => <ServerPing {...spProps} />} />
<Route render={(nfProps) => <NotFoundComponent {...nfProps} />} />
{/* <Redirect to='/' /> */}
</Switch>
);
return (
<div className="App">
<Layout>
<Suspense fallback={<p>Loading...</p>}>
{routes}
</Suspense>
</Layout>
</div>
);
};
export default App;
What my Components look like:
- App.js
- Layout (doesn't rerender) (which has 3 children) - MainPage (rerenders infinitely), ServerPing (doesn't rerender), NotFoundComponent (doesn't rerender)
The question is: why MainPage Component rerenders infinitely?
I mean MainPage Component and its children unmount and mount again, when socket data fetches which is weird behaviour.
MainPageComponent:
const MainPage = ({ axiosInstance, ...props }) => {
const isMounted = React.useRef(false);
const [loadingPage, setLoadingPage] = useState(true);
const [usernames, setUsernames] = useState([]);
const [currentDay] = useState(new Date().getDay());
useEffect(() => {
isMounted.current = true;
console.log('[MainPage.js] Mounted!');
getUsers();
return () => {
console.log('[MainPage.js] Unmounted!');
isMounted.current = false;
};
}, []);
const getUsers = async () => {
try {
const res = await axiosInstance.get('/users');
const newData = await res.data;
const newArray = [];
newData.map(user => (
newArray.push({id: user._id, flag: user.flag, text: user.name, value: user.name.toLowerCase()})
));
if (isMounted.current) {
setUsernames(newArray);
setLoadingPage(false);
}
} catch {
if (isMounted.current) {
setLoadingPage(false);
}
}
};
return...
Problem is that you are using component prop instead of render prop to render the MainPage which will remount the component on any re-render of App component if there is a callback given to a component prop.
Below code should fix the issue
<Route
path={['/', '/users/:id']}
exact
render={(props) => <MainPage {...props} axiosInstance={axiosInstance} />}
/>
According to the react-router-dom docs
When you use component (instead of render or children, below) the
router uses React.createElement to create a new React element from the
given component. That means if you provide an inline function to the
component prop, you would create a new component every render. This
results in the existing component unmounting and the new component
mounting instead of just updating the existing component. When using
an inline function for inline rendering, use the render or the
children prop (below).

Resources