Share state between components - javascript

I am working on a hobby gym management app, and I am puzzled by the mechanism of sharing state between three components in React-Native.
My three components are:
1. Schedule:
[...]
function Schedule() {
return (
<Stack.Navigator
initialRouteName="Monday"
screenOptions={{
headerStyle: { backgroundColor: "#f58220" },
headerTintColor: "#fff",
headerTitleStyle: { fontWeight: "bold" },
headerRight: () => <SwitchButton />,
}}
>
<Stack.Screen
name="TabStack"
component={TabStack}
options={{ title: "Aerobic Schedule" }}
/>
</Stack.Navigator>
);
}
export default Schedule;
I want the SwitchButton button in my Schedule component (1.) to alternate between DATA_AEROBIC and DATA_KIDS arrays props of the FlatList in (2.) based on the content of the listAerobic boolean variable.
2. MondayPage:
[...]
const MondayPage = () => {
const [selectedId, setSelectedId] = useState(null);
const [listAerobic, setListAerobic] = useState(true);
const renderItem = ({ item }) => {
const backgroundColor = item.id === selectedId ? "#6e3b6e" : "#f9c2ff";
return (
<Item
item={item}
onPress={() => setSelectedId(item.id)}
style={{ backgroundColor }}
/>
);
};
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={{ flex: 1, padding: 5 }}>
<SafeAreaView style={styles.container}>
<FlatList
data={listAerobic ? DATA_AEROBIC : DATA_KIDS}
renderItem={renderItem}
keyExtractor={(item) => item.id}
extraData={selectedId}
/>
</SafeAreaView>
</View>
</SafeAreaView>
);
};
However, I don't know how to link the listAerobic boolean variable to the state of the SwitchButton component (3.) , and how to make it toggle on and off.
3. SwitchButton:
const SwitchButton = () => {
const [isEnabled, setIsEnabled] = useState(false);
const toggleSwitch = () => setIsEnabled(previousState => !previousState);
return (
<View style={styles.container}>
<Switch
trackColor={{ false: "#767577", true: "#81b0ff" }}
thumbColor={isEnabled ? "#f5dd4b" : "#f4f3f4"}
ios_backgroundColor="#3e3e3e"
onValueChange={toggleSwitch}
value={isEnabled}
/>
<Text> aerobic/kids</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
marginRight: 5,
padding: 5,
}
});
export default SwitchButton;
Any guidance would be awesome! I mention I have really tried to look it up on different tutorials, but I can't seem to get the gist of it. It is my first project in React/React-Native.
Many thanks!

I think you just need 'value' to accept a prop passed into it on the switch button. Then wherever you use switch button just pass a boolean value into it from state e.g.
<SwitchButton enabled={this.state.switchEnabled}/>
As for setting state 'globally' so this.state.switchEnabled can be updated from various places / accessible all over the app you need to look into state management tools like Redux (or I hear 'React Hooks' is now a thing and preferred....)

Related

Changing values in a dropdown menu

