setState randomly failed which is caused by component unmount - javascript

I'm facing a weird situation ...
The setState sometimes worked fine, but sometimes will get the warning for setting state to the unmounted component.
If the user's cart is undefined, then call the API to get the cart information and set the state.
const loginData = () => {
const isUnmounted = useRef(false);
const handleData = () => {
if (isUnmounted.current) return false;
const setData = (user) => {
//setCart failed sometimes, because of setting State to the unmount component
if (cart == undefined) getUserData((response) => setCart(response.cart));
}
if (userData !== null) {
setUserData(user);
}
}
useEffect(() => {
handleData();
return () => {
isUnmounted.current = true;
}
}, [])
}
When I enter the page contain this component, it will sometimes success to work.
And the other time the page crashed because cart can't be set with the unmounted component (will get the warning in console), and the further cart.id will crash the process.
Looks like it just happened randomly...
Sometimes the process crash when I just enter the page,
but sometimes I can enter the page and refresh the page multiple times then the page crashed ...
Have no idea what is happened. May anyone give me some suggestions? Thanks!

Related

Unable to access state - React.js working with Websocket

This is my first post here, so nice to meet you all. I have recently started my adventure with React, so my question probably will not be very challenging (it could also be very basic stuff), however I got stuck with this problem for a while.
So I am working with WebSocket that when connected, sends one-time message with all products currently in stock. Then, every few seconds, it sends object with update of product stock changes.
I managed to update state with first recieved message, but then, when I try to access state in a function handleData, state is empty array. This is happening despite the fact that the data rendered on the page using state is still visible, state is visible in Firefox Developer Edition React DevTools, and the useEffect that is associated with the state change fires only once - at the start, so it doesn't change.
I want to be able to access data I put before in state, in function handleData, so that I can update the state with stock changes. It is interesting that when the websocket loads again, the "products" status variable is extended by the previous, unloaded products.
Here is code of my component:
import React, { useState, useEffect, useRef } from 'react';
import Product from './Product';
export default function ProductsList() {
const [products, setProducts] = useState([]);
const ws = useRef(null);
useEffect(() => {
ws.current = new WebSocket(<HERE IS WEBSOCKET URL BUT I CENSORED IT>);
ws.current.onmessage = (message) => handleData(message);
return () => ws.current.close();
},[]);
useEffect(() => {
console.log("Products useEffect:", products)
}, [products]) //prints fine here
const handleData = (message) => {
const data = JSON.parse(message.data);
switch (data.operation) {
case 'product.stock.decreased': {
console.log("Products Switch:", products) // prints empty array here
break;
}
default: {
setProducts(prevState => [...prevState, ...data]) // here state updates
break;
}
}
}
return (
<div className="ProductsList">
{products.map(p => <Product key={p.productId} productId={p.productId} name={p.name} price={p.price} stock={p.stock} />)}
</div>
);
}
Many thanks in advance for your help.

Why I am getting re-render errors on loading page?

I have created an app that consists of a loading screen. However, if the API is failed after 5 seconds, a failure message is to be conveyed. I am getting too-many re-renders error on the line which I have mentioned in the code below.
I have used setTimeout function to replace the message of failure if API fails after 5 seconds of loading the page. The rest of my app functionality is working fine.
My app code is: -
function App() {
//Is website loaded for first time?
const [loadingFirstTime, setLoadingFirstTime] = useState(true);
//Has the site loading failed? If yes, pass that to loading component
const [loadingFailed, setLoadingFailed] = useState(false);
//To be run first time the website is loaded
useEffect(() => {
getMovies()
.then(res => setMoviesDetails(res))
.then(() => setLoadingFirstTime(false));
}, [])
................... some code here .............
//If the details of api haven't been loaded or api loading failed
if (Object.keys(moviesDetails).length === 0 && loadingFirstTime) {
//------**Error occurs here after 5 seconds as per the console**-----------------
//check for the same thing after 5 seconds, if initial data still has been loaded?
setTimeout(() => {
if (Object.keys(moviesDetails).length === 0 && loadingFirstTime) {
setLoadingFailed(true);
}
}, 5000);
return (
<LoadingScreen status={loadingFailed} />
)
}
return (
<>
........ App components which are working fine .............
</>
);
}
Code for my loading component: -
function LoadingScreen({status}) {
const [loadingText, setLoadingText] = useState("Loading the site...");
//check if site loading failed and display appropiate message
if (status) {
setLoadingText("Website loading failed. Please reload or contact the administrator.");
}
return (
<div className="loading-screen">
<h1>{loadingText}</h1>
</div>
)
}
In React, you should avoid changing states when rendering, which is what you are doing in your LoadingScreen component when you are setting loadingText.
setLoadingText("Website loading failed. Please reload or contact the administrator.");
This is happening all the time you are passing a truthy value to status. That line is making LoadingScreen component to re-render itself again and again in an infinite loop.
Generally, it is better to implement this feature inside a useEffect function like this one:
function LoadingScreen({ status }) {
const [loadingText, setLoadingText] = useState("Loading the site...");
useEffect(() => {
if (status) {
const newLoadingText =
"Website loading failed. Please reload or contact the administrator.";
if (newLoadingText !== loadingText) {
// This state setter provokes a re-render so set only if new value is
// different than the actual value stored in state
setLoadingText(newLoadingText);
}
}
}, [loadingText, status]);
return (
<div className="loading-screen">
<h1>{loadingText}</h1>
</div>
);
}

