I'm working an a react app with a few forms and I am trying to implement an edit form for input items. The function first opens the list item in a pre-populated form.
The editItem function currently looks like this:
editItem(event) {
event.preventDefault();
const target = event.target.parentNode.parentNode;
const { key } = target.dataset;
const { className } = target;
const currState = { ...this.state[className] };
const currItem = currState.list[key];
for (let i in currItem) {
if (i !== "list" && i !== "hidden") {
currState[i] = currItem[i]
}
}
this.setState({ [className]: currState });
this.hideUnhide({target: {name: className}});
}
I have confirmed with console logs that currState is correctly set with the values that I am looking for, and that I am not having an async issue. I am using this same format to set state in other functions in my app and all of the others are working properly. If I directly mutate state in the same place, I get the behavior I'm looking for (form fields populate), but nothing happens when I use setState.
Link to my github repo: here. The function in question is in App.js.
As Brian Thompson points out in his comment, it turns out that the hideUnhide function call directly after my setState uses setState as well and writes over the first setState call with the previous state:
hideUnhide(event) {
const { name } = event.target;
const currState = { ...this.state[name] };
if (currState.hidden === true) {
currState.hidden = false;
}
this.setState({ [name]: currState });
}
The way to prevent that was to use hideUnhide as a callback to the setState in editItem:
this.setState({ [className]: currState }, () =>
this.hideUnhide({ target: { name: className } })
);
and now everything functions as intended.
Related
I'm developing with h5p standalone plugin in react (nextjs), passing the path as prop to a Modal Component which render the h5p activity.
useEffect(() => {
const initH5p = async (contentLocation) => {
const { H5P: H5PStandalone } = require('h5p-standalone')
const h5pPath = `https://cdn.thinkeyschool.com/h5p/${contentLocation}`
const options = {
id: 'THINKeyLesson',
h5pJsonPath: h5pPath,
frameJs: '/h5p/dist/frame.bundle.js',
frameCss: '/h5p/dist/styles/h5p.css',
}
let element = document.getElementById('h5p_container')
removeAllChildNodes(element)
await new H5PStandalone(element, options)
fireCompleteH5PTopic(H5P)
setIsLoaderVisible(false)
}
initH5p(location)
}, [location, session.data.user.id, course.slug, topic])
With that code, I get two h5p rendered in screen. So I'm using removeAllChildren() to eliminate them from the render.
function removeAllChildNodes(parent) {
console.log(parent)
while (parent.firstChild) {
parent.removeChild(parent.firstChild)
}
}
That hack is working fine, but when I try to send the xAPI statement to my database, it fires twice
const fireCompleteH5PTopic = async (H5P) => {
H5P.externalDispatcher.on("xAPI", (event) => {
// console.log('event fired')
if (event?.data?.statement?.result?.completion) {
setCounter(counter + 1)
completeH5PTopic(event, session.data.user.id, course.slug, topic)
return true
}
})
}
Any help regarding why it fires twice? I think it may be related to h5p rendering twice too.
Thanks in advance.
I tried using a state to render only once, but it is not working.
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.
the checkbox by default checked if this checkbox is by default checked then it should be automatically pushed into addtess_type array.
https://react-jkrrcb.stackblitz.io demo
handleInputChangeDom = (event) => {
const target = event.target;
var value = target.value;
const Address_type = this.state.Address_type;
if (event.target.checked) {
Address_type.push(event.target.value);
} else {
let index = Address_type.indexOf(event.target.value);
Address_type.splice(index, 1);
}
this.setState({
Address_type: Address_type,
});
};
I think you are trying to maintain a state of all the checked items.
One advice would be to use Set instead of an array. It will make life so much more easier for you.
const handleInputChangeDom = (event) => {
const newSet = new Set(this.state.Address_type); // create a clone, don't update current state
if(event.target.checked) {
newSet.add(event.target.value);
} else {
newSet.delete(event.target.value);
}
this.setState({ Address_type: newSet });
}
One suggestion - please try to use camelCase with React, that is suggested.
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();
}
When I click on button then onClick triggers correct function, run half through and jumps to other function which is not related to it and run through half of it and jumps back to first function, runs half trough again and drops error
Uncaught TypeError: _this.state.searchValue.toLowerCase is not a function
Interesting part is that I click other button before which triggers this function with toLowerCase() and there is no errors.
I dont have any idea whats going on here but so far i was trying to remove few lines to see which line cause it because I dont think that line with toLowerCase() realy is the reason. Everything works when I remove lines where is first this.setState.
Here is my function:
( Alerts is used to track where function is at, that how i know
that it run half through only. It never reach alert("DDD").
This function is which is triggered with button onClick like it should be )
onSelect = (e) => {
const data = e.target.getAttribute('data-id');
const itemId = e.target.getAttribute('data-id');
const itemIdState = !this.state[e.target.getAttribute('data-id')];
alert("AAA")
this.setState(state => { // <--- Somehow problem comes from this setState function
const newState = {};
for (const dataId in state) {
newState[dataId] = dataId === data
}
alert("BBB")
return newState
});
alert("CCC")
this.setState(State => ({
[itemId]: itemIdState,
}), function() {
alert("DDD")
if(this.state[itemId] === true){
this.setState({isAnySelected: true})
}else if(this.state[itemId] === false){
this.setState({isAnySelected: false})
}
})
}
This is other function which is triggered by mistake and is not related to other. It is just returning component which is displayed and when I press on its button then i have this issue.
filterSearch = (id, title, path) => {
let name = title.toLowerCase()
let filter = this.state.searchValue.toLowerCase()
if(name.includes(filter)){
return <SearchResult key={id} data-id={id} pName={path} onClick={this.onSelect} selected={this.state[id]} />
}
}
And here is from where filterSearch is triggered. Behind this.props.searchResult is Redux.
{this.props.searchResult ? this.props.searchResult.map(category =>
this.filterSearch(category.id, category.title, category.path)
) : null
}
I think I see what the problem is: in your problematic this.setState, you cast everything in your state to a boolean:
this.setState(state => {
const newState = {};
for (const dataId in state) {
newState[dataId] = dataId === data
}
alert("BBB")
return newState
});
Your for() statement ends up comparing searchValue to data (some kind of ID), which I imagine more often than not will not be the case, so searchValue ends up getting set to false.
And what happens when you try to do .toLowerCase() on a Boolean?
To fix this, consider structuring your state like this:
this.state = {
searchValue: '',
ids: {},
};
Then, replace your problematic this.setState with something like this:
this.setState((state) => {
const newIDs = {
// Create a clone of your current IDs
...state.ids,
};
Object.keys(newIDs).forEach(key => {
newIDs[key] = key === data
});
alert("BBB")
return {
// searchValue will remain untouched
...state,
// Only update your IDs
ids: newIDs,
}
});
What exactly are you wanting to do here?
this.setState(state => {
const newState = {}; // You are initializing an object
for (const dataId in state) {
newState[dataId] = dataId === data // You are putting in an array every property of state that is equal to data
}
return newState
});
So irrevocably, your this.state.searchValue property will be changed to something else, which is of boolean type. So toLowerCase being a function for string.prototype, you will get an error.
You should describe what you where aiming to get here.