I'm working with a custom dropdown menu in React Native and am having a problem changing the text when trying to reuse the dropdown in other components.
DropdownMenu.js
export const DropdownMenu = ({dropdownMenuItems}) => {
const [isActive, setIsActive] = React.useState(false);
const onPress = () => {
setIsActive(!isActive);
};
return (
<TouchableOpacity
activeOpacity={1}
onPress={() => setIsActive(false)}
style={{flex: 1}}>
<View>
<TouchableOpacity style={styles.img} onPress={onPress}>
<Image style={styles.imgimg} source={require('./icon.png')} />
</TouchableOpacity>
<Animated.View
style={{
...styles.menu,
opacity: fadeAnim,
transform: [{translateY: translateAnim}],
}}
pointerEvents={isActive ? 'auto' : 'none'}>
<FlatList
data={dropdownMenuItems}
renderItem={({item, index}) => (
<OpenURLButton
url={item.href}
label={item.name}
style={
index === 0
? {borderTopLeftRadius: 8, borderTopRightRadius: 8}
: index === dropdownMenuItems.length - 1
? {borderBottomLeftRadius: 8, borderBottomRightRadius: 8}
: {}
}
/>
)}
/>
</Animated.View>
</View>
</TouchableOpacity>
);
};
In the CompactMenu component, I import my <DropdownMenu /> and set my initial values for my menu:
CompactMenu.js
import React from 'react';
import {SafeAreaView} from 'react-native';
import {DropdownMenu} from './DropdownMenu';
const CompactMenu = () => {
const backgroundStyle = {
backgroundColor: '#fff',
flex: 1,
display: 'flex',
};
const dropdownMenuItems = [
{name: 'Messages', href: '/messages'},
{name: 'Trips', href: '/trips'},
{name: 'Saved', href: '/saved'},
];
return (
<SafeAreaView style={backgroundStyle}>
<DropdownMenu dropdownMenuItems={dropdownMenuItems} />
</SafeAreaView>
);
};
export default CompactMenu;
After importing <CompactMenu /> into another component, I try to change the name & the href in object:
example import:
<CompactMenu dropdownMenuItems={[{name: "changed name", href: "/somePath"}]} />
However, the same strings from CompactMenu.js are displayed in the dropdown.
Being new to RN, I'm not sure of two things here.
1.) Why do the text value for "name" not change when used in a different component?
2.) Shouldn't navigation to another screen use { navigation } instead of href:? I've tried adding onPress={() => navigation.navigate('SomeScreen') in the href but I get an error.
I'm not sure what the correct solution to this is. Any help would be appreciated.
You are not using the props dropdownMenuItems that you are passing to CompactMenu. Instead, you reuse the same menu items for the DropdownMenu component everytime you create a CompactMenu.
You need to use the props that you are passing. I have kept the static items as a default value. If you would like to have these items as well and to add additional items via props, then have a look at the second solution.
Notice that I have integrated some small changes to the rest of the code as well.
const defaultItems = [
{name: 'Messages', href: '/messages'},
{name: 'Trips', href: '/trips'},
{name: 'Saved', href: '/saved'},
];
const CompactMenu = ({dropdownMenuItems = defaultItems}) => {
return (
<SafeAreaView style={styles.backgroundStyle}>
<DropdownMenu dropdownMenuItems={dropdownMenuItems} />
</SafeAreaView>
);
};
const styles = StyleSheet.create({
backgroundStyle: {
backgroundColor: '#fff',
flex: 1,
}
});
Now using the CompactMenu component can receive dropdownMenuItems and will pas them to the DropdownMenu component.
<CompactMenu dropdownMenuItems={[{name: "changed name", href: "/somePath"}]} />
If you want to keep default items and add additional items via props, we could achieve this by merging the provided props with our default items.
const defaultItems = [
{name: 'Messages', href: '/messages'},
{name: 'Trips', href: '/trips'},
{name: 'Saved', href: '/saved'},
];
const CompactMenu = ({dropdownMenuItems}) => {
return (
<SafeAreaView style={styles.backgroundStyle}>
<DropdownMenu dropdownMenuItems={[...defaultItems, ...dropdownMenuItems]} />
</SafeAreaView>
);
};
Your second questions addresses the react-navigation framework for react-native. This is a very broad topic and I am assuming from your question that you don't know how this works yet, since you have not setup the necessary structure for using it. I encourage you to go through the documentation first.
In summary, you will need to define a navigator, e.g. a stack-navigator and add a name reference for each of your screens to the dropdown menu. To keep things short, here is a minimal example on how this could work.
const MenuScreen1 = (props) {
return (...)
}
const MenuScreen2 = (props) {
return (...)
}
const CompactMenu = ({dropdownMenuItems, navigation}) => {
return (
<SafeAreaView style={styles.backgroundStyle}>
<DropdownMenu dropdownMenuItems={dropdownMenuItems} navigation={navigation} />
</SafeAreaView>
);
};
const dropdownMenuItems = [
{name: 'Menu Item 1', screen: 'Item1'},
{name: 'Menu Item 2', screen: 'Item2'},
]
const Home = ({navigation}) {
return <CompactMenu navigation={navigation} dropdownMenuItems={dropdownMenuItems} />
}
const Stack = createNativeStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Item1" component={MenuScreen1} />
<Stack.Screen name="Item2" component={MenuScreen2} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default App;
Notice that I have created two screens whose names, which I have defined in the stack navigator, are provided to the CompactMenu. Notice as well that the navigation framework will pas the navigation object only to components that are defined as a screen, thus I have decided to pass the navigation object to the CompactMenu and the DropdownMenu component. You can use the useNavigation hook if you prefer this method.
Now, in your DropdownMenu you can navigate on click to the defined screens.
export const DropdownMenu = ({dropdownMenuItems, navigation}) => {
const [isActive, setIsActive] = React.useState(false);
const onPress = () => {
setIsActive(!isActive);
};
return (
<TouchableOpacity
activeOpacity={1}
onPress={() => setIsActive(false)}
style={{flex: 1}}>
<View>
<TouchableOpacity style={styles.img} onPress={onPress}>
<Image style={styles.imgimg} source={require('./icon.png')} />
</TouchableOpacity>
<Animated.View
style={{
...styles.menu,
opacity: fadeAnim,
transform: [{translateY: translateAnim}],
}}
pointerEvents={isActive ? 'auto' : 'none'}>
<FlatList
data={dropdownMenuItems}
renderItem={({item, index}) => (
<Button
onPress={() => navigation.navigate(item.screen)}
title={item.name}
style={
index === 0
? {borderTopLeftRadius: 8, borderTopRightRadius: 8}
: index === dropdownMenuItems.length - 1
? {borderBottomLeftRadius: 8, borderBottomRightRadius: 8}
: {}
}
/>
)}
/>
</Animated.View>
</View>
</TouchableOpacity>
);
};

Cannot read property of 'navigate' of undefined

I am trying to make a stack navigator using reactstack navigation. When the button clicks, it appears on the detail screen only, the title of the page is detail. I am not yet to parsing data array to the next screen, it just tries to navigate the screen into the detail screen, and gets this error. I am new to react. Please help me solve this problem.
import React from 'react'
import {Button, StyleSheet, ScrollView, Text, View} from 'react-native'
import {useState, useEffect} from 'react'
import axios from 'axios'
const Item = ({id, user_id, title, onPress, navigation}) => {
return (
<View style={styles.container}>
<Text style={styles.text}>Id :{id}
</Text>
<Text style={styles.text}>User Id :{user_id}
</Text>
<Text style={styles.text}>Tittle :{title}
</Text>
<View style={styles.container}>
<Button onPress={() => navigation.navigate('Detail')} title='Detail'></Button>
</View>
<View style={styles.line}></View>
</View>
)
}
const Berita = () => {
const [users,
setUsers] = useState([]);
useEffect(() => {
getData();
}, []);
const selectItem = (item) => {
console.log('Selected item: ', item)
}
const getData = () => {
axios
.get('https://gorest.co.in/public/v1/posts')
.then(res => {
console.log('res: ', res);
setUsers(res.data.data);
})
}
return (
<ScrollView style={styles.container}>
{users.map(user => {
return <Item key={user.id} id={user.id} user_id={user.user_id} title={user.title}/>
})}
</ScrollView>
)
}
export default Berita
const styles = StyleSheet.create({
container: {
padding: 15
},
text: {
color: "black",
marginTop: 5,
fontStyle: 'italic',
fontSize: 18,
fontFamily: 'Arial'
},
line: {
height: 1,
backgroundColor: 'black',
marginVertical: 20
},
title: {
fontSize: 25,
fontWeight: 'bold',
textAlign: 'center',
color: "black"
},
tombol: {
padding: 10
}
})
This is the stack screen navigator code
const Tab = createBottomTabNavigator();
const Stack = createStackNavigator();
const DetailBerita = () => {
return (
<Stack.Navigator >
<Stack.Screen
name='Berita'
component={Berita}
options={{
headerTitleAlign: 'center'
}}/>
<Stack.Screen
name="Detail"
component={Detail}
options={{
headerTitleAlign: 'center'
}}/>
</Stack.Navigator>
)
}
It appears that you are using Stack Navigators with different screen names, but you didn't send it. If possible, can you send that file, I would be able to help you a bit better. But from what I have I can try and explain how Stack navigation works. With this line of code:
navigation.navigate('Detail')
You specify that you want to navigate to the screen named "Detail", if you want to navigate to another screen then you can change it to the name of the screen. Let's say you want to navigate to your home screen, and its named "Home" in your Stack.Navigation component. Simply change your navigation to the following:
navigation.navigate('Home')
This is happening because the navigation prop is passed to the Berita component and you are destructuring the property in Item component not in Berita.
So the code should look like
...
const Berita = ({ navigation }) => {
// ...
return (
<ScrollView style={styles.container}>
{users.map(user => {
return (
<Item
key={user.id}
id={user.id}
user_id={user.user_id}
title={user.title}
navigation={navigation} // Pass navigation
/>
);
})}
</ScrollView>
);
};
Another way is - you can just use navigation in onPress (Berita) and pass down onPress to Item component
const Item = ({ id, user_id, title, onPress }) => {
return (
<View style={styles.container}>
<Text style={styles.text}>Id :{id}</Text>
<Text style={styles.text}>User Id :{user_id}</Text>
<Text style={styles.text}>Tittle :{title}</Text>
<View style={styles.container}>
<Button onPress={onPress} title="Detail" />
</View>
<View style={styles.line}></View>
</View>
);
};
const Berita = ({ navigation }) => {
const [users, setUsers] = useState([]);
useEffect(() => {
getData();
}, []);
const selectItem = item => {
console.log('Selected item: ', item);
};
const getData = () => {
axios.get('https://gorest.co.in/public/v1/posts').then(res => {
console.log('res: ', res);
setUsers(res.data.data);
});
};
return (
<ScrollView style={styles.container}>
{users.map(user => {
return (
<Item
key={user.id}
id={user.id}
user_id={user.user_id}
title={user.title}
onPress={() => navigation.navigate('Detail')}
/>
);
})}
</ScrollView>
);
};

react native : How do I make the text label that will check the checkbox while press on it?

How do I make the text "BIRD" label that will check the checkbox while press on it ?
According to my example it does not work and it is not clear to me.
import CheckBox from '#react-native-community/checkbox';
function App(props) {
const isSelected = useSelector((rootState) => rootState.BitzuaDigumReducer.checkBox);
return (
<TouchableOpacity
activeOpacity={1}
style={{
flexDirection: 'row',
alignSelf: 'flex-start',
top: 20,
left: 10,
}}
>
<CheckBox
value={isSelected}
onValueChange={(value) => dispatch(setCheckBox(value))}
style={styles.checkbox}
tintColors={{ true: 'white', false: 'white' }}
/>
<Text style={styles.label}>BIRD</Text>
</TouchableOpacity>
);
}
I mocked Checkbox on this example, but please review it. It should give you a basic idea on how to toggle checkbox value by pressing the label: https://snack.expo.io/#zvona/toggle-checkbox-on-label
Core code:
const App = () => {
const [isSelected, setSelected] = useState(false);
const toggleCheckbox = () => {
setSelected(!isSelected);
// Here you can dispatch the event to state management
};
return (
<View style={styles.container}>
<Checkbox onValueChange={toggleCheckbox} selected={isSelected} />
<TouchableOpacity activeOpacity={0.8} onPress={toggleCheckbox}>
<Text style={styles.label}>{'Label'}</Text>
</TouchableOpacity>
</View>
);
};

Use the same component for all screens REACT-NATIVE

im building a music-player and i want to implement this:
at the bottom, theres a bottomsheet, so if I go to albums, artist, it will still there, how can i do it? for now i have but only in one screen, in this case Tracks:
render ()
const props = {
playing: this.state.playing,
update_playing: this.update_playing.bind(this),
update_next_song: this.update_next_song.bind(this),
repeat_song: this.repeat_song.bind(this),
repeat_queue: this.repeat_queue.bind(this),
show_hide_icon: this.show_hide_icon.bind(this),
current_track_id: this.state.current_track_id,
current_track: (this.state.current_track === "") ? this.props.info_track[0]?.title : this.state.current_track,
cover: (this.state.current_track === "") ? this.props.info_track[0]?.cover : this.state.cover,
artist: (this.state.current_track === "") ? this.props.info_track[0]?.artist : this.state.artist,
show_icon: this.state.show_icon,
tracks: this.props?.tracks,
first_id_song: this.props?.first_id_song,
last_id_song: this.props?.last_id_song,
}
return (
<>
<View style = {{flex: 1, backgroundColor: "black"}}>
<Text style = {{fontSize: 30, color: "red"}}>{saludo}</Text>
<FlatList
data = {this.props.tracks}
keyExtractor = {(item) => item.id.toString()}
renderItem = {({item}) => (
<TouchableOpacity onPress = {() => {this.play_selected_music(item) ; this.get_track_cover_artist(item)}}>
<View style = {this.styles.tracks_container}>
<View style = {this.styles.tracks_info_container}>
{
(item?.cover) ? <Image source = {{uri:"file:///"+item.cover}} style={{ width: 100, height: 100, marginLeft: 20}}></Image>
: <Image source = {require("../assets/default_image.jpg")} style={{ width: 100, height: 100, marginLeft: 20}}></Image>
}
<View style = {this.styles.tracks}>
<View>
<Text style = {{color: "white", fontSize: 20, marginLeft: 10}} numberOfLines = {1}>
{
(item.title.length > 20) ?
item.title.substring(0,18).padEnd(20,".")
: item.title
}
</Text>
</View>
<View style = {this.styles.artist_duration}>
<Text style = {{color: "white", fontSize: 10, marginLeft: 10}}>
Artist: {(item.artist.length > 15) ?
item.artist.substring(0,14).padEnd(16,".")
: item.artist}
</Text>
<Text style = {{color: "white",fontSize: 10, marginLeft: 10}}>
Duration: {this.get_time(item.duration)}
</Text>
</View>
</View>
</View>
</View>
</TouchableOpacity>
)}
>
</FlatList>
<BottomSheet
ref = {ref => (this.sheetref = ref)}
initialSnap = {1}
snapPoints = {[Dimensions.get("window").height - StatusBar.currentHeight, 95]}
renderHeader = {() => <Player_Header {...props}></Player_Header>}
renderContent = {() => <Track_Zone {...props}></Track_Zone>}
enabledContentGestureInteraction = {false}
onOpenEnd = {this.hide_icon}
onCloseEnd = {this.show_icon}>
</BottomSheet>
</View>
</>)
it receives props from the component to get update everytime the track changes, thought about putting it outside the navigator, but then, how can get all the necessary props, functions, etc to update it?
this is my navigator:
<Tab.Navigator>
<Tab.Screen name = "Tracks" children = {({navigation}) => <Tracks navigation = {navigation}></Tracks>}></Tab.Screen>
<Tab.Screen name = "Chao" children = {({navigation}) => <Chao navigation = {navigation}></Chao>}></Tab.Screen>
</Tab.Navigator>
You can define a HOC to render the BottomSheet withTracKBottomSheet()
Now you can wrap every screen in which there should be a BottomSheet with withTrackBottomSheet().
Something like this
const withTrackBottomSheet = Component => {
// Do all the business logic
return (
<>
<Component />
<BottomSheet />
</>
);
};
In your case, the same state will be shared among multiple screens/components. Thus I will advise you to use some state management library like redux to make your work a little easier and less complex.

Rendering Child Component in React Native

I am using React Native and React Navigation to build a simple app.
I have got the basic structure working with stub state but I am having problem with changing state via callback and re-render.
In my screen, I have simple start button
`<View style={styles.buttonContainer}>
<TouchableOpacity
style={[myStyles.buttonStyle, { backgroundColor: color }]}
onPress={() => handlePress(button.title)}
>
<Text style={myStyles.textStyle}>{button.title}</Text>
</TouchableOpacity>
</View>`
Problem:
After I update my parent Component state, my child component does not instantly render to match the state change. I understood React will re-render all child components when parent state is changed?
Instead, I have to move back to previous screen and navigate again to my button screen to see that the button's color and text has changed correctly.
I've read about requiring a componentWillReceiveProps(nextProps) handler but I am not sure how to use it. I put a console.log('nextProps', nextProps) inside but it does not get fired.
From navigation perspective, the Root component is on index[0] and my button view is at index[3] so it's the 3rd screen from the root.
EDIT 1: Added Code
myButton screen:
export class TeamsScreen extends React.Component {
static navigationOptions = ({ navigation }) => ({
title: `${navigation.state.params.game.name}: Select Team`,
headerTintColor: 'white',
headerStyle: {
backgroundColor: 'black',
},
headerVisible: true
})
componentWillReceiveProps(nextProps) {
console.log('nextProps', nextProps);
}
render() {
const { navigate, setParams } = this.props.navigation;
const { game, player, setGameState } = this.props.navigation.state.params;
const color = game.status === 'Start' ? 'green' : 'red';
const index = game.indexOf(player);
const status = game.status;
console.log('index', index);
console.log('status', status);
return (
<View style={styles.container}>
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[myStyles.buttonStyle, { backgroundColor: color }]}
onPress={() => setGameState(index, status)}
>
<Text style={myStyles.textStyle}>{game.status}</Text>
</TouchableOpacity>
</View>
<View style={styles.buttonContainer}>
<Button
onPress={() => navigate('ChangeDriverScreen', { team, game })}
title='Change Driver'
/>
</View>
<View style={{ marginTop: 40, marginBottom: 20 }}>
<Text style={{ fontSize: 16, color: 'white', alignSelf: 'center' }}>Teams</Text>
</View>
<View style={{ height: 250 }}>
<FlatList
data={player.teams}
renderItem={({item}) =>
<View style={styles.buttonContainer}>
<Button
onPress={() => navigate('TeamSelectedStartScreen', { team: item })}
title={item.name}
/>
</View>}
keyExtractor={item => item.name}
/>
</View>
<Image
style={{ alignSelf: 'center', justifyContent: 'flex-end', height: 75, width: 250, resizeMode: 'stretch'}}
source={require('./images/icons/playerPlaceholder.png')}
/>
</View>
)}}
Then the onPress function that is called back:
setGameState = (gameIndex, status) => {
console.log('setGameState', gameIndex, status);
console.log('gameStateBefore', this.state.game);
const newGameState = this.state.game.map(t => {
console.log(this.state.game.indexOf(t));
if (this.state.game.indexOf(t) === gameIndex) {
const newStatus = status === 'Start' ? 'Stop' : 'Start';
t.status = newStatus; /*eslint no-param-reassign: "off"*/
console.log('inside if', t.status);
console.log('inside if game', t);
return t;
}
return t;
});
console.log('new Game State', newGameState);
this.setState(() => ({
game: newGameState
}));
}
So the setState method works (as re-navigating back to screen 3 shows the correct state but core question is how to get immediate re-render of screen 3 when setState is called from Screen 0.

Categories