How to prevent state updates from a function, running an async call

so i have a bit of a weird problem i dont know how to solve.
In my code i have a custom hook with a bunch of functionality for a fetching a list
of train journeys. I have some useEffects to that keeps loading in new journeys untill the last journey of the day.
When i change route, while it is still loading in new journeys. I get the "changes to unmounted component" React error.
I understand that i get this error because the component is doing an async fetch that finishes after i've gone to a new page.
The problem i can't figure out is HOW do i prevent it from doing that? the "unmounted" error always occur on one of the 4 lines listed in the code snippet.
Mock of the code:
const [loading, setLoading] = useState(true);
const [journeys, setJourneys] = useState([]);
const [hasLaterDepartures, setHasLaterDepartures] = useState(true);
const getJourneys = async (date, journeys) => {
setLoading(true);
setHasLaterDepartures(true);
const selectedDateJourneys = await fetchJourney(date); // Fetch that returns 0-3 journeys
if (condition1) setHasLaterDepartures(false); // trying to update unmounted component
if (condition2) {
if (condition3) {
setJourneys(something1); // trying to update unmounted component
} else {
setJourneys(something2) // trying to update unmounted component
}
} else {
setJourneys(something3); // trying to update unmounted component
}
};
// useEffects for continous loading of journeys.
useEffect(() => {
if (!hasLaterDepartures) setLoading(false);
}, [hasLaterDepartures]);
useEffect(() => {
if (hasLaterDepartures && journeys.length > 0) {
const latestStart = ... // just a date
if (latestStart.addMinutes(5).isSameDay(latestStart)) {
getJourneys(latestStart.addMinutes(5), journeys);
} else {
setLoading(false);
}
}
}, [journeys]);
I can't use a variable like isMounted = true in the useEffect beacuse it would reach inside the if statement and reach a "setState" by the time i'm on another page.
Moving the entire call into a useEffect doesn't seem to work either. I am at a loss.
Create a variable called mounted with useRef, initialised as true. Then add an effect to set mounted.current to false when the component unmounts.
You can use mounted.current anywhere inside the component to see if it's mounted, and check that before setting any state.
useRef gives you a variable you can mutate but which doesn't cause a rerender.
When you use useEffect hook with action which can be done after component change you should also take care about clean effect when needed. Maybe example help you, also check this page.
useEffect(() => {
let isClosed = false
const fetchData = async () => {
const data = await response.json()
if ( !isClosed ) {
setState( data )
}
};
fetchData()
return () => {
isClosed = true
};
}, []);
In your use case, you probably want to create a Store that doesn't reload everytime you change route (client side).
Example of a store using useContext();
const MyStoreContext = createContext()
export function useMyStore() {
const context = useContext(MyStoreContext)
if (!context && typeof window !== 'undefined') {
throw new Error(`useMyStore must be used within a MyStoreContext`)
}
return context
}
export function MyStoreProvider(props) {
const [ myState, setMyState ] = useState()
//....whatever codes u doing with ur hook.
const exampleCustomFunction = () => {
return myState
}
const getAllRoutes = async (mydestination) => {
return await getAllMyRoutesFromApi(mydestination)
}
// you return all your "getter" and "setter" in value props so you can use them outside the store.
return <MyStoreContext.Provider value={{ myState, setMyState, exampleCustomFunction, getAllRoutes }}>{props.children}</MyStoreContext.Provider>
}
You will wrap the store around your entire App, e.g.
<MyStoreProvider>
<App />
</MyStoreProvider>
In your page where you want to use your hook, you can do
const { myState, setMyState, exampleCustomFunction, getAllRoutes } = useMyStore()
const onClick = async () => getAllRouters(mydestination)
Considering if you have client side routing (not server side), this doesn't get reloaded every time you change your route.

useSWR() and mutate() do not behave as expected when component is unmounted

