Edit Note:Link with working version at bottom of post
I have run into some really weird behavior with my project
I have the parent which gets some data from a database and passes it to the child (which is a popup). The child then uses useEffect to detect when the data is ready (black part) and sets the value in the child (blue part).
useEffect(() => {
console.log(datachannels.length)
if (datachannels.length > 1) {
let newValueState = []
let c = 0
datachannels.forEach(element => {
newValueState.push({ id: c, dc: element.dataChannel, axis: "y" })
c += 1
});
setValueState(newValueState)
console.log(newValueState)
console.log("trigger")
}
}, [datachannels])
Then when a user changes a option this code runs to update it(Green part)
const handler = (event, id) => {
console.log(valueState)
const value = event.value
console.log(valueState.length)
if (valueState.length > 0) {
let newValueState = valueState
newValueState.forEach(key => {
if (key.id == id) {
console.log(key)
key.axis = value
}
});
setValueState(newValueState)
}
}
With inspect i can see that the value is not empty but the handler function trying to use it is telling me its empty. When giving valueState a default value and after useEffect code runs, I try and access it but it just returns the default value. I have no idea what is causeing this or what to google so if anyone can shead some light on this, I would be very happy
Update 1: Codesandebox (new link below) I have not been able to get the bug to show yet
Update 2:Code in sandbox now has error. As you can see when useEffect runs the object has 3 values but when the user changes the dropdown it only has one value
Update 3: To see the bug in the sandbox change one of the axis drop downs
Update 4: Porok12 answer works in the sand box but is not working in my actully project and i have no idea what is going on
Update 5: So when using Porok12 code and feeding in datachannels the same way as the sandbox it does work but with some other weird behavior that im hopping can point to the problem. When changing a dropdown this is outputted to the console. From input was changed to (3) [{},{},{}] is the correct behavior but then everything rerenders as seen from the other messages. I have no idea why it is behaving differently from porok12 code as it is the same. But this does point out a problem with how datachannels is being passed in? In the parent object I am storing the channels like this
if (data !== undefined) {
let parseData = JSON.parse(data);
let toPush = []
Object.keys(parseData).forEach(key => {
toPush.push({ "dataChannel": key })
});
setDataChannels(toPush);
And feeding it to the child like this <BuilderTable handleupdate={handleUpdate} datachannels={dataChannels} show={modalShow} onHide={() => setModalShow(false)} />
Update 6: So its now working with Porok12 code after i changed the useMemo [] to use valueState instead of datachannel. The only problem now is that the dropdown reset to the starting value every time you change it
Update 7: With the change from update 6 and feeding valueStatus into the dropdowns the problem is not fixed.
Update 8: https://codesandbox.io/s/spring-water-2ucq8u?file=/src/GraphBuilderFull.js Here is the sandbox link with a working version
Side note: you can change
let c = 0
datachannels.forEach(element => {
newValueState.push({ id: c, dc: element.dataChannel, axis: "y" })
c += 1
});
to
const newValueState = datachannels.map((element, index) => ({id: index, dc: element.dataChannel, axis: "y"}))
The problem is that you use useMemo that way:
const selectChannelTableColumn = React.useMemo(
() => [
{
Header: "Axis Selection",
Cell: ({ row }) => (
<div>
<Dropdown
id={row.id}
options={axis}
onChange={(e) => handler(e, row.id)}
placeholder="y"
/>
</div>
)
}
],
[] // missing `datachannels` xor `valueState` dependency
);
If you will add datachannels to dependencies it will work. You should also move handler definition to this useMemo code.
Here is fixed sandbox example
The changes I made:
Move handler definition to selectChannelTableColumn code
Fix handler logic
Move selectChannelTableColumn under (meaning below not inside) your useEffect
Add datachannels to useMemo hook dependencies
Related
I have two objects that I'm trying to loop through using an increment and decrement function. I have the variables being stored in localStorage and the expectation is that no matter which value has been selected last, upon refresh or reload, the counter should still work. However, I've experienced some unexpected occurrences upon using the increment and decrement functions where the variable does not line with the index of the object as shown by the reducer state.
const increment = (props) => {
return (props.state + 1) % props.length;
};
const decrement = (props) => {
return (props.state - 1 + props.length) % props.length;
};
const colors = [
{ id: 0, type: "Red" },
{ id: 1, type: "Blue" },
{ id: 2, type: "Green" },
{ id: 3, type: "Yellow" }
];
For example, at times when I call the state, it will tell me that the color is Yellow and the index is 2, and is generally inconsistent. I've tried storing the counter variable within localStorage and calling from that in hopes that it will synchronize the counter with the intended variable, however, this has not worked.
Here is a demo with CodeSandbox. I'm still relatively new to React and I'm not sure if using a counter is the best method for this problem.
Here is a reproducible example of the output I've received (upon clearing localStorage and refreshing the app) and the expected output for the shapes.
Output:
Square (next) Square (previous) Circle (previous) Square (previous) Octagon (next) Triangle
Expected output:
Square (next) Circle (previous) Square (previous) Octagon (previous) Triangle (next) Octagon
Having forked and refactored your original sandbox here there were a few challenges.
Firstly, the reducer actions were expanded to include increment and decrement. The example provided in React Hooks Reference has an example of incrementing and decrementing, though reducers are not just applicable for counting. The dispatch of your reducer will set the state, so having Action.Set is redundant.
Let's take a look at one of the original buttons' onClick methods. Originally, colorCount was being decremented and then an update to color occurs based on the state at the time of click, not when the state is updated. To visualize this on the original demo, log the state before and after setData.
onClick={() => {
setData({
payload: decrement({
state: state.colorCount,
length: colors.length
}),
name: "colorCount"
});
setData({
payload: colors[state.colorCount].type,
name: "color"
});
}}
Now, the same onClick calls the decrement method.
onClick={() => {
decrement({
name: "colorCount"
});
}}
The decrement method, moved to the context, just calls the dispatch with proper type and payload containing the name of the value to update.
const decrement = (payload) => {
dispatch({
type: ACTIONS.DECREMENT,
payload
});
};
Lastly, the reducer updates the states colorCount paired with its prefix color and shapeCount paired with its prefix shape
const reducer = (state, action) => {
// Verify which value we need to update along with its count
const isColor = action.payload.name.includes("color");
// Select the proper option in context
const options = isColor ? colors : shapes;
switch (action.type) {
case ACTIONS.INCREMENT:
// Increment the count for use in the value and type setting
const incrementedCount =
(state[action.payload.name] + 1) % options.length;
return {
...state,
// Set the new count
[action.payload.name]: incrementedCount,
// Set the new color or shape type
[isColor ? "color" : "shape"]: options[incrementedCount].type
};
case ACTIONS.DECREMENT:
// Decrement the count for use in the value and type setting
const decrementedCount =
(state[action.payload.name] - 1 + options.length) % options.length;
return {
...state,
// Set the new count
[action.payload.name]: decrementedCount,
// Set the new color or shape type
[isColor ? "color" : "shape"]: options[decrementedCount].type
};
default:
return state;
}
};
As far as updating the localStorage on update of a value, the easiest way is another useEffect dependent on the state values. Feel free to update the localStorage how and when you want, but for the purposes of keeping the state on reload the simplest approach was kept.
useEffect(() => {
localStorage.setItem("colorCount", JSON.stringify(state.colorCount));
localStorage.setItem("color", JSON.stringify(state.color));
localStorage.setItem("shapeCount", JSON.stringify(state.shapeCount));
localStorage.setItem("shape", JSON.stringify(state.shape));
}, [state.colorCount, state.color, state.shape, state.shapeCount]);
To the point made about contexts, this counter example does benefit from simplicity. The reducer can be used all within the App. Contexts are best used when passing down props to children becomes cumbersome.
What you've provided here is not sufficient for a minimum reproducible example. We can't offer much help if your problem is only happening "at times" -- please provide specific cases of what steps you take to obtain a specific problem.
Generally speaking, I think it might simplify your code to use two separate state variables. Context seems like overkill for this use case.
const [colorIdx, setColorIdx] = useState(0);
const [shapeIdx, setShapeIdx] = useState(0);
And, as a style note, it is usually a good idea to avoid inline function definitions. The following is much more readable, for example:
const incrementColorIdx = () => {
setColorIdx((colorIdx + 1) % colors.length);
}
...
<button onClick={incrementColorIdx}>Next</button>
When you push an array, data is pushed. However if you check in console.log, data is not imported. It seems to be a delay. Can you tell me why is this happening?
is there solution for this?
Expected console.log result showing input, however empty array shows and if you click checkbox again then input appears.
const [checked, setChecked] = useState<number[]>([])
const handleAddListToArray = (id: number) => {
console.log(checked)
if (setChecked.includes(id)) {
setChecked(checked.filter((item) => item !== id))
} else {
setChecked([...checked, id])
}
}
--- checkbox compornent ---
const [isChecked, setIsChecked] = useState(false)
const handleChange = () => {
setIsChecked(!isChecked)
handleAddListToArray(id)
}
<Checkbox checked={isChecked} onClick={() => handleChange()} />
when you push an array, data is pushed however if you check in
console.log data is not inported. it seems to be a delay can you tell
me why is this happening?
The state-setting functions are asynchronous. In other words, if you wrote:
const [foo, setFoo] = useState(0);
setFoo(1);
console.log(foo); // logs 0, NOT 1
you would see 0 logged, not 1 ... at least initially. However, there'd be a log entry below that would show 1.
This is because set* function don't change the value in the function, but they do cause the component to be re-rendered after, which means the function is run again, and now uses the correct value..
however empty array shows and if you click checkbox again then input
appears.
It's because of this code:
setIsChecked(!isChecked)
Initially you set isChecked to an array:
setChecked(checked.filter((item) => item !== id))
But then you changed it to !isChecked ... a boolean. Once you change it to a boolean, you can't change it back to an array.
You check the setState-function if it includes the input, on your fourth row of code:
if (setChecked.includes(id))
I believe you want to chech the checked-state instead, like so:
if (checked.includes(id))
Also, consider using the useState-callback when you mutate your state based on the previous one. So instead of:
setChecked(checked.filter((item) => item !== id))
try:
setChecked((prevState) => prevState.filter((item) => item !== id))
You can also use this when you setting your isChecked state. This ensure that you get your latest known state and avoids getting in some wierd states because React updates the state asynchronous.
Some suggestions
if (setChecked.includes(id)) {
should be (setChecked is a function)
if (checked.includes(id)) {
For setChecked(checked.filter((item) => item !== id))
better usage will be
setChecked(prevCheckedValues => prevCheckedValues.filter((item) => item !== id));
This way you will always get the current checked values when you do a setChecked.
Another way to use this is via a useCallback and pass the dependency array as [checked]
If you want to print checked values each time its changed you can use a useEffect (docs) with correct dependency array.
Usage example
useEffect(()=>{
console.log("Checked", checked);
}, [checked])
I have a table with checkboxes and I want to save selected checkboxes' ids in state. So here's my code.
Input looks like this (I use coreui so this is the inside of a table's scopedSlots):
selected: (item) => {
return (
<td style={{ width: '40px' }}>
<CInputCheckbox
className="mx-auto"
id={item.id}
onChange={(e) => handleSelect(e)}
/>
</td>
);}
And this is the rest:
const [selectedRows, setSelectedRows] = useState([]);
const handleSelect = (e) => {
const id = e.target.id;
const index = selectedRows.indexOf(id);
const rows = [...selectedRows];
if (index === -1) {
rows.push(id);
} else {
rows.split(index, 1);
}
setSelectedRows(rows);
};
And the weirdest thing happens - in the chrome's react devtools I see that the first id is being added to the selectedRows array and then when I select another row - the previous item in the array is being overwritten. When i console.log my selectedRows array it shows empty array always (even if I see in the devtools that there's one item). I have no idea what I'm doing wrong here.
There is not a lot of information to work with in your question, so I had to make many assumptions about how are they suppose to work, but this is what I got so far:
the handleSelect function has a little error when it comes to removing items from the list, your function is using split but I think what you meant was slice
I think the scopes of handleSelect and const [selectedRows, setSelectedRows] = useState([]); are wrong. For what I can see, they are at the same level of selected component, which will fail as they will only be able to keep track of one selected component at the time, and I think you are needing to keep track of multiple selected components.
Here is a working version of what I think you are trying to do
https://codepen.io/richard-unal/pen/eYWXXOo?editors=1111
If you need clarification on something, please let me know, I'm aware that I'm not the best explainer.
there are three drop down menus in the initial state.
after I select the first drop down the second drop down values gets loaded
after I select the second drop down values.
a new set of drop down loads.
when I select remove button of the second set.
it doesnt remove that set but it removes the first set.
when I debugged removeSelectedValue method there slices are happening correctly but its not updating in the updating
can you tell me how to pass the queryComponents values so that it will update in the UI.
can you tell me how to fix it.
so that in future I will fix it myself.
providing my relevant code snippet and sandbox below.
all my code is in demo.js
https://codesandbox.io/s/4x9lw9qrmx
removeSelectedValue = index => {
console.log("removeSelectedValue--->", index);
let seletedValues = this.state.queryComponents;
seletedValues.splice(index, 1);
console.log("spliced Values--->", seletedValues);
this.setState({ queryComponents: seletedValues });
};
render() {
let queryComp = this.state.queryComponents.map((value, index) => {
return (
<AutoCompleteComponent
key={index}
value={value}
index={index}
valueSelected={this.getSelectedValue}
removeSeleted={this.removeSelectedValue}
/>
);
});
return <div>{queryComp}</div>;
}
When you do let seletedValues = this.state.queryComponents;
you're creating a reference to that variable, instead of making a copy.
You need to make sure you replace your state with a new object/array for the re-render to happen.
Please try this:
removeSelectedValue = index => {
this.setState(prevState => ({
queryComponents: prevState.seletedValues.filter((a, i) => (i !== index));
});
};
That filter function is equivalent to the splice you were using, but returns a new array instead of modifying the original one.
On the other hand, I 'm passing setState a function that uses prevState making the code shorter.
See this gist for the complete picture.
Basically I will have this form:
When you click the plus, another row should appear with a drop down for day and a time field.
I can create the code to add inputs to the form, however I'm having trouble with the individual components (selectTimeInput is a row) actually updating their values.
The onChange in the MultipleDayTimeInput is receiving the correct data, it is just the display that isn't updating. I extremely new to react so I don't know what is causing the display to not update....
I think it is because the SelectTimeInput render function isn't being called because the passed in props aren't being updated, but I'm not sure of the correct way to achieve that.
Thinking about it, does the setState need to be called in the onChange of the MultipleDayTimeInput and the input that changed needs to be removed from the this.state.inputs and readded in order to force the render to fire... this seems a little clunky to me...
When you update the display value of the inputs in state, you need to use this.setState to change the state data and cause a re-render with the new data. Using input.key = value is not the correct way.
Using State Correctly
There are three things you should know about
setState().
Do Not Modify State Directly
For example, this will not re-render a
component:
// Wrong
this.state.comment = 'Hello';
Instead, use setState():
// Correct
this.setState({comment: 'Hello'});
The only place where you
can assign this.state is the constructor.
read more from Facebook directly here
I would actually suggest a little bit of a restructure of your code though. It's not really encouraged to have components as part of your state values. I would suggest having your different inputs as data objects in your this.state.inputs, and loop through the data and build each of the displays that way in your render method. Like this:
suppose you have one input in your this.state.inputs (and suppose your inputs is an object for key access):
inputs = {
1: {
selectedTime: 0:00,
selectedValue: 2
}
}
in your render, do something like this:
render() {
let inputs = Object.keys(this.state.inputs).map((key) => {
let input = this.state.inputs[key]
return (<SelectTimeInput
key={key}
name={'option_' + key}
placeholder={this.props.placeholder}
options={this.props.options}
onChange={this.onChange.bind(this, key)}
timeValue={input.selectedTime}
selectValue={input.selectedValue}
/>)
)}
return (
<div>
<button className="button" onClick={this.onAddClick}><i className="fa fa-plus" /></button>
{ inputs }
</div>
);
}
Notice how we're binding the key on the onChange, so that we know which input to update. now, in your onChange function, you just set the correct input's value with setState:
onChange(event, key) {
this.setState({
inputs: Immutable.fromJS(this.state.inputs).setIn([`${key}`, 'selectedTime'], event.target.value).toJS()
// or
inputs: Object.assign(this.state.inputs, Object.assign(this.state.inputs[key], { timeValue: event.target.value }))
})
}
this isn't tested, but basically this Immutable statement is going to make a copy of this.state.inputs and set the selectedTime value inside of the object that matches the key, to the event.target.value. State is updated now, a re-render is triggered, and when you loop through the inputs again in the render, you'll use the new time value as the timeValue to your component.
again, with the Object.assign edit, it isn't tested, but learn more [here]. 2 Basically this statement is merging a new timeValue value in with the this.state.inputs[key] object, and then merging that new object in with the entire this.state.inputs object.
does this make sense?
I modified the onChange in the MultipleDayTimeInput:
onChange(event) {
const comparisonKey = event.target.name.substring(event.target.name.length - 1);
const input = this.getInputState(comparisonKey);
input.selected = event.target.value;
input.display = this.renderTimeInput(input);
let spliceIndex = -1;
for (let i = 0; i < this.state.inputs.length; i++) {
const matches = inputFilter(comparisonKey)(this.state.inputs[i]);
if (matches) {
spliceIndex = i;
break;
}
}
if (spliceIndex < 0) {
throw 'error updating inputs';
}
this.setState({
inputs: [...this.state.inputs].splice(spliceIndex, 1, input)
});
}
The key points are:
// re render the input
input.display = this.renderTimeInput(input);
// set the state by copying the inputs and interchanging the old input with the new input....
this.setState({
inputs: [...this.state.inputs].splice(spliceIndex, 1, input)
});
Having thought about it though, input is an object reference to the input in the this.state.inputs so actually [...this.states.inputs] would have been enough??