Accessing child state from parent - javascript

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

Related

ScrollView Pagination causes all children to re-render

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.

React native: Update value of object in array in state

I have a component which changes the state when checkbox is checked and the data needs to be updated of the object in the array.
The component state looks something like this
{
key:1,
todo:"Something",
isChecked:false
}
i have 3 files:
AddTodo.js Which passes state & setState to an component TodoList which passes it the subcomponent TodoItem.
I am unable to update the state from TodoItem , I need to implement a function that finds the object from array and updates its isChecked state.
AddTodo.js
function AddTodo() {
const [state, setState] = useState(false);
const [todos, addTodos] = useState([]);
var keys = (todos || []).length;
return (
<View style={styles.container}>
<Modal
animationType="slide"
transparent={true}
visible={state}
statusBarTranslucent={true}
>
<View style={styles.itemsContainer}>
<GetInfoDialog
state={state}
stateChange={setState}
addItem={addTodos}
numKeys={keys}
/>
</View>
</Modal>
{(todos || []).length > 0 ? (
<TodoList data={todos} updateState={addTodos} />
) : null}
<TouchableOpacity
style={styles.btn}
onPress={() => {
setState(true);
}}
>
<Text style={styles.text}>Add New</Text>
</TouchableOpacity>
</View>
);
}
TodoList.js
function TodoList(props) {
return (
<View style={styles.todoList}>
<FlatList
data={props.data}
renderItem={({ item }) => {
console.log(item);
return (
<TodoItem
list={props.data}
itemKey={item.key}
todo={item.todo}
isChecked={item.isChecked}
updateState={props.updateState}
/>
);
}}
backgroundColor={"#000000"}
alignItems={"center"}
justifyContent={"space-between"}
/>
</View>
);
}
TodoItem.js
function TodoItem(props) {
const [checked, setCheck] = useState(props.isChecked);
return (
<View style={styles.todoItem}>
<Checkbox
value={checked}
onValueChange={() => {
setCheck(!checked);
}}
style={styles.checkbox}
/>
<Text style={styles.text}>{props.todo}</Text>
</View>
);
}
renderItem={({ item, index }) => {
console.log(item);
return (
<TodoItem
list={props.data}
itemKey={item.key}
todo={item.todo}
isChecked={item.isChecked}
updateState={props.updateState}
setChecked={(value)=>{
let updatedList = [...yourTodosList]
updatedlist[index].isChecked=value
setTodos(updatedList)
}}
/>
);
}}
and in your todo item
onValueChange={(value) => {
props.setChecked(value);
}}
i also don't think that you need an is checked state in your todo component since you are passing that through props (so delete const [checked, setCheck] = useState(props.isChecked) line and just use the value you are getting from props.isChecked)
didn't pay much attention to your variable names but this should put you on the right track
as per React Native Hooks you have to call
useEffect(() => {
setCheck(checked);
}, [checked]) // now this listens to changes in contact
in TodoItem.tsx

React Native, state changing for all items instead of clicked item

