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
Related
How can I make other filter button disappear when picked 1 (or multiple) value in same filter block.
Here is my code base:
const FilterBlock = props => {
const {
filterApi,
filterState,
filterFrontendInput,
group,
items,
name,
onApply,
initialOpen
} = props;
const { formatMessage } = useIntl();
const talonProps = useFilterBlock({
filterState,
items,
initialOpen
});
const { handleClick, isExpanded } = talonProps;
const classStyle = useStyle(defaultClasses, props.classes);
const ref = useRef(null);
useEffect(() => {
const handleClickOutside = event => {
if (ref.current && !ref.current.contains(event.target)) {
isExpanded && handleClick();
}
};
document.addEventListener('click', handleClickOutside, true);
return () => {
document.removeEventListener('click', handleClickOutside, true);
};
}, [isExpanded]);
const list = isExpanded ? (
<Form>
<FilterList
filterApi={filterApi}
filterState={filterState}
name={name}
filterFrontendInput={filterFrontendInput}
group={group}
items={items}
onApply={onApply}
/>
</Form>
) : null;
return (
<div
data-cy="FilterBlock-root"
aria-label={itemAriaLabel}
ref={ref}
>
<Menu.Button
data-cy="FilterBlock-triggerButton"
type="button"
onClick={handleClick}
aria-label={toggleItemOptionsAriaLabel}
>
<div>
<span>
{name}
</span>
<svg
width="8"
height="5"
viewBox="0 0 8 5"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.97291 0.193232C7.20854"
fill="currentColor"
/>
</svg>
</div>
</Menu.Button>
<div>
<div>
{list}
</div>
</div>
</div>
);
};
I am trying to achieve when I chose 1 value or more than 1 value inside filter block the other block will disappear but right now I achieved that when I chose 1 value the other filter block disappear but when I chose another value in the same block the other filter block appear again. Anyone have idea how can I work on this?
I am using React and Redux for this project
Thank you for helping me on this!!!!
Update:
Added parent component for FilterBlock.ks:
const FilterSidebar = props => {
const { filters, filterCountToOpen } = props;
const [selectedGroup, setSelectedGroup] = useState(null);
const talonProps = useFilterSidebar({ filters });
const {
filterApi,
filterItems,
filterNames,
filterFrontendInput,
filterState,
handleApply,
handleReset
} = talonProps;
const filterRef = useRef();
const classStyle = useStyle(defaultClasses, props.classes);
const handleApplyFilter = useCallback(
(...args) => {
const filterElement = filterRef.current;
if (
filterElement &&
typeof filterElement.getBoundingClientRect === 'function'
) {
const filterTop = filterElement.getBoundingClientRect().top;
const windowScrollY =
window.scrollY + filterTop - SCROLL_OFFSET;
window.scrollTo(0, windowScrollY);
}
handleApply(...args);
},
[handleApply, filterRef]
);
const result = Array.from(filterItems)
.filter(
([group, items]) =>
selectedGroup === null ||
selectedGroup === filterNames.get(group)
)
.map(([group, items], iteration) => {
const blockState = filterState.get(group);
const groupName = filterNames.get(group);
const frontendInput = filterFrontendInput.get(group);
return (
<FilterBlock
key={group}
filterApi={filterApi}
filterState={blockState}
filterFrontendInput={frontendInput}
group={group}
items={items}
name={groupName}
onApply={(...args) => {
console.log('args: ', ...args);
setSelectedGroup(prev =>
prev !== null ? null : groupName
);
return handleApplyFilter(...args);
}}
initialOpen={iteration < filterCountToOpen}
iteration={iteration}
/>
);
});
return (
<div className="container px-4 mx-auto">
<Menu
as="div"
className="my-16 justify-center flex flex-wrap py-5 border-y border-black border-opacity-5"
>
{result}
</Menu>
</div>
);
};
Updated added useFilterSideBar.js:
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from '#apollo/client';
import { useHistory, useLocation } from 'react-router-dom';
import { useAppContext } from '#magento/peregrine/lib/context/app';
import mergeOperations from '../../util/shallowMerge';
import { useFilterState } from '../FilterModal';
import {
getSearchFromState,
getStateFromSearch,
sortFiltersArray,
stripHtml
} from '../FilterModal/helpers';
import DEFAULT_OPERATIONS from '../FilterModal/filterModal.gql';
const DRAWER_NAME = 'filter';
export const useFilterSidebar = props => {
const { filters } = props;
const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
const { getFilterInputsQuery } = operations;
const [isApplying, setIsApplying] = useState(false);
const [{ drawer }, { toggleDrawer, closeDrawer }] = useAppContext();
const [filterState, filterApi] = useFilterState();
const prevDrawer = useRef(null);
const isOpen = drawer === DRAWER_NAME;
const history = useHistory();
const { pathname, search } = useLocation();
const { data: introspectionData } = useQuery(getFilterInputsQuery);
const attributeCodes = useMemo(
() => filters.map(({ attribute_code }) => attribute_code),
[filters]
);
// Create a set of disabled filters.
const DISABLED_FILTERS = useMemo(() => {
const disabled = new Set();
// Disable category filtering when not on a search page.
if (pathname !== '/search.html') {
disabled.add('category_id');
disabled.add('category_uid');
}
return disabled;
}, [pathname]);
// Get "allowed" filters by intersection of filter attribute codes and
// schema input field types. This restricts the displayed filters to those
// that the api will understand.
const possibleFilters = useMemo(() => {
const nextFilters = new Set();
const inputFields = introspectionData
? introspectionData.__type.inputFields
: [];
// perform mapping and filtering in the same cycle
for (const { name } of inputFields) {
const isValid = attributeCodes.includes(name);
const isEnabled = !DISABLED_FILTERS.has(name);
if (isValid && isEnabled) {
nextFilters.add(name);
}
}
return nextFilters;
}, [DISABLED_FILTERS, attributeCodes, introspectionData]);
const isBooleanFilter = options => {
const optionsString = JSON.stringify(options);
return (
options.length <= 2 &&
(optionsString.includes(
JSON.stringify({
__typename: 'AggregationOption',
label: '0',
value: '0'
})
) ||
optionsString.includes(
JSON.stringify({
__typename: 'AggregationOption',
label: '1',
value: '1'
})
))
);
};
// iterate over filters once to set up all the collections we need
const [
filterNames,
filterKeys,
filterItems,
filterFrontendInput
] = useMemo(() => {
const names = new Map();
const keys = new Set();
const frontendInput = new Map();
const itemsByGroup = new Map();
const sortedFilters = sortFiltersArray([...filters]);
for (const filter of sortedFilters) {
const { options, label: name, attribute_code: group } = filter;
// If this aggregation is not a possible filter, just back out.
if (possibleFilters.has(group)) {
const items = [];
// add filter name
names.set(group, name);
// add filter key permutations
keys.add(`${group}[filter]`);
// TODO: Get all frontend input type from gql if other filter input types are needed
// See https://github.com/magento-commerce/magento2-pwa/pull/26
if (isBooleanFilter(options)) {
frontendInput.set(group, 'boolean');
// add items
items.push({
title: 'No',
value: '0',
label: name + ':' + 'No'
});
items.push({
title: 'Yes',
value: '1',
label: name + ':' + 'Yes'
});
} else {
// Add frontend input type
frontendInput.set(group, null);
// add items
for (const { label, value } of options) {
items.push({ title: stripHtml(label), value });
}
}
itemsByGroup.set(group, items);
}
}
return [names, keys, itemsByGroup, frontendInput];
}, [filters, possibleFilters]);
// on apply, write filter state to location
useEffect(() => {
if (isApplying) {
const nextSearch = getSearchFromState(
search,
filterKeys,
filterState
);
// write filter state to history
history.push({ pathname, search: nextSearch });
// mark the operation as complete
setIsApplying(false);
}
}, [filterKeys, filterState, history, isApplying, pathname, search]);
const handleOpen = useCallback(() => {
toggleDrawer(DRAWER_NAME);
}, [toggleDrawer]);
const handleClose = useCallback(() => {
closeDrawer();
}, [closeDrawer]);
const handleApply = useCallback(() => {
setIsApplying(true);
handleClose();
}, [handleClose]);
const handleReset = useCallback(() => {
filterApi.clear();
setIsApplying(true);
}, [filterApi, setIsApplying]);
const handleKeyDownActions = useCallback(
event => {
// do not handle keyboard actions when the modal is closed
if (!isOpen) {
return;
}
switch (event.keyCode) {
// when "Esc" key fired -> close the modal
case 27:
handleClose();
break;
}
},
[isOpen, handleClose]
);
useEffect(() => {
const justOpened =
prevDrawer.current === null && drawer === DRAWER_NAME;
const justClosed =
prevDrawer.current === DRAWER_NAME && drawer === null;
// on drawer toggle, read filter state from location
if (justOpened || justClosed) {
const nextState = getStateFromSearch(
search,
filterKeys,
filterItems
);
filterApi.setItems(nextState);
}
// on drawer close, update the modal visibility state
if (justClosed) {
handleClose();
}
prevDrawer.current = drawer;
}, [drawer, filterApi, filterItems, filterKeys, search, handleClose]);
useEffect(() => {
const nextState = getStateFromSearch(search, filterKeys, filterItems);
filterApi.setItems(nextState);
}, [filterApi, filterItems, filterKeys, search]);
return {
filterApi,
filterItems,
filterKeys,
filterNames,
filterFrontendInput,
filterState,
handleApply,
handleClose,
handleKeyDownActions,
handleOpen,
handleReset,
isApplying,
isOpen
};
};
Update FilterList component:
const FilterList = props => {
const {
filterApi,
filterState,
filterFrontendInput,
name,
group,
itemCountToShow,
items,
onApply,
toggleItemOptionsAriaLabel
} = props;
const classes = useStyle(defaultClasses, props.classes);
const talonProps = useFilterList({ filterState, items, itemCountToShow });
const { isListExpanded, handleListToggle } = talonProps;
const { formatMessage } = useIntl();
// memoize item creation
// search value is not referenced, so this array is stable
const itemElements = useMemo(() => {
if (filterFrontendInput === 'boolean') {
const key = `item-${group}`;
return (
<li
key={key}
className={classes.item}
data-cy="FilterList-item"
>
<FilterItemRadioGroup
filterApi={filterApi}
filterState={filterState}
group={group}
name={name}
items={items}
onApply={onApply}
labels={labels}
/>
</li>
);
}
return items.map((item, index) => {
const { title, value } = item;
const key = `item-${group}-${value}`;
if (!isListExpanded && index >= itemCountToShow) {
return null;
}
// create an element for each item
const element = (
<li
key={key}
className={classes.item}
data-cy="FilterList-item"
>
<FilterItem
filterApi={filterApi}
filterState={filterState}
group={group}
item={item}
onApply={onApply}
/>
</li>
);
// associate each element with its normalized title
// titles are not unique, so use the element as the key
labels.set(element, title.toUpperCase());
return element;
});
}, [
classes,
filterApi,
filterState,
filterFrontendInput,
name,
group,
items,
isListExpanded,
itemCountToShow,
onApply
]);
const showMoreLessItem = useMemo(() => {
if (items.length <= itemCountToShow) {
return null;
}
const label = isListExpanded
? formatMessage({
id: 'filterList.showLess',
defaultMessage: 'Show Less'
})
: formatMessage({
id: 'filterList.showMore',
defaultMessage: 'Show More'
});
return (
<li className={classes.showMoreLessItem}>
<button
onClick={handleListToggle}
className="text-sm hover_text-indigo-500 transition-colors duration-sm"
data-cy="FilterList-showMoreLessButton"
>
{label}
</button>
</li>
);
}, [
isListExpanded,
handleListToggle,
items,
itemCountToShow,
formatMessage,
classes
]);
return (
<Fragment>
<ul className={classes.items}>
{itemElements}
{showMoreLessItem}
</ul>
</Fragment>
);
};
FilterList.defaultProps = {
onApply: null,
itemCountToShow: 5
};
Update FilterRadioGroup:
const FilterItemRadioGroup = props => {
const { filterApi, filterState, group, items, onApply, labels } = props;
const radioItems = useMemo(() => {
return items.map(item => {
const code = `item-${group}-${item.value}`;
return (
<FilterItemRadio
key={code}
filterApi={filterApi}
filterState={filterState}
group={group}
item={item}
onApply={onApply}
labels={labels}
/>
);
});
}, [filterApi, filterState, group, items, labels, onApply]);
const fieldValue = useMemo(() => {
if (filterState) {
for (const item of items) {
if (filterState.has(item)) {
return item.value;
}
}
}
return null;
}, [filterState, items]);
const field = `item-${group}`;
const fieldApi = useFieldApi(field);
const fieldState = useFieldState(field);
useEffect(() => {
if (field && fieldValue === null) {
fieldApi.reset();
} else if (field && fieldValue !== fieldState.value) {
fieldApi.setValue(fieldValue);
}
}, [field, fieldApi, fieldState.value, fieldValue]);
return (
<RadioGroup field={field} data-cy="FilterDefault-radioGroup">
{radioItems}
</RadioGroup>
);
};
FilterItemRadioGroup.defaultProps = {
onApply: null
};
The code seems overly complicated for what it does.
Could you perhaps make the filter block a state object like this:
filterGroup = {
title: "",
hasSelectedItems: boolean,
filterItems: []
}
Then it would be a simple matter of checking hasSelectedItems to conditionally render only one filterGroup in the UI.
I apologize I don't have the time to go in depth and try out your code, but I would put the filterBlock rendering inside the return statement and do something like this:
return(
{ (!!filterValue && (filterGroup === selectedGroup))
&&
<FilterGroup />
}
);
I find the best results come from using my state values as conditions inside my return statement.
That way, I don't have to manage complex logic in the .map() functions or with useEffect.
It looks like FilterSideBar is where you build the the array of FilterBlocks.
According to your question, I think you want to display all filter blocks if NO filters are selected, but once a filter is selected, you want the other FilterBlocks to not render, correct?
So, in your FilterSidebar module, I'd modify your "result" variable to include a check to see if anything has already been selected.
If something has been selected, then only render the FilterBlock that corresponds to that selected list item.
const result = Array.from(filterItems)
.filter(
([group, items]) =>
selectedGroup === null ||
selectedGroup === filterNames.get(group)
)
.map(([group, items], iteration) => {
const blockState = filterState.get(group);
const groupName = filterNames.get(group);
const frontendInput = filterFrontendInput.get(group);
return (
// This is the conditional rendering
(!!blockState.filterGroupSelected && blockState.filterGroupSelected === group) ?
<FilterBlock
key={group}
...
/>
: null
);
});
Then you'll have to add an item to your state object (unless you already have it) to track the group that was selected (e.g. "Printer Type"), and only allow that one to render.
If no group has a selected filter item, they should all render.
Also, you'll have to be sure to clear the selectedGroup whenever there are no selected items.
If this does not work, it may be that your state changes are not triggering a re-render. In which case it can be as simple as adding a reference to your state object in your component's return method like this:
{blockState.filterGroupSelected && " " }
It's a hack but it works, and keeps you from adding calls to useEffect.
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>
);
}
Below is my attempt to create an array of classes. The functionality of app is next: one can add or delete extra Input box and increase or decrease its value. As a result the app displays the sum of the all present tags. The issue comes with Delete function, when deleting any of components from created list it does correct math in array but rerenders the elements incorrectly. It always deletes the last component on the list even when you try to remove any others. Any hint why it's happening? Thanks
class Trade1 extends React.Component {
state = {
vl: this.props.value
}
change = (v) => {
let newValue
if (v) {
newValue = this.state.vl + 1
} else {
newValue = this.state.vl - 1
}
this.setState({vl: newValue})
this.props.onChange(newValue, this.props.index)
}
render() {
const {value, index} = this.props
return (
<div>
<button onClick={() => this.change(false)}>Down</button>
<input class="v_price" value={`${this.state.vl}`}/>
<button onClick={() => this.change(true)}>Up</button>
<button onClick={() => this.props.delete(this.props.index)}>Delete</button>
</div>
)
}
}
class Parent extends React.Component {
constructor(props){
super(props);
this.state = {
arr: [0,0,0]
}
}
onChange = (v, i) => {
let newArr = this.state.arr
newArr[i] = v
this.setState(newArr)
}
plus = () => {
let a = this.state.arr
a.push(0)
this.setState({arr: a})
}
minus = i => {
let a = this.state.arr
a.splice(i, 1)
console.log(a)
this.setState({arr: a})
}
render() {
return (
<div>
{this.state.arr.map((v, i) =>
{
return <Trade1 value={v} index={i} onChange={this.onChange} delete={this.minus}/>
}
)}
<div>{
this.state.arr.reduce((a, b) => a+b, 0 )
}</div>
<div><button onClick={this.plus}>Plus</button></div>
</div>
)
}
}
ReactDOM.render(<Parent />, document.getElementById('root'));
You are mutating the array, you should use filter and remove the element at index which you pass as an argument
minus = i => {
this.setState({
arr: this.state.arr.filter((x, j) => j !== i)
})
}
Issue
You've some state mutations. Try to use functional state updates and always return new state objects.
onChange = (v, i) => {
this.setState(prevState => ({
arr: prevState.arr.map((el, index) => index === i ? v : el)
}));
}
plus = () => {
this.setState(prevState => ({
arr: [...prevState.arr, 0],
}));
}
minus = i => {
this.setState(prevState => ({
arr: prevState.arr.filter((_, index) => index !== i),
}));
}
I created a suggestions search and its built to break up the fetch based on the current page. The state is console.loged correctly, but the render is one page click event behind. This is obviously not the behavior we want. It seems like the state is being updated fine. I have tried to refactor the code difference ways, and even tried this.forceUpdate()
Here is the code
SearchOrderBar.js
import React, { Component } from "react";
import {Input, Label, Table, Icon, Header, Menu} from 'semantic-ui-react';
import "./SearchOrderBar.css";
// import { resolve } from "dns";
// import PropTypes from 'prop-types';
import Pagination from '../Search/Pagination';
class SearchOrderBar extends Component {
constructor(props) {
super(props);
this.text = "";
this.state = {
suggestions: [],
addToQuery: false,
Query: [],
pagesNeeded: 0,
page: 1
};
let searchTerm = null;
const {pageLimit = null, keyTimer = null, } = props;
this.pageLimit = typeof pageLimit === 'number' ? pageLimit : 10;
this.handlePageClick = this.handlePageClick.bind(this);
this.fetchCallBack = this.fetchCallBack.bind(this);
// this.addToQuery = this.addToQuery.bind(this);
this.keyUpHandler = this.keyUpHandler.bind(this);
this.keyDownHandler = this.keyDownHandler.bind(this);
}
handlePageClick(page){
this.forceUpdate();
this.setState({
page: page
})
this.fetchCallBack();
}
//This fetch should be called in a dynamic switch case
fetchCallBack() {
let y = this.pageLimit;
let x = this.state.page > 1 ? (this.pageLimit*this.state.page) - this.pageLimit : 0;
// Return a promise
return new Promise((resolve, reject) => {
let searchTerm = this.searchTerm;
return fetch(`http://localhost:5000/api/searchorders/${searchTerm}/${x}/${y}`)
.then(res => {
if (!res.ok) {
throw res;
}
// Convert serialized response into json
return res.json()
}).then(data => {
//Use data
let searchTerm = data.map(data => {
let rData = {};
rData = data;
return rData;
})
this.item = searchTerm;
//console.log('here from callback')
this.setState({
suggestions: []
})
return searchTerm;
}).then( data => {
// console.log(this.totalRecords)sd
//console.log(data)
if (searchTerm.length === 0) {
this.setState({
suggestions: [],
rangeCount_URL: `http://localhost:5000/api/searchorderscount/${searchTerm}`
});
} else {
const suggestions = data.filter(function(v){
if(Object.values(v).includes(searchTerm.toLowerCase()) !== -1 || Object.values(v).includes(searchTerm.toUpperCase()) !== -1){
return v
}
})
console.log(suggestions)
this.text = searchTerm;
this.setState({ suggestions: suggestions.sort()});
}
})
})
}
pageCountCallBack(){
return new Promise((resolve, reject) => {
let searchTerm = this.searchTerm;
return fetch(`http://localhost:5000/api/searchorderscount/${searchTerm}/`)
.then(res => {
if (!res.ok) {
throw res;
}
// Convert serialized response into json
return res.json()
}).then(data => {
//Use data
let searchTerm = data.map(data => {
let rData = {};
rData = data;
return rData;
})
this.item = searchTerm;
// console.log('here from Page Count callback')
this.renderSuggestions();
resolve(searchTerm)
})
})
}
keyUpHandler = (e) => {
if(e.target.value.length >= 3){
this.keyTimer = setTimeout(this.countFetch(e), 1500);
} else {
this.setState(() => {
return {
suggestions : [],
pagesNeeded : 0
}
})
clearTimeout(this.keyTimer);
}
}
keyDownHandler = (e) => {
clearTimeout(this.keyTimer);
}
//Any time text is changed in the text field
countFetch = (e) => {
const value = e.target.value;
this.searchTerm = value;
this.pageCountCallBack().then(data => {
const totalRecords = data[0].rows;
this.setState(() => {
return {pagesNeeded : Math.ceil(totalRecords / this.pageLimit)}
})
//console.log("total" + totalRecords);
//console.log("page limit"+this.pageLimit);
//console.log("Needed" + this.state.pagesNeeded );
})
this.fetchCallBack();
}
renderSuggestions() {
//const { suggestions } = this.state;
const tableStyle = {
'tableLayout': 'fixed',
'overflowWrap': 'break-word'
}
return (
<Table style={tableStyle} celled>
{this.state.suggestions.length === 0 ?
(<Table.Body>
<Table.Cell colSpan="7">
<div className="ui fluid warning icon message">
<Icon name="exclamation triangle" size="huge" color="orange"/>
<div className="content">
<Header>No Records Found</Header>
<p>Try Seaching by one of the following:</p>
<ul>
<dt>Name</dt>
<dt>Order Number</dt>
<dt>Address (Shipping or Billing )</dt>
<dt>Phone Number</dt>
<dt>Email</dt>
</ul>
</div>
</div>
</Table.Cell>
</Table.Body>)
: (
<>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Order#</Table.HeaderCell>
<Table.HeaderCell>Billing Address</Table.HeaderCell>
<Table.HeaderCell>Shipping Address</Table.HeaderCell>
<Table.HeaderCell>Email</Table.HeaderCell>
<Table.HeaderCell>Phone Number</Table.HeaderCell>
<Table.HeaderCell>Sales Channel</Table.HeaderCell>
<Table.HeaderCell>Order Date</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{this.state.suggestions.map((item, index) => (
<Table.Row className="hoverRow">
<Table.Cell key={index} onClick={() => this.addToQuery(item)}>
{item.customerPO}
</Table.Cell>
<Table.Cell>
{item.billToAddress}
</Table.Cell>
<Table.Cell>{item.shipToAddress}</Table.Cell>
<Table.Cell>{item.email}</Table.Cell>
<Table.Cell>{item.phone}</Table.Cell>
<Table.Cell>{item.customerContact}</Table.Cell>
<Table.Cell>{item.dateCreated}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</>
)
}
<Pagination key={this.state.pagesNeeded} tableCols="7" pagesNeeded={this.state.pagesNeeded} btnLimit={5} pageClick={this.handlePageClick} currPage={this.state.page} pageLimit={this.pageLimit}/>
</Table>
);
}
handleIconClick(){
console.log('icon clicked ' + this.state.Query )
}
render() {
const {text} = this.state
//console.log(this.state)
return (
<>
<div className="App-Component">
<div className="App-Search">
<Input icon={{ name: 'search', circular: true, link: true, onClick: () => this.handleIconClick() }} placeholder="Search" value={text} type="text" onKeyUp={this.keyUpHandler} onKeyDown={this.keyDownHandler} className="App-Search"/>
{this.renderSuggestions()}
</div>
</div>
</>
);
}
}
export default SearchOrderBar;
Here is the pagination but I don't think this matters as much for the solution. It is relevant for the page button click.
import React, {Component} from 'react';
import {Input, Label, Table, Icon, Header, Menu} from 'semantic-ui-react'
/**
* Helper Method for creating a range of Numbers
* Range )( )
*/
const range = (from, to, step = 1) => {
let i = from;
const range = [];
while (i<=to) {
range.push(i);
i+=step;
}
}
export default class Pagination extends Component {
constructor(props){
super(props)
const { totalRecords = null, pageNeighbours = 0, rangeCount_URL = this.props.rangeCount_URL, pageArray = [] } = props;
this.pageArray = typeof pageArray === 'array' ? pageArray : [];
}
renderPagination = () => {
//console.log("hello from pagination");
let n = this.props.pagesNeeded;
let pArray = [];
let page = this.props.currPage;
//console.log(n)
if (page > 1){
pArray.push(<Menu.Item as='a' icon onClick={() => this.props.pageClick(page-1)}>
<Icon name='chevron left' />
</Menu.Item>)
}
for(let i = (page >1 ? page-1: page); pArray.length < (page > this.props.btnLimit ? this.props.btnLimit+1 : this.props.btnLimit); i++){
//console.log(i);
pArray.push(<Menu.Item index={i} className={i == page ? 'active' : ''} onClick={() => this.props.pageClick(i)} as='a'>{i}</Menu.Item>)
}
if (page < n){
pArray.push(<Menu.Item as='a' icon onClick={() => this.props.pageClick(page+1)}>
<Icon name='chevron right' />
</Menu.Item>)
}
this.pageArray = pArray;
return pArray;
}
render(){
const pageCount = (() => {
const totalRecords = this.totalRecords;
if(totalRecords > 0){
return (this.totalPages = Math.ceil(this.totalRecords / this.props.pageLimit))
}
})();
//console.log(this.pageArray);
return(
<Table.Footer>
{ this.props.pagesNeeded > 1 &&
<Table.Row>
<Table.HeaderCell colSpan={this.props.tableCols}>
<Menu floated='right' pagination>
{this.renderPagination()}
</Menu>
</Table.HeaderCell>
</Table.Row>
}
</Table.Footer>
)
}
}
setState is batched and invoked asynchronously, meaning when you call to this.setState({page}) then read this.state.page in fetchCallBack you probably get the "old" page and not the new page.
Either pass the page directly to fetchCallBack
this.fetchCallBack(page)
And read the page from it and not directly from the state
Or call it as the second argument of setState which is a callback that react will invoke right after the state has been updated.
this.setState({ page }, this.fetchCallBack);
At the point fetchCallBack is called, this.state.page is not updated yet because setState is called asynchronously, that's why it's using the old value. Try this:
handlePageClick(page) {
this.setState({ page }, this.fetchCallBack);
}
The callback syntax allows you to run the function in the next iteration.
I found a lot of solutions about this problem but none of them work.
I have a view which renders dynamically components depending on the backend response
/**
* Module dependencies
*/
const React = require('react');
const Head = require('react-declarative-head');
const MY_COMPONENTS = {
text: require('../components/fields/Description'),
initiatives: require('../components/fields/Dropdown'),
vuln: require('../components/fields/Dropdown'),
severities: require('../components/fields/Dropdown'),
};
const request = restclient({
timeout: 5000,
baseURL: '/api',
});
const { DropdownItem } = Dropdown;
class CreateView extends React.Component {
constructor(props) {
super(props);
this.state = {
modal: false,
states: props.states,
error: props.error,
spinner: true,
state: props.state,
prevState: '',
components: [],
};
this.handleChange = this.handleChange.bind(this);
this.getRequiredFields = this.getRequiredFields.bind(this);
this.onChangeHandler = this.onChangeHandler.bind(this);
this.changeState = this.changeState.bind(this);
this.loadComponents = this.loadComponents.bind(this);
}
componentDidMount() {
this.loadComponents();
}
onChangeHandler(event, value) {
this.setState((prevState) => {
prevState.prevState = prevState.state;
prevState.state = value;
prevState.spinner = true;
return prevState;
}, () => {
this.getRequiredFields();
});
}
getRequiredFields() {
request.get('/transitions/fields', {
params: {
to: this.state.state,
from: this.state.prevState,
},
})
.then((response) => {
const pComponents = this.state.components.map(c => Object.assign({}, c));
pComponents.forEach((c) => {
c.field.required = 0;
c.field.show = false;
});
response.data.forEach((r) => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
ob.field.required = r.required;
ob.field.show = true;
}
});
this.setState({
components: pComponents,
fields: response.data,
spinner: false,
});
})
.catch(err => err);
}
loadComponents() {
this.setState((prevState) => {
prevState.components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
};
return {
field, component: MY_COMPONENTS[k],
};
});
return prevState;
});
}
handleChange(field, value) {
this.setState((prevState) => {
prevState[field] = value;
return prevState;
});
}
changeState(field, value) {
this.setState((prevState) => {
prevState[`${field}`] = value;
return prevState;
});
}
render() {
const Components = this.state.components;
return (
<Page name="CI" state={this.props} Components={Components}>
<Script src="vendor.js" />
<Card className="">
<div className="">
<div className="">
<Spinner
show={this.state.spinner}
/>
{Components.map((component, i) => {
const Comp = component.component;
return (<Comp
key={i}
value={this.state[component.field.name]}
field={component.field}
handleChange={this.handleChange}
modal={this.state.modal}
changeState={this.changeState}
/>);
})
}
</div>
</div>
</div>
</Card>
</Page>
);
}
}
module.exports = CreateView;
and the dropdown component
const React = require('react');
const request = restclient({
timeout: 5000,
baseURL: '/api',
});
const { DropdownItem } = Dropdown;
class DrpDwn extends React.Component {
constructor(props) {
super(props);
this.state = {
field: props.field,
values: [],
};
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('state', this.state.field);
console.log('prevState', prevState.field);
console.log('prevProps', prevProps.field);
console.log('props', this.props.field);
}
render() {
const { show } = this.props.field;
return (show && (
<div className="">
<Dropdown
className=""
onChange={(e, v) => this.props.handleChange(this.props.field.name, v)}
label={this.state.field.name.replace(/^./,
str => str.toUpperCase())}
name={this.state.field.name}
type="form"
value={this.props.value}
width={100}
position
>
{this.state.values.map(value => (<DropdownItem
key={value.id}
value={value.name}
primary={value.name.replace(/^./, str => str.toUpperCase())}
/>))
}
</Dropdown>
</div>
));
}
module.exports = DrpDwn;
The code actually works, it hide or show the components correctly but the thing is that i can't do anything inside componentdidupdate because the prevProps prevState and props are always the same.
I think the problem is that I'm mutating always the same object, but I could not find the way to do it.
What I have to do there is to fill the dropdown item.
Ps: The "real" code works, i adapt it in order to post it here.
React state is supposed to be immutable. Since you're mutating state, you break the ability to tell whether the state has changed. In particular, i think this is the main spot causing your problem:
this.setState((prevState) => {
prevState.components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
}; return {
field, component: MY_COMPONENTS[k],
};
});
return prevState;
});
You mutate the previous states to changes its components property. Instead, create a new state:
this.setState(prevState => {
const components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
};
return {
field, component: MY_COMPONENTS[k],
};
});
return { components }
}
You have an additional place where you're mutating state. I don't know if it's causing your particular problem, but it's worth mentioning anyway:
const pComponents = [].concat(this.state.components);
// const pComponents = [...this.state.components];
pComponents.forEach((c) => {
c.field.required = 0;
c.field.show = false;
});
response.data.forEach((r) => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
ob.field.required = r.required;
ob.field.show = true;
}
});
You do at make a copy of state.components, but this will only be a shallow copy. The array is a new array, but the objects inside the array are the old objects. So when you set ob.field.required, you are mutating the old state as well as the new.
If you want to change properties in the objects, you need to copy those objects at every level you're making a change. The spread syntax is usually the most succinct way to do this:
let pComponents = this.state.components.map(c => {
return {
...c,
field: {
...c.field,
required: 0,
show: false
}
}
});
response.data.forEach(r => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
// Here it's ok to mutate, but only because i already did the copying in the code above
ob.field.required = r.required;
ob.field.show = true;
}
})