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>
);
}
Related
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
Beginner question here, not sure exactly what this would be considered, but I'm trying to make a form where a user can add and remove input rows upon pressing a button. What do I need to change to render new components or remove components when the Add or Remove buttons are pressed? Right now, the Add and Remove button change the textInput array appropriately, but components are not actually being added or removed.
Here is my current code:
FormScreen.js
import React from 'react';
import { View, StyleSheet, ScrollView } from 'react-native';
import { Button, Caption } from 'react-native-paper';
import InputCard from '../components/InputCard';
const FormScreen = props => {
const textInput = [1,2,3];
const addTextInput = () => {
let currArr = textInput;
let lastNum = currArr[currArr.length -1]
let nextNum = lastNum + 1
console.log(currArr, lastNum, nextNum);
textInput.push(
nextNum
);
console.log(textInput);
};
const removeTextInput = () => {
textInput.pop();
console.log(textInput);
};
return (
<ScrollView>
<View style={styles.col}>
<View style={styles.row}>
<Caption>Favorite colors?</Caption>
</View>
<View style={styles.row}>
<View>
{textInput.map(key => {
return (
<InputCard key={key}/>
);
})}
</View>
</View>
<View>
<View style={styles.col}>
<Button title='Add' onPress={() => addTextInput()}>Add</Button>
</View>
<View style={styles.col}>
<Button title='Remove' onPress={() => removeTextInput()}>Remove</Button>
</View>
</View>
</View>
</ScrollView>
);
};
export default FormScreen;
InputCard.js
import React, { useState } from "react";
import { View, StyleSheet } from 'react-native';
import { Caption, Card, TextInput } from "react-native-paper";
const InputCard = (props) => {
const [input, setInput] = useState('');
return (
<View>
<Card>
<Card.Content>
<Caption>Item {props.key}</Caption>
<View style={styles.row}>
<View style={styles.half}>
<TextInput
label="Input"
value={input}
onChangeText={input => setInput(input)}
mode="outlined"
style={styles.textfield}
/>
</View>
</View>
</Card.Content>
</Card>
</View>
);
}
export default InputCard;
Instead of storing it in a array, try to do something like this, using 2 states.
const [totalTextInput, setTotalTextInput] = useState([])//initial state, set it to any data you want.
const [count, setCount] = useState(0);
const addTextInput = () => {
setCount((prevState) => prevState + 1);
setTotalTextInput((prevState) => {
const newTextInput = Array.from(prevState); // CREATING A NEW ARRAY OBJECT
newTextInput.push(count);
return newTextInput;
});
};
const removeTextInput = () => {
setTotalTextInput((prevState) => {
const newTextInput = Array.from(prevState); // CREATING A NEW ARRAY OBJECT
newTextInput.pop();
return newTextInput;
});
};
And in your code:
<View>
{totalTextInput.map(key => {
return (
<InputCard key={key}/>
);
})}
</View>
I am new to react native and my JS is a bit rusty. I need to be able to change the value of my collection for the firestore. I have two buttons that will change the value of typeOfPost by setting the state. Component1 can successfully get "this.state.typeOfPost". However, when I click one of the buttons and update the state my log inside of the async function is not being called. It is only called when the app initially renders. What I find weird is that my log on the top of Component1 will display as expected. Is there any better way of doing this?
class Forum extends Component {
state = {
typeOfPost: ' '
}
onPressSitter = () => {
this.setState({
typeOfPost: 'sitterPosts'
})
}
onPressNeedSitter = () => {
this.setState({
typeOfPost: 'needPosts'
})
}
render() {
return (
<View style={styles.container}>
<View style={styles.row}>
<TouchableOpacity
style={styles.button}
onPress={this.onPressSitter}
>
<Text>I am a sitter</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.button}
onPress={this.onPressNeedSitter}
>
<Text>Need a sitter</Text>
</TouchableOpacity>
</View>
<View>
<Component1 typeOfPost = {this.state.typeOfPost}> </Component1>
</View>
</View>
)
}
}
const Component1 = (props) => {
console.log("type of post " + props.typeOfPost);
const [loading, setLoading] = useState(true); // Set loading to true on component mount
const [data, setData] = useState([]); // Initial empty array of data
const getData = async () => {
console.log("type of post inside async " + props.typeOfPost);
const subscriber = firestore()
.collection(props.typeOfPost) // need to be able to update this
.onSnapshot(querySnapshot => {
const data = [];
querySnapshot.forEach(documentSnapshot => {
data.push({
...documentSnapshot.data(),
key: documentSnapshot.id,
});
});
setData(data);
setLoading(false);
});
// Unsubscribe from events when no longer in use
return () => subscriber();
}
useEffect(() => {
getData();
}, [])
if (loading) {
return <ActivityIndicator />;
}
return (
<FlatList
data={data}
ListEmptyComponent={
<View style={styles.flatListEmpty}>
<Text style={{ fontWeight: 'bold' }}>No Data</Text>
</View>
}
renderItem={({ item }) => (
<View>
<Text>User ID: {item.fullName}</Text>
</View>
)}
/>
)
}
There is a difference between mount and render. I see no problem with your code except the few remarks I have made. The thing is that when you change typeOfPost, the component is rerendered, but the useEffect is not called again, since you said, it's just called when it was first mounted:
useEffect(() => {
}, []) // ---> [] says to run only when first mounted
However here, you want it to run whenever typeOfPost changes. So here is how you can do this:
useEffect(() => {
getData();
}, [typeofPost])
class Forum extends Component {
state = {
typeOfPost: ' '
}
onPressSitter = () => {
this.setState({
typeOfPost: 'sitterPosts'
})
}
onPressNeedSitter = () => {
this.setState({
typeOfPost: 'needPosts'
})
}
render() {
return (
<View style={styles.container}>
<View style={styles.row}>
<TouchableOpacity
style={styles.button}
onPress={this.onPressSitter}
>
<Text>I am a sitter</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.button}
onPress={this.onPressNeedSitter}
>
<Text>Need a sitter</Text>
</TouchableOpacity>
</View>
<View>
<Component1 typeOfPost = {this.state.typeOfPost}> </Component1>
</View>
</View>
)
}
}
const Component1 = (props) => {
const { typeOfPost } = props
console.log("type of post " + props.typeOfPost);
const [loading, setLoading] = useState(true); // Set loading to true on component mount
const [data, setData] = useState([]); // Initial empty array of data
const getData = () => {
setLoading(true)
console.log("type of post inside async " + props.typeOfPost);
const subscriber = firestore()
.collection(props.typeOfPost) // need to be able to update this
.onSnapshot(querySnapshot => {
const data = [];
querySnapshot.forEach(documentSnapshot => {
data.push({
...documentSnapshot.data(),
key: documentSnapshot.id,
});
});
setData(data);
setLoading(false);
});
// Unsubscribe from events when no longer in use
return () => subscriber();
}
useEffect(() => {
getData();
}, [typeofPost])
if (loading) {
return <ActivityIndicator />;
}
return (
<FlatList
data={data}
ListEmptyComponent={
<View style={styles.flatListEmpty}>
<Text style={{ fontWeight: 'bold' }}>No Data</Text>
</View>
}
renderItem={({ item }) => (
<View>
<Text>User ID: {item.fullName}</Text>
</View>
)}
/>
)
}
you are using a class based component to access react hook which is a bad practice, i will advice you use a functional component and you have access to react useCallback hook which will handle your request easily
const ButtonPressed = useCallback(() => {
setLoading(true);
getData()
}).then(() => setLoading(false));
}, [loading]);
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
I have a component that looks like this:
const criteriaList = [
'Nur Frauen',
'Freunde Zweiten Grades',
];
export const FilterCriteriaList: React.FunctionComponent = () => {
const [state, setState] = useState(false);
useEffect(() => {
console.log('state is,', state);
});
const myFunction = () => {
console.log('checking state', state);
if (state == false) {
setState(true);
} else {
setState(false);
}
};
return (
<View style={styles.container}>
<View style={styles.horizontalLine} />
{criteriaList.map((item: string, index: number) => (
<View key={index}>
<View style={styles.criteriaRow}>
<TouchableOpacity
onPress={() => {
myFunction();
}}>
<Icon
name="circle-thin"
color="#31C283"
size={moderateScale(20)}
/>
</TouchableOpacity>
<Text style={styles.text}>{item}</Text>
</View>
<View style={styles.horizontalLine} />
</View>
))}
</View>
);
};
Currently, I am using the circle-thin icon. I want to change it such that everytime I click on an icon, it changes to the dot-circle-o icon. Like radio buttons. However, I am not quite sure how to do so.
I thought of using ternary operators but since I am mapping my fields Idk how to setStates collectively. Maybe using the index? I don't want to make a separate state for each field. Here's a similar snack demo:
https://snack.expo.io/toTSYc2fD
I want to be able to select multiple/unselect options. I don't want to apply the same rule on all fields together.
Note: the onPress function can also be used on the Icon directly instead of the TouchableOpacity (though it is not preferred)
Using a ternary sounds like the right approach to me. Can you not do something like:
name={state ? 'dot-circle-o' : 'circle-thin'}
You could also refactor your function:
const myFunction = () => {
console.log('checking state', state);
setState(!state)
};
If you have multiple fields then there are many ways to handle it. You could call useState multiple times, eg:
const [field1, setField1] = useState(false);
const [field2, setField2] = useState(false);
You could also store all fields in the same state:
const [state, setState] = useState({field1: false, field2: false});
...
const myFunction = (fieldName) => {
console.log('checking state', state);
setState({...state, [fieldName]: !state[fieldName]})
};
I guess you'd then use the item as the "fieldName? In which case:
return (
<View style={styles.container}>
<View style={styles.horizontalLine} />
{criteriaList.map((item: string, index: number) => (
<View key={index}>
<View style={styles.criteriaRow}>
<TouchableOpacity
onPress={() => {
myFunction(item);
}}>
<Icon
name={state[item] ? 'dot-circle-o' : 'circle-thin'}
color="#31C283"
size={moderateScale(20)}
/>
</TouchableOpacity>
<Text style={styles.text}>{item}</Text>
</View>
<View style={styles.horizontalLine} />
</View>
))}
</View>
);
And to create the initial state:
const initialState = {}
criteriaList.forEach(item => initialState[item] = false)
const [state, setState] = useState(initialState);
The code would be something like below.
You have to set the index of the selected item as the state and use it to chose the icon.
const criteriaList = [
{title:'My List',checked:false},
{title:'Friends listt',checked:false},
{title:'Not Good',checked:false},
{title:'Sweet and sour',checked:false},
{title:'Automatic',checked:false},
];
export const FilterCriteriaList: React.FunctionComponent = () => {
const [state, setState] = useState(criteriaList);
useEffect(() => {
console.log('state is,', state);
});
const myFunction = (index) => {
console.log('checking state', state);
const arr=[...state];
arr[index].checked=arr[index].checked?false:true;
setState(arr);
};
return (
<View style={styles.container}>
<View style={styles.horizontalLine} />
{criteriaList.map((item: Any,index:number) => (
<View key={item}>
<View key={item} style={styles.criteriaRow}>
<Icon
style={styles.icon}
name={item.checked?"circle":"circle-thin"}
color="#31C283"
size={moderateScale(20)}
onPress= {()=>myFunction(index)}
/>
<Text style={styles.text}>{item.title}</Text>
</View>
<View style={styles.horizontalLine} />
</View>
))}
</View>
);
};