I have 2 components named A and B, In B I have a list of Languages to be selected and updated in component A. I am using Redux for state management when I change the Language from the list I can see that the states are updated(using redux-logger to get logs). But the problem is when I go back to Component A using react-navigation the updated state value is not updated I can see only the old value of state only
ScreenA.js
class ScreenA extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedLanguage: this.props.state.defaultLangName
}
}
render(
return (
<Container>
<Content style={{ backgroundColor: item.backgroundColor }}>
<View style={styles.logoContainer}>
<Text>test page</Text>
</View>
<View style={styles.cardParent}>
<View style={styles.card}>
<Item style={styles.listItem}>
<Button transparent style={styles.contentChecked} onPress={() => this._openLang()}>
<Text style={styles.listbtn}>{this.state.selectedLanguage}</Text>
<Icon name='ios-arrow-forward' style={styles.iconChecked}/>
</Button>
</Item>
</View>
</View>
</Content>
</Container>
);
)
}
export default connect(
state => ({ state : state.introauthenticate }),
dispatch => ({
actions: bindActionCreators(introActions, dispatch)
})
)(ScreenA);
ScreenB.js
class ScreenB extends Component {
constructor(props){
super(props);
this.state = {
langChecked: '0'
};
}
FlatListItemSeparator = () => {
return (
<View
style={{
height: 1,
width: "100%",
backgroundColor: "#999",
}}
/>
);
}
_selectLanguage (val, name){
this.setState({ langChecked: val });
this.props.actions.changeLanguage(val,name, this.props.navigation.navigate);
//this.props.navigation.navigate('Intro');
}
renderItem = (item )=> {
return(
<TouchableHighlight
style={styles.boxSelect}
underlayColor="transparent"
onPress={() => this._selectLanguage(item.Value, item.Name)}
>
<View style={styles.contentChecked}>
<Text style={styles.item} > {item.Name} </Text>
{this.state.langChecked === item.Value && <Icon name="ios-checkmark-circle" style={styles.iconChecked}/>}
</View>
</TouchableHighlight>
)
}
render() {
return (
<Container>
<Header>
<Left>
<Button transparent onPress={() => this.props.navigation.goBack()}>
<Icon name='ios-arrow-back' />
</Button>
</Left>
<Body>
<Title>Languages</Title>
</Body>
<Right />
</Header>
<Content>
<FlatList
data={ langs }
keyExtractor={(item) => item.Value}
ItemSeparatorComponent = {this.FlatListItemSeparator}
renderItem={({item}) => this.renderItem(item)}
/>
</Content>
</Container>
);
}
}
export default connect(
state => ({ state: state.introauthenticate }),
dispatch => ({
actions: bindActionCreators(introActions, dispatch)
})
)(ScreenB);
reducer.js
export const CHANGE_LANGUAGE = "CHANGE_LANGUAGE";
export function changeLanguage(langValue,langName,navigateTo) { // Fake authentication function
return async dispatch => {
try {
if (langValue && langName) { //If the email and password matches
const session = { langValue : langValue,langName:langName } // Create a fake token for authentication
setTimeout(() => { // Add a delay for faking a asynchronous request
dispatch(setLanguage(session)) // Dispatch a successful sign in after 1.5 seconds
navigateTo('Intro') // If successfull login navigate to the authenticated screen
}, 1500)
}
} catch (err) { // When something goes wrong
console.log(err)
}
};
}
function setLanguage(lang){
return {
type: types.CHANGE_LANGUAGE,
data: {
lang: lang
}
};
}
const initialsliderState = {
defaultLang:'en',
defaultLangName:'English',
};
export default function introauthenticate(state = initialsliderState, action = {}) {
switch (action.type) {
case types.CHANGE_LANGUAGE:
return {
...state,
defaultLang: action.data.lang.langValue,
defaultLangName: action.data.lang.langName,
};
default:
return state;
}
}
Logger:
LOG %c prev state "introauthenticate": {"defaultLang": "nl", "defaultLangName": "Deutsch", "isAuthSlider": false, "requestingsliderRestore": false}
LOG %c action {"data": {"lang": {"langName": "English", "langValue": "en"}}, "type": "CHANGE_LANGUAGE"}
LOG %c next state "introauthenticate": {"defaultLang": "en", "defaultLangName": "English", "isAuthSlider": false, "requestingsliderRestore": false}}
You are initializing the state of ScreenA with a value passed as a prop and never update it. As you are using redux to store the current language you do not need any state in ScreenA. When you connect a component you pass it the relevant data from your store as props. It seems like you are trying to "override" the state by passing it in as state but that does not update the state as it will be in this.props.state rather then in this.state. What you need to do is to just pass the language as a prop to ScreenA:
export default connect(
state => ({ selectedLanguage : state.introauthenticate.defaultLang }),
dispatch => ({
actions: bindActionCreators(introActions, dispatch)
})
)(ScreenA);
and then read the selected language from props:
<Text style={styles.listbtn}>{this.props.selectedLanguage}</Text>
When you update your store the component will re-render with the new language. You do not need any additional state in the component itself for data that you have in your redux store.
Related
I am new to React and React Native and I am trying to build an cooking recipe app. I am trying to update my dish state in DishTabNavigator.js from DishIngredients.js so that I can send the data to firebase from DishTabNavigator.js, however I do not know how I can update the state. I tried lifting the state up but I couldn't do it. Been stuck on this for a day now. Any help would be appreciated
DishTabNavigator.js
const Tab = createMaterialTopTabNavigator();
export default function MyTabs({ navigation }) {
const [dish, setDish] = useState([{ ingredients: [] }]);
React.useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity style={styles.iconright}>
<Text>Send dish to Firebase</Text>
<FontAwesome name="save" size={24} color="black" />
</TouchableOpacity>
),
});
});
return (
<Tab.Navigator>
<Tab.Screen
name="Ingredient"
component={DishIngredients}
ingredients={dish}
/>
</Tab.Navigator>
);
}
DishIngredients.js
class DishIngredients extends Component {
constructor(props) {
super(props);
this.state = {
ingredients: [],
textInput: [],
};
//Add TextInput
addTextInput = (index) => {
let textInput = this.state.textInput;
textInput.push(
<TextInput
style={styles.textInput}
onChangeText={(text) => this.addValues(text, index)}
/>
);
this.setState({ textInput });
};
//Function to add values into the states
addValues = (text, index) => {
let dataArray = this.state.ingredients;
let checkBool = false;
if (dataArray.length !== 0) {
dataArray.forEach((element) => {
if (element.index === index) {
element.text = text;
checkBool = true;
}
});
}
if (checkBool) {
this.setState({
ingredients: dataArray,
});
} else {
dataArray.push({ text: text, index: index });
this.setState({
ingredients: dataArray,
});
}
};
//function to console the output
getValues = () => {
console.log("Data", this.state.ingredients);
this.props.ingredients = this.state.ingredients;
console.log(this.props.ingredients);
};
render() {
return (
<View>
<View style={styles.icon}>
<View style={{ margin: 10 }}>
<TouchableOpacity>
<Ionicons
name="add"
size={24}
color="black"
onPress={() => this.addTextInput(this.state.textInput.length)}
/>
</TouchableOpacity>
</View>
<View style={{ margin: 10 }}>
<Button title="Remove" onPress={() => this.removeTextInput()} />
</View>
</View>
{this.state.textInput.map((value) => {
return value;
})}
<Button title="Get Values" onPress={() => this.getValues()} />
</View>
);
}
}
export default DishIngredients;
You could use React Context
const DishContext = React.createContext({
dish: [],
setDish: () => {},
});
export const useDishContext = () => React.useContext(DishContext);
export const AppDishProvider = ({children}) => {
const [dish, setDish] = useState([{ingredients: []}]);
const defaultDish = {
dish,
setDish,
};
return (
<DishContext.Provider value={defaultDish}>{children}</DishContext.Provider>
);
};
Then
/** Wrap your TabNavigator with AppDishProvider */
<AppDishProvider>
<YourTabNavigator />
</AppDishProvider>
Then
/**
* In DishIngredients, or your TabNavigator...
* You get access to dish-state, and setDish using your
* custom-hook `useDishContext`
*/
const { dish, setDish } = useDishContext();
And if you're using class-based component
class DishIngredient extends React.Component {
static contextType = DishContext;
/**
* Current context-value could be accessed by
* `this.context`
*/
render() {
const {dish, setDish} = this.context;
/** .... */
}
}
Assuming you're not having redux wired in your app... ReactContext is your way to go...
Without changing a lot in your current code you can do like this
const addIngredient = (ingredient) => {
// TODO: ingredient
};
return (
<Tab.Navigator>
<Tab.Screen name="Ingredient">
{(props) => (
<DishIngredients
{...props}
ingredients={dish}
addIngredient={addIngredient}
/>
)}
</Tab.Screen>
</Tab.Navigator>
);
As you see there is a function addIngredient (You can rename as you wish) that will be introduced as prop in DishIngredients like this.props.addIngredient(<Your Ingredient text>).
ReactContext also a nice solution above but I think, this will force your app re-render every time your context changes. It might impact on your performance.
I created a Boolean state for my shopping app in which when a user clicks the buy button, it should set that state to true and display the cart total in a container, but it's not working.
Below is my code:
reducer:
if (action.type === SHOW_CART) {
return {
...state,
show: action.showCart,
};
const initialstate = {
show: false,
}
That's my reducer with initial state :
export const showCart = () => {
return {
type: SHOW_CART,
showCart: true,
};
};
That's my action:
<View style={styles.buy}>
<Text
onPress={() => {
this.props.addToCart(item.id);
this.props.showCart();
}}
>
Buy Once
</Text>
</View>
const mapStateToProps = (state) => {
return {
items: state.clothes.jeans,
};
};
const mapDispatchToProps = (dispatch) => {
return {
addToCart: (id) => dispatch(addToCart(id)),
showCart: () => dispatch(showCart()),
};
};
In this code I used a flat list, in the flat list I used onPress to show the cart container and to dispatch my action
App
export default function App() {
return (
<>
<Provider store={Store}>
<NavigationContainer>
<AppNavigation />
</NavigationContainer>
<ViewChart />
</Provider>
</>
);
}
ViewCart.js
<View>
{this.props.show ? (
<View style={styles.total}>
<Text style={styles.totaltext}>Total:</Text>
<Text style={styles.priceTotal}>{this.props.total}</Text>
<View style={styles.onPress}>
<Text
style={styles.pressText}
onPress={() => this.props.navigation.navigate("Cart")}
>
View Cart
</Text>
</View>
</View>
) : null}
</View>
const mapStateToProps = (state) => {
return {
total: state.clothes.total,
show: state.clothes.show,
};
};
export default connect(mapStateToProps)(ViewChart);
In App.js I added the conatiner of view cart, which should be displayed when user clicks buy
and in view cart I'm getting error undefined is not an object (evaluating '_this.props.navigation.navigate')
Change
this.props.showCart;
to
this.props.showChart();
I'm working on a music app, and I have a Purchase quota, that allows users to use the app.
So if a user plays 20 track music I will appear to him a modal or something.
So I have a component that's for play music "controller" name as <MusicPlayer/>,
I add it in every screen I have music tracks there when user press to any track card I navigate them to screen that contains
so i want to do some counter when user playing a music increase it +1
so i don't know where can I make this dispatch, in MusicPlayer components?
or in every screen that contained this component and pass it as props?
code
actions/countPlayAction.js
import {SET_COUNT} from './types';
export const setCount = count => {
return {
type: SET_COUNT,
payload: count,
};
};
reducer/countPlayReducer.js
import {SET_COUNT} from '../actions/types';
let initial_state = {
count: 0,
};
const countPlayReducer = (state = initial_state, action) => {
switch (action.type) {
case SET_COUNT:
state = {
...state,
count: state.count + action.payload,
};
break;
}
return state;
};
export default countPlayReducer;
musicPlayer component
class MusicPlayer extends Component {
constructor(props) {
super(props);
this.state = {
tunes: props.tunes,
currentTrackIndex: props.currentTrackIndex,
rate: 1,
duration: 1,
currentTime: 0,
paused: true,
loading: true,
};
}
onLoad = data => {
this.setState({
duration: Math.floor(data.duration),
loading: false,
paused: true,
});
};
render() {
return (
<View>
<View>
<Video
ref={ref => {
this.player = ref;
}}
source={{
uri: this.state.tunes[this.state.currentTrackIndex].url,
}}
paused={this.state.paused}
playInBackground={true}
playWhenInactive={true}
onLoad={this.onLoad}
onProgress={this.onProgress.bind(this)}
onEnd={this.onEnd}
controls={false}
/>
<View style={styles.time}>
<View style={styles.timeChildView}>
<Text style={styles.timeChildViewText}>
{this.minutesAndSeconds(this.state.currentTime)}
</Text>
</View>
<View style={styles.timeChildView}>
<Text style={styles.timeChildViewText}>
{this.minutesAndSeconds(this.state.duration)}
</Text>
</View>
</View>
<View style={styles.slider}>
{/* For circle play */}
<Slider
thumbTintColor="#ff4865"
maximumTrackTintColor="grey"
minimumTrackTintColor="#ff4865"
style={styles.seekBar}
step={1}
minimumValue={0}
maximumValue={this.state.duration}
value={this.state.currentTime}
onValueChange={this.changeValue}
onSlidingComplete={this.onSlidingComplete}
/>
</View>
<View style={styles.controls}>
{this.state.loading ? (
<ActivityIndicator size="large" color="#ff4865" />
) : (
<View style={styles.flexRow}>
<View>
{!this.state.paused ? (
<Button
transparent
style={styles.btnSection}
color="white"
onPress={() =>
this.setState({paused: !this.state.paused})
}>
<Icon name="md-pause" style={styles.iconColor} />
</Button>
) : (
<Button
transparent
style={styles.btnSection}
color="white"
onPress={() =>
this.setState({paused: !this.state.paused})
}>
<Icon name="md-play" style={styles.iconColor} />
</Button>
)}
</View>
</View>
)}
</View>
</View>
</View>
);
}
}
export default MusicPlayer;
What I made, it's increase count +1 in every time i play a track, but i don't know if that's a right way or not
in music player component
onLoad = data => {
this.setState({
duration: Math.floor(data.duration),
loading: false,
paused: true,
});
this.props.setCount(1)
reactotron.log(store.getState());
};
const mapDispatchToProps = dispatch => {
return {
setCount: count => {
dispatch(setCount(count));
},
};
};
// export default MusicPlayer;
export default connect(null, mapDispatchToProps)(MusicPlayer);
It looks logically done, although have a feeling of overhead introduced by redux (that's usual on small scale apps). I would also advice to change setCount to incrementCount, so that you don't need to have an absolute value in the component which increases the value.
You followed the Component-Container principle, what is not bad, but in your case you don't get any data from mapStateToProps and it forced you to have some boilerplate code inside of mapDispatchToProps.
An idea how to simplify it - you can wrap your component with connect and then you'll have dispatch available under props object inside of your component, so that you can immediately dispatch the action from onLoad. It will look like this:
onLoad = data => {
this.props.dispatch(setCount(1));
};
Final couple of words, if you don't have any other use cases for redux have a look at React.Context as you'll have less boilerplate with it :)
I am building my first react native app. I am using Firebase, Redux, and React Native to build my app.
I am using a thunk to fetch data from my database and I want to set my component's local state with that data.
When I console.log(this.props.room) in the render function I can see the data from my database but it's not being added to the local state in componentDidMount(), so I know my thunk and the backend is working properly.
I think my component is rendering before my local state is set in the componentDidMount function. Is there a way to prevent it from rendering while this.setState() is running? My code is below. Or is there another reason this.setState isn't working?
import { connect } from 'react-redux';
import {
Image,
Platform,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
TextInput
} from 'react-native';
import { submitIdea, getRoom } from '../redux/reducers/rooms/actions'
class RoomScreen extends React.Component {
static navigationOptions = {
title: 'Undecided!'
};
constructor(props) {
super(props)
this.state = {
currentUser: '',
submittedIdea: false,
currentUserIdea: '',
roomName: '',
ownerName: '',
peopleAndIdeas: [],
prompt: '',
room: {}
}
this.handleIdeaSubmit = this.handleIdeaSubmit.bind(this)
}
handleIdeaSubmit() {
const { user, roomName } = this.props.navigation.state.params
this.setState({ submittedIdea: true })
this.props.submitIdea(user, this.state.userIdea, roomName)
}
async componentDidMount() {
const { user, roomName } = this.props.navigation.state.params
await this.props.getRoom(roomName)
this.setState({
room: this.props.room
})
}
render() {
return (
<View style={styles.container}>
<ScrollView contentContainerStyle={styles.contentContainer}>
<View>
<Text>Room name: {this.state.room.name}</Text>
</View>
<View>
<Text>This room was created by: {this.state.room.owner}</Text>
</View>
<View>
<Text>What are we deciding? {this.state.room.prompt}</Text>
</View>
{/* checking whether or not the user has submitted an idea and altering the view */}
{!this.state.submittedIdea ?
<View style={styles.container}>
<TextInput
style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
onChangeText={(text) => this.setState({ userIdea: text })}
value={this.state.createRoomName}
placeholder="Enter your idea."
/>
<TouchableOpacity
style={styles.button}
onPress={this.handleIdeaSubmit}
>
<Text> Submit Idea. </Text>
</TouchableOpacity>
</View> :
<View>
<Text>Your idea was: {this.props.userIdea}</Text>
</View>
}
<View style={styles.getStartedContainer}>
<Text>IDEAS</Text>
</View>
<View>
<Text style={styles.getStartedText}>USERS</Text>
</View>
</ScrollView>
</View >
);
}
}
const mapStateToProps = state => ({
room: state.room.room
})
const mapDispatchToProps = (dispatch) => ({
getRoom: (roomName) => dispatch(getRoom(roomName)),
submitIdea: (user, idea, roomName) => dispatch(submitIdea(user, idea, roomName))
})
export default connect(mapStateToProps, mapDispatchToProps)(RoomScreen)```
This is because setState actions are asynchronous and are batched for performance gains. This is explained in documentation of setState.
setState() does not immediately mutate this.state but creates a
pending state transition. Accessing this.state after calling this
method can potentially return the existing value. There is no
guarantee of synchronous operation of calls to setState and calls may
be batched for performance gains.
So you can do something like this,
this.setState({foo: 'bar'}, () => {
// Do something here.
});
async componentDidMount() {
const { user, roomName } = this.props.navigation.state.params
await this.props.getRoom(roomName)
this.setState({
room: this.props.room
})
}
You don't have to use local state in this case.
render() {
const {room: {name}} = this.props;
return (
<View style={styles.container}>
<ScrollView contentContainerStyle={styles.contentContainer}>
<View>
<Text>Room name: {name}</Text>
</View>
</ScrollView>
</View>
)
}
Here since you are using redux (global state), you don't have to add it in your local state. You can directly access the value of the room from the props.
I have a page.js with different components that inside them make setState in componentDidMount(). And the render of parent execute every time (10 in total). But is a problem because I want execute a request on api rest on componentDidUpdate and this function execute very times.
There are any way for the setstate in a child not affecting the render of parent?
Update with code
page.js
import Header from 'Pages/userprofile/general/HeaderMenuUser';
import UserHeader from 'Pages/userprofile/basic/header/UserHeader';
import UserContent from 'Pages/userprofile/basic/content/UserContent';
...
class UserProfile extends Component {
static navigationOptions = ({navigation}) => {
const user = navigation.getParam('user', '');
return {
title: 'User Profile',
header: (navigation) => <Header {...navigation} title="My profile" user={user} origin="UserProfile" edit={false} />,
headerTintColor: '#000',
headerTitleStyle: {
fontWeight: 'bold',
},
};
};
getProfile(){
//const { user } = this.props.navigation.state.params;
//Profile.get(user.id).then( response => {
ProfileApi.get().then( response => {
this.setState({ user: response.user });
this.props.navigation.setParams({
user: response.user,
});
});
}
render() {
//console.warn(this.props.isFocused ? 'Focused' : 'Not focused');
return (
<ScrollView style={[styles.scroll, globalStyles.generalScrollContainer]}>
<View style={{backgroundColor:'#fff', minHeight: 50}}>
<GradientBackground />
<UserHeader user={this.state.user} edit={false}/>
</View>
{ typeof this.state.user === "object" && (
<View style={styles.container}>
<UserContent user={this.state.user} />
</View>
)}
</ScrollView>
);
}
}
export default withNavigationFocus(UserProfile);
UserContent.js
export default class UserContent extends Component {
updateState = (update) => {
if (update['simple']){
this.setState({ [update['key']]: update['value'] });
} else {
projects = this.state.user.projects;
projects.push(update['value']);
this.setState( prevState => ({
user: {
...prevState.user,
projects: projects
}
}));
this.setState( prevState => ({
user: {
...prevState.user,
can_create_projects: update['value'].can_create_projects
}
}));
Functions.storeItem('user', this.state.user);
}
};
componentDidMount() {
Functions.getApiToken().then((response) => {
this.setState({ url: url + response });
});
}
render() {
return (
<View>
<View style={styles.content}>
<UserBiography user={this.state.user} />
<View style={[globalStyles.rowBetween, globalStyles.rowVerticalCenter]}>
<Text style={globalStyles.titleText}>Projects</Text>
{ this.state.user.can_create_projects && (
<View>
<TouchableOpacity
style={styles.button}
onPress={() => { this.setState({collapsed: !this.state.collapsed}) }}>
<Text style={[globalStyles.textTag, this.state.collapsed ? globalStyles.backgroundBlue : globalStyles.backgroundGary ]}>{this.state.collapsed ? "Add" : "Close add"} project</Text>
</TouchableOpacity>
</View>
) }
</View>
<View style={[globalStyles.borderBottomView, {marginBottom: 30}]}></View>
<UserCreateProject collapsed={this.state.collapsed} updateState={this.updateState}/>
{typeof this.state.user === 'object' && this.state.user.projects.length > 0 && (this.state.user.projects.map((project, index) => {
return (<UserProject project={project} key={project.id} updateState={this.updateState} />)
}))}
</View>
</View>
)
}
}
You can use shouldComponentUpdate() method to control your render:
shouldComponentUpdate(nextProps, nextState) {
if (nextState.render_screen) {
return true;
} else {
return false;
}
}