Can't setState inside useEffect with "useIsFocused()" from react-navigation as dependency - javascript

I use the useIsFocused() function from react-navigation/native as a dependency of useEffect to listen to screen focus, so when I navigate back it enters the if with the isFocused and the myRoute.params.isChecked to execute the function.
But it's not updating the route state on setRoute(newRoute); and setIsLoading(false). The setIsLoading(true) inside the function is called but the one inside the useEffect route isn't because it never call the route update. The screen keeps loading infinitely because of that.
Why it not updates the isLoading and route states?
import React, { useEffect, useState } from 'react';
import { useFocusEffect, useIsFocused } from '#react-navigation/native';
const MyRoute = ({ navigation, route: myRoute }) => {
const [route, setRoute] = useState({});
const [isLoading, setIsLoading] = useState(true);
const isFocused = useIsFocused();
/** ... */
useEffect(() => {
console.log('route call')
if (Object.keys(route).length) {
setIsLoading(false);
}
}, [route]);
useEffect(() => {
console.log(isLoading);
}, [isLoading]);
useEffect(() => {
// trigger route refresh on navigate when checkin/checkout
if (isFocused && myRoute.params?.isChecked) {
const updateRouteWithCheckInOut = () => {
setIsLoading(true);
const newRoute = route;
for (let i = 0; i < newRoute.visitas.length; i++) {
const { sub_id, check_in_out } = myRoute.params;
if (newRoute.visitas[i].sub_id === sub_id) {
newRoute.visitas[i].realizada = true;
newRoute.visitas[i].check_in_out = check_in_out;
}
}
debugger;
setRoute(newRoute);
};
updateRouteWithCheckInOut();
}
}, [isFocused]);
/** ... */
}

you are updating some properties of visitas array in your for loop.
the thing is you are just mutating some propery and update your route with the same object. if you check route === newRoute after the for loop you see that it evaluates to true. because they are the same object. and when you update your route with the same object react doesn't rerender your component.
you should update your route like this:
setRoute(...newRoute)

Related

Why is my function not dispatching in react component?

