I know lots of developers had similar kinds of issues in the past like this. I went through most of them, but couldn't crack the issue.
I am trying to update the cart Context counter value. Following is the code(store/userCartContext.js file)
import React, { createContext, useState } from "react";
const UserCartContext = createContext({
userCartCTX: [],
userCartAddCTX: () => {},
userCartLength: 0
});
export function UserCartContextProvider(props) {
const [userCartStore, setUserCartStore] = useState([]);
const addCartProduct = (value) => {
setUserCartStore((prevState) => {
return [...prevState, value];
});
};
const userCartCounterUpdate = (id, value) => {
console.log("hello dolly");
// setTimeout(() => {
setUserCartStore((prevState) => {
return prevState.map((item) => {
if (item.id === id) {
return { ...item, productCount: value };
}
return item;
});
});
// }, 50);
};
const context = {
userCartCTX: userCartStore,
userCartAddCTX: addCartProduct,
userCartLength: userCartStore.length,
userCartCounterUpdateCTX: userCartCounterUpdate
};
return (
<UserCartContext.Provider value={context}>
{props.children}
</UserCartContext.Provider>
);
}
export default UserCartContext;
Here I have commented out the setTimeout function. If I use setTimeout, it works perfectly. But I am not sure whether it's the correct way.
In cartItemEach.js file I use the following code to update the context
const counterChangeHandler = (value) => {
let counterVal = value;
userCartBlockCTX.userCartCounterUpdateCTX(props.details.id, counterVal);
};
CodeSandBox Link: https://codesandbox.io/s/react-learnable-one-1z5td
Issue happens when I update the counter inside the CART popup. If you update the counter only once, there won't be any error. But when you change the counter more than once this error pops up inside the console. Even though this error arises, it's not affecting the overall code. The updated counter value gets stored inside the state in Context.
TIL that you cannot call a setState function from within a function passed into another setState function. Within a function passed into a setState function, you should just focus on changing that state. You can use useEffect to cause that state change to trigger another state change.
Here is one way to rewrite the Counter class to avoid the warning you're getting:
const decrementHandler = () => {
setNumber((prevState) => {
if (prevState === 0) {
return 0;
}
return prevState - 1;
});
};
const incrementHandler = () => {
setNumber((prevState) => {
return prevState + 1;
});
};
useEffect(() => {
props.onCounterChange(props.currentCounterVal);
}, [props.currentCounterVal]);
// or [props.onCounterChange, props.currentCounterVal] if onCounterChange can change
It's unclear to me whether the useEffect needs to be inside the Counter class though; you could potentially move the useEffect outside to the parent, given that both the current value and callback are provided by the parent. But that's up to you and exactly what you're trying to accomplish.
Related
In my React functional components, I am trying to use custom usePrevious to get/compare the previous value of my context object. However, it seems to be having the latest value always and not the previous value. Below is my code. Please let me know if I am doing something wrong here.
function MyHeaderComponent(props) {
const [myPref, setMyPref] = React.useContext(MyContext);
const prevPreferences = usePrevious(myPref.preferences);
useEffect(() => {
console.log("prevPreferences : " + prevPreferences); // Prints current context object, instead of the previous one
// I want to call myInfo only if prevPreferences is not the same as myPref.preferences (i.e. current)
}, [myPref]);
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const myInfo = async () => {
setMyPref(myPref);
}
return (
<>
<div>
Some JSX
</div>
</>
)
}
export default withRouter(withStyles()(MyHeaderComponent));
function MyPreferences(props) {
const [myPref, setMyPref] = React.useContext(MyContext);
// somewhere in code
setMyPref(myPref);
}
That implementation of useLatest (which should not be within the component, by the way) does not copy the value into the ref box, it just assigns a reference.
I'd wager myPref.preferences gets internally modified (instead of being reassigned a new object), which is why you always see the same value.
I think the problem is you are using usePrevious hook inside another functional component. Could you try to create a file for the usePrevious hook.
usePreviousState.js
function usePrevious(value, initialValue) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
if (ref.current === undefined && initialValue !== undefined) {
return initialValue;
}
return ref.current;
}
I am trying to implement a use case with React Hooks and React Context API with mouse events.
I want to add mousemove event for a container. If user moves over an object (rectangle), a dispatch action is called and context value is updated. I want to achieve that the action is not dispatched repeatedly by checking context value before dispatching. The issue is that function doesn't get current context value.
This the event function useMouseEvents.js
import * as React from "react";
import { DragToCreateContext, actionTypes } from "./reducer";
export function useMouseEvents(elRef) {
const { dragToCreate, dispatchDragToCreate } = React.useContext(
DragToCreateContext
);
console.log("out of callback", dragToCreate);
const handleMouseMove = React.useCallback(
(evt) => {
if (evt.target.tagName === "P") {
console.log("inside callback", dragToCreate);
}
if (evt.target.tagName === "P" && dragToCreate.sourceNodeId === null) {
console.log("dispatch");
dispatchDragToCreate({
type: actionTypes.ACTIVATE,
sourceNodeId: 1
});
}
},
[dragToCreate]
);
React.useEffect(() => {
const el = elRef?.current;
if (el) {
el.addEventListener("mousemove", handleMouseMove);
return () => {
el.addEventListener("mousemove", handleMouseMove);
};
}
}, [elRef, handleMouseMove]);
}
codesandbox.io
If you hover over rectangle, you will see in console log:
inside callback {sourceNodeId: null}
dispatch
out of callback {sourceNodeId: 1}
inside callback {sourceNodeId: null}
dispatch
out of callback {sourceNodeId: 1}
inside callback {sourceNodeId: 1}
inside callback {sourceNodeId: null}
but it should be
inside callback {sourceNodeId: null}
dispatch
out of callback {sourceNodeId: 1}
inside callback {sourceNodeId: 1}
The behaviour that you see is because your listeners on mouseMove are removed and added whenever your context value changes. Also since your listener is recreated in useEffect it might so happen that before a new listener is attached, an old one executes and you get an old value from the closure.
To solve such scenarios, you can make use of a ref to keep track of updated context values and use that inside your listener callback. This way you will be able to avoid addition and removal of mouse event listener
import * as React from "react";
import { DragToCreateContext, actionTypes } from "./reducer";
export function useMouseEvents(elRef) {
const { dragToCreate, dispatchDragToCreate } = React.useContext(
DragToCreateContext
);
console.log("out of callback", dragToCreate);
const dragToCreateRef = React.useRef(dragToCreate);
React.useEffect(() => {
dragToCreateRef.current = dragToCreate;
}, [dragToCreate]);
const handleMouseMove = React.useCallback((evt) => {
if (evt.target.tagName === "P") {
console.log("inside callback", dragToCreateRef.current);
}
if (
evt.target.tagName === "P" &&
dragToCreateRef.current.sourceNodeId === null
) {
console.log("dispatch");
dispatchDragToCreate({
type: actionTypes.ACTIVATE,
sourceNodeId: 1
});
}
}, []);
React.useEffect(() => {
const el = elRef?.current;
if (el) {
el.addEventListener("mousemove", handleMouseMove);
return () => {
el.addEventListener("mousemove", handleMouseMove);
};
}
}, [elRef, handleMouseMove]);
}
Working codesandbox demo
I can't really find the answers I was hoping for but here's a couple things for you and maybe anyone else wanting to write an answer:
The inside callback {sourceNodeId: null} tells me that something is causing the Context to reset to it's initial values and the way you used your Provider is pretty atypical to what I usually see (changing this didn't really seem to fix anything though).
I thought maybe the useContext inside of useMouseEvents is getting just the default context, but I tried moving things around to guarentee that wasn't the case but that didn't seem to work. (Someone else might want to retry this?)
Edit: Removed this suggestion
Kinda unrelated to the issue, but you're going to want to change your useEffect too:
React.useEffect(() => {
const el = elRef?.current;
if (el) {
el.addEventListener("mousemove", handleMouseMove);
return () => {
el.removeEventListener("mousemove", handleMouseMove);
};
}
useState does not update the state immediately.
I'm using react-select and I need to load the component with the (multi) options selected according to the result of the request.
For this reason, I created the state defaultOptions, to store the value of the queues constant.
It turns out that when loading the component, the values are displayed only the second time.
I made a console.log in the queues and the return is different from empty.
I did the same with the defaultOptions state and the return is empty.
I created a codesandbox for better viewing.
const options = [
{
label: "Queue 1",
value: 1
},
{
label: "Queue 2",
value: 2
},
{
label: "Queue 3",
value: 3
},
{
label: "Queue 4",
value: 4
},
{
label: "Queue 5",
value: 5
}
];
const CustomSelect = (props) => <Select className="custom-select" {...props} />;
const baseUrl =
"https://my-json-server.typicode.com/wagnerfillio/api-json/posts";
const App = () => {
const userId = 1;
const initialValues = {
name: ""
};
const [user, setUser] = useState(initialValues);
const [defaultOptions, setDefaultOptions] = useState([]);
const [selectedQueue, setSelectedQueue] = useState([]);
useEffect(() => {
(async () => {
if (!userId) return;
try {
const { data } = await axios.get(`${baseUrl}/${userId}`);
setUser((prevState) => {
return { ...prevState, ...data };
});
const queues = data.queues.map((q) => ({
value: q.id,
label: q.name
}));
// Here there is a different result than emptiness
console.log(queues);
setDefaultOptions(queues);
} catch (err) {
console.log(err);
}
})();
return () => {
setUser(initialValues);
};
}, []);
// Here is an empty result
console.log(defaultOptions);
const handleChange = async (e) => {
const value = e.map((x) => x.value);
console.log(value);
setSelectedQueue(value);
};
return (
<div className="App">
Multiselect:
<CustomSelect
options={options}
defaultValue={defaultOptions}
onChange={handleChange}
isMulti
/>
</div>
);
};
export default App;
React don't update states immediately when you call setState, sometimes it can take a while. If you want to do something after setting new state you can use useEffect to determinate if state changed like this:
const [ queues, setQueues ] = useState([])
useEffect(()=>{
/* it will be called when queues did update */
},[queues] )
const someHandler = ( newValue ) => setState(newValue)
Adding to other answers:
in Class components you can add callback after you add new state such as:
this.setState(newStateObject, yourcallback)
but in function components, you can call 'callback' (not really callback, but sort of) after some value change such as
// it means this callback will be called when there is change on queue.
React.useEffect(yourCallback,[queue])
.
.
.
// you set it somewhere
setUserQueues(newQueues);
and youre good to go.
no other choice (unless you want to Promise) but React.useEffect
Closures And Async Nature of setState
What you are experiencing is a combination of closures (how values are captured within a function during a render), and the async nature of setState.
Please see this Codesandbox for working example
Consider this TestComponent
const TestComponent = (props) => {
const [count, setCount] = useState(0);
const countUp = () => {
console.log(`count before: ${count}`);
setCount((prevState) => prevState + 1);
console.log(`count after: ${count}`);
};
return (
<>
<button onClick={countUp}>Click Me</button>
<div>{count}</div>
</>
);
};
The test component is a simplified version of what you are using to illustrate closures and the async nature of setState, but the ideas can be extrapolated to your use case.
When a component is rendered, each function is created as a closure. Consider the function countUp on the first render. Since count is initialized to 0 in useState(0), replace all count instances with 0 to see what it would look like in the closure for the initial render.
const countUp = () => {
console.log(`count before: ${0}`);
setCount((0) => 0 + 1);
console.log(`count after: ${0}`);
};
Logging count before and after setting count, you can see that both logs will indicate 0 before setting count, and after "setting" count.
setCount is asynchronous which basically means: Calling setCount will let React know it needs to schedule a render, which it will then modify the state of count and update closures with the values of count on the next render.
Therefore, initial render will look as follows
const countUp = () => {
console.log(`count before: 0`);
setCount((0) => 0 + 1);
console.log(`count after: 0`);
};
when countUp is called, the function will log the value of count when that functions closure was created, and will let react know it needs to rerender, so the console will look like this
count before: 0
count after: 0
React will rerender and therefore update the value of count and recreate the closure for countUp to look as follows (substituted the value for count).This will then update any visual components with the latest value of count too to be displayed as 1
const countUp = () => {
console.log(`count before: 1`);
setCount((1) => 1 + 1);
console.log(`count after: 1`);
};
and will continue doing so on each click of the button to countUp.
Here is a snip from codeSandbox. Notice how the console has logged 0 from the intial render closure console log, yet the displayed value of count is shown as 1 after clicking once due to the asynchronous rendering of the UI.
If you wish to see the latest rendered version of the value, its best to use a useEffect to log the value, which will occur during the rendering phase of React once setState is called
useEffect(() => {
console.log(count); //this will always show the latest state in the console, since it reacts to a change in count after the asynchronous call of setState.
},[count])
You need to use a parameter inside the useEffect hook and re-render only if some changes are made. Below is an example with the count variable and the hook re-render only if the count values have changed.
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
The problem is that await api.get() will return a promise so the constant data is not going to have it's data set when the line setUserQueues(queues); is run.
You should do:
api.get(`/users/${userId}`).then(data=>{
setUser((prevState) => {
return { ...prevState, ...data };
});
const queues = data.queues.map((q) => ({
value: q.id,
label: q.name,
}));
setUserQueues(queues);
console.log(queues);
console.log(userQueues);});
I have a component which has a local variable
let endOfDocument = false;
And I have a infinite scroll function in my useEffect
useEffect(() => {
const { current } = selectScroll;
current.addEventListener('scroll', () => {
if (current.scrollTop + current.clientHeight >= current.scrollHeight) {
getMoreExercises();
}
});
return () => {
//cleanup
current.removeEventListener('scroll', () => {});
};
}, []);
In my getMoreExercises function I check if we reached the last document in firebase
function getMoreExercises() {
if (!endOfDocument) {
let ref = null;
if (selectRef.current.value !== 'All') {
ref = db
.collection('exercises')
.where('targetMuscle', '==', selectRef.current.value);
} else {
ref = db.collection('exercises');
}
ref
.orderBy('average', 'desc')
.startAfter(start)
.limit(5)
.get()
.then((snapshots) => {
start = snapshots.docs[snapshots.docs.length - 1];
if (!start) endOfDocument = true; //Here
snapshots.forEach((exercise) => {
setExerciseList((prevArray) => [...prevArray, exercise.data()]);
});
});
}
}
And when I change the options to another category I handle it with a onChange method
function handleCategory() {
endOfDocument = false;
getExercises();
}
I do this so when we change categories the list will be reset and it will no longer be the end of the document. However the endOfDocument variable does not update and getMoreExercise function will always have the endOfDocument value of true once it is set to true. I cannot change it later. Any help would be greatly appreciated.
As #DevLoverUmar mentioned, that would updated properly,
but since the endOfDocument is basically never used to "render" anything, but just a state that is used in an effect, I would put it into a useRef instead to reduce unnecessary rerenders.
Assuming you are using setExerciseList as a react useState hook variable. You should use useState for endOfDocument as well as suggested by Brian Thompson in a comment.
import React,{useState} from 'react';
const [endOfDocument,setEndOfDocument] = useState(false);
function handleCategory() {
setEndOfDocument(false);
getExercises();
}
There is a board game that every time the user changes something in one square in it is affect the status of all other squares. To change a square's state I use useReducer, and to handle the change on the other squares I use useEffect.
The problem is when the user requests the computer to resolve by own the situation (there is such an option). My preferred solution is to trigger a recursive function (resolveBoard) that goes over all the squares and change them, one after one. The calculation on what to do in each square is based on the side effect of the former decision in the previous squares (what should be done by the useEffect mentioned above).
Unfortunately, the useReducer's dispatcher (and, as result, the useEffect) is be called only after the recursive function finished to go all over the board, which spoils the calculation and causes it returns an incorrect result.
How it can be done?
const [squaresValues, dispatchSquaresValue] = useReducer(
setSquaresValue,
{ rows: props.rows, columns: props.columns },
InilizeBoard
);
useEffect(() => {
calculateValues();
}, [squaresValues]);
function resolveBoard(row, index) {
...
if (resolveSingleSquare(row, index)) {
const nextIndex = ...
const nextRow = ....
return resolveBoard(nextRow, nextIndex);
}
return false;
}
function resolveSingleSquare(row, index) {
...calculations based on the state of the others squares
if (working) {
dispatchSquaresValue(squareProp);
return true;
}
return false;
}
function setSquaresValue(prevValues, squareProp) {
--- be called only after all finished:(
}
Ciao, in order to dispatch dispatchSquaresValue before re-call resolveBoard(nextRow, nextIndex); you have to use a Promise mechanism. So you cannot use useReducer. There are several ways to solve your problem, I used 2:
redux-promise:
export const updateSquareValue = (key, value)=>
Promise.resolve({
type:'UPDATE_SQUARE_VALUE',
key, value
})
Then in your component:
import { useDispatch } from 'react-redux';
...
const dispatch = useDispatch();
...
function resolveBoard(row, index) {
...
dispatch(updateSquareValue(squareProp)).then(() => {
resolveBoard(nextRow, nextIndex);
});
}
redux-thunk:
export const updateSquareValue = (key, value) => dispatch => {
dispatch({
type: 'UPDATE_SQUARE_VALUE',
key,
value,
});
return Promise.resolve();
};
Then in your component:
import { useDispatch } from 'react-redux';
...
const dispatch = useDispatch();
...
function resolveBoard(row, index) {
...
dispatch(updateSquareValue(squareProp)).then(() => {
resolveBoard(nextRow, nextIndex);
});
}