I have a method which would use the value from useSelector and another dispatch which would update my value from the useSelector, however, it seems the value does not get updated after dispatch, for example
const userProfile = (props) => {
const hasValidationError = useSelector(state => {
state.hasValidationError;
}
const dispatch = useDispatch():
const updateProfile = async (userId) => {
dispatch(startValidation()); // <-- this would change the hasValidationError in state
if (hasValidationError) {
console.log('should not update user');
await updateUser(userId);
dispatch(showSuccessMsg());
} else {
conosole.log('can update user');
}
}
}
The hasValidationError would always be false, even if the value did changed from state, how could I get the updated value immediately after dispatch(startValidation()) ?
I also tried something different, like creating a local state value to monitor my global state by using useState() and useEffect()
const [canUpdateUser, setCanUpdateUser] = useState(false);
useEffect(() => {
console.log('useEffect hasValidationError :>> ', hasValidationError);
setCanUpdateUser(!hasValidationError);
}, [hasValidationError]);
Then use canUpdateUser as my conditional flag in updateProfile (if (canUpdateUser)), however, this seems to work only the first time when validation triggers, but after that, the canUpdateUser value is always the old value from my updateProfile again...
How could I resolve this? Is there any way to ensure getting updated value from global state after certain dispatch fires?
Could you maybe try from a slightly different approach (combining both) since it seems you want to be listening on changes of hasValidationError, using a useEffect with a dependency on that variable can maybe resolve your issue.
const userProfile = (props) => {
const { hasValidationError } = useSelector(state => state);
const dispatch = useDispatch():
const [userId, setUserId] = useState();
const updateProfile = async (userId) => {
dispatch(startValidation());
setUserId(userId);
};
useEffect(() => {
if (hasValidationError) {
console.log('should not update user');
await updateUser(userId);
dispatch(showSuccessMsg());
} else {
conosole.log('can update user');
}
}, [hasValidationError]);
}
Related
Hi i have a problem with my react app, I'm using useContext to parse my user to a voting site, but I have a small problem, my app is dobble loading, then my context turns to undefined, i can see the user at the first console.log but i cannot access it.
const { user, setUser } = useContext(UserContext)
const [medarbejdere, setMedarbejdere] = useState({})
const userLogIn = document.getElementById('id').value;
const user = medarbejdere?.filter(userid => userid.id === parseInt(userLogIn)).map(currentUser => console.log(currentUser))
setUser(user);
navigate("/votingsite")```
console.log is a void return, you are mapping a currentUser value to undefined and then updating likely the user state to an array of undefined values. Don't use Array.prototype.map for side-effects like console logging a value.
const { user, setUser } = useContext(UserContext);
const [medarbejdere, setMedarbejdere] = useState({});
const userLogIn = document.getElementById('id').value;
const user = medarbejdere?.filter(userid => userid.id === parseInt(userLogIn));
// console log the entire `user` array
console.log(user);
// or use `Array.protype.forEach` to issue side-effect on each element
user.forEach(currentUser => console.log(currentUser));
You also may be seeing the setUser and navigate calls as unintentional side-effects because they are in the component body. These should be in a callback or useEffect hook.
useEffect(() => {
setUser(user);
navigate("/votingsite");
}, []);
I am using React-native and in it, I have a custom Hook called useUser that gets the user's information from AWS Amplify using the Auth.getUserInfro method, and then gets part of the returned object and sets a state variable with it. I also have another Hook called useData hook that fetches some data based on the userId and sets it to a state variable.
useUser custom-Hook:
import React, { useState, useEffect } from "react";
import { Auth } from "aws-amplify";
const getUserInfo = async () => {
try {
const userInfo = await Auth.currentUserInfo();
const userId = userInfo?.attributes?.sub;
return userId;
} catch (e) {
console.log("Failed to get the AuthUserId", e);
}
};
const useUserId = () => {
const [id, setId] = useState("");
useEffect(() => {
getUserInfo().then((userId) => {
setId(userId);
});
}, []);
return id;
};
export default useUserId;
import useUserId from "./UseUserId";
// ...rest of the necessary imports
const fetchData = async (userId) = > { // code to fetch data from GraphQl}
const useData = () => {
const [data, setData] = useState();
useEffect(() => {
const userId = useUser();
fetchData(userId).then( // the rest of the code to set the state variable data.)
},[])
return data
}
When I try to do this I get an error telling me
*Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.*
I think the problem is that I am calling the Hook useUser inside of the use effect, but using it inside the function will cause the problem described here, and I can't use it outside the body of the fetchData since the useData itself is a hook, and it can be only used inside a functional component's or Hook's body. So I don't know how to find a way around this problem.
Correct, React hooks can only be called from React function components and other React hooks. The useEffect hook's callback isn't a React hook, it's a callback. According to the Rules of Hooks, don't call hooks inside loops, conditions, or nested functions.
I suggest refactoring the useData hook to consume the userId as an argument, to be used in the dependency array of the useEffect.
const fetchData = async (userId) => {
// code to fetch data from GraphQl
};
const useData = (userId) => {
const [data, setData] = useState();
useEffect(() => {
fetchData(userId)
.then((....) => {
// the rest of the code to set the state variable data.
});
}, [userId]);
return data;
};
Usage in Function component:
const userId = useUser();
const data = useData(userId);
If this is something that is commonly paired, abstract into a single hook:
const useGetUserData = () => {
const userId = useUser();
const data = useData(userId);
return data;
};
...
const data = useGetUserData();
Though you should probably just implement as a single hook as follows:
const useGetUserData = () => {
const [data, setData] = useState();
useEffect(() => {
getUserInfo()
.then(fetchData) // shortened (userId) => fetchData(userId)
.then((....) => {
// the rest of the code to set the state variable data.
setData(....);
});
}, []);
return data;
};
You can't call hook inside useEffect, Hook should be always inside componet body not inside inner function/hook body.
import useUserId from "./UseUserId";
// ...rest of the necessary imports
const fetchData = async (userId) => {
// code to fetch data from GraphQl}
};
const useData = () => {
const [data, setData] = useState();
const userId = useUser();
useEffect(() => {
if (userId) {
fetchData(userId).then(setData);
}
}, [userId]);
return data;
};
Please consider the following code:
Parent:
const Messages = (props) => {
const [targetUserId, setTargetUserId] = useState(null);
const [currentChat, setCurrentChat] = useState(null);
useEffect(() => {
const { userId } = props;
const initiateChat = async (targetUser) => {
const chatroom = `${
userId < targetUser
? `${userId}_${targetUser}`
: `${targetUser}_${userId}`
}`;
const chatsRef = doc(database, 'chats', chatroom);
const docSnap = await getDoc(chatsRef);
if (docSnap.exists()) {
setCurrentChat(chatroom);
} else {
await setDoc(chatsRef, { empty: true });
}
};
if (props.location.targetUser) {
initiateChat(props.location.targetUser.userId);
setTargetUserId(props.location.targetUser.userId);
}
}, [props]);
return (
...
<Chat currentChat={currentChat} />
...
);
};
Child:
const Chat = (props) => {
const {currentChat} = props;
useEffect(() => {
const unsubscribeFromChat = () => {
try {
onSnapshot(
collection(database, 'chats', currentChat, 'messages'),
(snapshot) => {
// ... //
}
);
} catch (error) {
console.log(error);
}
};
return () => {
unsubscribeFromChat();
};
}, []);
...
The issue I'm dealing with is that Child's UseEffect clean up function, which depends on the chatroom prop passed from its parent, throws a TypeError error because apparently chatroom is null. Namely, it becomes null when the parent component unmounts, the component works just fine while it's mounted and props are recognized properly.
I've tried different approaches to fix this. The only way I could make this work if when I moved child component's useEffect into the parent component and defined currentChat using useRef() which honestly isn't ideal.
Why is this happening? Shouldn't useEffect clean-up function depend on previous state? Is there a proper way to fix this?
currentChat is a dependency of that effect. If it's null, the the unsubscribe should just early return.
const {currentChat} = props;
useEffect(() => {
const unsubscribeFromChat = () => {
if(!currentChat) return;
try {
onSnapshot(
collection(database, 'chats', currentChat, 'messages'),
(snapshot) => {
// ... //
}
);
} catch (error) {
console.log(error);
}
};
return () => {
unsubscribeFromChat();
};
}, [currentChat]);
But that doesn't smell like the best solution. I think you should handle all the subscribing/unsubscribing in the same component. You shouldn't subscribe in the parent and then unsubscribe in the child.
EDIT:
Ah, there's a bunch of stuff going on here that's not good. You've got your userId coming in from props - props.location.targetUser.userId and then you're setting it as state. It's NOT state, it's only a prop. State is something a component owns, some data that a component has created, some data that emanates from that component, that component is it's source of truth (you get the idea). If your component didn't create it (like userId which is coming in on props via the location.targetUser object) then it's not state. Trying to keep the prop in sync with state and worry about all the edge cases is a fruitless exercise. It's just not state.
Also, it's a codesmell to have [props] as a dependency of an effect. You should split out the pieces of props that that effect actually needs to detect changes in and put them in the dependency array individually.
I am trying to implement a simple search algorithm for my products CRUD.
The way I thought to do it was entering the input in a search bar, and the products that matched the search would appear instantly every time the user changes the input, without needing to hit a search button.
However, the way I tried to do it was like this:
function filterProducts (productName, productList) {
const queryProducts = productList.filter((prod)=> {
return prod.title === productName;
});
return queryProducts;
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [popupTrigger, setPopupTrigger] = useState('');
const [productDeleteId, setProductDeleteId] = useState('');
const [queryString, setQueryString] = useState('');
let history = useHistory();
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
return;
}
const queryProducts = filterProducts(queryString, productList);
setProductList(queryProducts);
}, [queryString, productList]);
I know that productList changes every render, and that's probably why it isn't working. But I didn't figure out how can I solve the problem. I've seen other problems here and solutions with useReducer, but I none of them seemed to help me.
The error is this one below:
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
what you are doing here is fetching a product list and filtering it based on the query string and using that filtered list to render the UI. So ideally your filteredList is just a derived state based on your queryString and productList. So you can remove the filterProducts from your useEffect and move it outside. So that it runs when ever there is a change in the state.
function filterProducts (productName = '', productList = []) {
return productName.trim().length > 0 ? productList.filter((prod)=> {
return prod.title === productName;
}); : productList
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [queryString, setQueryString] = useState('');
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
}
}, [queryString]);
// query products is the derived state
const queryProducts = filterProducts(queryString, productList);
// Now instead of using productList to render something use the queryProducts
return (
{queryProducts.map(() => {
.....
})}
)
If you want the filterProducts to run only on change in queryString or productList then you can wrap it in useMemo
const queryProducts = React.useMemo(() => filterProducts(queryString, productList), [queryString, productList]);
When you use a setState function in a useEffect hook while having the state for that setState function as one of the useEffect hook's dependencies, you'll get this recursive effect where you end up infinitely re-rendering your component.
So, first of all we have to remove productList from the useEffect. Then, we can use a function to update your state instead of a stale update (like what you're doing in your example).
function filterProducts (productName, productList) {
const queryProducts = productList.filter((prod)=> {
return prod.title === productName;
});
return queryProducts;
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [popupTrigger, setPopupTrigger] = useState('');
const [productDeleteId, setProductDeleteId] = useState('');
const [queryString, setQueryString] = useState('');
let history = useHistory();
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
return;
}
setProductList(prevProductList => {
return filterProducts(queryString, prevProductList)
});
}, [queryString]);
Now, you still get access to productList for your filter, but you won't have to include it in your dependencies, which should take care of the infinite re-rendering.
I recommend several code changes.
I would separate the state that immediately reflects the user input at all times from the state that represents the query that is send to the backend. And I would add a debounce between the two states. Something like this:
const [query, setQuery] = useState('');
const [userInput, setUserInput] = useState('');
useDebounce(userInput, setQuery, 750);
I would split up the raw data that was returned from the backend and the filtered data which is just derived from it
const [products, setProducts] = useState([]);
const [filteredProducts, setFilteredProducts] = useState([]);
I would split up the useEffect and not mix different concerns all into one (there is no rule that you cannot have multiple useEffect)
useEffect(() => {
if (query.trim() === '') {
Axios
.get("http://localhost:3001/api/product/get-all")
.then((data) => { setProducts(data.data) });
}
}, [query]);
useEffect(
() => setFilteredProducts(filterProducts(userInput, products)),
[userInput, products]
);
I am receiving the user properly from my provider but for some reason the state does not change with the user object.
This returns the user object properly:
console.log(receivedUser);
However, after I try to do this, there's no object:
setUser(receivedUser);
console.log(user);
What seems to be the issue here guys? Why doesn't the state change at all?
Full code:
const [user, setUser] = useState({})
// this prevents this providerValue changing unless value or setValue changes
const providerValue = useMemo(() => ({user, setUser}), [user, setUser])
useEffect(() => {
async function fetchUser(){
const receivedUser = await AuthService.getCurrentUser();
if (receivedUser) {
// console.log(receivedUser);
setUser(receivedUser);
console.log(user);
} else {
console.log("user not logged in");
}
}
fetchUser();
}, []);
updation of state is asynchronous. you need to use useEffect to console out the value of state. A seperate useEffect can be created which would be triggered every time the user in the state changes
const [user, setUser] = useState({})
// this prevents this providerValue changing unless value or setValue changes
const providerValue = useMemo(() => ({user, setUser}), [user, setUser])
useEffect(() => {
async function fetchUser(){
const receivedUser = await AuthService.getCurrentUser();
if (receivedUser) {
// console.log(receivedUser);
setUser(receivedUser);
console.log(user);
} else {
console.log("user not logged in");
}
}
fetchUser();
}, []);
useEffect(()=> {
console.log('user',user)
}, [user])
You can do it like that, first setUser is an async.
You have to understand the life circle of useState
Will give you an example how to handle this right and wrong..
see below to example..
const [text, setText] = useState("default");
useEffect(() => {
setText("test");
console.log(text) // this will print defult
}, [])
useEffect(() => {
console.log(text) // this will print test
}, [text])
setUser it's kind of asynchronous function. State of user doesn't change immediately after you call this function. If you want to handle when user state change add another on useEffect which checking if user state change:
useEffect( () => {
/* this will be call any time user state change */
if ( user ) {
/* do something if user was fetched */
}
}, [user])