How to abstract React state logic to custom hook? - javascript

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?

Related

Re rendering a component with an async function inside

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]);

Responding to changes in the state of a child component

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

SearchBar from react-native-elements with FlatList only allows me to type characters one by one

I'm using SearchBar from react-native-elements in my application inside a functional component.
However there's one issue I'm unable to fix: I can only type characters one by one. I cannot continuously write my text inside my bar and then search. Instead I must type every character ony by one to find a specific word.
Here's my code:
export default function InstitutionsScreen({navigation}) {
const [institutions, setInstitutions] = useState('');
const [refresh, setRefresh] = useState(false);
const [fullData, setFullData] = useState([]);
const [value, setValue] = useState('');
useEffect(() => {
if(!institutions) {
setRefresh(false);
getInstitutions();
}
}, []);
const contains = (institutionName, query) => {
if (institutionName.includes(query)) {
return true
}
return false
}
const handleSearch = text => {
setValue(text);
const formattedQuery = text.toLowerCase()
console.log("flaco me diste " + formattedQuery)
const data = filter(fullData, inst => {
return contains(inst.name.toLowerCase(), formattedQuery)
})
setInstitutions(data);
}
const onRefresh = () => {
setRefresh(true);
getInstitutions();
};
const renderHeader = () => (
<View
>
<SearchBar
lightTheme
clearIcon
onChangeText={(text) => handleSearch(text)}
value={value}
placeholder='Buscar...' />
</View>
)
if (!institutions || refresh) {
return (
<ScrollView contentContainerStyle={{alignItems: "center", flex: 1, justifyContent: 'center'}}>
<Spinner isVisible={true} size={100} type={'Pulse'} color={'#013773'}/>
</ScrollView>
);
} else {
return (
<View>
<SafeAreaView>
<FlatList
data={institutions}
renderItem={({ item }) =>
<InstitutionItem
title={item.name}
image={require('../../../assets/hands.jpg')}
logo={require('../../../assets/institution.png')}
location={item.address}
phone={item.phone}
email={item.email}
navigation={navigation}
id={item._id}
/>}
keyExtractor={(item, index) => index.toString()}
refreshing={refresh}
onRefresh={() => onRefresh()}
ListHeaderComponent={renderHeader}
/>
</SafeAreaView>
</View>
);
}
}
I tried your code, and it doesn't seem to be something related with the SearchBar, it works as it should outside of the FlatList. The problem that you are having is that you are losing the input focus for some reason on how you are "injecting" the SearchBar into the FlatList. So what I did, was to put the SearchBar right into the code, like this:
<FlatList
data={institutions}
keyExtractor={(item, index) => index.toString()}
refreshing={refresh}
onRefresh={() => onRefresh()}
ListHeaderComponent={
<SearchBar
lightTheme
clearIcon
onChangeText={handleSearch}
value={value}
placeholder='Buscar...' />
}
/>
and it worked, you are now able to keep writing without losing the focus.
This is not a bad solution, it's an easy solution, but if this is not very to your liking, you should try to find how you can insert any component in the header of the FlatList from a function or a const. Abrazo!

How do I get data from firebase realtime databese with React Native?

