Prerquisite
I'm fetching a list of accounts (Ajax request) which I display on page load (with a checkbox next to them). By default all the accounts are selected and added to the store (redux).
Goal
Add/remove accounts from array & store (redux) when checkbox are checked/unchecked:
checbox is checked --> add account to array & store
checkbox is unchecked --> remove account from array & store
Logic
I created two separate actions & reducers:
one to manage the checkbox status
one to manage the addition/removal of the account to the array &
store
When testing my code, it works fine at the beginning but eventually the accounts added/removed are not correct. The issue must be in savingAccount() but not sure what I'm doing wrong?
My code
Pre-populating data to the store in ComponentWillMount():
componentWillMount = () => {
let defaultAccount = this.props.account
let defaultCheckbox = this.props.checkboxStatus
for(let i =0; i < this.props.products.arrangements.items.length; i++){
const data = {}
data['id'] = i
data['isSelected'] = true
data['sortCode'] = this.props.products.arrangements.items[i].sortCode
data['accountNumber'] = this.props.products.arrangements.items[i].accountNumber
data['accountName'] = this.props.products.arrangements.items[i].accountName
defaultAccount = defaultAccount.concat(data)
const checkboxesArray = {}
checkboxesArray['id'] = i
checkboxesArray['checked'] = true
defaultCheckbox = defaultCheckbox.concat(checkboxesArray)
}
this.props.addAccount(defaultAccount)
this.props.toggleCheckbox(defaultCheckbox)
}
Displaying list of accounts from Ajax response (this.props.products.arrangements.items)
render() {
return (
<div>
{typeof(this.props.products.arrangements.items) !== 'undefined' &&
(Object.keys(this.props.account).length > 0) &&
(typeof(this.props.checkboxStatus) !== 'undefined') &&
(Object.keys(this.props.checkboxStatus).length > 0) &&
(Object.keys(this.props.products.arrangements.items).length > 0) &&
<div>
{this.props.products.arrangements.items.map((item,i) =>
<div className="accountContainer" key={i}>
<Checkbox
required
label={"Account Number "+item.accountNumber+" Product Name "+item.accountName}
value={true}
checked={this.props.checkboxStatus[i].checked === true? true: false}
onChange = {(e) => {
this.toggleChange(this.props.checkboxStatus[i])
this.saveAccount(e, i, item.accountNumber, item.accountName)
}}
/>
</div>
)}
</div>
}
</div>
)
}
Updating isSelected value when checkbox is checked/unchecked:
saveAccount = (e, i, accountNumber, productName) => {
const data = {};
data['id'] = i
data['accountNumber'] = accountNumber
data['productName'] = productName
if(this.props.checkboxStatus[i].checked === true){
let accountArray = Array.from(this.props.account)
accountArray[i].isSelected = true
this.props.addAccount(accountArray)
}
else {
let accountArray = Array.from(this.props.account)
accountArray[i].isSelected = false
this.props.addAccount(accountArray)
}
}
Reducer
function Eligible(state = { products: {}, account: [], checkboxStatus: [] }, action){
switch (action.type){
case ADD_PRODUCTS:
return {
...state,
products: action.data
}
case ADD_ACCOUNT:
return {
...state,
account: action.data
}
case TOGGLE_CHECKBOX:
return {
...state,
checkboxStatus: action.data
}
default:
return state
}
}
Actions
export const ADD_PRODUCTS = 'ADD_PRODUCTS'
export const ADD_ACCOUNT = 'ADD_ACCOUNT'
export const TOGGLE_CHECKBOX = 'TOGGLE_CHECKBOX'
export function addProducts(data){
return {type: ADD_PRODUCTS, data}
}
export function addAccount(data) {
return { type: ADD_ACCOUNT, data}
}
export function toggleCheckbox(data) {
return { type: TOGGLE_CHECKBOX, data}
}
Updating checkbox status:
toggleChange = (checkbox) => {
let toggleCheckbox = this.props.checkboxStatus
toggleCheckbox[checkbox.id].checked = !checkbox.checked
this.props.toggleCheckbox(toggleCheckbox)
}
I think the asynchronicity of this.setState is probably causing an issue.
this.state contains both accounts and checkboxes:
this.state = {
accounts: [],
checkboxes: []
}
In your change event handler, you call two functions:
onChange = {(e) => {
this.toggleChange(this.props.checkboxStatus[i])
this.saveAccount(e, i, item.accountNumber, item.accountName)
}}
First toggleChange:
toggleChange = (checkbox) => {
let toggleCheckbox = [...this.state.checkboxes];
toggleCheckbox[checkbox.id].checked = !checkbox.checked
this.setState({
checkboxes: toggleCheckbox
})
this.props.toggleCheckbox(this.state.checkboxes)
}
You're updating the checkboxes property of the state (via this.setState) - all good there. But on the last line, you're passing this.state.checkboxes out. Since this.setState is async, this will likely not reflect the changes you just made (you could send toggleCheckbox instead).
The next function called in the event handler is saveAccount, which contains (partially):
const addAccountState = this.state
if(this.props.checkboxStatus[i].checked === true){
addAccountState.accounts = addAccountState.accounts.concat(data)
this.setState(addAccountState)
this.props.addAccount(addAccountState.accounts)
}
Here you're taking the current value of this.state (which may be old due to the async setState). You update the .accounts property of it, then send the whole thing (which includes .accounts and .checkboxes) to this.setState.
Since the .checkboxes state may have been old (the previous this.setState may not have fired yet), this would queue up the old .checkboxes state to overwrite the new state you tried to save in toggleChange().
A quick and dirty fix there could be to use this.setState({accounts: addAccountState.accounts}) instead, but there may be other issues floating around too (like the modifying of this.state properties directly).
Because setState is asynchronous, subsequent calls in the same update
cycle will overwrite previous updates, and the previous changes will
be lost.
Beware: React setState is asynchronous!
Regarding the separation of store and state... one option might be to not store the checkboxes separately at all, but rather compute them based on which accounts are selected.
It will depend on the needs of your application of course, so I'll be making a few assumptions for the sake of example...
Your application needs a list of selected accounts
Your component needs to show a list of all accounts
Your component has checkboxes for each account: checked = selected = part of application's 'selected accounts' list.
In this case, I would have the list of selected accounts passed in via props.
Within the component I would have the list of all accounts in the local state (if your 'all accounts' list is passed in via props already, then just use that - no local state needed).
Within the render function of the component, I would compute whether the checkbox should be checked or not based on whether or not the account exists in the 'selected accounts' list. If it exists, it's checked. If not, not checked.
Then when the user clicks to check/uncheck the box, I would dispatch the function to add or remove the account from the 'selected accounts' list, and that's it. The account would be added or removed, which would cause your props to update, which would re-render your component, which would check or uncheck the boxes as appropriate.
That may not jive exactly with your particular application's needs, but I hope it gives you some ideas! :)
Related
I have a numeric select, when I select a number the component is rendered N times, the component has several checkboxes inside if I activate one the others are activated and that should not happen
Annex code in sandbox
https://codesandbox.io/s/polished-hill-li6529?file=/src/App.js
I believe that issue here is that when React tries to update the state, it can't be sure of which element in the array to update, so it updates all of them to be safe.
One possible solution, with minimum code changes is to transform the state from an array to an object (having the index as key), so that when you set the state you can specify which key should be updated.
The code changes that are needed here are the following:
On the state initialisation (App.js):
const [wizard, setWizard] = useState({});
On wizard initialisation (App.js):
for (let count = 0; count < sizeSelect; count++) {
list[count] = { ...obj, id: count + 1, nameTable: `Users ${count}` }
}
On rendering the wizard (App.js):
{Object.values(wizard).map((service, index) => (
<Wizard .... />
))}
On handleChange() function (Wizard.js):
const handleChange = (code, value) => {
const auxWizard = {
...wizard,
[index]: {
...wizard[index],
apiServices: {
...wizard[index].apiServices,
[code]: {
...wizard[index].apiServices[code],
active: !value
}
}
}
};
setChange(auxWizard);
};
I've tried to find a solution to this, but nothing seems to be working. What I'm trying to do is create a TreeView with a checkbox. When you select an item in the checkbox it appends a list, when you uncheck it, remove it from the list. This all works, but the problem I have when I collapse and expand a TreeItem, I lose the checked state. I tried solving this by checking my selected list but whenever the useEffect function runs, the child component doesn't have the correct parent state list.
I have the following parent component. This is for a form similar to this (https://www.youtube.com/watch?v=HuJDKp-9HHc)
export const Parent = () => {
const [data,setData] = useState({
name: "",
dataList : [],
// some other states
})
const handleListChange = (newObj) => {
//newObj : { field1 :"somestring",field2:"someotherString" }
setDataList(data => ({
...data,
dataList: data.actionData.concat(newObj)
}));
return (
{steps.current === 0 && <FirstPage //setting props}
....
{step.current == 3 && <TreeForm dataList={data.dataList} updateList={handleListChange}/>
)
}
The Tree component is a Material UI TreeView but customized to include a checkbox
Each Node is dynamically loaded from an API call due to the size of the data that is being passed back and forth. (The roots are loaded, then depending on which node you select, the child nodes are loaded at that time) .
My Tree class is
export default function Tree(props) {
useEffect(() => {
// call backend server to get roots
setRoots(resp)
})
return (
<TreeView >
Object.keys(root).map(key => (
<CustomTreeNode key={root.key} dataList={props.dataList} updateList={props.updateList}
)))}
</TreeView>
)
CustomTreeNode is defined as
export const CustomTreeNode = (props) => {
const [checked,setChecked] = useState(false)
const [childNodes,setChildNodes] = useState([])
async function handleExpand() {
//get children of current node from backend server
childList = []
for( var item in resp) {
childList.push(<CustomTreeNode dataList={props.dataList} updateList={props.updateList} />)
}
setChildNodes(childList)
}
const handleCheckboxClick () => {
if(!checked){
props.updateList(obj)
}
else{
//remove from list
}
setChecked(!checked)
}
// THIS IS THE ISSUE, props.dataList is NOT the updated list. This will work fine
// if I go to the next page/previous page and return here, because then it has the correct dataList.
useEffect(() => {
console.log("Tree Node Updating")
var isInList = props.dataList.find(function (el) {
return el.field === label
}) !== undefined;
if (isInList) {
setChecked(true);
} else {
setChecked(false)
}
}, [props.dataList])
return ( <TreeItem > {label} </TreeItem> )
}
You put props.data in the useEffect dependency array and not props.dataList so it does not update when props.dataList changes.
Edit: Your checked state is a state variable of the CustomTreeNode class. When a Tree is destroyed, that state variable is destroyed. You need to store your checked state in a higher component that is not destroyed, perhaps as a list of checked booleans.
So in my recipe App, users are able to mark or unmark recipes as their favorite.
The only thing I can't wrap my head around is How to make it instant. my current code supports makes a post call to mark the recipe as favorite but you see the change of icon (i.e the filled one) they have to refresh the page.
I do need some suggestion on how can I make it work on the click.
Here is my code:
class CuisineViewById extends Component {
constructor(props) {
super(props);
this.state = {
user: {},
access_token: '',
};
this.toggleFavorite = this.toggleFavorite.bind(this);
}
componentDidMount() {
this.props.getUser(() => {
this.props.getAccessToken(this.props.user.profile.sub, () => {
console.log(this.props.user);
this.props.getCuisineById(this.props.match.params.id, this.props.accessToken);
this.props.getFavoriteRecipes(this.props.accessToken);
});
});
}
toggleFavorite(userID, recipeID, marked) {
const userpreference = {
userid: userID,
recipe: recipeID,
favorite: marked
};
axios
.post('/api/userpreference', userpreference, {
headers: {'access_token': this.props.access_token}
})
.then(res => console.log(res));
}
displayFavorite = recipeId => {
let favoriteRecipes = this.props.userPreferred;
for (var i = 0; i < favoriteRecipes.length; i++) {
if (favoriteRecipes[i].recipe === recipeId) {
return true;
} else {
}
}
};
render() {
const that = this;
const {user} = this.props;
const {cuisine} = this.props;
return (
<CuisineTileHeading
label={cuisine.label}
totalNoRecipes={cuisine.totalRecipes]}
key={cuisine.id}
>
{cuisine.recipes && cuisine.recipes.map(function(asset, index)
{
let marked = recipe.isFavorite ? 'no' : 'yes';
return (
<RecipesCards
title={recipe.title}
description={recipe.description}
chef={recipe.owner}
lastUpdated={recipe.lastUpdated}
recipeType={recipe.assetType}
key={'RecipesCard' + index}
thumbnail={recipe.thumbnailBase64}
recipeId={recipe.id}
cuisine={cuisine}
favorite={that.displayFavorite(recipe.id)}
toggleFavorite={() =>
that.toggleFavorite(userId, recipe.id, marked)
}
/>
);
})}
</CuisneTileHeading>
)
}
}
const mapStateToProps = state = ({
cuisine : state.cuisine.cuisne,
user: state.user.user,
userPreferred: state.recipe.userPrefered,
accessToken: state.asset.accessToken
)}
In my component did mount, I am calling functions to get user information, then access token and then cuisines and then user favorite recipes.
toggleFavorite is the function that makes a recipe favorite or not favorite.
displayFavorite is a function that return either true or false is recipe id matches to the recipe ID store in userpreference object.
Right now, you compute that "this recipe is favorite" from a function that returns true or false.
ReactJS has no way to automatically trigger a re-rendering of the favorite icon since it is not linked to the recipe's state at all.
If you put "isFavorite" in the recipe's state and toggle that to true or false with the onClick event, which will change the recipe's state value for "isFavorite", React should know to call a re-render on the recipe card's icon ... then you just make sure it outputs the HTML for a filled icon when true and empty icon when false. React will know to re-render all DOM elements linked to that "slice" of the state, "isFavorite recipe" in this case.
TL;DR: leverage React's state concept instead of computing if the recipe is favorited by the user through a function which does not modify the state, since re-renders are done by React when the state changes.
I have the following reducer in my React app:
const initialState = {
genderRadio : false,
ageRadio : false
}
const reducer = ( state = initialState , action ) => {
switch(action.type) {
case "VALI_RADIO_INP":
console.log(action.payload.target);
return state
}
return state;
}
export default reducer;
action.payload is basically the event object that is passed to the reducer, like so from my component:
validateRadioInput : (e) => dispatch({ type: 'VALI_RADIO_INP' , payload : e })
What I would like to do in my reducer is check if the input element has been checked or not and update the state. How do I using the event object check if a element is checked or not checked?
NOTE::-
Before integrating redux I was checking if the checkbox is checked calling a method that resided right inside my component like so:
Array.from(document.getElementsByName('customer_gender')).some( (elem , idx) => { return elem.checked })
But of course I can't use this anymore; any suggestions on how I can validate the checkbox in my reducer using the event object?
First set attribute name to your checkbox element like so:
<input type="checkbox" name="genderRadio"/>
or:
<input type="checkbox" name="ageRadio"/>
And modify your code that set correct piece of state depending on the attribute name of checkbox element.
Example:
const reducer = ( state = initialState , action ) => {
switch(action.type) {
case "VALI_RADIO_INP":
console.log(action.payload.target);
return { ...state, [payload.target.name]: payload.target.checked };
}
return state;
}
export default reducer;
How do i using the event object check if a element is checked or not checked ?
You shouldn't do that. Your Redux reducers shouldn't be coupled to the DOM if you can help it. Though, it is possible that you can traverse the DOM from the event's target, if you're using React you shouldn't be depending on the DOM at all.
One way to do it is to get your component to have a data representation of the view. This could be your React component's state. Or, you could grab it from the DOM if you're not using React with something like this:
validateRadioInput: (e) => {
const checkedArr = Array.from(document.getElementsByName('customer_gender'))
.map(elem => elem.checked);
return dispatch({
type: 'VALI_RADIO_INP',
payload: checkedArr,
});
}
// reducer
const reducer = ( state = initialState , action ) => {
switch(action.type) {
case "VALI_RADIO_INP":
const valid = action.payload.some(checked => checked);
return { ...state, valid };
}
return state;
}
Ultimately, though, I don't agree with the concept of doing form validation sort of logic in Redux -- just do it in the component and then dispatch some action to Redux if it's valid. Redux shouldn't have to deal with every nitty-gritty state in your application; just state that affects multiple components in potentially complex ways.
Also, note that you may be trying to fix an HTML problem in JS since you can only check one radio button at a time, anyway, and you could just make the HTML have a required field. See HTML5: How to use the "required" attribute with a "radio" input field
So I have been struggling with getting this section of the application working 100% as can be seen with these related questions:
Method renders correctly when triggered one at a time, but not using _.map in React-Redux container
Object passed into Redux store is not reflecting all key/values after mapStateToProps
So the setup is this... a bunch of buttons are dynamically generated based on the number of data "cuts" for a specific item (basically different ways of looking at the data like geography, business segment, etc.). The user can select one button at a time, click a Select All. This will retrieve the data related to the cut from the server and generate a table below the buttons.
One-at-a-time selections is working, select all is working, clear all is working. However, what I am trying to setup now is if the person clicks the same button again, it toggles that data point off.
This is where I was left after one of my previous questions:
onCutSelect(cut) {
this.setState(
({cuts: prevCuts}) => ({cuts: {...prevCuts, [cut]: cut}}),
() => this.props.bqResults(this.state.cuts)
);
}
Works fine for one at a time selections and the Select All (this function is called via a map from a different function).
Modified to this, which I was hoping would toggle the data point off:
onCutSelect(cut) {
this.setState(
({cuts: prevCuts}) => (
this.state.cuts.hasOwnProperty(cut)
?
delete this.state.cuts[cut]
:
{cuts: {...prevCuts, [cut]: cut}}),
() => this.props.bqResults(this.state.cuts)
);
What I would think should be happening, is checks if the key is there and if it is to delete it which will toggle the button off. And it does, it changes the button status to unselected.
However, what I would think should also happen, is since this.state.cuts is being modified, it will send the new this.state to the this.props.bqResults action. While the button is toggling off, it still shows the data related to the cut, so that store is not being updated for whatever reason. How should I be handling this?
Here is the remainder of the related code:
// results.js actions
export function bqResults(results) {
console.log(results); // shows all of the selected cuts here
return function(dispatch) {
dispatch({
type: FILTER_RESULTS,
payload: results
})
}
}
// results.js reducer
import {
FILTER_RESULTS
} from '../actions/results';
export default function(state = {}, action) {
switch(action.type) {
case FILTER_RESULTS:
console.log(action.payload); //prints out all the cuts
return {
...state,
filter_results: action.payload
}
default:
return state;
}
return state;
}
const rootReducer = combineReducers({
results: resultsReducer,
});
export default rootReducer;
onCutSelect(cut) {
this.setState(
({cuts: prevCuts}) => {
if (cuts.hasOwnProperty(cut)) {
const newCut = {...this.state.cuts}
delete newCut[cut]
return newCut
} else {
return {cuts: {...prevCuts, [cut]: cut}}
}
},
() => this.props.bqResults(this.state.cuts)
);
}
A few things. First, don't mutate state directly like that. Using delete removes the index number of the existing this.state.cuts. Perform this operation in a way that creates a completely new array when assigning your new value. I use the spread operator for this.
Also, when you return a delete operation, it's not returning what the array is after using delete. It returns delete's return value, which in this case is a boolean.
function delIdx(arr, idx) {
return delete arr[idx]
}
console.log(delIdx([1,3,4], 3))