useContext returns undefined when my redux store updates - reactjs

I am having trouble understanding why my useContext value updates from the expected values to undefined when the redux store is updated. I am using create context to house my socket connection for my discussions. The following is my context class
import React, { createContext, useRef } from 'react'
export const WebSocketContext = createContext()
export default ({ children }) => {
const webSocket = useRef(null);
let ws;
const openedDiscussionID = useSelector(state => state.presence.discussionId || '')
const sendMessages = (messageObject) => {
webSocket.current.send(JSON.stringify(messageObject))
}
if (!webSocket.current){
webSocket.current = new WebSocket(``)
webSocket.current.onmessage = (message) => {
const discussionMessage = JSON.parse(message.data)
};
ws = {
webSocket: webSocket.current,
sendMessages
}
}
return(
<WebSocketContext.Provider value={ws}>
{children}
</WebSocketContext.Provider>
)
}
I am currently using it in my parent component called projectDetailContainer,
import WebSocketProvider from '../../redux-state/middleware/socketService'
function ProjectDetailContainer() {
return (
<WebSocketProvider>
<div>parent component</div>
</WebSocketProvider>
)
}
export default ProjectDetailContainer
somewhere down the chain, I have the following discussion component:
import { WebSocketContext } from '../../redux-state/middleware/socketService'
function DiscussionOptionContainer() {
const ws = useContext(WebSocketContext)
...
useEffect(() => {
if(openedDiscussion.title?.length){
dispatch(setOpenDiscussion(openedDiscussion))
}
},[openedDiscussion])
const sendMessage = () => {
const mes = {
action:'discussion',
message:{
customerId:'7240304',
projectId:projectId,
message:message,
itemId:openedDiscussion.discussionId,
sender: me
}
}
debugger
ws.sendMessages(mes)
}
...}
The thing I noticed was that as soon as the app loads, I see the sendMessages function being returned but as soon as the dispatch occurs, it changes the ws (useContext) to undefined. If I comment out the part that updates the redux store, it works as expected. My theory is that the redux store provider is higher in the chain than my WebSocketProvider but i cant confirm.

useSelector triggers a rerender when selected value changes. ws is undefined by default and gets a value if webSocket.current is falsy. And it seems to happen only on the initial render. Every consecutive render will set ws to undefined and pass it to WebSocketContext.Provider.
Try moving ws = {.....} outside condition.

Related

Pull data from firestore using useEffect works on re-render only

Here is my code:
import React, { useEffect, useState } from 'react';
import { getDocs, collection } from 'firebase/firestore';
import { db } from '../firebase-config';
const Home = () => {
const [postList, setPostList] = useState([]);
const postsCollectionRef = collection(db, "data");
useEffect(() => {
const getPosts = async () => {
const data = await getDocs(postsCollectionRef);
let postListArray = []
data.forEach((doc) => {
const post = { ...doc.data() }
postListArray.push(post)
});
setPostList(postListArray)
};
getPosts();
console.log(postList);
}, []);
return (
<div>test</div>
);
};
export default Home;
On loading, the console.log returned an empty array. The spooky thing is when i changed anything , for example
return (
<div>test_epic</div>
);
The console.log shows that it is an array. Anyone has any idea as to why? Please refer to the screepcap as attached.
the first render on loading
I changed anything and components rerendered
Setting state in react is asynchronous, so the data is loaded and the state is set but the console.log statement is executed before the setting state async operation is complete
To make it a bit more clear this is how it works step by step
Component is rendered and postList is initialized with a value of []
useEffect is triggered
Data is fetched
A call to set a new value of postList is placed using setPostList (key here is a call is placed not that the data is actually updated)
You print console.log with a value from Step 1
The call from Step 4 is complete and now the data is actually updated
Here is an article that explains it with examples
And here is another answer that explains this deeply

React: useContext vs variables to store cache

