Background
I'm building a counter component for my app. This counter is a small component which allows the user to add or remove a product from their cart. This is the way I've coded it:
export default function MenuCounter () {
const [count, setCount] = useState(0);
return (
<View style={styles.adder}>
<TouchableOpacity onPress={() => {count === 0 ? setCount(count) : setCount(count - 1)}}>
<Text style={styles.less}>-</Text>
</TouchableOpacity>
<Text style={styles.counter}>{count}</Text>
<TouchableOpacity onPress={() => setCount(count + 1)}>
<Text style={styles.more}>+</Text>
</TouchableOpacity>
</View>
)
}
What I have tried
I've tried handling the change with a function and an onChange={} method. Code looks like this:
const [amount, setAmount] = useState(0)
const handleStateChange=()=>{
amount = count;
setAmount(amount)
}
And then
<MenuCounter onChange={handleStateChange}/>
Of course, this doesn't work, but I have no clue on how to fix it.
Question
How can I listen to state changes in a child component in order to be able to use it in its parent?
Edit 1:
Forgot to mention that the MenuCounter is rendered within a FlatList item. That was the initial reason I had the state in the child rather than the parent. The answers provided so far (9/12/18 10:17) update every component at the same time.
It's better to use MenuCounter as child component which receives props from the parent component and child component can handle props and invoke parent methods ( as per your requirements) to update the counter as below.
Parent Component
export default ParentComponent = () => {
const [count, setCounter] = useState(0);
const updateCounter = () => {
//logic
//setCounter(count + 1);
// setCounter(counter - 1);
}
return (
<div>
<MenuCounter updateCounter={updateCounter} count={count}/>
</div>
);
}
Child component (MenuCounter)
export default MenuCounter = ({count, updateCounter}) => {
<View style={styles.adder}>
<TouchableOpacity onPress={() => updateCounter()}>
<Text style={styles.less}>-</Text>
</TouchableOpacity>
<Text style={styles.counter}>{count}</Text>
<TouchableOpacity onPress={() => updateCounter()}>
<Text style={styles.more}>+</Text>
</TouchableOpacity>
</View>
}
MenuCounter.propTypes = {
count: PropTypes.string,
updateCounter: PropTypes.func
};
Create a function in your parent
const change_amount = () => {
// CODE: Increase amount by 1
setAmount((value) => value + 1)
}
which you pass to in the components onPress={()=>{change_amount()}}
edit: or return a value from the child component
Related
Problem:
When using a ScrollView in ReactNative for horizontal pagination it re-renders all children, but I would like to keep the state values of certain local input fields and local variables of children components.
In the code below, if I were in the middle of updating a TextInput within the NotesSection but wanted to swipe back to the BatchSection to review some metadata, the code re-renders NotesSection and resets a local state holding the text value.
Diagnosis:
I'm very new to React and React Native, but my best guess here is that this happens due to the parent state variable "horizontalPos" which takes an integer to reflect what page is in focus.
This is simply used in the ProductHeader component to highlight a coloured bottomBorder showing the user a kind of small "menu" at the top of the screen.
The "horizontalPos" state can be updated in 2 ways:
First one is simply when clicking the wanted header (TouchableOpacity) within ProductHeader which triggers a state change and uses useRef to automatically move the ScrollView.
Second option is when the user swipes on the ScrollView. Using OnScroll to run a function "handleHorizontalScroll" which in turn sets the "horizontalPos" state using simple maths from the contentOffset.x.
Question / Solution:
If "horizontalPos" state was INSIDE ProductHeader I suspect this would solve the issue but I can't wrap my mind around how to do this as I don't believe it's possible to pass a function through to the child based on a change in the parent component.
I'm dependent on registering the OnScroll on the main ScrollView and the remaining components likewise have to be inside the main ScrollView but I don't want them to re-render every time the "horizontalPos" state updates.
Code:
const ProductScreen = (props) => {
const [horizontalPos, setHorizontalPos] = useState(0)
const scrollRef = useRef()
const toggleHorizontal = (page) => {
setHorizontalPos(page)
scrollRef.current.scrollTo({x:page*width, y:0, animated:false})
}
const handleHorizontalScroll = (v) => {
const pagination = Math.round(v.nativeEvent.contentOffset.x / width)
if (pagination != horizontalPos){
setHorizontalPos(pagination)
}
}
const ProductHeader = () => {
return(
<View style={styles.scrollHeaderContainer}>
<TouchableOpacity style={[styles.scrollHeader, horizontalPos == 0 ? {borderColor: AppGreenDark,} : null]} onPress={() => toggleHorizontal(0)}>
<Text style={styles.scrollHeaderText}>Meta Data</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.scrollHeader, horizontalPos == 1 ? {borderColor: AppGreenDark,} : null]} onPress={() => toggleHorizontal(1)}>
<Text style={styles.scrollHeaderText}>{"Notes"}</Text>
</TouchableOpacity>
</View>
)
}
return (
<View style={styles.container}>
<ProductHeader/>
<ScrollView
ref={scrollRef}
decelerationRate={'fast'}
horizontal={true}
showsHorizontalScrollIndicator={false}
snapToInterval={width}
onScroll={handleHorizontalScroll}
scrollEventThrottle={16}
disableIntervalMomentum={true}
style={{flex: 1}}
>
<View style={[styles.horizontalScroll]}>
<View style={styles.mainScrollView}>
<BatchSection/>
</View>
<ScrollView style={styles.notesScrollView}>
<NotesSection/>
</ScrollView>
</View>
</ScrollView>
</View>
)
}
As you outlined, updating horizontalPos state inside ProductScreen will cause a whole screen to re-render which is not an expected behavior.
To avoid this scenario, let's refactor the code as below:
function debounce(func, timeout = 500){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
class ProductHeader extends React.Component {
state = {horizontalPos:0 }
toggleHorizontal = (page) => {
this.setState({horizontalPos:page});
this.props.onPositionChange(page);
};
render () {
const {horizontalPos} = this.state
return (
<View style={styles.scrollHeaderContainer}>
<TouchableOpacity
style={[
styles.scrollHeader,
horizontalPos == 0 ? { borderColor: AppGreenDark } : null,
]}
onPress={() => this.toggleHorizontal(0)}
>
<Text style={styles.scrollHeaderText}>Meta Data</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.scrollHeader,
horizontalPos == 1 ? { borderColor: AppGreenDark } : null,
]}
onPress={() => this.toggleHorizontal(1)}
>
<Text style={styles.scrollHeaderText}>{"Notes"}</Text>
</TouchableOpacity>
</View>
);
}
};
const ProductScreen = (props) => {
const scrollRef = useRef();
const productHeaderRef = useRef()
let horizontalPos = 0;
const handleHorizontalScroll = (v) => {
const pagination = Math.round(v.nativeEvent.contentOffset.x / width);
if (pagination != horizontalPos) {
productHeaderRef.current?.toggleHorizontal(pagination)
}
};
const debouncedHorizontalScroll= debounce(handleHorizontalScroll,500)
const onPositionChange = (page) => {
horizontalPos = page;
scrollRef.current.scrollTo({ x: page * width, y: 0, animated: false });
};
return (
<View style={styles.container}>
<ProductHeader onPositionChange={onPositionChange} ref={productHeaderRef} />
<ScrollView
ref={scrollRef}
decelerationRate={"fast"}
horizontal={true}
showsHorizontalScrollIndicator={false}
snapToInterval={width}
onScroll={debouncedHorizontalScroll}
scrollEventThrottle={16}
disableIntervalMomentum={true}
style={{ flex: 1 }}
>
<View style={[styles.horizontalScroll]}>
<View style={styles.mainScrollView}>
<BatchSection />
</View>
<ScrollView style={styles.notesScrollView}>
<NotesSection />
</ScrollView>
</View>
</ScrollView>
</View>
);
};
I hope this will stop the whole screen from rerendering and maintaining pagination.
In the below code, I have used 2 "useState" hooks. Whenever I call the "setState" function of any one of these hooks, the whole component is re-rendered(Check "console.log" statements). I want if I call "setState" for one variable, only the element associated with that variable should render, rather the entire component
Code Snippet:
const App = () => {
const [count, setCount] = React.useState(0)
const [count2, setCount2] = React.useState(0)
return (
<SafeAreaView>
<TouchableOpacity onPress={() => setCount(count => count + 1)}>
{console.log('Counter 1')}
<Text>{count}</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => setCount2(count2 => count2 + 1)}>
{console.log('Counter 2')}
<Text>{count2}</Text>
</TouchableOpacity>
</SafeAreaView>
);
};
Render each Text in a stand-alone component ... This way whenever each Counter component receives a new-prop .. it will re-render.
function FirstCounter ({ count }) {
console.log('FirstCounter', count);
return (
<Text>{count}</Text>
);
}
export default React.memo(FirstCounter);
function SecondCounter ({ count }) {
console.log('SecondCounter', count);
return (
<Text>{count}</Text>
);
}
export default React.memo(SecondCounter);
function App() {
/** state is it was */
const [count, setCount] = React.useState(0);
const [count2, setCount2] = React.useState(0);
return (
<SafeAreaView>
<TouchableOpacity onPress={() => setCount(count => count + 1)}>
<FirstCounter count={count} />
</TouchableOpacity>
<TouchableOpacity onPress={() => setCount2(count2 => count2 + 1)}>
<SecondCounter count={count2} />
</TouchableOpacity>
</SafeAreaView>
);
}
Background
I'm building an app which has at some point a FlatList which renders products. The code for the list looks like this:
<FlatList
data={data}
renderItem={({ item }) => (
<View style={styles.container}>
<View style={styles.left}>
<Text style={styles.title}>{item.name}</Text>
<Text style={styles.description}>{item.description}</Text>
<Text style={styles.price}>${item.price}</Text>
<Counter />
</View>
<Image style={styles.right} source={{uri: item.image}}/>
</View>
)}
/>
The data for this list is brought over from a Google Cloud Firestore document. Within this list you can see a component called Counter, its job is to allow the user to add and delete products from their cart. This is its code:
export default function Counter () {
const [count, setCount] = useState(0);
const handleAddition=()=>{
setCount(count + 1)
}
const handleDeletion=()=>{
{count === 0 ? setCount(count) : setCount(count - 1)}
}
return (
<View style={styles.adder}>
<TouchableOpacity onPress={() => {handleDeletion()}}>
<Text style={styles.less}>-</Text>
</TouchableOpacity>
<Text style={styles.counter}>{count}</Text>
<TouchableOpacity onPress={() => {handleAddition()}}>
<Text style={styles.more}>+</Text>
</TouchableOpacity>
</View>
)
}
Problem
As you can see from the fact that I'm rendering the counter within a FlatList, I need to keep the state stored in the child rather than in the parent, as having the count in the parent would mean that if the user selects one product, every item is added at the same time.
I need to have the a button show up when the user selects a product that allows them to navigate to their purchase summary and also I need that button to display the total cost of their selection and amount of products chosen. As you might imagine, I've no idea how to access the child's state in the parent component.
So to sum it all up:
I have a child with a state update that I need to access from its parent, but I do not know how to do it.
Question¨
Is there any way to listen to event changes in a child's state or passing it up as a prop or something like that?
Thanks a lot in advance!
Extra information
This is image shows the UI of the screen. When pressing the "+" button it updates the count +1 and it should also display a button showing the info I mentioned before.
In renderItem you can pass method callback in here
<Counter onPressFunctionItem={(isPlus) => { // handle from parent here }} />
export default function Counter ({ onPressFunctionItem }) {
const [count, setCount] = useState(0);
const handleAddition=()=>{
setCount(count + 1)
if (onPressFunctionItem) {
onPressFunctionItem(true)
}
}
const handleDeletion=()=>{
{count === 0 ? setCount(count) : setCount(count - 1)}
if (onPressFunctionItem) {
onPressFunctionItem(false)
}
}
return (...)
}
Final Output:
You don't really need to pass the child component's state to the parent to achieve the same result, you can do that very easily the conventional way.
Here is the source code of above example:
export default function App() {
const [products, setProducts] = useState(data);
/*
with this function we increase the quantity of
product of selected id
*/
const addItem = (item) => {
console.log("addItem");
let temp = products.map((product) => {
if (item.id === product.id) {
return {
...product,
quantity: product.quantity + 1,
};
}
return product;
});
setProducts(temp);
};
/*
with this function we decrease the quantity of
product of selected id, also put in the condition so as
to prevent that quantity does not goes below zero
*/
const removeItem = (item) => {
console.log("removeItem");
let temp = products.map((product) => {
if (item.id === product.id) {
return {
...product,
quantity: product.quantity > 0 ? product.quantity - 1 : 0,
};
}
return product;
});
setProducts(temp);
};
/*
this varible holds the list of selected products.
if required, you can use it as a seperate state and use it the
way you want
*/
let selected = products.filter((product) => product.quantity > 0);
/**
* below are two small utility functions,
* they calculate the total itmes and total price of all
* selected items
*/
const totalItems = () => {
return selected.reduce((acc, curr) => acc + curr.quantity, 0);
};
const totalPrice = () => {
let total = 0;
for (let elem of selected) {
total += elem.quantity * elem.price;
}
return total;
};
useEffect(() => {
console.log(products);
}, [products]);
return (
<View style={styles.container}>
<FlatList
data={products}
renderItem={({ item }) => {
return (
<Card style={styles.card}>
<View style={styles.textBox}>
<Text>{item.name}</Text>
<Text>$ {item.price.toString()}</Text>
<View style={{ flexDirection: "row" }}></View>
<View style={styles.buttonBox}>
<Button
onPress={() => removeItem(item)}
title="-"
color="#841584"
/>
<Text>{item.quantity.toString()}</Text>
<Button
onPress={() => addItem(item)}
title="+"
color="#841584"
/>
</View>
</View>
<Image
style={styles.image}
source={{
uri: item.image,
}}
/>
</Card>
);
}}
/>
<View style={{ height: 60 }}></View>
{selected.length && (
<TouchableOpacity style={styles.showCart}>
<View>
<Text style={styles.paragraph}>
{totalItems().toString()} total price ${totalPrice().toString()}
</Text>
</View>
</TouchableOpacity>
)}
</View>
);
}
You can find the working app demo here: Expo Snack
I have a component like this:
export const MyComp = () => {
const [showRules, setShowRules] = useState(false)
return (
<TouchableOpacity onPress={() => setShowRules(!showRules)} activeOpacity={1}>
<View>
<Inner expand={showRules}>
<Text>Content here </Text>
</Inner>
</View>
</TouchableOpacity>
)
}
I have another component where I expand/collapse stuff just like this. the logic is duplicated, just the state has different names. thought it could be a good use case for a custom hook
so I wrote this
export const useExpander = (setExpand) => {
const [show, setShow] = useState(false)
useEffect(() => {
if (setExpand) {
setShow(true)
} else {
setShow(false)
}
}, [])
return show
}
but I can't figure out how to use it?
I cant change this line: <TouchableOpacity onPress={() => useExpander(expand)} activeOpacity={1}>
as I think this violates hook rules
any ideas?
I have a button that when clicked calls a function that sorts the products by case amount. I am updating the products array so I assumed this would trigger a re-render of the products being mapped in the code below but it is not. Does anyone know how to get this to trigger the products.map to be re-rendered again thus displaying the new sorted products?
render() {
const {products} = this.props;
const cartIcon = (<Icon name="shopping-cart" style={styles.cartIcon} />);
sortCartItems = () => {
products.sort((a, b) => a.cases > b.cases);
}
return (
<View style={styles.itemsInCartContent}>
<View style={styles.cartHeader}>
<TouchableOpacity onPress={sortCartItems}>
{cartIcon}
<Text>Sort</Text>
</TouchableOpacity>
</View>
{products.map((product, index) =>
<CartProductItem
ref="childCartProductItem"
key={product.id}
product={product}
index={index}
activeIndex={this.state.active}
triggerParentUpdate={() => this.collapseAllOtherProducts}
/>
)}
</View>
);
}
A component should not mutate it's own props. If your data changes during the lifetime of a component you need to use state.
Your inline arrow function sortCartItems tries to mutate the products that come from props. Your need to store the products in the components state instead and call setState to change them which will trigger a re-render.
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
products: props.products,
}
}
sortCartItems = () => {
this.setState(prevState => ({products: prevState.products.sort((a, b) => a.cases > b.cases);}))
}
render() {...}
}
Note that you need to use a callback in setState whenever you are updating the state based on the previous state. The callback receives the old state as a parameter and returns the new state.
I used a combination of messerbill's and trixn's answers to come up with the following which is now working. And I added a products property to state which receives its data from props.products
render() {
const cartIcon = (<Icon name="shopping-cart" style={styles.cartIcon} />);
sortCartItems = () => {
this.setState({
products: this.state.products.sort((a, b) => a.cases > b.cases)
});
}
return (
<View style={styles.itemsInCartContent}>
<View style={styles.cartHeader}>
<TouchableOpacity onPress={sortCartItems}>
{cartIcon}
<Text>Sort</Text>
</TouchableOpacity>
</View>
{this.state.products.map((product, index) =>
<CartProductItem
ref="childCartProductItem"
key={product.id}
product={product}
index={index}
activeIndex={this.state.active}
triggerParentUpdate={() => this.collapseAllOtherProducts}
/>
)}
</View>
);
}