useEffect and SocketIO Infinite Rerendering, why is it happening? - reactjs

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).

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

why is data initially re-renders with empty and then displaying my productdetails page with delay while routing? used useCallback & useMemo

This is my code. By using useCallback and useMemo the initial empty re render is not controlled. Initially empty data gets rendered ,after routing the same product get replaced with the selected product.
Routing component
<Header/>
<Routes>
<Route path="/" element={<ProductListing/>} />
<Route exact path="/product/:productId" element={<ProductDetail/>} />
<Route>404 Not Found!</Route>
</Routes>
<Link to={`/product/${id}`}>`
...
const product = useSelector((state) => state.product);
const { productId } = useParams();
console.log(productId);
const { title, image, price, category ,description} = product;
const dispatch = useDispatch();
console.log(product);
const fetchProductDetail = async (id) => {
const response = await axios
.get(`https://fakestoreapi.com/products/${id}`)
.catch((err) => {
console.log("Err: ", err);
});
dispatch(selectedProducts(response.data));
};
useEffect(() => {
if (productId && productId !== "") fetchProductDetail(productId);
return () => {
dispatch(removeSelectedProducts());
};
}, [productId]);
please resolve the issue

How to use a hook instead of context provider using react and typescript?

i want to access isLoading state ineach component of the main component. basically when load() in useAnother hook starts and ends i set loading state to true and false.
below is my code without context provider,
function useAnother(Id: string) {
const [compId, setCompId] = React.useState(undefined);
const [isLoading, setIsLoading] = React.useState(false);
const comp = useCurrentComp(Id);
const load = useLoad();
if (comp && comp.id !== compId) {
setCompId(comp.id);
const prevCompId = compId !== undefined;
if (prevCompId) {
setIsLoading(true);
load().then(() => {
setIsLoading(false);
});
}
}
}
function Main ({user}: Props) {
useAnother(user.id); //fetching isLoading here from useHook
return (
<Wrapper>
<React.suspense>
<Switch>
<Route
path="/"
render={routeProps => (
<FirstComp {...routeProps} />
)}
/>
<Route
path="/items"
render={routeProps => (
<SecondComp {...routeProps} />
)}
/>
//many other routes like these
</Switch>
</React.suspense>
</Wrapper>
);
}
Now with using context provider
interface LoadingContextState {
isLoading: boolean;
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
}
const initialLoadingState: LoadingContextState = {
isLoading: false, setIsLoading: () => {},
};
export const LoadingContext = React.createContext<LoadingContextState>(
initialLoadingState
);
export const LoadingContextProvider: React.FC = ({ children }) => {
const [isLoading, setIsLoading] = React.useState<boolean>(false);
return (
<LoadingContext.Provider
value={{
isLoading,
setIsLoading,
}}
>
{children}
</LoadingContext.Provider>
);
};
function App() {
return (
<LoadingContextProvider>
<Main/>
</LoadingContextProvider>
);
}
function useAnother(Id: string) {
const [compId, setCompId] = React.useState(undefined);
const {setIsLoading} = React.useContext(LoadingContext);
const comp = useCurrentComp(Id);
const load = useLoad();
if (comp && comp.id !== compId) {
setCompId(comp.id);
const prevCompId = compId !== undefined;
if (prevCompId) {
setIsLoading(true);
load().then(() => {
setIsLoading(false);
});
}
}
}
function Main ({user}: Props) {
useAnother(user.id);
return (
<Wrapper>
<React.suspense>
<Switch>
<Route
path="/"
render={routeProps => (
<FirstComp {...routeProps} />
)}
/>
<Route
path="/items"
render={routeProps => (
<SecondComp {...routeProps} />
)}
/>
//many other routes like these
</Switch>
</React.suspense>
</Wrapper>
);
}
function FirstComponent () {
const {isLoading} = React.useContext(LoadingContext);
return (
<Wrapper isLoading={isLoading}/>
);
}
this works. but i dont want to use context provider instead is it possible to use hook instead of context for this.
could someone help me with this. thanks.
}
Now with using context provider
interface LoadingContextState {
isLoading: boolean;
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
}
const initialLoadingState: LoadingContextState = {
isLoading: false, setIsLoading: () => {},
};
export const LoadingContext = React.createContext<LoadingContextState>(
initialLoadingState
);
This is possible if you choose to return the value of isLoading from useAnother hook.
function useAnother(Id: string) {
const [compId, setCompId] = React.useState(undefined);
const [isLoading, setIsLoading] = React.useState(false);
const comp = useCurrentComp(Id);
const load = useLoad();
if (comp && comp.id !== compId) {
setCompId(comp.id);
const prevCompId = compId !== undefined;
if (prevCompId) {
setIsLoading(true);
load().then(() => {
setIsLoading(false);
});
}
}
return isLoading; // return the isLoading value from the hook
}
When you call useAnother hook in the Main component you can get the isLoading value and pass it as props to the children of Main component.
For example,
const isLoading = useAnother(user.id)
// when you render FirstComp pass isLoading as prop also, the FirstComp
// needs to have appropriate code for handling the `isLoading` value
<FirstComp {...routeProps} isLoading={isLoading} />
The issue with this approach, suppose you have another component which is a child of the FirstComponent and it also needs the isLoading value. To provide the isLoading value you have to pass isLoading drilling the prop through several components in the hierarchy which is an anti-pattern.
That is why I suggest keep using the context API approach,
Using contexts can generate some complex boilerplate code when using typescript, but it saves you from drilling the prop in the component hierarchy.