I am having an issue with some unexpected behavior with regards to mutate(key). In my data fetching code, I have these hooks/functions:
const recentOptions = {
refreshInterval: 0,
revalidateOnFocus: false,
dedupingInterval: 180000000 //or 500
}
export const useRecent = () => {
const {data, error} = useSWR(
`${config.apiUrl}/GetRecentFoundations`,
getThenResolve,
recentOptions
);
return {
recent: data,
isLoading: !error && !data,
isError: error
};
}
export const setRecent = async(fid) => {
await postThenResolve(
`${config.apiUrl}/SetFoundationRecent`,
{fid});
//TODO: So the behavior I am seeing tends to indicate that
//mutate(key) sends out a message to swr to refetch, but doesn't
//necessarily cause the cache to be invalidated.
//This means that mutate will cause any CURRENTLY MOUNTED useSWR()
//to refetch and re-render, but ones that aren't mounted will
//return with stale data.
mutate(`${config.apiUrl}/GetRecentFoundations`);
}
I also have a component that fetches data with useRecent():
const FoundationRecents = props => {
const {recent, isLoading} = useRecent();
if(isLoading) return <div>...LOADING...</div>;
if(!recent) return null;
return <SimpleCard
titlebg={colors.blue}
titlefg={colors.white}
titlesz='small'
title='Recent'
contentbg={colors.white}
component={<FoundationRecentsView recent={recent}/>}
/>
}
I use this pattern in two places currently, one where the component is mounted when the setRecent() occurs. The other, where the component is unmounted. When the component is mounted, everything works fine. When mutate() is called, everything seems to refetch and rerender.
However, if setRecent() is called when the component is unmounted, and I later return to it, I get stale data, and no refetch.
I think I must be misunderstanding what mutate(key) does, or maybe I am unclear as to what the dedupingInterval is. I thought, that even with a high deduping interval, the mutate would cause the GetRecentFoundations cache to be invalid, and thus the next call to useSWR() would require a revalidate.

How to get data from server repeatedly in React + Redux application?

I am developing React + Redux single page application. I have a table with documents in page and I need to refresh data every 20 seconds. There are two functions in javascript: setTimeout and setInterval. I guess I can't use setInterval, because it just call function after some period of time. In my case I need to call function and wait for response (request processing in backend takes some time). So I used setTimeout and wrote this component (now it's simplified):
import {connect} from 'react-redux';
const { DATA_REFRESH_TIMEOUT, RETRY_REFRESH_TIMEOUT } = __ENVIRONMENT_CONFIG__;
#connect(
(state) => ({
documents: state.documents.documents,
loadingDocuments: state.documents.loading
}),
(dispatch) => bindActionCreators(
{
dispatchLoadDocuments: loadDocuments
},
dispatch
)
)
export default class Dashboard extends Component {
documentasTimeoutId;
constructor(props) {
super(props);
this.state = {
documentType: null
};
}
....
handleDocumentTypeChange = (event, documentType) => {
//If document type was changed I must to abort current timer
//and get documents with particular type immediately
this.setState({documentType: documentType});
this.clearTimeoutAndGetDocuments(documentType);
};
getDocuments = (documentType) => {
//Here I am checking for document loading phase
//If it is loading, we will wait and repeat loading after short time
const{ loadingDocuments } = this.props;
if(!loadingDocuments) {
this.props.dispatchLoadDocuments(documentType);
} else {
this.documentasTimeoutId = setTimeout(() => { this.getDocuments(documentType); }, RETRY_REFRESH_TIMEOUT);
}
};
clearTimeoutAndGetDocuments = (documentType) => {
//Abort delayed data getting and get data immediately
clearTimeout(this.documentasTimeoutId);
this.getDocuments(documentType);
};
componentDidMount(){
//Load documents on start up
this.props.dispatchLoadDocuments();
}
componentWillReceiveProps(newProps) {
//Here I trying to get event when documents loaded
let areDocumentsJustLoaded = this.props.loadingDocuments && !newProps.loadingDocuments;
if(areDocumentsJustLoaded) {
//If they loaded, I am setting timeout to refresh documents after some time
this.documentasTimeoutId = setTimeout(() => { this.getOutstandingFailures(this.state.search); }, DATA_REFRESH_TIMEOUT);
}
}
render() {
const {columns, documents} = this.props;
return (
//.....
<DataTable
columns={columns}
source={documents}
title="Documents"
name="documents"
emptyMessage="No data"/>
//....
);
}
}
As you can see I'm getting documents and loadingDocuments from reducer. I put documents to my DataTable, and by loadingDocuments changes I can define when data loading completed.
It's working, but I'am not sure for correct react and redux using (I am a newbie in React/Redux). Maybe there a better approach to do same actions? Maybe we can somehow create a separate component for this purpose and reuse it in other pages?

Categories

Resources