React different state properties getting bound - javascript

I am trying to implement a table in react where the user can edit individual rows by clicking the edit button on a row and then submit once he has made his change. I have say two components App.js and its child Table.js to implement this.
The way I thought of doing this initially was letting each of this component have their own state for rows and then the Table component reads from the props send to it by parent initially and only change the parent rows when users submits the change as oppose to onChange event. But I've read that reading props into state is an anti-pattern.
So decided to have everything in the parent by having two values for row (oldrows,newrows). And using them to maintain state instead, This is the design I came up with :
But what happens is whenever I click cancel the oldRows get bound to the newRows, here is a codePen example I put up:
https://codepen.io/snedden-gonsalves/pen/zYOVMWz
handleChangeRowInput = (event, keyValue) => {
let keyVals = [...this.state.newValuesArray];
keyVals[this.state.editIndex][keyValue] = event.currentTarget.value;
this.setState({
newValuesArray: keyVals
})
}
handleCancelRowInput = () => {
this.setState({
newValuesArray: [...this.state.oldValuesArray],
editIndex: -1
})
console.log('array', this.state.newValuesArray)
}
handleSubmitRowInput = () => {
this.setState({
oldValuesArray: [...this.state.newValuesArray],
editIndex: -1
})
}
In the codePen example if you enter a new value then cancel and then try adding a new value again the the old values and new values get bound.
I tried using lodash deepClone but it didn't work out, not sure why this is happening.
Also if you could comment on what is the best way to design this in react that would be awesome as I am very new to react and just trying to learn ..

I didn't find any issue after the cancel function. For me, the issue was coming up after I called the save function.
After clicking on the save button and then editing again, the old values and new values were get bound.
The handleSubmitRowInput function should create a new array for the oldValuesArray using the cloneDeep function
handleSubmitRowInput = () => {
this.setState({
oldValuesArray: _.cloneDeep(this.state.newValuesArray),
editIndex: -1
})
}

Related

Getting initial useState list values when returning from a sub componet to save data instead of current state

I have recreated my problem in a simplified code example.
Follow these steps to reproduce it:
Click on one of the three list items to edit the value in the off-canvas box.
Change the value and click the save button.
Select a different item to edit & save.
Note: the original edited item has reverted back to its initial state.
I have the console.logging in the save method to show that the list(state) is not the current (visible) version but the initial state.
Sandbox code example
I have an inelegant solution(workaround) that I will put as an answer but it doesn't explain what or why this is happening. I have 3 off-canvas editors like this on my page & one does work as expected but the other two loose state when calling their save functions in the parent.
Here is my bad (ugly) workaround:
Pass the state object (myList) to the child in props object.
App.js
function open(e, item, itemIndex) {
setPropArgs({ ...propArgs, editThis: item, index: itemIndex, show: true, itemList: myList });
}
Return the list back to the parent in save method call & use the passed variable instead of state.
MyEditor.js
<Button type="button" onClick={(e) => props.save(edit, props.index, props.itemList)}>
Save
</Button>
App.js
function save(edit, index, itemList) {
// console.log(myList);
const newList = [...itemList];
newList[index] = edit;
setMyList(newList);
setPropArgs({ ...propArgs, show: false });
}
This is bad because MyEditor shouldn't need to know anything about the parent, it doesn't read or edit the list at all & there shouldn't be a need to pass around a copy of the state that could become out of date (if I wasn't blocking with the canvas).
Okay... I found the answer that I was looking for.
Thanks to Marco Nisi for this Answer to a very similar question that has not got much attention
My forked code solution here
The solution is to move the callback (save function) into be created when the canvas is shown instead of when the functional component is initialized creating a fresher clone of the state. I can still see this as being problematic if you have an example where the state is being updated in the background or if the off-canvas is not blocking other edits. Note: You don't need the callback defined in the open function but it does need to be added(replaced) to the props object there.
function open(e, item, itemIndex) {
save = (edit, index) => {
console.log(myList);
const newList = [...myList];
newList[index] = edit;
setMyList(newList);
setPropArgs({ ...propArgs, show: false });
};
setPropArgs({
...propArgs,
editThis: item,
index: itemIndex,
show: true,
save: save
});
}

Call function only after multiple states have completed updating