React-router re-render on URL change with route using regex

I have an App like this:
function App() {
return (
<Router>
<OpNavbar/>
<Route exact={true} path="/" render={() => (
<h1>This is the welcome page!</h1>
)}/>
<Route path="/([a-z]{3,4})/([a-z]+)list" component={OpTable}/>
</Router>
);
}
If I am in "/" and switch paths by clicking a link to for example "/pfm/examplelist" and viceversa it renders the respective component without any problem. However if I am in say "/pfm/examplelist" and switch to "/pfm/anotherlist" the url changes but my component will not be re-rendered. I assume it's because both the old and the new paths match my regex? How can re-render my component on every url change?
Here is a stripped-down version of my Table component:
function OpTable(props) {
const [apiData, setData] = useState([]);
const [columns, setColumns] = useState([{dataField: "Dummy", text: "Loading, Please Wait..."}]);
useEffect(() => {
axios.get(props.match.url)
.then(response => {
let res_data = response.data;
setData(res_data.data);
setColumns(res_data.columns);
})
.catch(error => {
console.log(error);
})
}, [])
return (
<BootstrapTable
keyField="id"
data={ apiData }
columns={ columns }
/>
);
}
This is the case when subsequent url calls same component.If you want to rerender,one of the way is to track your path(url) in useEffect.
useEffect(() => {
axios.get(props.match.url)
.then(response => {
let res_data = response.data;
setData(res_data.data);
setColumns(res_data.columns);
})
.catch(error => {
console.log(error);
})
}, [props.location.pathname])
I am not a regular user of hooks(I may be wrong with the syntax). But the logic is to call api(whenever there is change in url) required for that component which in turn sets state and rerender happen

Child component not re-rendering even though parent state is updated using hooks

I have a functional component App with a useEffect hook and I'm trying to get the <Redirect> child component to re-render on a state change, specifically the call to setUserInSystem, which should update userInSystem, which is explicitly referenced in the render method. However the component does not re-render when userInSystem changes and I can't figure out why. (Note: both async functions getUserInfo and getUserByWorkEmail are working as expected and retrieving correct data.)
const App = (props) => {
const { authState, authData } = props;
const [signedIn, setSignedIn] = useState(false);
const [userInfo, setUserInfo] = useState(undefined);
const [userInSystem, setUserInSystem] = useState(false);
useEffect(() => {
setSignedIn(!(authState !== 'signedIn'));
const fetchUser = async () => {
const data = await getUserInfo();
const userFound = await getUserByWorkEmail(data);
setUserInSystem(userFound);
setUserInfo(data);
};
if (authState === 'signedIn') {
fetchUser();
}
}, [authState]);
return (
<div>
<BrowserRouter>
<Switch>
<Redirect
exact={true}
path="/"
to={userInSystem ? '/dashboard' : '/unverified'}
/>
</Switch>
</BrowserRouter>
</LayoutProvider>
</div>
);
};
Create a new hook and envelope the Redirect with a condition that is mutated every fetch:
const [loading, setLoading] = useState(true);
In the fetch setLoading to false after the fetch is resolved.
const fetchUser = async () => {
setLoading(true);
const data = await getUserInfo();
const userFound = await getUserByWorkEmail(data);
setUserInSystem(userFound);
setUserInfo(data);
setLoading(false);
};
Then in the return:
<Switch>
{ !loading ? <Redirect
exact={true}
path="/"
to={userInSystem ? '/dashboard' : '/unverified'}
/>
: <div>Loading....</div>
}
</Switch>
Try adding userInSystem in the useEffect dependency array.
useEffect(() => {
setSignedIn(!(authState !== 'signedIn'));
const fetchUser = async () => {
const data = await getUserInfo();
const userFound = await getUserByWorkEmail(data);
setUserInSystem(userFound);
setUserInfo(data);
};
if (authState === 'signedIn') {
fetchUser();
}
}, [authState, ***userInSystem***]);

Resources