I'm having problems with React recognizing a nested state update. I've read through the excellent responses here but still have questions.
I can confirm that the state change is being made and recognized in my components. The only thing I'm not seeing is React updating the tree, which I've read usually means React doesn't recognize my state updates (only looks for shallow copies). But, React does update the tree enough to trigger console.log()
updates in my consumer.
Maybe I'm not binding something in the right spot? My consumer is also pretty intense but otherwise working. Also open to refactoring options.
FilterContext:
import React from 'react';
export const FilterContext = React.createContext();
class FilterProvider extends React.Component {
constructor(props) {
super(props);
this.toggleEuphoric = () => {
this.setState(prevState => ({
...prevState,
emotions: {
...prevState.emotions,
anxious: false,
aroused: false,
calm: false,
cerebral: false,
creative: false,
energetic: false,
euphoric: true,
focused: false,
giggly: false,
happy: false,
hungry: false,
meditative: false,
mellow: false,
noemotion: false,
relaxed: false,
sleepy: false,
talkative: false,
tingly: false,
uplifted: false,
}
}))
},
this.state = {
emotions: {
anxious: null,
aroused: null,
calm: null,
cerebral: null,
creative: null,
energetic: null,
euphoric: null,
focused: null,
giggly: null,
happy: null,
hungry: null,
meditative: null,
mellow: null,
noemotion: null,
relaxed: null,
sleepy: null,
talkative: null,
tingly: null,
uplifted: null,
},
toggleEuphoric: this.toggleEuphoric,
}
}
render() {
return (
<FilterContext.Provider value={ this.state }>
{ this.props.children }
</FilterContext.Provider>
)
}
}
export default FilterProvider;
Filter Consumer:
<FilterContext.Consumer>
{ filterToggle => (
// Check the emotions part of the FilterContext state object
// If all options are null, it means no interactions have taken place
// So we show all results using .map() over the pageContext.productData array coming out of gatsby-node.js
Object.values(filterToggle.emotions).every(item => item === null) ? (
this.props.pageContext.productData.map( (product) => {
return <ProductListItem
desc={ product.shortDescription }
handle={ product.handle }
image={ product.image }
key={ product.id }
productType={ product.productType }
strain={ product.strain }
tac={ product.tac }
tags={ product.tags }
title={ product.title }
/>
})
) : (this.props.pageContext.productData.map( product => {
emotion = Object.keys(filterToggle.emotions).find(key => filterToggle.emotions[key])
return product.tags.forEach( (tag) => {
tag.toLowerCase() === emotion &&
console.log(this),
<ProductListItem
desc={ product.shortDescription }
handle={ product.handle }
image={ product.image }
key={ product.id }
productType={ product.productType }
strain={ product.strain }
tac={ product.tac }
tags={ product.tags }
title={ product.title }
/>
}
)
}))
)}
</FilterContext.Consumer>
The state updates were not the issue - state was updating as indicated in the question. My issue was in my provider, where I was previously trying to do a forEach()
on the return
of a .map()
array.
The breakthrough was two fold: discovering .includes()
method was the first part. Handling a conditional inside of a .forEach()
was a non-starter in my question (and likely why nothing was showing up in the DOM). .includes()
is essentially a conditional statement in hiding (does this array contain any of X?), so using it inside of the .filter()
achieves the same thing.
The second breakthrough was chaining array methods together (see more here gomakethings), specifically .filter()
with .map()
. I didn't realize .filter()
doesn't return an array to the DOM, so I needed to chain a .map()
to it in order to show the results.
My new consumer, with copious comments:
<FilterContext.Consumer>
{ filterToggle => (
console.log( ),
/**
* ==================
* No filters active
* ==================
* filterToggle gives us access to the context state object in FilterContext
* filterToggle.emotion is a nested object inside of state, which is bad practice but we need it
* Our filters have three states: null (initial), true if active, false when others are active
* This first line of code iterated through the filterToggle.emotions with Object.values
* .every() takes each value and looks to make sure all of them are null
* Using a ternary, if all emotions are null, we know to load all product objects into the DOM
* We pass them all down to ProductListItem component with a .map on productData coming out of gatsby-node.js
*
* ==================
* Filters active
* ==================
* If all the filterEmotions aren't null, that means some filter has been engaged
* .filter() is then used to filter down the list of products
* It's basically returning an array after checking a condition
* In our case, we want to see if the product.tags part of productData contains the active filter
* But how? Well, each filter has a key with it's name (like anxious, euphoric, etc)
* So we can run Object.keys(filterToggle.emotions) to get all the keys
* Then we run .find() over the keys, basically looking for the true one, which means that filter is engaged
* If that whole mess is true, we know the active filter and the current product in the array match
* That means the filter has found the right item and we want to display it
* To do the display, we have to pass that array over to a .map()
* .map() returns the filteredProducts straight into ProductListItem, iterating through each one
*/
Object.values(filterToggle.emotions).every(item => item === null) ? (
this.props.pageContext.productData.map( (product) => {
return <ProductListItem
desc={ product.shortDescription }
handle={ product.handle }
image={ product.image }
key={ product.id }
productType={ product.productType }
strain={ product.strain }
tac={ product.tac }
tags={ product.tags }
title={ product.title }
/>
})
) : this.props.pageContext.productData.filter( (product) => {
return product.tags.includes(
Object.keys( filterToggle.emotions )
.find(key => filterToggle.emotions[key])) === true
}).map( (filteredProduct) => {
return <ProductListItem
desc={ filteredProduct.shortDescription }
handle={ filteredProduct.handle }
image={ filteredProduct.image }
key={ filteredProduct.id }
productType={ filteredProduct.productType }
strain={ filteredProduct.strain }
tac={ filteredProduct.tac }
tags={ filteredProduct.tags }
title={ filteredProduct.title }
/>
})
)}
</FilterContext.Consumer>