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'm trying to create a switcher with react that when I click it it change the price of another component and when I click again over it return the original price.
My first approach was this:
I crated the input with the type of a checkbox so when checked is true change the price and when is false return the original price, some kind of toggle all handled with the funcion handleDiscount
<input
checked={toggle}
onChange={handleDiscount}
type="checkbox"
className="switch-input"
/>
I created the handleDiscount function that change the toggle from his initial state which is false to true and and after that a ternary operator that check the condition to set the price.
const handleDiscount = () => {
setToggle(!toggle);
toggle === true ? setPrice(10) : setPrice(20);
};
the problem is that when is click over the checkbox again the price don't change.
I have to work with the useEffect hook? which it's the best approach for this kind of work?
for example I wrote the same code with my knowledge in vanilaJS and work, here is the example:
const switcher = document.querySelector("input");
switcher.addEventListener("click", () => {
const priceBasic = document.querySelector(".priceBasic");
const pricePro = document.querySelector(".pricePro");
const priceMaster = document.querySelector(".priceMaster");
if (switcher.checked == true) {
priceBasic.innerHTML = `<h1>$49.99</h1>`;
pricePro.innerHTML = "$69.99";
priceMaster.innerHTML = "$89.99";
} else {
priceBasic.innerHTML = "$19.99";
pricePro.innerHTML = "$24.99";
priceMaster.innerHTML = "$39.99";
}
});
Issue
The main issue here is that state updates are asynchronous, the toggle value isn't the updated value you just enqueued.
const handleDiscount = () => {
setToggle(!toggle);
toggle === true ? setPrice(10) : setPrice(20);
};
Solution
Use an useEffect to toggle the price state when the toggle state updates.
useEffect(() => {
setPrice(toggle ? 10 : 20);
}, [toggle]);
...
const handleDiscount = () => {
setToggle(toggle => !toggle);
};
Alternatively you can make the checkbox uncontrolled and directly set the price state when the checkbox value updates
const handleDiscount = (e) => {
const { checked } = e.target;
setPrice(checked ? 10 : 20);
};
...
<input
onChange={handleDiscount}
type="checkbox"
className="switch-input"
/>
I am assuming toggle is a state variable and setToggle is s state function, in which case, this won't work:
const handleDiscount = () => {
setToggle(!toggle);
toggle === true ? setPrice(10) : setPrice(20);
};
As setToggle is asynchronous. So toggle does not actually toggle by the time it reaches toggle === true ? setPrice(10) : setPrice(20);
You should instead use useEffect and listen to the change of toggle like so:
useEffect(() => {
toggle === true ? setPrice(10) : setPrice(20);
},[toggle])
And get rid of toggle === true ? setPrice(10) : setPrice(20); inside handleDiscount altogether.
While your specific question of getting the toggle to work has been answered, your example in vanilla javascript hasn't been addressed.
Ideally you wouldn't be hard coding different price options directly into the component, but would be serving an array of products which each held their own specific details including pricing options.
There are various ways of handling this, but in the snippet below your toggle has become a pointer stored in state in individual Product components that can be used to access the appropriate price property of the product object passed to that component. eg.
let product = {
id: 1,
name: 'Product One',
defaultPricePoint: 'red',
pricePoints: {
red: "$49.99",
blue: "$69.99",
}
};
const [pricePoint, setPricePoint] = useState('red');
let price = product.pricePoints[pricePoint];
// "$49.99"
The change handler simply sets the pricePoint state using the value of the input that changed (which were mapped from the keys of the object's prices to begin with). And a useEffect is implemented to set the initial radio selection based on a default set in the product object.
The result is two fairly simple components and decent separation between data and logic.
(note the use of key values that are unique for each mapped element, and not index values)
const App = ({ products }) => {
return (
<div>
{products.map(product =>
<Product key={product.id} product={product} />
)}
</div>
)
}
const Product = ({ product }) => {
const [pricePoint, setPricePoint] = React.useState();
React.useEffect(() => {
setPricePoint(product.defaultPricePoint)
}, [product]);
const handlePricePoint = (e) => {
setPricePoint(e.target.value);
}
return (
<div>
<h3>{product.name}</h3>
<p>{product.pricePoints[pricePoint]}</p>
<form>
{Object.keys(product.pricePoints).map(p =>
<div key={product.id + p}>
<input
type="radio"
name="price-point"
value={p}
onChange={handlePricePoint}
checked={pricePoint === p}
/>
<label for={p}>{p}</label>
</div>
)}
</form>
</div>
);
}
ReactDOM.render(
<App products={products} />,
document.getElementById("root")
);
<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>
const products = [
{
id: 1,
name: 'Product One',
defaultPricePoint: 'red',
pricePoints: {
red: "$49.99",
blue: "$69.99",
}
},
{
id: 2,
name: 'Product Two',
defaultPricePoint: 'pro',
pricePoints: {
basic: "$19.99",
pro: "$24.99",
master: "$39.99",
}
}
]
</script>
<div id="root"></div>
#codemonkey's answer is correct but if you want something a little simpler:
function YourComponent() {
const [price, setPrice] = useState(20);
const [toggle, setToggle] = useState(false);
const handleDiscount = () => {
const newValue = !toggle;
setToggle(newValue);
setPrice(newValue ? 10 : 20);
}
// ...
}
Or if price is only computed based on toggle:
function YourComponent() {
const [toggle, setToggle] = useState(false);
const price = useMemo(
() => toggle ? 10 : 20,
[toggle]
);
const handleDiscount = () => {
setToggle(!toggle);
}
// ...
}
I am trying to use the tagPicker from fluent ui. I am using as starting point the sample from the site:
https://developer.microsoft.com/en-us/fluentui#/controls/web/pickers
The problem is that the object I have has 3 props. the objects in the array are {Code:'string', Title:'string', Category:'string'}. I am using a state with a useeffect to get the data. SO far works fine, the problem is that the suggestion are rendered blank. It filter the items but does not show the prop I want.
Here is my code:
import * as React from 'react';
import {
TagPicker,
IBasePicker,
ITag,
IInputProps,
IBasePickerSuggestionsProps,
} from 'office-ui-fabric-react/lib/Pickers';
import { mergeStyles } from 'office-ui-fabric-react/lib/Styling';
const inputProps: IInputProps = {
onBlur: (ev: React.FocusEvent<HTMLInputElement>) => console.log('onBlur called'),
onFocus: (ev: React.FocusEvent<HTMLInputElement>) => console.log('onFocus called'),
'aria-label': 'Tag picker',
};
const pickerSuggestionsProps: IBasePickerSuggestionsProps = {
suggestionsHeaderText: 'Suggested tags',
noResultsFoundText: 'No color tags found',
};
const url="url_data"
export const TestPicker: React.FunctionComponent = () => {
const getTextFromItem = (item) => item.Code;
const [state, setStateObj] = React.useState({items:[],isLoading:true})
// All pickers extend from BasePicker specifying the item type.
React.useEffect(()=>{
if (!state.isLoading) {
return
} else {
caches.open('cache')
.then(async cache=> {
return cache.match(url);
})
.then(async data=>{
return await data.text()
})
.then(data=>{
const state = JSON.parse(data).data
setStateObj({items:state,isLoading:false})
})
}
},[state.isLoading])
const filterSuggestedTags = (filterText: string, tagList: ITag[]): ITag[] => {
return filterText
? state.items.filter(
tag => tag.Code.toLowerCase().indexOf(filterText.toLowerCase()) === 0 && !listContainsTagList(tag, tagList),
).slice(0,11) : [];
};
const listContainsTagList = (tag, state?) => {
if (!state.items || !state.items.length || state.items.length === 0) {
return false;
}
return state.items.some(compareTag => compareTag.key === tag.key);
};
return (
<div>
Filter items in suggestions: This picker will filter added items from the search suggestions.
<TagPicker
removeButtonAriaLabel="Remove"
onResolveSuggestions={filterSuggestedTags}
getTextFromItem={getTextFromItem}
pickerSuggestionsProps={pickerSuggestionsProps}
itemLimit={1}
inputProps={inputProps}
/>
</div>
);
};
I just got it, I need to map the items to follow the {key, name} from the sample. Now it works.
setStateObj({items:state.map(item => ({ key: item, name: item.Code })),isLoading:false})
I have this component:
// imports
export default class TabViewExample extends Component {
state = {
index: 0,
routes: [
{ key: 'first', title: 'Drop-Off', selected: true },
{ key: 'second', title: 'Pick up', selected: false },
],
};
handleIndexChange = index => this.setState({ index });
handleStateIndexChange = () => { // FUNCTION WITH THE ERROR
const { index } = this.state;
this.setState(({ routes }) => ({
routes: routes.map((route, idx) => ({
...route,
selected: idx === index,
})),
}));
};
renderTabBar = props => {
const { routes } = this.state;
this.handleStateIndexChange(); // HERE I GET THE ERROR
return (
<View style={tabViewStyles.tabBar}>
{props.navigationState.routes.map((route, i) => {
return (
<>
<TouchableOpacity
key={route.key}
style={[
tabViewStyles[`tabStyle_${i}`],
]}
onPress={() => this.setState({ index: i })}
>
<Text>
{route.title}
</Text>
// THE FUNCTION ATTEMPTS TO SHOW AN ELEMENT WHEN
// THE INDEX OF A ROUTE IS selected true
{routes[i].selected && (
<View
style={{
flex: 1,
}}
>
<View
style={{
transform: [{ rotateZ: '45deg' }],
}}
/>
</View>
)}
</TouchableOpacity>
</>
);
})}
</View>
);
};
renderScene = SceneMap({
first: this.props.FirstRoute,
second: this.props.SecondRoute,
});
render() {
return (
<TabView
navigationState={this.state}
renderScene={this.renderScene}
renderTabBar={this.renderTabBar}
onIndexChange={this.handleIndexChange}
/>
);
}
}
Full error:
Invariant Violation: Invariant Violation: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
This is the function giving the error:
handleStateIndexChange = () => { // FUNCTION WITH THE ERROR
const { index } = this.state;
this.setState(({ routes }) => ({
routes: routes.map((route, idx) => ({
...route,
selected: idx === index,
})),
}));
};
All I need is to set the state of selected to true so I can toggle the visibility of a component.
As stated in the comment above, you cannot call setState directly inside your render function.
I do not see any reason to keep the selected value in your state as it only depends on another value already in it, this information is redundant.
You should remove selected from all your objects in routes and change your JSX condition :
{routes[i].selected &&
To the following :
{i === this.state.index &&
To produce the same result.
You can now remove handleStateIndexChange from your code
Because of the way React will batch state updates, if you need to set state based off previous state, you should do all the destructuring in the callback:
handleStateIndexChange = () => {
this.setState(({ index, routes }) => ({
routes: routes.map((route, idx) => ({
...route,
selected: idx === index,
})),
}));
};
Also, as #Adeel mentions, you should not call this.setState from inside the render execution context. Move that call the componentDidUpdate instead.
Calling setState in render is the actual reason for infinite rerender loop,
because setting state calls rerender, which calls another setState which calls another rerender etc.
So I would suggest to move the setState part into componentDidUpdate and control it there. Here is the approximation of what I mean with little changes to describe the point.
class App extends React.Component {
state = {
index: 0,
routes: [
{
key: "first",
title: "Drop-Off",
selected: true
},
{
key: "second",
title: "Pick up",
selected: false
}
]
};
componentDidUpdate(_, prevState) {
if (prevState.index !== this.state.index) {
this.handleStateIndexChange();
}
}
handleIndexChange = () => {
const randomIndexBetweenZeroAndOne = (Math.random() * 100) % 2 | 0;
this.setState({
index: randomIndexBetweenZeroAndOne
});
};
getMappedRoutes = (routes, index) =>
routes.map((route, idx) => ({
...route,
selected: idx === index
}));
handleStateIndexChange = () => {
this.setState(({ routes, index }) => ({
routes: this.getMappedRoutes(routes, index)
}));
};
render() {
const { routes } = this.state;
return (
<>
<button onClick={this.handleIndexChange}>Change Index</button>
{this.state.routes.map(JSON.stringify)}
</>
);
}
}
I can't for the life of me figure out why I'm getting error:
Maximum call stack size exceeded
When this code is run. If I comment out:
const tabs = this.getTabs(breakpoints, panels, selectedTab);
the error goes away. I have even commented out other setState() calls to try and narrow down where the problem was at.
Code (removed the extra functions):
export default class SearchTabs extends Component {
constructor() {
super();
this.state = {
filters: null,
filter: null,
isDropdownOpen: false,
selectedFilter: null,
};
this.getTabs = this.getTabs.bind(this);
this.tabChanged = this.tabChanged.bind(this);
this.setSelectedFilter = this.setSelectedFilter.bind(this);
this.closeDropdown = this.closeDropdown.bind(this);
this.openDropdown = this.openDropdown.bind(this);
}
componentDidMount() {
const { panels } = this.props;
if (!panels || !panels.members || panels.members.length === 0) {
this.props.fetchSearch();
}
}
getTabs(breakpoints, panels, selectedTab) {
const tabs = panels.member.map((panel, idx) => {
const { id: panelId, headline } = panel;
const url = getHeaderLogo(panel, 50);
const item = url ? <img src={url} alt={headline} /> : headline;
const classname = classNames([
searchResultsTheme.tabItem,
(idx === selectedTab) ? searchResultsTheme.active : null,
]);
this.setState({ filter: this.renderFilters(
panel,
breakpoints,
this.setSelectedFilter,
this.state.selectedFilter,
this.state.isDropdownOpen,
) || null });
return (
<TabItem
key={panelId}
classname={`${classname} search-tab`}
headline={headline}
idx={idx}
content={item}
onclick={this.tabChanged(idx, headline)}
/>
);
});
return tabs;
}
render() {
const { panels, selectedTab } = this.props;
if (!panels || panels.length === 0) return null;
const tabs = this.getTabs(breakpoints, panels, selectedTab);
return (
<div className={searchResultsTheme.filters}>
<ul className={`${searchResultsTheme.tabs} ft-search-tabs`}>{tabs}</ul>
<div className={searchResultsTheme.dropdown}>{this.state.filter}</div>
</div>
);
}
}
export const TabItem = ({ classname, content, onclick, key }) => (
<li key={key} className={`${classname} tab-item`} onClick={onclick} >{content}</li>
);
Because of this loop:
render -----> getTabs -----> setState -----
^ |
| |
|____________________________________________v
You are calling getTabs method from render, and doing setState inside that, setState will trigger re-rendering, again getTabs ..... Infinite loop.
Remove setState from getTabs method, it will work.
Another issue is here:
onclick={this.tabChanged(idx, headline)}
We need to assign a function to onClick event, we don't need to call it, but here you are calling that method, use this:
onclick={() => this.tabChanged(idx, headline)}