In my application I have component like that:
const MyComponent = props => {
const { attrOneDefault, attrTwoDefault, formControl } = props;
const [inputValue, setInputValue] = useState({
attr_one: attrOneDefault,
attr_two: attrTwoDefault
});
const getValue = ( attr ) => {
return inputValue[attr];
}
const setValue = ( attr, val ) => {
if( attr === 'attr_one' ) {
if( val === 'bar' && getValue(attr) !== 'foo' ) {
val = 'foo bar';
}
}
setInputValue( {...inputValue, [attr]: val} );
}
useEffect( () => {
if( formControl ) {
Object.keys(inputValue).forEach( attribute => {
formControl.subscribeToValueCollecting( attribute, () => {
return getValue(attribute);
});
formControl.subscribeToValueChange( attribute, ( value ) => {
setValue( attribute, value );
return true;
});
});
}
return () => {
if( formControl ) {
Object.keys(inputValue).forEach( attribute => formControl.unsubscribe(attribute) );
}
}
}, []);
return (
<div class="form-field">
<input
type="text"
value={getValue('attr_one')}
onChange={ e => setValue('attr_one', e.target.value)}
/>
<input
type="checkbox"
checked={getValue('attr_two')}
onChange={ e => setValue('attr_two', !!e.target.checked)}
/>
</div>
);
}
And inside functions setValue and getValue I always have default values in inputValue - I can't get updated state inside this functions. How i can organize my code to solve this problem?
P. S.
1) With useCallback I have the same results:
const getValue = useCallback( ( attr ) => {
return inputValue[attr];
}, [inputValue]);
const setValue = useCallback( ( attr, val ) => {
if( attr === 'attr_one' ) {
if( val === 'bar' && getValue(attr) !== 'foo' ) {
val = 'foo bar';
}
}
setInputValue( {...inputValue, [attr]: val} );
}, [inputValue]);
2) With useEffect functions setValue and getValue are unavailable at first render:
let getValue, setValue;
useEffect( () => {
getValue = ( attr ) => {
return inputValue[attr];
}
setValue = ( attr, val ) => {
if( attr === 'attr_one' ) {
if( val === 'bar' && getValue(attr) !== 'foo' ) {
val = 'foo bar';
}
}
setInputValue( {...inputValue, [attr]: val} );
}
}, [inputValue]);
Write custom hooks to extract your logic into separate units of code. Since your state changes rely in part on the previous state, you should call useReducer() instead of useState() to make the implementation easier and the state changes atomic:
const useAccessors = initialState => {
const [state, dispatch] = useReducer((prev, [attr, val]) => {
if (attr === 'attr_one') {
if (val === 'bar' && getValue(attr) !== 'foo') {
val = 'foo bar';
}
}
return { ...prev, [attr]: val };
}, initialState);
const ref = useRef(state);
useEffect(() => {
ref.current = state;
}, [ref]);
const getValue = useCallback(
attr => ref.current[attr],
[ref]
);
const setValue = useCallback((attr, val) => {
dispatch([attr, val]);
}, [dispatch]);
return { getValue, setValue, ref };
};
Now your useEffect() is omitting dependencies from the second argument. This tends to cause problems like you're currently experiencing. We can employ useRef() to work around this.
Let's move your useEffect() into a custom hook as well and fix it:
const useFormControl = (formControl, { getValue, setValue, ref }) => {
useEffect(() => {
if (formControl) {
const keys = Object.keys(ref.current);
keys.forEach(attribute => {
formControl.subscribeToValueCollecting(attribute, () => {
return getValue(attribute);
});
formControl.subscribeToValueChange(attribute, value => {
setValue(attribute, value);
return true;
});
});
return () => {
keys.forEach(attribute => {
formControl.unsubscribe(attribute);
});
};
}
}, [formControl, getValue, setValue, ref]);
};
Since getValue, setValue, and ref are memoized, the only dependency that actually changes is formControl, which is good.
Putting all this together, we get:
const MyComponent = props =>
const { attrOneDefault, attrTwoDefault, formControl } = props;
const { getValue, setValue, ref } = useAccessors({
attr_one: attrOneDefault,
attr_two: attrTwoDefault
});
useFormControl(formControl, { getValue, setValue, ref });
return (
<div class="form-field">
<input
type="text"
value={getValue('attr_one')}
onChange={e => setValue('attr_one', e.target.value)}
/>
<input
type="checkbox"
checked={getValue('attr_two')}
onChange={e => setValue('attr_two', e.target.checked)}
/>
</div>
);
};
Try this:
const getValue = ( attr ) => {
return inputValue[attr];
}
const getValueRef = useRef(getValue)
const setValue = ( attr, val ) => {
setInputValue( inputValue =>{
if( attr === 'attr_one' ) {
if( val === 'bar' && inputValue[attr] !== 'foo' ) {
val = 'foo bar';
}
}
return {...inputValue, [attr]: val} );
}
}
useEffect(()=>{
getValueRef.current=getValue
})
useEffect( () => {
const getCurrentValue = (attr)=>getValueRef.current(attr)
if( formControl ) {
Object.keys(inputValue).forEach( attribute => {
formControl.subscribeToValueCollecting( attribute, () => {
return getCurrentValue(attribute);
});
formControl.subscribeToValueChange( attribute, ( value ) => {
setValue( attribute, value );
return true;
});
});
}
return () => {
if( formControl ) {
Object.keys(inputValue).forEach( attribute => formControl.unsubscribe(attribute) );
}
}
}, []);
Related
Here is my codesandbox link. https://codesandbox.io/s/bold-lederberg-d2h508?file=/src/Components/Products/Products.js In this filtering when i click highest price its showing correcly and when i again click all filters how can i set back to its original state. please provide any solution for this.
import React, { useState } from "react";
import Products from "./Components/Products/Products";
import SearchInput from "./Components/SearchInput/SearchInput";
import data from "./utils/data";
const App = () => {
const [searchValue, setSearchValue] = useState("");
const [productsInfo, setProductsInfo] = useState([]);
const handleChange = (e) => {
setSearchValue(e.target.value);
};
const selectedChangeFilter = (e) => {
const { value } = e.target;
if (value === "sporting goods") {
const sportingGoods = data.filter(
(product) => product.category === "Sporting Goods"
);
setProductsInfo(sportingGoods);
}
if (value === "electronics") {
const electronicsGoods = data.filter(
(product) => product.category === "Electronics"
);
setProductsInfo(electronicsGoods);
}
if (value === "lowest price") {
const lowestPriceGoods = data.sort((el1, el2) =>
el1.price.localeCompare(el2.price, undefined, { numeric: true })
);
setProductsInfo([...lowestPriceGoods]);
}
if (value === "highest price") {
const highestPriceGoods = data.sort((el1, el2) =>
el2.price.localeCompare(el1.price, undefined, { numeric: true })
);
setProductsInfo([...highestPriceGoods]);
}
if (value === "all") {
setProductsInfo(data);
}
};
const searchProducts = (products) => {
if (searchValue.toLowerCase().trim() === "") {
setProductsInfo(products);
} else {
const seekedItem = productsInfo.filter(
(product) =>
product.name.toLowerCase().trim().includes(searchValue) ||
product.category.toLowerCase().trim().includes(searchValue)
);
setProductsInfo(seekedItem);
}
};
return (
<div>
<SearchInput
handleChange={handleChange}
searchValue={searchValue}
selectedChangeFilter={selectedChangeFilter}
/>
<Products
data={data}
searchValue={searchValue}
productsInfo={productsInfo}
searchProducts={searchProducts}
/>
</div>
);
};
export default App;
I try this but it doesn't work
if (value === "all") {
const copiedData = [...data]
setProductsInfo(copiedData);
}
Best regards
I have created two tabs that when clicked need to show a different set of products and a different set of filters for each selection. My problem is that when I click either tab and call setOptions within changeTab, I need to click each tab twice before it will update 'options', 'options' needs to contain each filter.
Obviously calling setOptions within the click handler is not correct but I can't figure out where or how to correctly update 'options'. Help greatly appreciated.
In the console logs below 'dedupedOptions' updates correctly on click
function filterProducts() {
const [categoryType, setCategory] = useState("Canine");
const [activeTabIndex, setActiveTabIndex] = useState(0);
const {
productData: {
products: { products }
}
} = useContext(AppContext);
const productsByCategory = products
.filter((product) => {
const { tags } = product;
return !!tags.find((tag) => tag.includes(categoryType));
})
.map((product) => ({
...product,
category: product.tags
.find((tag) => tag.includes("category:"))
.split(":")[1]
}));
let dedupedOptions = [];
productsByCategory.forEach((product) => {
const { tags } = product;
tags.forEach((tag) => {
const parts = tag.split(":");
const key = parts[0];
const value = parts[1] || null;
const validTag = tagKeysToDisplay.find(
(tagKeyToDisplay) => tagKeyToDisplay === key
);
if (
validTag &&
!dedupedOptions.find((dedupedOption) => dedupedOption.value === value)
) {
dedupedOptions = [
...dedupedOptions,
{
label: titleCase(value),
value,
selected: false
}
];
}
});
});
const [options, setOptions] = useState(dedupedOptions);
console.log(dedupedOptions);
console.log(options);
const changeTab = (index, category) => {
setCategory(category);
setActiveTabIndex(index);
setOptions(dedupedOptions);
};
const setFilter = useCallback(
(selectedOption) => {
const optionIsActive = options.find(
(option) => option.value === selectedOption.value
)?.selected;
let newOptions = [];
newOptions = [
...options.map((option) => {
if (option.value === selectedOption.value) {
return {
...option,
selected: !optionIsActive
};
}
return option;
})
];
setOptions(newOptions);
},
[options]
);
}
And the two elements set up as tabs to handle the click events. These are rendered within the same filterProducts function.
<div className="filter-products__tabs">
<div
className={`filter-products__tab
${activeTabIndex === 0 ? "is-active" : ""}`}
onClick={changeTab.bind(this, 0, "Canine")}
>
<span>DOG</span>
</div>
<div
className={`filter-products__tab
${activeTabIndex === 1 ? "is-active" : ""}`}
onClick={changeTab.bind(this, 1, "Feline")}
>
<span>CAT</span>
</div>
</div>
I reproduced your question by some changes in variable declarations in state.
be careful to declare variables in state and do the updates by listening the variable changes inside the useEffect.
here is the working code:\
https://codesandbox.io/s/quirky-http-e264i?file=/src/App.js
import "./styles.css";
import { useState, useContext, useCallback, useEffect } from "react";
export default function App() {
const [categoryType, setCategory] = useState("Canine");
const [activeTabIndex, setActiveTabIndex] = useState(0);
const [productsByCategory, setProductsByCategory] = useState([]);
const [dedupedOptions, setDedupedOptions] = useState([]);
const [options, setOptions] = useState(dedupedOptions);
const products = [
{ tags: ["category:Feline"], name: "one" },
{ tags: ["category:Canine"], name: "two" }
];
useEffect(() => {
const productsByCategory = products
.filter((product) => {
const { tags } = product;
return !!tags.find((tag) => tag.includes(categoryType));
})
.map((product) => ({
...product,
category: product.tags
.find((tag) => tag.includes("category:"))
.split(":")[1]
}));
setProductsByCategory(productsByCategory);
}, [categoryType]);
useEffect(() => {
let tmp_dedupedOptions = [];
const tagKeysToDisplay = ["category"];
productsByCategory.forEach((product) => {
const { tags } = product;
tags.forEach((tag) => {
const parts = tag.split(":");
const key = parts[0];
const value = parts[1] || null;
const validTag = tagKeysToDisplay.find(
(tagKeyToDisplay) => tagKeyToDisplay === key
);
if (
validTag &&
!tmp_dedupedOptions.find(
(dedupedOption) => dedupedOption.value === value
)
) {
tmp_dedupedOptions = [
...tmp_dedupedOptions,
{
label: value,
value,
selected: false
}
];
}
});
});
setDedupedOptions(tmp_dedupedOptions);
setOptions(tmp_dedupedOptions);
}, [productsByCategory]);
console.log("options: ", options);
const changeTab = (index, category) => {
setCategory(category);
setActiveTabIndex(index);
};
const setFilter = useCallback(
(selectedOption) => {
const optionIsActive = options.find(
(option) => option.value === selectedOption.value
)?.selected;
let newOptions = [];
newOptions = [
...options.map((option) => {
if (option.value === selectedOption.value) {
return {
...option,
selected: !optionIsActive
};
}
return option;
})
];
setOptions(newOptions);
},
[options]
);
// }
return (
<div>
<div className="filter-products__tabs">
<div
className={`filter-products__tab
${activeTabIndex === 0 ? "is-active" : ""}`}
onClick={changeTab.bind(this, 0, "Canine")}
>
<span>DOG</span>
</div>
<div
className={`filter-products__tab
${activeTabIndex === 1 ? "is-active" : ""}`}
onClick={changeTab.bind(this, 1, "Feline")}
>
<span>CAT</span>
</div>
</div>
</div>
);
}
Here I'm working on AutoComplete and Auto fill of react.
I'm trying to convert it to react hooks as I have written all of my code is in hooks only.
I've to some level converted it to hooks based as per my understanding. But I'm not able to completely convert it.
Original code
import React, { Component } from "react";
class App extends Component {
constructor(props) {
super(props);
this.state = {
item: {
code: "",
name: "",
unit: "",
rate: ""
},
cursor: 0,
searchItems: []
};
this.autocomplete = this.autocomplete.bind(this);
this.handleKeyup = this.handleKeyup.bind(this);
this.handleKeydown = this.handleKeydown.bind(this);
this.handleListKeydown = this.handleListKeydown.bind(this);
this.selectItem = this.selectItem.bind(this);
this.handleChange = this.handleChange.bind(this);
}
autocomplete(evt) {
let text = evt.target.value;
fetch(`https://invoiceitems.herokuapp.com/items?name_like=${text}&_limit=6`)
.then((res) => res.json())
.then((data) => {
this.setState({ searchItems: data });
});
}
handleKeyup(evt) {
if (evt.keyCode === 27) {
this.setState({ searchItems: [] });
return false;
}
}
handleKeydown(evt) {
const { cursor, searchItems } = this.state;
// arrow up/down button should select next/previous list element
if (evt.keyCode === 38 && cursor > 0) {
this.setState((prevState) => ({
cursor: prevState.cursor - 1
}));
} else if (evt.keyCode === 40 && cursor < searchItems.length - 1) {
this.setState((prevState) => ({
cursor: prevState.cursor + 1
}));
}
if (evt.keyCode === 13) {
let currentItem = searchItems[cursor];
if (currentItem !== undefined) {
const { name, code, rate, unit } = currentItem;
this.setState({ item: { name, code, rate, unit }, searchItems: [] });
}
}
if (evt.keyCode === 8) {
this.setState({ item: { name: "", code: "", rate: "", unit: "" } });
}
}
selectItem(id) {
const { searchItems } = this.state;
let selectedItem = searchItems.find((item) => item.code === id);
const { code, name, unit, rate } = selectedItem;
this.setState({ item: { code, name, unit, rate } });
this.setState({ searchItems: [] });
}
handleListKeydown(evt) {
console.log(evt.keyCode);
}
handleChange(evt) {
this.setState({ item: { [evt.target.name]: evt.target.value } });
}
render() {
const { searchItems, cursor, item, handleChange } = this.state;
const { code, name, unit, rate } = item;
return (
<div className="container mt-3">
<h1 className="h2 text-center">Autocomplete Example</h1>
<div className="form-group">
<label htmlFor="autocomplete">Item Name </label>
<input
type="text"
id="autocomplete"
onChange={this.autocomplete}
onKeyUp={this.handleKeyup}
onKeyDown={this.handleKeydown}
value={name}
className="custom-input form-control"
/>
{searchItems.length > 0 && (
<ul className="list-group">
{searchItems.map((item, idx) => (
<li
className={
cursor === idx
? "active list-group-item"
: "list-group-item"
}
key={idx}
onClick={() => this.selectItem(item.code)}
onKeyDown={(evt) => this.handleListKeydown(evt, item.code)}
>
{item.name}
</li>
))}
</ul>
)}
</div>
</div>
);
}
}
export default App;
Link to original code: https://codepen.io/regexp/details/RwPNaLe
Using hooks
Here is the code that I tried to convert to hooks.
import React, { useState } from "react";
export default function FunctionName(props) {
const [item, setItem] = useState({
vendorNameData: invoiceDetail[0].invoiceData.vendor,
vendorAccountData: invoiceDetail[0].invoiceData.vendaAccount,
vendorAddressData: invoiceDetail[0].invoiceData.vendorAddress
});
const [cursor, setCursor] = useState(0);
const [searchItems, SetSearchItems] = useState([]);
function AutoComplete(evt) {
let text = evt.target.value;
console.log(text);
fetch(`https://invoiceitems.herokuapp.com/items?name_like=${text}&_limit=6`)
.then((res) => res.json())
.then((data) => {
SetSearchItems(data);
});
}
function HandleKeyUp(evt) {
if (evt.keyCode === 27) {
SetSearchItems([]);
return false;
}
}
function HandleKeyDown(evt) {
// const [cursor, setCursor] = useState();
// const [searchItems, SetSearchItems] = useState()
if (evt.keyCode === 38 && cursor > 0) {
setCursor((cursor) => ({ cursor: cursor + 1 }));
} else if (evt.keyCode === 40 && cursor < searchItems.length - 1) {
setCursor((cursor) => ({ cursor: cursor + 1 }));
}
if (evt.keyCode === 13) {
let currentItem = searchItems[cursor];
if (currentItem !== undefined) {
const {
vendorNameData,
vendorAccountData,
vendorAddressData
} = currentItem;
setItem({ vendorNameData, vendorAccountData, vendorAddressData });
SetSearchItems([]);
}
}
if (evt.keyCode === 8) {
setItem({
vendorNameData: "",
vendorAccountData: "",
vendorAddressData: ""
});
}
}
function SelectItem(id) {
const [searchItems, SetSearchItems] = useState();
let selectedItem = searchItems.find((item) => item.code === id);
const {
vendorNameData,
vendorAccountData,
vendorAddressData
} = selectedItem;
setItem({ vendorNameData, vendorAccountData, vendorAddressData });
SetSearchItems([]);
}
function HandleListKeyDown(evt) {
console.log(evt.keyCode);
}
function HandleChange(evt) {
setItem({ item: { [evt.target.name]: evt.target.value } });
}
}
It would be really helpful to point me out where I'm lagging. I've tried my best but this is what I could come up with.
Any help would really be appreciated.
I've 'traduced' and cleaned up your original component, resulting as follows: (please see notes below)
import React, {useState, useCallback} from 'react';
function Input() {
const [item, setItem] = useState({
code: '',
name: '',
unit: '',
rate: ''
});
const [cursor, setCursor] = useState(0);
const [searchItems, setSearchItems] = useState([]);
const autocomplete = useCallback((evt) => {
const text = evt.target.value;
fetch(
`https://invoiceitems.herokuapp.com/items?name_like=${text}&_limit=6`
)
.then((res) => res.json())
.then((data) => {
setSearchItems(data);
});
}, []);
const handleKeyup = useCallback((evt) => {
if (evt.keyCode === 27) {
setSearchItems([]);
return false;
}
return true;
}, []);
const handleKeydown = useCallback(
(evt) => {
// arrow up/down button should select next/previous list element
if (evt.keyCode === 38 && cursor > 0) {
setCursor((prevCursor) => prevCursor - 1);
} else if (evt.keyCode === 40 && cursor < searchItems.length - 1) {
setCursor((prevCursor) => prevCursor + 1);
}
if (evt.keyCode === 13) {
let currentItem = searchItems[cursor];
if (currentItem !== undefined) {
const {code, name, unit, rate} = currentItem;
setItem({code, name, unit, rate});
setSearchItems([]);
}
}
if (evt.keyCode === 8) {
setItem({code: '', name: '', unit: '', rate: ''});
}
},
[cursor, searchItems]
);
const selectItem = useCallback(
(id) => {
let selectedItem = searchItems.find((item) => item.code === id);
const {code, name, unit, rate} = selectedItem;
setItem({code, name, unit, rate});
setSearchItems([]);
},
[searchItems]
);
const handleListKeydown = useCallback((evt) => {
console.log(evt.keyCode);
}, []);
return (
<div className={'container mt-3'}>
<h1 className={'h2 text-center'}>{'Autocomplete Example'}</h1>
<div className={'form-group'}>
<label htmlFor={'autocomplete'}>{'Item Name'}</label>
<input
type={'text'}
id={'autocomplete'}
onChange={autocomplete}
onKeyUp={handleKeyup}
onKeyDown={handleKeydown}
value={item.name}
className={'custom-input form-control'}
/>
{searchItems.length > 0 && (
<ul className={'list-group'}>
{searchItems.map((item, idx) => (
<li
className={
cursor === idx
? 'active list-group-item'
: 'list-group-item'
}
key={idx}
onClick={() => selectItem(item.code)}
onKeyDown={handleListKeydown}>
{item.name}
</li>
))}
</ul>
)}
</div>
</div>
);
}
export {Input};
useState replaces Class Components State management. It is good practice to split your state into smaller pieces as possible, because new values will completely replace old ones (there is no merging like Class Components this.setState does). Using cursor as an example, do const [cursor, setCursor] = useState(0); to initialize your state (in this case initial state is 0). Then use cursor to use the value and call setCursor(newValue) to update it. Each time you update your state you will most likely trigger a re-rendering.
useCallback allows you to only re-declare a function when its dependencies have changed, thus improving performance. Function dependencies are specified in the second argument array. On each render, React will compare the new values with the old ones and will always return the same function when dependencies have not changed.
The return statement replaces your previous render method.
Follows a working snippet. Please note that OP original code behavior has not been changed.
const {useState, useCallback, StrictMode} = React;
function Input() {
const [item, setItem] = useState({
code: '',
name: '',
unit: '',
rate: ''
});
const [cursor, setCursor] = useState(0);
const [searchItems, setSearchItems] = useState([]);
const autocomplete = useCallback((evt) => {
const text = evt.target.value;
fetch(
`https://invoiceitems.herokuapp.com/items?name_like=${text}&_limit=6`
)
.then((res) => res.json())
.then((data) => {
setSearchItems(data);
});
}, []);
const handleKeyup = useCallback((evt) => {
if (evt.keyCode === 27) {
setSearchItems([]);
return false;
}
return true;
}, []);
const handleKeydown = useCallback(
(evt) => {
// arrow up/down button should select next/previous list element
if (evt.keyCode === 38 && cursor > 0) {
setCursor((prevCursor) => prevCursor - 1);
} else if (evt.keyCode === 40 && cursor < searchItems.length - 1) {
setCursor((prevCursor) => prevCursor + 1);
}
if (evt.keyCode === 13) {
let currentItem = searchItems[cursor];
if (currentItem !== undefined) {
const {code, name, unit, rate} = currentItem;
setItem({code, name, unit, rate});
setSearchItems([]);
}
}
if (evt.keyCode === 8) {
setItem({code: '', name: '', unit: '', rate: ''});
}
},
[cursor, searchItems]
);
const selectItem = useCallback(
(id) => {
let selectedItem = searchItems.find((item) => item.code === id);
const {code, name, unit, rate} = selectedItem;
setItem({code, name, unit, rate});
setSearchItems([]);
},
[searchItems]
);
const handleListKeydown = useCallback((evt) => {
console.log(evt.keyCode);
}, []);
return (
<div className={'container mt-3'}>
<h1 className={'h2 text-center'}>{'Autocomplete Example'}</h1>
<div className={'form-group'}>
<label htmlFor={'autocomplete'}>{'Item Name'}</label>
<input
type={'text'}
id={'autocomplete'}
onChange={autocomplete}
onKeyUp={handleKeyup}
onKeyDown={handleKeydown}
value={item.name}
className={'custom-input form-control'}
/>
{searchItems.length > 0 && (
<ul className={'list-group'}>
{searchItems.map((item, idx) => (
<li
className={
cursor === idx
? 'active list-group-item'
: 'list-group-item'
}
key={idx}
onClick={() => selectItem(item.code)}
onKeyDown={handleListKeydown}>
{item.name}
</li>
))}
</ul>
)}
</div>
</div>
);
}
ReactDOM.render(
<StrictMode>
<Input />
</StrictMode>,
document.getElementById('root')
);
.active {
color: #ff0000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.0/umd/react-dom.production.min.js"></script>
<div id='root' />
I want to add new Objects when user click on checkbox. For example , When user click on group , it will store data {permission:{group:["1","2"]}}. If I click on topgroup , it will store new objects with previous one
{permission:{group:["1","2"]},{topGroup:["1","2"]}}.
1st : The problem is that I can not merge new object with previous one . I saw only one objects each time when I click on the group or topgroup.
onChange = value => checked => {
this.setState({ checked }, () => {
this.setState(prevState => {
Object.assign(prevState.permission, { [value]: this.state.checked });
});
});
};
<CheckboxGroup
options={options}
value={checked}
onChange={this.onChange(this.props.label)}
/>
Here is my codesanbox:https://codesandbox.io/s/stackoverflow-a-60764570-3982562-v1-0qh67
It is a lot of code because I've added set and get to set and get state. Now you can store the path to the state in permissionsKey and topGroupKey. You can put get and set in a separate lib.js.
In this example Row is pretty much stateless and App holds it's state, this way App can do something with the values once the user is finished checking/unchecking what it needs.
const Checkbox = antd.Checkbox;
const CheckboxGroup = Checkbox.Group;
class Row extends React.Component {
isAllChecked = () => {
const { options, checked } = this.props;
return checked.length === options.length;
};
isIndeterminate = () => {
const { options, checked } = this.props;
return (
checked.length > 0 && checked.length < options.length
);
};
render() {
const {
options,
checked,
onChange,
onToggleAll,
stateKey,
label,
} = this.props; //all data and behaviour is passed by App
return (
<div>
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.isIndeterminate()}
onChange={e =>
onToggleAll(e.target.checked, stateKey)
}
checked={this.isAllChecked()}
>
Check all {label}
</Checkbox>
<CheckboxGroup
options={options}
value={checked}
onChange={val => {
onChange(stateKey, val);
}}
/>
</div>
</div>
);
}
}
//helper from https://gist.github.com/amsterdamharu/659bb39912096e74ba1c8c676948d5d9
const REMOVE = () => REMOVE;
const get = (object, path, defaultValue) => {
const recur = (current, path) => {
if (current === undefined) {
return defaultValue;
}
if (path.length === 0) {
return current;
}
return recur(current[path[0]], path.slice(1));
};
return recur(object, path);
};
const set = (object, path, callback) => {
const setKey = (current, key, value) => {
if (Array.isArray(current)) {
return value === REMOVE
? current.filter((_, i) => key !== i)
: current.map((c, i) => (i === key ? value : c));
}
return value === REMOVE
? Object.entries(current).reduce((result, [k, v]) => {
if (k !== key) {
result[k] = v;
}
return result;
}, {})
: { ...current, [key]: value };
};
const recur = (current, path) => {
if (path.length === 1) {
return setKey(
current,
path[0],
callback(current[path[0]])
);
}
return setKey(
current,
path[0],
recur(current[path[0]], path.slice(1))
);
};
return recur(object, path, callback);
};
class App extends React.Component {
state = {
permission: { group: [] },
topGroup: [],
some: { other: [{ nested: { state: [] } }] },
};
permissionsKey = ['permission', 'group']; //where to find permissions in state
topGroupKey = ['topGroup']; //where to find top group in state
someKey = ['some', 'other', 0, 'nested', 'state']; //where other group is in state
onChange = (key, value) => {
//use set helper to set state
this.setState(set(this.state, key, arr => value));
};
isIndeterminate = () =>
!this.isEverythingChecked() &&
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
result || get(this.state, key).length,
false
);
toggleEveryting = e => {
const checked = e.target.checked;
this.setState(
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
set(result, key, () =>
checked
? this.plainOptions.map(({ value }) => value)
: []
),
this.state
)
);
};
onToggleAll = (checked, key) => {
this.setState(
//use set helper to set state
set(this.state, key, () =>
checked
? this.plainOptions.map(({ value }) => value)
: []
)
);
};
isEverythingChecked = () =>
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
result &&
get(this.state, key).length ===
this.plainOptions.length,
true
);
plainOptions = [
{ value: 1, name: 'Apple' },
{ value: 2, name: 'Pear' },
{ value: 3, name: 'Orange' },
];
render() {
return (
<React.Fragment>
<h1>App state</h1>
{JSON.stringify(this.state)}
<div>
<Checkbox
indeterminate={this.isIndeterminate()}
onChange={this.toggleEveryting}
checked={this.isEverythingChecked()}
>
Toggle everything
</Checkbox>
</div>
{[
{ label: 'group', stateKey: this.permissionsKey },
{ label: 'top', stateKey: this.topGroupKey },
{ label: 'other', stateKey: this.someKey },
].map(({ label, stateKey }) => (
<Row
key={label}
options={this.plainOptions}
// use getter to get state selected value
// for this particular group
checked={get(this.state, stateKey)}
label={label}
onChange={this.onChange} //change behaviour from App
onToggleAll={this.onToggleAll} //toggle all from App
//state key to indicate what state needs to change
// used in setState in App and passed to set helper
stateKey={stateKey}
/>
))}
</React.Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<link href="https://cdnjs.cloudflare.com/ajax/libs/antd/4.0.3/antd.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/antd/4.0.3/antd.js"></script>
<div id="root"></div>
I rewrite all the handlers.
The bug in your code is located on the usage of antd Checkbox.Group component with map as a child component, perhaps we need some key to distinguish each of the Row. Simply put them in one component works without that strange state update.
As the demand during communication, the total button is also added.
And, we don't need many states, keep the single-source data is always the best practice.
import React from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { Checkbox } from "antd";
const group = ["group", "top"];
const groupItems = ["Apple", "Pear", "Orange"];
const CheckboxGroup = Checkbox.Group;
class App extends React.Component {
constructor() {
super();
this.state = {
permission: {}
};
}
UNSAFE_componentWillMount() {
this.setDefault(false);
}
setDefault = fill => {
const temp = {};
group.forEach(x => (temp[x] = fill ? groupItems : []));
this.setState({ permission: temp });
};
checkLength = () => {
const { permission } = this.state;
let sum = 0;
Object.keys(permission).forEach(x => (sum += permission[x].length));
return sum;
};
/**
* For total
*/
isTotalIndeterminate = () => {
const len = this.checkLength();
return len > 0 && len < groupItems.length * group.length;
};
onCheckTotalChange = () => e => {
this.setDefault(e.target.checked);
};
isTotalChecked = () => {
return this.checkLength() === groupItems.length * group.length;
};
/**
* For each group
*/
isIndeterminate = label => {
const { permission } = this.state;
return (
permission[label].length > 0 &&
permission[label].length < groupItems.length
);
};
onCheckAllChange = label => e => {
const { permission } = this.state;
const list = e.target.checked ? groupItems : [];
this.setState({ permission: { ...permission, [label]: list } });
};
isAllChecked = label => {
const { permission } = this.state;
return !groupItems.some(x => !permission[label].includes(x));
};
/**
* For each item
*/
isChecked = label => {
const { permission } = this.state;
return permission[label];
};
onChange = label => e => {
const { permission } = this.state;
this.setState({ permission: { ...permission, [label]: e } });
};
render() {
const { permission } = this.state;
console.log(permission);
return (
<React.Fragment>
<Checkbox
indeterminate={this.isTotalIndeterminate()}
onChange={this.onCheckTotalChange()}
checked={this.isTotalChecked()}
>
Check all
</Checkbox>
{group.map(label => (
<div key={label}>
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.isIndeterminate(label)}
onChange={this.onCheckAllChange(label)}
checked={this.isAllChecked(label)}
>
Check all
</Checkbox>
<CheckboxGroup
options={groupItems}
value={this.isChecked(label)}
onChange={this.onChange(label)}
/>
</div>
</div>
))}
</React.Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById("container"));
Try it online:
Please try this,
onChange = value => checked => {
this.setState({ checked }, () => {
this.setState(prevState => {
permission : { ...prevSatate.permission , { [value]: this.state.checked }}
});
});
};
by using spread operator you can stop mutating the object. same way you can also use object.assign like this.
this.setState(prevState => {
permission : Object.assign({} , prevState.permission, { [value]: this.state.checked });
});
And also i would suggest not to call setState in a callback. If you want to access the current state you can simply use the current checked value which you are getting in the function itself.
so your function becomes ,
onChange = value => checked => {
this.setState({ checked });
this.setState(prevState => {return { permission : { ...prevSatate.permission, { [value]: checked }}
}});
};
Try the following
//Inside constructor do the following
this.state = {checkState:[]}
this.setChecked = this.setChecked.bind(this);
//this.setChecked2 = this.setChecked2.bind(this);
//Outside constructor but before render()
setChecked(e){
this.setState({
checkState : this.state.checkState.concat([{checked: e.target.id + '=>' + e.target.value}])
//Id is the id property for a specific(target) field
});
}
//Finally attack the method above.i.e. this.setChecked to a form input.
Hope it will address your issues
I want to have a dynamic number of input field and also able to remove it. I can do it if this is only client side but I am getting the data from server and the issue is that I am not getting the id from the server, and that I need to be able to change the number of fields on the fly in the UI.
since I am also getting some fields from the server the manually added id was not longer in sync with the # of items.
How do i achieve this?
Here is my code
let _id = 0;
function guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
}
return s4() + s4();
}
class Rules extends React.PureComponent {
constructor(props, context) {
super(props, context);
this.state = {
rules: {
rule_regulations: {}
},
numberOfInput: []
};
}
componentDidMount() {
this.props.loadRules();
}
componentWillReceiveProps(nextProps) {
if (nextProps.rules.size && nextProps.rules !== this.props.rules) {
nextProps.rules
.entrySeq()
.map(([key, value]) => {
this.setState(state => ({
rules: {
...state.rules,
rule_regulations: {
...state.rules.rule_regulations,
[key]: value
}
},
numberOfInput: [...state.numberOfInput, guid()]
}));
})
.toArray();
}
}
handleChange = e => {
const { value, name } = e.target;
this.setState({
rules: {
...this.state.rules,
rule_regulations: {
...this.state.rules.rule_regulations,
[name]: value
}
}
});
};
handleAddRules = e => {
this.setState({
numberOfInput: [...this.state.numberOfInput, guid()]
});
};
handleRemoveRules = (e, num) => {
e.preventDefault();
this.setState({
numberOfInput: this.state.numberOfInput.filter(input => input !== num)
});
};
handleSave = e => {
e.preventDefault();
const obj = {
rule_regulations: Object.values(this.state.rules.rule_regulations)
};
this.props.postRules(obj);
};
render() {
const { numberOfInput } = this.state;
return (
<div className="basic-property">
<span className="circleInputUi" onClick={this.handleAddRules}>
+
</span>
<RulesInputContainer
numberOfInput={numberOfInput}
handleChange={this.handleChange}
handleRemoveRules={this.handleRemoveRules}
handleSave={this.handleSave}
value={this.state.rules.rule_regulations}
/>
</div>
);
}
}
const RulesInputContainer = props => {
return (
<div className="rules-input">
{props.value &&
Object.keys(props.value).map(key =>
<RulesInput
key={key}
num={key}
value={props.value}
handleChange={props.handleChange}
handleRemoveRules={props.handleRemoveRules}
/>
)}
<button className="button" onClick={props.handleSave}>
Save
</button>
</div>
);
};
export default RulesInputContainer;
const RulesInput = props => {
return (
<form className="form">
<InputField
label={`Rule ${props.num + 1}`}
type="text"
name={`${props.num}`}
value={props.value[props.num] || ""}
onChange={props.handleChange}
/>
<Button onClick={e => props.handleRemoveRules(e, props.num)}>
Remove
</Button>
</form>
)
}
I want the synchronization with server data and client
You can store returned data from your server into your component state.
componentDidMount() {
this.loadRules();
}
loadRules() {
let rules = this.props.loadRules();
this.setState({rules});
}
You can keep the synchronization by setting your state with data you received from your server.
In addition, it's not related to your question. Since RulesInputContainer and RulesInput are presentational components I think you can combine those into 1 component.