I created a button in flat list, when user click an specific item, it's button should change state and increment button should appear, but button changing state for all the items. I pass id too but it's not working, can someone please help me... below is my code
Items.js
<FlatList
data={this.props.items}
extraData={this.props}
keyExtractor={(items) => items.id.toString()}
numColumns={2}
renderItem={({ item }) => (
<CardBuyItem>
<Image style={styles.image} source={item.image} />
<View style={styles.detailContainer}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.subTitle} numberOfLines={1}>
{item.subTitle}
</Text>
<Text style={styles.price}>Rs {item.price}</Text>
</View>
{this.props.button && this.props.added.length > 0 ? (
<View style={styles.add}>
<Text style={styles.quantity}>{item.quantity}</Text>
<MaterialCommunityIcons
style={styles.iconUp}
size={20}
name="plus-circle-outline"
onPress={() => this.props.addQuantity(item.id)}
/>
<MaterialCommunityIcons
style={styles.iconDown}
size={20}
name="minus-circle-outline"
onPress={() => this.props.subtractQuantity(item.id)}
/>
</View>
) : (
<View style={styles.buy}>
<Text
style={styles.buyonce}
onPress={() => {
this.props.addToCart(item.id);
this.props.showCart();
this.props.showButton(item.id);
}}
>
Buy Once
</Text>
</View>
)}
</CardBuyItem>
)}
/>
const mapStateToProps = (state) => {
return {
items: state.clothes.jeans,
button: state.clothes.showButton,
added: state.clothes.addedItems,
};
};
const mapDispatchToProps = (dispatch) => {
return {
addToCart: (id) => dispatch(addToCart(id)),
addQuantity: (id) => dispatch(addQuantity(id)),
subtractQuantity: (id) => dispatch(subtractQuantity(id)),
showCart: () => dispatch(showCart()),
showButton: (id) => dispatch(showButton(id)),
};
};
That's my item list with mapStateToProsp and mapDispatchToProps here button should change it's state
reducer.js
if (action.type === SHOW_BUTTON) {
let addedItem = state.jeans.find((item) => item.id === action.id);
return {
...state,
addedItem: addedItem,
showButton: action.showButton,
};
}
const initialstate = { showButton: false}
it's my reducer function with initial state of that button
action.js
export const showButton = (id) => {
return {
type: SHOW_BUTTON,
showButton: true,
id,
};
};
it's my action where I'm describing action for my reducer
You are having a common state variable for this which causes it to show all buttons.
You can do a simple solution like this.
In your flatlist you can have a logic to display the button
{this.props.added.find(x=>x.id==item.id) !=null ? (
Or if you have to use the reducer, you will have to have a property in the array and update it which would be complex to maintain.

How to toggle the state of an item inside a map funtion

I'm trying to make a tag selection, the problem is, I don't know how to make a state for each item in the map, right now I have just one state, that, of course, will change all items.
That's the state and the function to toggle the state
const [selectedActivity, setSelectedActivity] = useState(false);
const toggleSelectedActivity = () => {
setSelectedActivity(!selectedActivity);
};
and that's the map function
<View style={styles.tags}>
{activitiesObject.map((data, i) => (
<TouchableOpacity
key={data.activity}
onPress={() => toggleSelectedActivity(i)}
>
<Text style={selectedActivity ? styles.selectedTag : styles.tagsText}>
{data.activity}
</Text>
</TouchableOpacity>
))}
</View>;
the image below shows what I expect to happen every time the user selects a tag
Here is the full code: https://snack.expo.io/KIiRsDPQv
You can do one of following options
change state to an array
const [selectedActivity, setSelectedActivity] = useState(Array.from({ length: activitiesObject.length }, _ => false))
const toggleSelectedActivity = (index) =>
setSelectedActivity(prev => prev.map((bool, i) => i == index ? !bool : bool))
while passing the index to function, and use selectedActivity[i] ? ...
extract
<TouchableOpacity key={data.activity} onPress={() => toggleSelectedActivity(i)}>
<Text style={selectedActivity ? styles.selectedTag : styles.tagsText}>{data.activity}</Text>
</TouchableOpacity>
to its own component, and inside it declare the state
{activitiesObject.map((data, i) => <MyComp data={data} i={i} />
const MyComp = ({ data, i }) => {
const [selectedActivity, setSelectedActivity] = useState(false)
return <TouchableOpacity key={data.activity} onPress={() => setSelectedActivity(prev => !prev)}>
<Text style={selectedActivity ? styles.selectedTag : styles.tagsText}>{data.activity}</Text>
</TouchableOpacity>
}

How to add multiple items to a flatlist using textinput?

i'm trying to add items to a flatlist via textinput. at the moment i can only add one item and when i try to add a second it just updates the first item, although the data from the textinput should appear below the previous textinput. i have found a few instances of a similar kind of problem and i know that i probably need to add something to my code but i just can't figure out what. below is my code. i would be grateful for any kind of help :)
function FlatlistComponent({ }) {
const listItems = [];
const [arrayHolder, setArrayHolder] = React.useState([]);
const [textInputHolder, setTextInputHolder] = React.useState('');
useEffect(() => {
setArrayHolder(listItems)
}, [])
const joinData = () => {
listItems.push({ name : textInputHolder });
setArrayHolder(listItems);
}
const FlatListItemSeparator = () => {
return (
<View
style={{
height: 1,
width: "95%",
backgroundColor: '#00678A',
alignSelf: 'center'
}} />
);
}
// Delete note
deleteNote = id => () => {
const filteredData = arrayHolder.filter(item => item.id !== id);
setArrayHolder({ data: filteredData });
}
return (
<View style={styles.MainContainer}>
<FlatList
data={arrayHolder}
width='100%'
extraData={arrayHolder}
keyExtractor={(item) => item.id}
ItemSeparatorComponent={FlatListItemSeparator}
renderItem={({ item }) => <Text style={styles.item} onPress={deleteNote(item.id)}> {item.name} </Text>}
/>
<TextInput
placeholder='Kirjoita uusi'
onChangeText={data => setTextInputHolder(data)}
style={styles.textInputStyle}
underlineColorAndroid='transparent'
clearTextOnFocus={true}
value={listItems}
/>
<TouchableOpacity onPress={joinData} style={styles.button} >
<Text style={styles.buttonText}> + </Text>
</TouchableOpacity>
</View>
);
}
listItems is always an empty array after component re-rendered, you should concat previous arrayHolder with new item:
const joinData = () => {
setArrayHolder([... arrayHolder, {name: textInputHolder }]);
// or use update function
// setArrayHolder(prev => [...prev, {name: textInputHolder }]);
}

Categories

Resources