Logic:
I have a dialog for converting units. It has two stages of choice for the user: units to convert from and units to convert to. I keep this stage as a state, dialogStage, for maintainability as I'm likely going to need to reference what stage the dialog is in for more features in the future. Right now it's being used to determine what action to take based on what unit is clicked.
I also have a state, dialogUnits, that causes the component to rerender when it's updated. It's an array of JSX elements and it's updated via either foundUnitsArray or convertToUnitsArray, depending on what stage the dialog is at. Currently both states, dialogStage and dialogUnits, are updated at the same moment the problem occurs.
Problem:
When choosing the convertTo units, displayConversionTo() was still being called, as though dialogStage was still set to 'initial' rather than 'concertTo'. Some debugging led to confusion as to why the if (dialogStage == 'initial') was true when I'd set the state to 'convertTo'.
I believe that my problem was that the dialogStage state wasn't updated in time when handleUnitClick() was called as it's asynchronous. So I set up a new useEffect that's only called when dialogStage is updated.
The problem now is that the dialog shows no 'convertTo' units after the initial selection. I believe it's now because dialogUnits hasn't updated in time? I've swapped my original problem from one state not being ready to another state not being ready.
Question
How do I wait until both states are updated before continuing to call a function here (e.g. handleUnitClick()?).
Or have I mistaken what the problem is?
I'm new to react and, so far, I'm only familiar with the practice of state updates automatically rerendering a component when ready, unless overridden. Updating dialogUnits was displaying new units in the dialog until I tried to update it only when dialogStage was ready. It feels like an either/or situation right now (in terms of waiting for states to be updated) and it's quite possible I've overlooked something more obvious, as it doesn't seem to fit to be listening for state updates when so much of ReactJs is built around that already being catered for with rerenders, etc.
Component code:
function DialogConvert(props) {
const units = props.pageUnits;
const [dialogUnits, setDialogUnits] = useState([]);
const [dialogStage, setDialogStage] = useState('initial');
let foundUnitsArray = [];
let convertToUnitsArray = [];
units.unitsFound.forEach(element => {
foundUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});
useEffect(() => {
setDialogUnits(foundUnitsArray);
}, []);
useEffect(() => {
if (dialogStage == "convertTo") {
setDialogUnits(convertToUnitsArray);
}
}, [dialogStage]);
function handleClickClose(event) {
setDialogStage('initial');
props.callbackFunction("none");
}
function handleUnitClick(homogName) {
if (dialogStage == "initial") {
// getConversionChoices is an external function that returns an array. This returns fine and as expected
const choices = getConversionChoices(homogName);
displayConversionTo(choices);
} else if (dialogStage == "convertTo") {
// Can't get this far
// Will call a function not displayed here once it works
}
}
function displayConversionTo(choices) {
let canConvertTo = choices[0]["canconvertto"];
if (canConvertTo.length > 0) {
canConvertTo.forEach(element => {
convertToUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});
setDialogStage('convertTo');
}
}
return (
<React.Fragment>
<div className="dialog dialog__convertunits" style={divStyle}>
<h2 className="dialogheader">Convert Which unit?</h2>
<div className='js-dialogspace-convertunits'>
<ul className="list list__convertunits">
{dialogUnits}
</ul>
</div>
<button className='button button__under js-close-dialog' onClick={handleClickClose}>Close</button>
</div>
</React.Fragment>
)
}
So, there are some issues with your implementations:
Using non-state variables to update the state in your useEffect:
Explanation:
In displayConversionTo when you run the loop to push elements in convertToUnitsArray, and then set the state dialogStage to convertTo, you should be facing the issue that the updated values are not being rendered, as the change in state triggers a re-render and the convertToUnitsArray is reset to an empty array because of the line:
let convertToUnitsArray = [];
thus when your useEffect runs that is supposed to update the
dialogUnits to convertToUnitsArray, it should actually set the dialogueUnits to an empty array, thus in any case the updated units should not be visible on click of the initial units list.
useEffect(() => {
if (dialogStage == "convertTo") {
// as your convertToUnitsArray is an empty array
// your dialogue units should be set to an empty array.
setDialogUnits(convertToUnitsArray)
}
}, [dalogStage]);
You are trying to store an array of react components in the state which is not advisable:
http://web.archive.org/web/20150419023006/http://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html#what-components-should-have-state
Also, refer https://stackoverflow.com/a/53976730/10844020
Solution: What you can do is try to save your data in a state, and then render the components using that state,
I have created a code sandbox example how this should look for your application.
I have also made some changes for this example to work correctly.
In your code , since you are passing units as props from parent, can you also pass the foundUnitsArray calculated from parent itself.
setDialogUnits(props.foundUnitsArray);
and remove the below operation,
units.unitsFound.forEach(element => {
foundUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});

setState not clearing selected values - React

I have a button that submits selected values to api. Once this has been submitted I am then trying to turn button state to disable and rest the values selected back to original state before nay where selected.
This is what I am doing on upload handle:
handleStatusEditsUpload = () => {
const { value, status } = this.state;
this.setState({
value: selected,
status: {}
});
};
In my real version locally status is clearing, status is used when changing all values at the same time by clicking the header title, a dialog appears to change all values in that column.
The main one I am having trouble is with the value. Value is populated with a new array that looks at table cell and row.
Here is demo to my project: https://codesandbox.io/s/50pl0jy3xk
Why isnt the state changing? any help appreciated as always.
What is happening is that you are mutating state in your "handleValue" method.
const newValue = [...this.state.value]; // this holds reference
newValue[rowIdx][cellIdx] = val; // so that here your state is mutated ( and const "selected" with it)
In the long term you probably should change your data structure a bit, so it would be easier to merge updates in to your state value. But a quick fix would be to clone the state value before mutating it:
handleValue = (event, val, rowIdx, cellIdx) => {
const newValue = _.cloneDeep(this.state.value); // no reference anymore
newValue[rowIdx][cellIdx] = val; // update the cloned value
this.setState({
value: newValue
});
};
I just ran your code in the sandbox you provided and it's throwing errors when you click the confirm button (trying to spread non-iterable). Once that is corrected, the state updates correctly. See my fork below:
https://codesandbox.io/s/385y99575m
I've left in a few console logs so you can see the component state updating when your onClick fires.
Why are you passing in your props to the handleStatusEditsUpload method? It doesn't take an argument. Was this just part of your debugging process?

