Background
I have added a toggle button in my Redux app that allows someone to toggle whether they like a specific tv show or not. When I click the button once, I can toggle it on (make the button active), but when I click it again the value does not revert back to its original value (the button is not active).
What I've Already Tried
The semantic-ui-react documentation gives an example of usuage, but I am not sure how to incorporate this logic into my current code since I am already using a handleWatchlist callback for another change to state.
I know the problem lies in the way I am handling the value being passed to the active property of my button. Here, I am passing watchlistValue which is always either true or false.
<Popup
trigger={
<Button
toggle
active={watchlistValue}
onClick={(_) => this.handleWatchlist(programId,
watchlistValue)}
icon='heart'
/>}
content="Add to Watchlist."
size='tiny'/>
Here is my current handleWatchlist method.
handleWatchlist = (programId, watchlistValue) => {
this.props.toggleWatchlist(programId, watchlistValue)
}
Here is how I have defined the program whose watchlist value (the heart button) I wish to toggle.
let program = this.props.program ? this.props.program : this.props.programs[this.props.match.params.id - 1]
let programId = this.props.program ? this.props.program.id : null
let watchlistValue = this.props.program ? this.props.program.watchlist : null
Here is a link to the whole file if you need to see the all on one page.
The toggle function already updates the value of my watchlist item in the database. In the DOM, clicking it makes it go active once. Unfortunately, it will not toggle off (to a false value).
Thank you in advance for your time and please let me know if I need to provide additional details.
In your reducer change
let programToBeToggled = copyOfPrograms.find(program => program.id === action.id);
programToBeToggled.watchlist = !action.watchlist;
to
let programIndex = copyOfPrograms.findIndex(program => program.id === action.id);
copyOfPrograms[programIndex].watchlist = !copyOfPrograms[programIndex].watchlist;
copyOfPrograms.find is creating a new object which you are toggling the watchList value of. However that does not change the boolean in copyOfPrograms which you are then returning from the reducer.
Full case with console logs to help spot bug:
case 'TOGGLE_WATCHLIST':
/*
Make a deep copy of our current state by using JSON.stringify to turn our array of programs into a string.
After we have created the stringifiedPrograms, we then use JSON.parse to turn it back into a brand new array of objects.
We then take our copyOfPrograms and find the specific program that we want to update (here we find it by id).
After isolating that program, we update the value of watchlist.
Then we return a copy of state, with the program key set to our copyOfPrograms array of objects.
Updating my programToBeToggled watchlist value still updates it in the copyOfPrograms array.
*/
console.log('state.programs:');
console.log(state.programs);
let stringifiedPrograms = JSON.stringify(state.programs);
console.log('stringifiedPrograms:');
console.log(stringifiedPrograms);
let copyOfPrograms = JSON.parse(stringifiedPrograms);
console.log('copyOfPrograms:');
console.log(copyOfPrograms);
let programIndex = copyOfPrograms.findIndex(program => program.id === action.id);
copyOfPrograms[programIndex].watchlist = !copyOfPrograms[programIndex].watchlist;
console.log('copyOfPrograms after switcheroo:');
console.log(copyOfPrograms);
return {...state, programs: copyOfPrograms};
Your action is missing the watchlist key, leading the toggle value to always be true.
From actions/toggleWatchlist.js:
// dispatched action has a `type` and an `id`
.then(res => dispatch({type: 'TOGGLE_WATCHLIST', id: programId}))
from reducers/programReducer.js:
// action.watchlist is undefined so !action.watchlist is always true
programToBeToggled.watchlist = !action.watchlist
Be careful with when and where you're toggling the values as well, you should only toggle them once, either in the action or the reducer so make sure you don't fix the above issue only to toggle in both action and reducer, negating the toggle.
Related
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
});
}
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} />);
});
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
})
}
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?
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