here is the context
I am invoking a function invoked(..) from another component. Inside invoked I setState but the state does not get set. It does not trigger the useEffect nor does it take the updated value in the useInterval.
Below is the code.
Component 1 = exampleComp.ts
const exampleComp = ({initValue: ExampleProps}) {
const [params, setParams] = useState<{field1: null | string}> ({field1: null});
const invoked = (x: string) => {
console.log("inside invoked"); // this gets printed
setParams({"field1": x});
};
useEffect(() => {
console.log(params); // does not come here :(
}, [params]);
const fetchData = {...};
useInterval(
() => {
if (!params.field1 || fetching) return;
console.log("in interval", params); // does not come here too :(
fetchData(params.field1);
},
params && !fetching ? 5000: null,
);
return {invoked, ...}
}
Component 2:
const newComp = ({initValue: ExampleProps}) {
const {invoked, ..} = useExampleComp({...}); //Example comp is the above component.
useEffect(() => {
invoked(x);
}, []);
}
Any help will be appreciated! thanks.
Turns out that the component was unmounting too early. This happens when the scope of the component is too narrow.
If you have a similar problem, try logging the component to see if this is indeed happening.
Related
I have a component that is using Context like so:
export const MyContext = React.createContext({});
export const MyComponent = React.memo(({children}) => {
const [myVar, setMyVar] = React.useState({});
const myFunction = () => {
console.log(myVar);
setMyVar({...myVar, {extraData: 'hi there'}});
};
const updateMyVar = React.useCallback((data) => {
setMyVar(data);
}, []);
const doSomethingElse = React.useCallback(() => {
myFunction();
}, []);
return (
<MyContext.Provider value={{myVar, updateMyVar, doSomethingElse}}>{children}</MyContext.Provider>
);
});
And then using it in a component:
const {myVar, updateMyVar, doSomethingElse} = React.useContext(FormContext);
The child component can seem to update myVar just fine, but inside of the MyComponent component, when I try to read myVar in something like the myFunction function just returns whatever the state was initially initialized with. It never updates to show the data that is there currently. The funny thing is that the child component always reads the correct data.
Since there is no code sandbox linked to the question, I can only take a guess. I think your myFunction will be stale if it is called within doSomethingElse as the dependencies are stale.
Can you try this ?
const doSomethingElse = React.useCallback(() => {
myFunction();
}, [myFunction]);
If you don't want to do that, another way would be to do this. Here you are accessing the current value of the state using the callback variant of state setter function. Let me know if this helps.
const myFunction = () => {
console.log(myVar);
setMyVar(currentMyVar => {... currentMyVar, {extraData: 'hi there'}});
};
I want to use UseState hook for updating data in my Table component. The data to be used in the Table component is fetched by another function which is imported paginationForDataAdded.
Its look like stackoverflow due to re-rendering.
setAllData(searchResults); will re-render the component and again make api call and repated.
right way to call API.
const [allData, setAllData] = useState([]);
useEffect(function () {
const {
searchResults,
furnishedData,
entitledData
} = paginationForDataAdded({
searchFunction: search,
collectionsData: collections
});
setAllData(searchResults);
});
Assuming paginationForDataAdded is a function that returns a Promise which resolves with an object that looks like the following:
{
searchResults: { resultarray: [...] },
furnishedData: [...],
entitledData: [...]
}
You should do the following your in component:
function App(props) {
const [allData, setAllData] = React.useState([]);
// ...
React.useEffect(() => {
paginationForDataAdded({
searchFunction: search,
collectionsData: collections,
})
.then(
({ searchResults, furnishedData, entitledData }) => {
const nextAllData = searchResults.resultarray || [];
setAllData(nextAllData);
}
)
.catch(/* handle errors appropriately */);
// an empty dependency array so that this hooks runs
// only once when the component renders for the first time
}, [])
return (
<Table
id="pop-table"
data={allData}
tableColumns={[...]}
/>
);
}
However, if paginationForDataAdded is not an asynchronous call, then you should do the following:
function App(props) {
const [allData, setAllData] = React.useState([]);
// ...
React.useEffect(() => {
const {
searchResults,
furnishedData,
entitledData,
} = paginationForDataAdded({
searchFunction: search,
collectionsData: collections
});
const nextAllData = searchResults.resultarray || [];
setAllData(nextAllData)
// an empty dependency array so that this hooks runs
// only once when the component renders for the first time
}, [])
return (
<Table
id="pop-table"
data={allData}
tableColumns={[...]}
/>
);
}
Hope this helps.
I have a mystery. Consider the following custom React hook that fetches data by time period and stores the results in a Map:
export function useDataByPeriod(dateRanges: PeriodFilter[]) {
const isMounted = useMountedState();
const [data, setData] = useState(
new Map(
dateRanges.map(dateRange => [
dateRange,
makeAsyncIsLoading({ isLoading: false }) as AsyncState<MyData[]>
])
)
);
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(new Map(data.set(period, asyncState)));
}
},
[setData, data, isMounted]
);
useEffect(() => {
if (dateRanges.length === 0) {
return;
}
const loadData = () => {
const client = makeClient();
dateRanges.map(dateRange => {
updateData(dateRange, makeAsyncIsLoading({ isLoading: true }));
return client
.getData(dateRange.dateFrom, dateRange.dateTo)
.then(periodData => {
updateData(dateRange, makeAsyncData(periodData));
})
.catch(error => {
const errorString = `Problem fetching ${dateRange.displayPeriod} (${dateRange.dateFrom} - ${dateRange.dateTo})`;
console.error(errorString, error);
updateData(dateRange, makeAsyncError(errorString));
});
});
};
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dateRanges /*, updateData - for some reason when included this triggers infinite renders */]);
return data;
}
The useEffect is being repeatedly triggered when updateData is added as a dependency. If I exclude it as a dependency then everything works / behaves as expected but eslint complains I'm violating react-hooks/exhaustive-deps.
Given updateData has been useCallback-ed I'm at a loss to understand why it should repeatedly trigger renders. Can anyone shed any light please?
The problem lies in the useCallback/useEffect used in combination. One has to be careful with dependency arrays in both useCallback and useEffect, as the change in the useCallback dependency array will trigger the useEffect to run.
The “data” variable is used inside useCallback dependency array, and when the setData is called react will rerun function component with new value for data variable and that triggers a chain of calls.
Call stack would look something like this:
useEffect run
updateData called
setState called
component re-renders with new state data
new value for data triggers useCallback
updateData changed
triggers useEffect again
To solve the problem you would need to remove the “data” variable from the useCallback dependency array. I find it to be a good practice to not include a component state in the dependency arrays whenever possible.
If you need to change component state from the useEffect or useCallback and the new state is a function of the previous state, you can pass the function that receives a current state as parameter and returns a new state.
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(existingData => new Map(existingData.set(period, asyncState)));
}
},
[setData, isMounted]
);
In your example you need the current state only to calculate next state so that should work.
This is what I now have based on #jure's comment above:
I think the problem is that the "data" variable is included in the dependency array of useCallback. Every time you setData, the data variable is changed that triggers useCallback to provide new updateData and that triggers useEffect. Try to implement updateData without a dependecy on the data variable. you can do something like setData(d=>new Map(d.set(period, asyncState)) to avoid passing "data" variable to useCallback
I adjusted my code in the manners suggested and it worked. Thanks!
export function useDataByPeriod(dateRanges: PeriodFilter[]) {
const isMounted = useMountedState();
const [data, setData] = useState(
new Map(
dateRanges.map(dateRange => [
dateRange,
makeAsyncIsLoading({ isLoading: false }) as AsyncState<MyData[]>
])
)
);
const updateData = useCallback(
(period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
if (isSafeToSetData) {
setData(existingData => new Map(existingData.set(period, asyncState)));
}
},
[setData, isMounted]
);
useEffect(() => {
if (dateRanges.length === 0) {
return;
}
const loadData = () => {
const client = makeClient();
dateRanges.map(dateRange => {
updateData(dateRange, makeAsyncIsLoading({ isLoading: true }));
return client
.getData(dateRange.dateFrom, dateRange.dateTo)
.then(traffic => {
updateData(dateRange, makeAsyncData(traffic));
})
.catch(error => {
const errorString = `Problem fetching ${dateRange.displayPeriod} (${dateRange.dateFrom} - ${dateRange.dateTo})`;
console.error(errorString, error);
updateData(dateRange, makeAsyncError(errorString));
});
});
};
loadData();
}, [dateRanges , updateData]);
return data;
}
With old class based React.js code, I can do like this, to remember that a component has been unmounted:
componentWillUnmount: function() {
this.isGone = true;
},
loadUsers: function() {
Server.loadUsers(..., (response) => {
if (this.isGone) return;
...
});
}
How can one do the same thing, with hooks based components?
Here's a sample hooks function component, where I'm unsure about how to remember that it's been unmounted, so I can return before calling any setSomeState:
export const GroupMembers = React.createFactory(function(props) {
const [membersOrNull, setMembers] = React.useState(null);
let isGone = false; // ? (se [222] below
React.useEffect(() => {
Server.listGroupMembers(someGroupId, (response) => {
if (/* is unmounted ?? */) return;
setMembers(response.members);
});
return () => { /* how remember is unmounted?
would isGone = true; work? */ };
}, []);
...
return ..., Button({ title: "Remove all members", onClick: () => {
Server.removeAllMembers(someGroupId, () => {
if (/* is unmounted ?? */) return;
setMembers([]);
});
}});
I suppose I cannot use const [isGone, setGone] = useState(false) because I shouldn't try to access the state (read isGone) after has been unmounted. And if I [222] add a local let isGone = false inside the function, it seems to me that various callbacks created inside the function, will refer to different "instances" of this local variable, depending on in which different GroupMembers(..) invokation the different callbacks were created? or am I mistaken and this works? — Maybe I could create an outer wrapper function with a local let isGone = false, however, this adds another wrapping function and indentation :- /
Since you are using the isGone flag outside of the useEffect method too, you can make use of useRef to store variables like
export const GroupMembers = React.createFactory(function(props) {
const [membersOrNull, setMembers] = React.useState(null);
const isGone = useRef(false); // ? (se [222] below
React.useEffect(() => {
Server.listGroupMembers(someGroupId, (response) => {
if (isGone.current) return;
setMembers(response.members);
});
return () => {
isGone.current = true;
};
}, []);
...
return ..., Button({ title: "Remove all members", onClick: () => {
Server.removeAllMembers(someGroupId, () => {
if (isGone.current) return;
setMembers([]);
});
}});
PS: A better way to handle such things is to cancel the request when
you are leaving the page instead of waiting for the response only to
neglect it.
just use local variable accessed through closure
React.useEffect(() => {
let isActual = true;
Server.listGroupMembers(someGroupId, (response) => {
if (!isActual) return;
setMembers(response.members);
});
return () => {isActual = false;};
}, []);
in this case flag would be updated on unmounting only. but in general case(with some unempty dependencies for useEffect) it will use as well. So in case of sequential renderings you could be sure you never process older request.
PS most universal way is cancelling request when it's possible.
I'm trying to update the state 'allMovieList' to render a list of movies.
The idea was to set a dynamic URL in my GET request, by updating the 'page' state when clicking on the button. Unfortunately this doesn't trigger a re-rendering since the request is made only one time in componentDidMount() method.
state = {
allMovieList: [],
page: 1
}
componentDidMount() {
this.changePage();
}
async changePage() {
try {
const response = await axios.get(`https://api.themoviedb.org/4/discover/movie?api_key=${apiKey}&page=${this.state.page}`);
const movieList = response.data.results.slice(0, 10);
const movies = movieList.map(movie => movie);
const totalPages = response.data.total_pages;
this.setState({
...this.state,
allMovieList: movies,
})
} catch (event) {
console.log(event);
}
}
onNextPage = () => {
this.setState((previousState, currentProps) => {
return { page: previousState.page + 1 };
});
}
render() {
return (
<div className='MovieList'>
...
<button onClick={this.onNextPage}></button>
</div>
);
}
To solve this, I tried to call the changePage() function inside my onNextPage() function.
onNextPage = () => {
this.setState((previousState, currentProps) => {
return { page: previousState.page + 1 };
});
this.changePage();
}
This partially solved this issue but for some reason the next page is actually only rendered on second click! I also noticed my component is being re-rendered twice on every click.
I also tried to call the changePage() function inside componentDidUpdate(), this solved the issue but now my app is constantly re-rendering which causes huge performance issues.
Can anyone help me with this? It would be greatly appreciated!
Option 1) Use setState's callback function:
this.setState((previousState, currentProps) => {
return { page: previousState.page + 1 };
}, this.changePage); // state will be updated when this gets called
Option 2) Use function arguments:
async changePage(page) {
try {
const response = await
axios.get(`https://api.themoviedb.org/4/discover/movie?page=${page}`);
...
}
}
...
onNextPage = () => {
this.setState((previousState, currentProps) => {
const page = previousState.page + 1
this.changePage(page);
return { page };
});
}