React.js - how do I call a setState function after another one has finished

I must be missing something obvious here. I have a to-do list app which uses a function for creating new lists. Once createList is called I want to then highlight the list by setting its selected prop to true So below are the two methods for doing this. I'm trying to call one after the other. Both of them modify state using the appropriate callback that uses prevState, yet for whatever reason createList does not set the new list in state before toggleSelected gets called, and so listName is undefined in toggleSelected. Is there anyway to ensure the new list object is set in state before calling toggleSelected? I should probably be using Redux but I didn't want to get into it for my first React app.
createList = (listName) => {
const lists = {...this.state.lists};
lists[listName] = {
listName: listName,
selected: false,
todos: {}
};
this.setState(prevState => {
return {lists: prevState.lists};
});
};
toggleSelected = (listName) => {
let selected = this.state.lists[listName].selected;
selected = !selected;
this.setState(prevState => {
return {
bookLists: update(prevState.lists, {[listName]: {selected: {$set: selected}}})
};
});
};
Both methods are called in another component like so after an onSubmit handler with the new list name being passed in:
this.props.createList(newListName);
this.props.toggleSelected(newListName);
PS - If you're wondering what's up with update(), it's from an immutability-helper plugin that allows for easily setting nested values in a state object(in this case, state.lists[listName].selected)--another reason I probably should have gone with Redux.
PPS - I realize I can just set the new list's selected prop to true from the start in creatList but there's more to the app and I need to set it after creation.
Don't do what you're doing in toggleSelected right now, instead toggle the selected flag in your list (without extracting it) and then let your component know you updated the lists data by rebinding the resulting object:
class YourComponent {
...
toggleSelected(listName) {
let lists = this.state.lists;
let list = lists[listName];
list.selected = !list.selected;
this.setState({ lists });
}
..
}
Then make sure that in your render function, where you create the UI for each list, you check whether selected is true or false so you can set the appropriate classNames string.
(Also note that in your code, you used selected = !selected. That isn't going to do much, because you extracted a boolean value, flipped it, and then didn't save it back to where it can be consulted by other code)
The problem is not in the second setState function. It is at the first line of the toggleSelected() method.
When the toggleSelected() method is executed, the first setState haven't been executed.
The flow of the your code is:
createList();
toggleSelected();
setState() in createList();
setState() in toggleSelected();
Solution 1:
Use await and async keywords
Solution 2:
Use redux

React adding child outside render()

The title could be clearer, but this is really the best I could come up with, sorry.
So, I am trying to create a filtered table component in React. However, I want the filter to be defined independently from the definition of the table itself. So, here is what I am doing.
I created a Filter component:
var Filter = React.createClass({
handleChange : function (value) {
this.props.updateTable(this.props.columnName,value);
},
render : function () {
//an input that will report its value to this.handleChange
}
});
Then, I create a Table component:
var Table = React.createClass({
filterChanged : function (column, value) {
//this will be wired as a updateTable prop for the Filter
},
render : function () {
//I am trying not to define a filter here,
//I am trying to use the previously-defined Filter component.
//I want the Table component to remain generic and re-usable,
//with optional filters.
var thisComponent = this;
//I can have as many filters as I want.
var filterToRender = React.Children.map(this.props.children, function (child) {
var filterUI;
if (child.type.displayName === 'Filter') {
filterUI = React.cloneElement(child, {updateTable : thisComponent.filterChanged});
}
return (<div>{filterUI}</div>);
});
//along with the rest of the table UI,
return (<div>
<table>bla bla</table>
{filterToRender}
</div>);
}
});
Then, in my main page, I render it like this:
ReactDOM.render( (<Table>
<Filter columnName='status'></Filter>
</Table>), document.getElementById('appHolder'));
It renders fine. The change functions also seem to be wired fine. However, I find that every time the filter value is changed, it triggers the Table's filterChanged method, increasing number of times. First change, it will trigger 2 times; second change, 6 times; 3rd change, 14 times.
Weird and uncanny. What am I doing wrong here?
The above procedure for doing things is correct as far as React is concerned. The bug I was getting was due to another framework (Materialize) that uses a jquery plugin to initialize some components. Since it mutates the DOM, I need to manually make sure the onChange events are properly attached to the right node.
Fwiw, I was attaching the onChange to this.handleChange in a drop-down list in the ComponentDidMount and ComponentDidUpdate of the Filter component. Removing initialization from ComponentDidUpdate, solved the problem. This, because multiple instances of handleChange were being bound to the onChange event each time the component updated.

Categories