why is fetchReviews not fetching?
Originally didn't use fetchData in use effect.
Ive tried using useDispatch.
BusinessId is being passed into the star component.
no errors in console.
please let me know if theres other files you need to see.
thank you!
star component:
import React, { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import {AiFillStar } from "react-icons/ai";
import { fetchReviews } from '../../actions/review_actions';
function Star(props) {
const [rating, setRating] = useState(null);
// const [reviews, setReview] = useState(props.reviews)
// const dispatch = useDispatch();
useEffect(() => {
const fetchData = async () => {
await fetchReviews(props.businessId)
};
fetchData();
console.log(props);
// getAverageRating();
});
const getAverageRating = () => {
let totalStars = 0;
props.reviews.forEach(review => {totalStars += review.rating});
let averageStars = Math.ceil(totalStars / props.reviews.length);
setRating(averageStars);
}
return (
<div className='star-rating-container'>
{Array(5).fill().map((_, i) => {
const ratingValue = i + 1;
return (
<div className='each-star' key={ratingValue}>
<AiFillStar
className='star'
color={ratingValue <= rating ? '#D32322' : '#E4E5E9'}
size={24} />
</div>
)
})}
</div>
);
};
export default Star;
star_container:
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import Star from "./star";
import { fetchReviews } from "../../actions/review_actions";
const mSTP = state => {
return {
reviews: Object.values(state.entities.reviews)
};
}
const mDTP = dispatch => {
return {
fetchReviews: businessId => dispatch(fetchReviews(businessId))
};
};
export default connect(mSTP, mDTP)(Star);
console image
why is fetchReviews not fetching? Originally didn't use fetchData in use effect. Ive tried using useDispatch. BusinessId is being passed into the star component. no errors in console.
edit!***
made some changes and added useDispatch. now it wont stop running. its constantly fetching.
function Star(props) {
const [rating, setRating] = useState(null);
const [reviews, setReview] = useState(null)
const dispatch = useDispatch();
useEffect(() => {
const fetchData = async () => {
const data = await dispatch(fetchReviews(props.businessId))
setReview(data);
};
fetchData();
// console.log(props);
// getAverageRating();
}), [];
ended up just calling using the ajax call in the useEffect.
useEffect(() => {
const fetchReviews = (businessId) =>
$.ajax({
method: "GET",
url: `/api/businesses/${businessId}/reviews`,
});
fetchReviews(props.businessId).then((reviews) => getAverageRating(reviews));
}), [];
if anyone knows how i can clean up and use the dispatch lmk.
ty all.
useEffect(() => {
const fetchData = async () => {
const data = await dispatch(fetchReviews(props.businessId))
setReview(data);
};
fetchData();
// console.log(props);
// getAverageRating();
}), [];
dependency array is outside the useEffect. Since useEffect has no dependency option passed, function inside useEffect will run in every render and in each render you keep dispatching action which changes the store which rerenders the component since it rerenders code inside useEffect runs
// pass the dependency array in correct place
useEffect(() => {
const fetchData = async () => {
const data = await dispatch(fetchReviews(props.businessId))
setReview(data);
};
fetchData();
// console.log(props);
// getAverageRating();
},[]), ;
Passing empty array [] means, code inside useEffect will run only once before your component mounted

How to fetch API as soon as page is loaded in React?

Whenever I visit a page it should automatically fetch the API
import React from 'react'
const Component = () => {
fetch("api url").then((res) => console.log(res))
return (
<div>comp</div>
)
}
export default Component
It is very simple using react hook use effect please learn basics of useffect hook on react docs or any youtube tutorial and as for the answer
import React, { useEffect } from 'react'
const comp = () => {
useEffect(() => {
fetch("api url").then((res)=>console.log(res))
}, [])
return (
<div>comp</div>
)
}
export default comp
here empty dependency means every time page loads only once
use the useEffect for this.
The useEffect method will execute the passed callback on the mount of the component and on every time one of the dependency array parameters is changed. therefore:
const Comp = () => {
useEffect(() => {
fetch("api url").then((res)=>console.log(res))
}, []);
return (
<div>comp</div>
)
}
Will make the callback to fire only once (because the empty dependency array) on the component mount.
You should use the useEffect Hook in your principal component like app.js
import React, {useEffect} from 'react'
useEffect(() => {
fetch("api url").then((res)=>console.log(res))
}, []);
Be careful, this manipulation can consume a lot of resources (a lot of data to fetch etc.)
Thery
import React, { useState, useEffect } from 'react'
const Comp = () => {
const [ data, setData ] = useState([]);
const getData = async () => {
const res = await fetch("api url");
const data = await res.json();
setData(data)
}
useEffect(()=>{ getData() },[]);
return (
<>
<div>comp</div>
// dispaly your data here from data state
</>
)
}
export default Comp;
Fetch and use data with useState
const initialValue = {};
const comp = () => {
const [data, setData] = useState(initialValue);
useEffect(() => {
let ignore = false;
const fetchData = async () => {
const res = fetch("api url");
if (ignore) { return; }
setData(res.json())
return () => {
ignore = true;
}
}
, [])
return (
<div>comp {data.prop}</div>
)
}
More on working with state
More about useEffect life cycle
Hope it helps
You don't need to use the API function like this, it will be called continuously, you need to use useEffect hook, when your component reloads useEffect will be called, and you can learn about the useEffect dependency here,
import React, { useEffect, useState } from 'react'
const comp = () => {
const [data, setData] = useState([]);
useEffect(() => {
fetch("api url").then((res)=> {
console.log(res)
setData(res)
} )
}, [])
return (
// use data state to show the data here
<div>comp</div>
)
}
export default comp;

How to stop initial render for useEffect hook

Earlier I had a Class component, so I didn't face any issues while using lifecycle methods, but after converting to useEffect hooks, I am facing the initial render issue which I don't want to happen.
Class
componentDidMount() {
this.setState({
patchVal:this.props.patchTaskVal,
startTime:this.props.patchStartTime,
setEndTime:this.props.patchEndTime
})
}
componentDidUpdate(prevProps) {
if (prevProps.patchTaskVal !== this.props.patchTaskVal) {
this.callValidation()
}
if (prevProps.closeTask !== this.props.closeTask) {
this.setState({
showValue:false,
progressValue:[],
startTime:new Date(),
setEndTime:""
})
}
if (prevProps.patchStartTime !== this.props.patchStartTime || prevProps.endTime !== this.props.endTime && this.props.endTime !== "") {
this.setState({
startTime:this.props.patchStartTime,
setEndTime:parseInt(this.props.endTime)
})
}
}
Functional
const [patchTaskVal, setPatchTaskVal]=useState(/*initial value */)
const [startTime, setStartTime]=useState()
const [endTime, setEndTime] = useState()
**// I want only this useEffect to run on the initial render**
useEffect(() => {
setPatchTaskVal(props.patchTaskVal)
...//set other states
}, [])
useEffect(() => {
callValidation()
}, [props.patchTaskVal])
useEffect(() => {
//setShowValue...
}, [props.closeTask])
useEffect(() => {
if (props.endTime != "") {
// set states...
}
}, [props.patchStartTime,props.endTime])
Here I am facing an issue where all the useEffects are running on the initial render, Please suggest a solution for this so that only the first useEffect will run on the initial render and all other useEffects will run according to its dependency prop values.
You basically need a ref which will tell you whether this is the first render on not. Refs values persist over rerenders. You can start with a truthy value and toggle it to false after the first render (using a useEffect with an empty array[]). Based on that you can run your desired code.
You can also put the whole thing in a custom hook:
import { useEffect, useRef } from "react";
const useOnUpdate = (callback, deps) => {
const isFirst = useRef(true);
useEffect(() => {
if (!isFirst.current) {
callback();
}
}, deps);
useEffect(() => {
isFirst.current = false;
}, []);
};
export default useOnUpdate;
You can call this hook in your component like :
useOnUpdate(() => {
console.log(prop);
}, [prop]);
In the hook:
After the initial render, both useEffects run. But when the first effect runs the value of the isFirst.current is true. So the callback is not called. The second useEffect also runs and sets isFirst.current to false.
Now in subsequent renders only the first useEffect run (when dependencies change), and isFirst.current is false now so callback is executed.
The order of the two useEffects is very important here. Otherwise, in the useEffect with deps, isFirst.current will be true even after the first render.
Link
If you compare the functional and the class component you can notice that there is one part missing - previous props.
Functional component does not have previous props in scope, but you can save them yourself with a small trick: save them to reference so it will not impact you render cycle.
Since now you have the previous props and the current props you can apply the same logic you did for class component.
import React, { useRef, useEffect, useState } from "react";
default function App() {
const [input, setInput] = useState("");
const [commitInput, setCommitInput] = useState("");
return (
<>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button onClick={() => setCommitInput(input)}>apply</button>
<Child test={commitInput} />
</>
);
}
function Child(props) {
const prev = useRef(props.test);
useEffect(() => {
if (prev.current !== props.test) {
alert("only when changes");
}
}, [props.test]);
return <div>{props.test}</div>;
}
try this...
let init = true;
useEffect( ()=>{
if(init) {
setPatchTaskVal(props.patchTaskVal)
init = false;
...//set other states}
}, [])
useEffect( ()=> {
!init && callValidation()
},[props.patchTaskVal])
useEffect( ()=>{
//!init && setShowValue...
},[props.closeTask])
useEffect( ()=>{
if(props.endTime!="" && !init){
// set states...
}
},[props.patchStartTime,props.endTime])
Hope my understanding is right about your question.
Why not just add a if statement to check the state is not undefined or default value
useEffect( ()=> {
if (props.patchTaskVal) {
callValidation()
}
},[props.patchTaskVal])
useEffect( ()=>{
if (props.closeTask) {
//setShowValue...
}
},[props.closeTask])
useEffect( ()=>{
if(props.patchStartTime){
// set states...
}
if(props.endTime){
// set states...
}
},[props.patchStartTime,props.endTime]
And according your class component,
this.setState({
patchVal:this.props.patchTaskVal,
startTime:this.props.patchStartTime,
setEndTime:this.props.patchEndTime
})
The function component should map props to component's state. Like this
const [patchTaskVal, setPatchTaskVal]=useState(props.patchTaskVal)
const [startTime, setStartTime]=useState(props.patchStartTime)
const [endTime, setEndTime] = useState(props.patchEndTime)

calling useQuery conditionally returns wrong output only for the first time

Please help me understand why am I getting no items on first request.
I have this component ExportData that calls Context only when prompted, dialogConfirmation is linked to a button, snippet:
ExportData:
import useItem from '../../context/ItemsContext'
const ExportData = () => {
const { items, getAll } = useItem()
const dialogConfirmation = confirm => {
if (confirm) {
getAll()
}
}
}
ItemsContext:
import React, {
useState,
useReducer,
createContext,
useContext,
useEffect,
} from 'react'
import { useQuery } from '#apollo/client'
import { CURRENT_MONTH, ALL_ITEMS } from '../graphql/queries'
import itemReducer from '../reducers/itemReducer'
export const ItemProvider = ({ children }) => {
let items = []
const [all, setAll] = useState(false)
const selectedMonth = dayjs(new Date()).format('YYYY-MM')
const [state, dispatch] = useReducer(itemReducer, [])
const result = useQuery(all ? ALL_ITEMS : CURRENT_MONTH, {
variables: all ? null : { selectedMonth },
})
if (result.data && result.data.getCurrentMonth) {
items = [...result.data.getCurrentMonth]
}
if (result.data && result.data.getItems) {
items = [...result.data.getItems]
}
useEffect(() => {
if (result.data && result.data.getCurrentMonth) {
dispatch({
type: 'ALL',
items: result.data.getCurrentMonth,
})
}
if (result.data && result.data.getItems) {
dispatch({
type: 'ALL',
items: result.data.getItems,
})
}
}, [result.data])
const getAll = () => {
setAll(true)
}
const value = {
items: state.items,
getAll,
}
return <ItemContext.Provider value={value}>{children}</ItemContext.Provider>
}
Reducer:
const itemReducer = (state, action) => {
switch (action.type) {
case 'ALL':
return {
...state,
items: action.items,
}
default:
return state
}
}
Query CURRENT_MONTH returns only items from current month - that should be the default one.
Query ALL_ITEMS returns all items in database.
What happens currently, when I use the getAll function in ExportData, on the first try, it returns only the items from current month, however on the second try, it returns correct output(all data).
When debugging itemReducer, all items are in the action.items however the output within the component gets only the old data...On the second try however, it works fine.
What am I doing wrong here?
What is happening here:
Component is initially rendered (no data yet)
useEffects are fired: (inside component) does nothing, (inside useQuery) fires request to gql api
after some time when request is fulfilled state inside useQuery is set with data
Becouse of that component gets rerender (data is present but not yet used in state.items becouse dispatch was not fired yet)
useEffects are fired and this time dispatch is called.
Component rerenders again because dispatch changed useReducer state and this time state has items inside
In this process react renders 3 times. If you place console.log(state.items) inside component it will look like this:
[] // no data yet
[] // data is present but not yet used by `state`
[<your data>] // data is present and used by state
You can solve that by removing useEffect and useReducer:
Component is initially rendered (no data yet)
useEffects are fired: (inside component) does nothing, (inside useQuery) fires request to gql api
after some time when request is fulfilled state inside useQuery is set with data
Becouse of that component gets rerender (data is passed directly to value)
In this process react renders 2 times. If you place console.log(items) inside component it will look like this:
[] // no data yet
[<your data>] // data is present and used
import React, {
useState,
createContext,
useContext,
} from 'react'
import { useQuery } from '#apollo/client'
import { CURRENT_MONTH, ALL_ITEMS } from '../graphql/queries'
export const ItemProvider = ({ children }) => {
const [all, setAll] = useState(false)
const selectedMonth = dayjs(new Date()).format('YYYY-MM')
const result = useQuery(all ? ALL_ITEMS : CURRENT_MONTH, {
variables: all ? null : { selectedMonth },
})
const getAll = () => {
setAll(true)
}
const items = result?.data.getCurrentMonth || result?.data.getItems || []
const value = {
items,
getAll,
}
return <ItemContext.Provider value={value}>{children}</ItemContext.Provider>
}

How to re-render a custom hook after initial render

I have custom hook named useIsUserSubscribed that checks to see a specific user is subscribed. It returns true if the user is subscribed and false if the user is not subscribed...
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import { checkSubscription } from "../services";
// this hook checks if the current user is subscribed to a particular user(publisherId)
function useIsUserSubscribed(publisherId) {
const [userIsSubscribed, setUserIsSubscribed] = useState(null);
const currentUserId = useSelector((state) => state.auth.user?.id);
useEffect(() => {
if (!currentUserId || !publisherId) return;
async function fetchCheckSubscriptionData() {
try {
const res = await checkSubscription(publisherId);
setUserIsSubscribed(true);
} catch (err) {
setUserIsSubscribed(false);
}
}
fetchCheckSubscriptionData();
}, [publisherId, currentUserId]);
return userIsSubscribed;
}
export default useIsUserSubscribed;
...I have a button using this hook that renders text conditionally based on the boolean returned from useIsUserSubscribed...
import React, { useEffect, useState } from "react";
import { add, remove } from "../../services";
import useIsUserSubscribed from "../../hooks/useIsUserSubscribed";
const SubscribeUnsubscribeBtn = ({profilePageUserId}) => {
const userIsSubscribed = useIsUserSubscribed(profilePageUserId);
const onClick = async () => {
if (userIsSubscribed) {
// this is an API Call to the backend
await removeSubscription(profilePageUserId);
} else {
// this is an API Call to the backend
await addSubscription(profilePageUserId);
}
// HOW CAN I RERENDER THE HOOK HERE!!!!?
}
return (
<button type="button" className="sub-edit-unsub-btn bsc-button" onClick={onClick}>
{userIsSubscribed ? 'Subscribed' : 'Unsubscribed'}
</button>
);
}
After onClick I would like to rerender my the useIsUserSubscribed hook So that my button text toggles. Can this be done?
you can not use useEffect in your hook for that purpose try this :
hook :
function useIsUserSubscribed() {
const currentUserId = useSelector((state) => state.auth.user?.id);
const checkUser = useCallback(async (publisherId, setUserIsSubscribed) => {
if (!currentUserId || !publisherId) return;
try {
const res = await checkSubscription(publisherId);
setUserIsSubscribed(true);
} catch (err) {
setUserIsSubscribed(false);
}
}, [currentUserId]);
return {checkUser};
}
export default useIsUserSubscribed;
component :
const SubscribeUnsubscribeBtn = ({profilePageUserId}) => {
const [userIsSubscribed,setUserIsSubscribed]=useState(false);
const { checkUser } = useIsUserSubscribed();
useEffect(()=>{
checkUser(profilePageUserId,setUserIsSubscribed)
},[checkUser,profilePageUserId]);
const onClick = async () => {
if (userIsSubscribed) {
// this is an API Call to the backend
await removeSubscription(profilePageUserId);
} else {
// this is an API Call to the backend
await addSubscription(profilePageUserId);
}
// HOW CAN I RERENDER THE HOOK HERE!!!!?
checkUser(profilePageUserId,setUserIsSubscribed)
}
return (
<button type="button" className="sub-edit-unsub-btn bsc-button" onClick={onClick}>
{userIsSubscribed ? 'Subscribed' : 'Unsubscribed'}
</button>
);
}
you can also add some loading state in your hook and return them too so you can check if process is already done or not
Add a dependece on useIsUserSubscribed's useEffect.
hook :
function useIsUserSubscribed(publisherId) {
const [userIsSubscribed, setUserIsSubscribed] = useState(null);
const currentUserId = useSelector((state) => state.auth.user?.id);
// add refresh dependece
const refresh = useSelector((state) => state.auth.refresh);
useEffect(() => {
...
}, [publisherId, currentUserId, refresh]);
...
}
component :
const onClick = async () => {
...
// HOW CAN I RERENDER THE HOOK HERE!!!!?
// when click, you can dispatch a refresh flag.
dispatch(refreshSubState([]))
}
Expose forceUpdate metheod.
hook :
function useIsUserSubscribed(publisherId) {
const [update, setUpdate] = useState({});
const forceUpdate = () => {
setUpdate({});
}
return {userIsSubscribed, forceUpdate};
}
component :
const {userIsSubscribed, forceUpdate} = useIsUserSubscribed(profilePageUserId);
const onClick = async () => {
...
forceUpdate();
}
Here is another solution by user #bitspook
SubscribeUnsubscribeBtn has a dependency on useIsUserSubscribed, but useIsUserSubscribed don't depend on anything from SubscribeUnsubscribeBtn.
Instead, useIsUserSubscribed is keeping a local state. You have a couple of choices here:
Move the state regarding whetehr user is subscribed or not one level up, since you are using Redux, perhaps in Redux.
Communicate to useIsUserSubscribed that you need to change its internal state.
For 1)
const [userIsSubscribed, setUserIsSubscribed] = useState(null);
move this state to Redux store and use it with useSelector.
For 2), return an array of value and callback from the hook, instead of just the value. It will allow you to communicate from component back into the hook.
In useIsUserSubscribed,
return [userIsSubscribed, setUserIsSubscribed];
Then in onClick, you can call setUserIsSubscribed(false), changing the hook's internal state, and re-rendering your component.

Categories