I have the following code which I implemented caching for my users:
import { IUser } from "../../context/AuthContext";
export const usersCache = {};
export const fetchUserFromID = async (id: number): Promise<IUser> => {
try {
const res = await fetch("users.json");
const users = await res.json();
Object.keys(users).forEach((userKey) => {
const currentUser = users[userKey];
if (!currentUser) {
console.warn(`Found null user: ${userKey}`);
return;
}
usersCache[users[userKey].id] = currentUser;
});
const user = usersCache[id];
return user;
} catch (e) {
console.error(`Failed to fetch user from ID: ${id}`, e);
throw Error("Unable to fetch the selected user.");
}
};
As you can see, the variable userCache stores all the users.
It works fine and I can access this variable from all my components.
I decided that I want to "notify" all my components that the userCache has changed, and I had to move this logic to a react Context and consume it with useContext.
So the questions are:
How I can set the userCache context although the above code is not a react component? (it's just a typescript file I called 'UserService')?
I can't do:
export const fetchUserFromID = async (id: number): Promise<IUser> => {
const { setUserCache } = useContext(MembersContext);
...
}
React Hook "useContext" is called in function "fetchUserFromID" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. (react-hooks/rules-of-hooks)eslint
Is there a reason to prefer Context over variable as above altough the data is not subject to change frequently?
Thanks.
How I can set the userCache context although the above code is not a react component? (it's just a typescript file I called 'UserService')?
You need to declare your context value somewhere in a component in order to use it.
const MembersContext = React.createContext({}); // initial value here
function App() {
const [users, setUsers] = useState({});
const fetchUserFromID = async (id: number) => {
/* ... */
// This will call `setUsers`
/* ... */
}
return (
<MembersContext.Provider value={users}>
{/* your app components, `fetchUserFromId` is passed down */}
</MembersContext.Provider>
);
}
function SomeComponent() {
const users = useContext(MembersContext);
return (/* you can use `users` here */);
}
Is there a reason to prefer Context over variable as above altough the data is not subject to change frequently?
If you need your components to update when the data changes you have to go with either :
a context
a state passed down through props
a redux state
More info here on how to use Contexts.

UseEffect doesn't behave as expected -

When I refresh the page useEffect only render one function out of the two inside, and when I change routes it works normally.
I am using redux to set global state with the vehicles and dispatching the API in useEffect
so I have vehicles available all the time. however, the second function vchStatusNumbers that it should return the length of the array filtered as per its status , it only runs once, and when i add its state as dependency i get an infinite loop!
I need to understand how i should approach it?
Below is the component
import React,{useEffect,useState} from "react";
import StatisticBanner from "./StatisticBanner";
import {getAllVehicles,fetchVehiclesReport } from "./vehiclesReducer";
import { useSelector, useDispatch } from "react-redux";
const Home= ()=> {
const {vehicles} = useSelector(getAllVehicles); // get the state
const [statusTotal, setStatusTotal] = useState({})
const dispatch = useDispatch(); // dispatch fn to reducers
useEffect(() => {
dispatch(fetchVehiclesReport());
vchStatusNumbers();
}, [dispatch]);
const vchStatusNumbers = () =>{
const status = {}
let availableLength = 0
let parkedLength = 0
let serviceLength = 0
vehicles.map(vch=>{
if(vch.status === 'available'){
++availableLength
status.available = availableLength
}
if(vch.status === 'parked'){
++parkedLength
status.parked = parkedLength
}
if(vch.status === 'service'){
++serviceLength
status.service = serviceLength
}
})
setStatusTotal (status)
}
return (
<>
<div style={{ margin: 20 }}>
<StatisticBanner key={"statics"} statusTotal={statusTotal} />
</div>
</>
);
}
export default Home
Yes, if you add a dependency to an useEffect hook that ultimately updates that dependency value then this will cause render looping.
Seems vchStatusNumbers should be in a separate useEffect hook with a dependency on the vehicles redux state value.
useEffect(() => {
vchStatusNumbers();
}, [vehicles]);
This is because it is derived state from the vehicles data and won't be updated yet in the first effect that dispatches the action to update it.

React context provider not setting value on page load/refresh

I have the following hook for Pusher and I use it to share one instance across the application.
import React, { useContext, useEffect, useRef } from "react";
import Pusher from "pusher-js";
const PusherContext = React.createContext<Pusher | undefined>(undefined);
export const usePusher = () => useContext(PusherContext)
export const PusherProvider: React.FC = (props) => {
const pusherRef = useRef<Pusher>();
useEffect(() => {
pusherRef.current = new Pusher(PUSHER_APP_KEY, {
cluster: 'eu'
})
return () => pusherRef.current?.disconnect()
}, [pusherRef]);
return (
<PusherContext.Provider value={pusherRef.current}>
{props.children}
</PusherContext.Provider>
)
}
The problem is that the provider always has an undefined value on page refresh/load. But when I trigger a re-render the value is correctly set. I would like to have the instance without the need of re-rendering.
Why is this happening?
I believe you can use the next construction:
export const PusherProvider = (props) => {
const pusher = useMemo(() => new Pusher(APP_PUSHER_KEY, { cluster: 'eu' }), [])
useEffect(() => () => pusher.disconnect(), [pusher])
return <PusherContext.Provider value={pusher}>{props.children}</PusherContext.Provider>
}
I have solved this issue by following way.
If you set State/const data inside "useEffect" will not work. as that will not run when page refresh but the state declaration those are outside the "useEffect" will run. Hence it will reset default values.
So I resolved by setting the state/const value outside of "useEffect" and done.

What is causing Error: Too many re-renders in this code?

Can anybody please look at the following ReactJS component and tell what is causing it to return the error:
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
import React, { useState, useEffect } from 'react';
function Lab() {
const [questions, setQuestions] = useState([]);
const addQuestion = (question) => {
let q = [...questions];
q.push(question);
setQuestions(q);
}
addQuestion('What is your name?');
addQuestion('Where do you belong?');
return (
<div>
{
questions.map( q => <div>{q}</div>)
}
</div>
);
}
export default Lab;
Edit:
I can use some default values in useState([]), but that would make my code much messy because the data structure is quite complicated. That's why I want to push default values from within a helper function. Isn't it possible this way?
PROBLEM
Lab function is executed.
addQuestion is executed which triggers a re-render
Re-render triggers another execution of addQuestion
and thereby causes an infinite loop of re-renders and execution of addQuestion.
SOLUTION
add your default question as the default state in your Lab component.
import React, { useState, useEffect } from 'react';
const defaultQuestions = [
'What is your name?', 'What is your name?'
]
function Lab() {
const [questions, setQuestions] = useState(defaultQuestions);
const addQuestion = (question) => {
let q = [...questions];
q.push(question);
setQuestions(q);
}
return (
<div>
{
questions.map( q => <div>{q}</div>)
}
</div>
);
}
export default Lab;
Whenever the state update, the whole function component will re-run again.
Therefore, in your code, when the questions variable is updated, the execution of the addQuestion function will be called again, and the function itself will update the state again, and that causes the infinite loop.
To prevent this kind of situation, it's better to let an event trigger the function.
Edit
If adding some default values is your main purpose, this is how you can do it:
import React, { useState, useEffect, useRef } from 'react';
function Lab() {
const isDefaultValueLoaded = useRef(false);
const [questions, setQuestions] = useState([]);
const addQuestion = (question) => {
let q = [...questions];
q.push(question);
setQuestions(q);
}
// To set the default values:
// use an useEffect hook to load the values when the component just mount.
useEffect(() => {
// to prevent the infinite loop,
// use a ref object as a flag to make sure the function will only run once.
if (isDefaultValueLoaded.current === false) {
loadDefaultValue();
isDefaultValueLoaded.current = true;
}
}, []);
// by writing the code above, you can now seperate the logic into an "helper function" as you mentioned.
const loadDefaultValue = () => {
addQuestion('What is your name?');
addQuestion('Where do you belong?');
}
return (
<div>
{
questions.map(q => <div>{q}</div>)
}
</div>
);
}
export default Lab;

Resources