I'm trying to get a data firebase realtime database. I can show data with console.log() but I cannot show in Flat list. I tried different ways to solve this problem. But, I could not find any certain solution.
Here, my JSON tree in firebase:
Here, my code:
const NotesList = (props) => {
const { colors } = useTheme();
const styles = customStyles(colors);
const user = auth().currentUser
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const getData = () => {
firebase.database().ref(`notes/`).on('value', function (snapshot) {
console.log(snapshot.val())
});
}
useEffect(() => {
setTimeout(() => {
setData(getData);
setLoading(true);
}, 1000);
}, []);
const renderItem = ({ item }) => {
return (
<View style={{ marginRight: 10, marginLeft: 10 }}>
<TouchableOpacity>
<NoteCard title={item.noteTitle} icerik={item.noteDetails} date={item.timestamp} />
</TouchableOpacity>
</View>
);
};
const split = () => {
let icerik = data.map((item, key) => item.icerik);
return icerik.slice(0, 1) + '...';
};
return (
<View style={styles.container}>
<StatusBar barStyle="light-content" />
<NoteSearchBar />
{!loading ? (
<View style={{ alignItems: 'center' }}>
<ActivityIndicator color="#ff5227" />
</View>
) : (
<FlatList
data={data}
numColumns={2}
renderItem={renderItem}
keyExtractor={(item, index) => index.toString()}
/>
)}
<TouchableOpacity
style={styles.addButton}
onPress={() => props.navigation.navigate('AddNote')}>
<Plus width={40} height={40} fill="white" margin={10} />
</TouchableOpacity>
</View>
);
};
I tried many different ways, but no suggestions solved the problem. I would be glad if you could help.
Your getData doesn't return the data, as the data is loaded asynchronously. You should instead move he call to setData into your getData:
const getData = () => {
firebase.database().ref(`notes/`).on('value', function (snapshot) {
setData(snapshot.val());
});
}
useEffect(() => {
setTimeout(() => {
getData()
setLoading(true);
}, 1000);
}, []);
Now the call to setData will put the JSON into the state, which in turn forces React to repaint the UI with the new data.
Ciao, basically you made 2 mistake:
getData does not return nothing so data will be never updated with values from firebase;
useEffect has a strange implementation (it's useless call setTimeout you are already waiting data from firebase);
My suggestion is to call getData from useEffect like this:
useEffect(() => {
getData()
}, []);
And modify getData like this:
const getData = () => {
firebase.database().ref(`notes/`).on('value', function (snapshot) {
setData(snapshot.val());
setLoading(true);
});
}
I found solution and I want to share it with you guys.
I kept the data from firebase as a child at a new value, then I put it in setData. In this way, I can display the data in the flat list.
const getData = () => {
firebase.database().ref(`notes/`).on('value', snapshot => {
let responselist = Object.values(snapshot.val())
setData(responselist)
console.log(snapshot.val())
setLoading(true);
});
}
useEffect(() => {
getData();
}, []);
Also, while firebase realtime DB was working, a new method called Object method helped when taking child values in the JSON tree.

React-Native: Dispatch on submit is not working

I am trying to do a basic todo list however when I dispatch an action after pressing add it doesn't dispatch .
I've taken the dispatch(todo(todoList))out of every function and left it in the main ToDo component to do multiple calls on every letter typed into the search box and I can see an update in my redux store in Redux-dev tools so I know my todo works but It won't dispatch() when I try to submit. Please can someone help me .
This is my code:
import {useDispatch } from 'react-redux'
import { todo } from './action/todo'
const ToDo = () => {
const [todo, setTodo] = useState('')
const [todoList, setTodoList] = useState([])
const dispatch = useDispatch()
const handleSubmit = (id , todo) => {
const newTodoList = todoList.concat({ id: id, val: todo })
return (
setTodo(''),
todo.length === 0
? <Text/>
: setTodoList(newTodoList) // if I put the dispatch here it doesn't work either
)
}
return (
<View style={styles.addPhotoCont}>
<TextInput
placeholder={props.textInputPlaceholder}
onChangeText={props => setTodo(props)}
value={todo}
/>
<TouchableOpacity
onPress={() => handleSubmit(Date.now(), todo) && dispatch(todo(todoList))}>
<Text style={styles.addButton}>Add</Text>
</TouchableOpacity>
</View>
)
}
It looks like you set todo twice, once as an import, and the second time as state. When you call dispatch and pass in todo it is calling the state version.
You should put the dispatch in the handleSubmit function.
Also, looking at the handleSubmit function, the return will not work. You can only return one thing. You can place the other functions above the return statement.
Edit:
Code sample below:
import { useDispatch } from 'react-redux'
import { todo } from './action/todo'
const ToDo = (props) => {
const [todoInputValue, setTodoInputValue] = useState('')
const dispatch = useDispatch()
const handleSubmit = (todo) => {
dispatch(todo({id: Date.now(), val: todoInputValue}))
setTodoInputValue('')
}
return (
<View style={styles.addPhotoCont}>
<TextInput
placeholder={props.textInputPlaceholder}
onChangeText={value => setTodoInputValue(value)}
value={todoInputValue}
/>
<TouchableOpacity
onPress={() => handleSubmit(Date.now())}>
<Text style={styles.addButton}>Add</Text>
</TouchableOpacity>
</View>
)
}

Categories

Resources