React-Native-Navigation V2 with redux cannot pass props - react-native

I have a simple two screens app with redux and React-Native-Navigation V2. I try to pass an item from a list to another view as a prop. Unfortunately, I get an error:
TypeError: Cannot read property 'id' of undefined
The item is passed but not received as a prop in the second view. Everything works fine when working without Redux. Am I registering the views correctly?
Views registration:
export default (store) => {
Navigation.registerComponent('example.app.FirstScreen', reduxStoreWrapper(FirstScreen, store));
Navigation.registerComponent('example.app.SecondScreen', reduxStoreWrapper(SecondScreen, store));
}
function reduxStoreWrapper (MyComponent, store) {
return () => {
return class StoreWrapper extends React.Component {
render () {
return (
<Provider store={store}>
<MyComponent />
</Provider>
);
}
};
};
}
First View:
class FirstScreen extends Component {
componentDidMount() {
this.props.listItems();
}
onItemPress = (item: Item) => {
Navigation.push(item._id, {
component: {
name: 'example.app.SecondScreen',
passProps: {
item: item
}
}
});
};
render() {
return (
<View>
<ItemsList items={this.props.items} onItemPress={this.onItemPress}/>
</View>
);
}
}
const mapStateToProps = state => {
let items = state.itemsReducer.items.map(item => ({ key: item.id, ...item }));
return {
items: items
};
};
const mapDispatchToProps = {
listItems
};
export default connect(mapStateToProps, mapDispatchToProps)(FirstScreen);
Second View:
class SecondScreen extends Component {
static propTypes = {
item: PropTypes.object.isRequired,
};
componentDidMount() {
const { item } = this.props;
this.props.listSubitems(item.id);
}
render() {
const { subitems } = this.props;
return (
<View>
<SubitemsList subitems={subitems}/>
</View>
);
}
}
const mapStateToProps = state => {
let subitems = state.subitemsReducer.subitems.map(subitem => ({ key: subitem.id, ...subitem }));
return {
subitems: subitems
};
};
const mapDispatchToProps = {
listSubitems
};
export default connect(mapStateToProps, mapDispatchToProps)(SecondScreen);

Views should be registered this way:
export default (store, Provider) => {
Navigation.registerComponentWithRedux('example.app.FirstScreen', () => FirstScreen, Provider, store);
Navigation.registerComponentWithRedux('example.app.SecondScreen', () => SecondScreen, Provider, store);
}

Related

consuming Context on a Class Component

I'm new to context so may be a very easy error. I'm getting this error when triing to update the context:
[Unhandled promise rejection: TypeError: setUserInfo is not a function. (In 'setUserInfo(newUser)', 'setUserInfo' is undefined)]
Here is what I've done:
AppContext.js
import React, { Component } from 'react'
const AppContext = React.createContext();
class UserProvider extends Component {
// Context state
state = {
userInfo: {},
userChats: {},
userSettings: {},
}
// Method to update state
setUserInfo = (user) => {
this.setState((prevState) => ({ user }))
}
// Method to update state
setUserChats = (userChats) => {
this.setState((prevState) => ({ userChats }))
}
// Method to update state
setUserSettings = (settings) => {
this.setState((prevState) => ({ settings }))
}
render() {
const { children } = this.props
const { userInfo } = this.state
const { setUserInfo } = this
const { userChats } = this.state
const { setUserChats } = this
const { userSettings } = this.state
const { setUserSettings } = this
return (
<UserContext.Provider
value={{
userInfo,
setUserInfo,
userChats,
setUserChats,
userSettings,
setUserSettings,
}}
>
{children}
</UserContext.Provider>
)
}
}
export default AppContext
export { UserProvider }
Wrapping the App Component:
const defaultProviderValue = {userInfo: {}, userChats: {}, userSettings: {}}
<AppContext.Provider value = {defaultProviderValue}>
<Container>
<AppNavigator />
</Container>
</AppContext.Provider>
and then finally trying to update it on a class component:
import React, { Component} from 'react';
import AppContext from '../Context/AppContext.js'
class startScreen extends Component {
static contextType = AppContext
constructor(props) {
super(props);
this.state = {
};
}
async componentDidMount() {
const { userInfo, setUserInfo } = this.context
console.log("CONTEXT!!!!! : " + JSON.stringify(userInfo));
const newUser = { name: 'Joe', loggedIn: true };
setUserInfo(newUser); // ERROR fired: [Unhandled promise rejection: TypeError: setUserInfo is not a function. (In 'setUserInfo(newUser)', 'setUserInfo' is undefined)]
console.log("NEW CONTEXT!!!!! : " + JSON.stringify(userInfo));
}
render() {
return(null);
}
}
export default startScreen;
So how can I solve the error? It seems that can not find the method to update the value, but it's defined.
You are trying to use AppContext but you haven't set anything on itt apart from defaultValues. I guess you would want to use UserProvider and use AppContext within it
import React, { Component } from 'react'
const AppContext = React.createContext();
class UserProvider extends Component {
// Context state
state = {
userInfo: {},
userChats: {},
userSettings: {},
}
// Method to update state
setUserInfo = (user) => {
this.setState((prevState) => ({ user }))
}
// Method to update state
setUserChats = (userChats) => {
this.setState((prevState) => ({ userChats }))
}
// Method to update state
setUserSettings = (settings) => {
this.setState((prevState) => ({ settings }))
}
render() {
const { children } = this.props
const { userInfo } = this.state
const { setUserInfo } = this
const { userChats } = this.state
const { setUserChats } = this
const { userSettings } = this.state
const { setUserSettings } = this
return (
<AppContext.Provider
value={{
userInfo,
setUserInfo,
userChats,
setUserChats,
userSettings,
setUserSettings,
}}
>
{children}
</AppContext.Provider>
)
}
}
export default AppContext;
export { UserProvider };
<UserProvider>
<Container>
<AppNavigator />
</Container>
</UserProvider>
Post this change, you will be able to consume the context correctly

React-Native Redux unable to set state

I use Redux to set three Counters for my navigation bar. I can access the values in initial state. But I am unable to change the values. My Reducer looks like this:
const SET_FREUNDE = 'SET_FREUNDE';
const SET_CHATS = 'SET_CHATS';
const SET_VOTES = 'SET_VOTES';
export function setFreunde(value) {
return {
type: SET_FREUNDE,
value,
}
}
export function setChats(value) {
return {
type: SET_CHATS,
value,
}
}
export function setVotes(value) {
return {
type: SET_VOTES,
value,
}
}
const defaults =
{
countervotes: 2,
counterchats: 1,
counterfreunde: 1
};
function counter(state=defaults, action) {
switch (action.type) {
case SET_FREUNDE:
return {...state,counterfreunde: action.value}
case SET_CHATS:
return {...state,counterchats: action.value}
case SET_VOTES:
return {...state,countervotes: action.value}
default:{
return state;
}
}
}
export default counter;
Now I want to set the counters on a other screen:
import * as React from "react";
import { Image, StyleSheet,... } from "react-native";
...
import {connect} from 'react-redux';
import { setChats, setFreunde, setVotes } from '../redux/counter';
class ... extends React.Component<{}, State> {
constructor(props) {
super(props);
}
...
render(){
return(
<SafeAreaView style={styles.container}>
<Button onPress={() => setFreunde(2)}/>
...
</SafeAreaView>
);
}
}
const styles = StyleSheet.create({
...
});
const mapDispatchToProps = (dispatch, ownProps) => ({
setFreunde: () => {
const { value } = ownProps
dispatch(setFreunde(value))
},
setChats: () => {
const { value } = ownProps
dispatch(setChats(value))
},
setVotes: () => {
const { value } = ownProps
dispatch(setVotes(value))
}
})
export default connect( null, mapDispatchToProps )(NavStack)
If I log the setFreunde(2) the console says the following:
Object { "type": "SET_FREUNDE", "value": 2, }
However, the value does not change which I retrieve in my App.tsx as follows:
const counterfreunde = useSelector((state)=>state.counterfreunde);
What is my mistake?
Problem: Not Dispatching Action
<Button onPress={() => setFreunde(2)}/>
This code right here is just calling the action creator setFreunde. It does not dispatch it.
Your mapDispatchToProps function adds a prop setFreunde to your component which takes no arguments and dispatches the action with the value from props.value. You are not using this prop. You are just using the action creator. You need to call the function from props:
<Button onPress={this.props.setFreunde} title="Set Freunde" />
That fixes the functionality. You do need to add a lot of annotations if you are trying to use typescript here. It's easier with function components!
import { createStore } from "redux";
import { Provider, connect } from "react-redux";
import React, { Dispatch } from "react";
import { SafeAreaView, Button, Text } from "react-native";
const SET_FREUNDE = "SET_FREUNDE";
const SET_CHATS = "SET_CHATS";
const SET_VOTES = "SET_VOTES";
export function setFreunde(value: number) {
return {
type: SET_FREUNDE,
value
};
}
export function setChats(value: number) {
return {
type: SET_CHATS,
value
};
}
export function setVotes(value: number) {
return {
type: SET_VOTES,
value
};
}
const defaults = {
countervotes: 2,
counterchats: 1,
counterfreunde: 1
};
type State = typeof defaults;
type Action = ReturnType<typeof setVotes | typeof setChats | typeof setFreunde>;
function counter(state: State = defaults, action: Action): State {
switch (action.type) {
case SET_FREUNDE:
return { ...state, counterfreunde: action.value };
case SET_CHATS:
return { ...state, counterchats: action.value };
case SET_VOTES:
return { ...state, countervotes: action.value };
default: {
return state;
}
}
}
const store = createStore(counter);
const mapDispatchToProps = (
dispatch: Dispatch<Action>,
ownProps: OwnProps
) => ({
setFreunde: () => {
const { value } = ownProps;
dispatch(setFreunde(value));
},
setChats: () => {
const { value } = ownProps;
dispatch(setChats(value));
},
setVotes: () => {
const { value } = ownProps;
dispatch(setVotes(value));
}
});
const mapStateToProps = (state: State) => ({
freund: state.counterfreunde
});
interface OwnProps {
value: number;
}
type Props = OwnProps &
ReturnType<typeof mapDispatchToProps> &
ReturnType<typeof mapStateToProps>;
class NavStack extends React.Component<Props> {
render() {
return (
<SafeAreaView>
<Text>Freunde Value: {this.props.freund}</Text>
<Button onPress={this.props.setFreunde} title="Set Freunde" />
</SafeAreaView>
);
}
}
const Test = connect(mapStateToProps, mapDispatchToProps)(NavStack);
export default function App() {
return (
<Provider store={store}>
<Test value={5} />
</Provider>
);
}
Code Sandbox Demo

React navigation and loading component in react native

Hello I have a react component like which either display a list of items or opens another component which allowes me to select some value. Here is the code
import React, { PureComponent } from "react";
import { Text, View } from "react-native";
import { withNavigationFocus } from "react-navigation-is-focused-hoc";
import { NavigationActions } from "react-navigation";
import { connect } from "react-redux";
import ClassListing from '../../components/ClassListing/ClassListing';
import Actions from "../../state/Actions";
import { default as stackStyles } from "./styles";
import {
StyleCreator
} from "../../utils";
let styles = StyleCreator(stackStyles.IndexStyles)();
#withNavigationFocus
#connect(() => mapStateToProps, () => mapDispatchToProps)
export default class ClassList extends PureComponent {
static navigationOptions = ({ navigation }) => {
const { params } = navigation.state;
return {
header: null,
tabBarOnPress({ jumpToIndex, scene }) {
params.onTabFocus();
jumpToIndex(scene.index);
},
};
};
constructor(props) {
super(props);
this.state = {};
console.ignoredYellowBox = ["Warning: In next release", "FlatList"];
}
componentDidMount() {
console.log("componentDidMount");
this.props.navigation.setParams({
onTabFocus: this.handleTabFocus
});
}
componentDidUpdate() {
console.log("I am updated");
}
_handleNavigationBar = () => (
<View style={styles.headerContainer}>
<Text style={styles.headerLeftTitle}>Klasselister</Text>
</View>
);
handleTabFocus = () => {
this.props.resetClassList();
// setTimeout(() => {
// this._openChildSchoolSelector();
// },500);
};
_openChildSchoolSelector = () => {
if (
!this.props.childrenSelection.selectedDependant ||
!this.props.childrenSelection.selectedSchool
) {
console.log("navigate to child selector");
this.props.navigation.dispatch(
NavigationActions.navigate({
routeName: "ChildSchoolSelector",
})
);
}
};
render() {
return (
<View>
{this._handleNavigationBar()}
{this.props.classList &&
this.props.classList.classList &&
this.props.classList.classList.length > 0 ? (
<ClassListing list={this.props.classList.classList} />
) : (
null
// this._openChildSchoolSelector()
)}
</View>
);
}
}
const mapStateToProps = (state) => {
const { classList, childrenSelection } = state.appData;
console.log("[Classlist] mapStateToProps", classList);
console.log("[Classlist] mapStateToProps", childrenSelection);
return {
classList,
childrenSelection
};
};
const mapDispatchToProps = (dispatch) => ({
resetClassList: () => {
dispatch(Actions.resetClassList());
},
});
My issue is that when I come to this tab, I want to reset the list which came from server and then open the school selector again, which I am doing in handleTabFocus. But there I have to first call
this.props.resetClassList();
this._openChildSchoolSelector()
The issue is that this._openChildSchoolSelector() is called while this.props.resetClassList() hasn't fininshed.
this.props.resetClassList() works like this
it calls an action like this
static resetClassList() {
return {
type: ActionTypes.RESET_CLASS_LIST
};
}
which then cleans the classlist like this
case ActionTypes.RESET_CLASS_LIST:
return createDefaultState();
in ClassListReducer.js
and also in ChildrenSelectionRedcuer.js
case ActionTypes.RESET_CLASS_LIST:
return createDefaultState();
Any hints to solve this? My project uses very old React navigation v1
One thing which I did was to use this
componentWillReceiveProps(nextProps) {
console.log(this.props);
console.log(nextProps);
if (
nextProps.isFocused &&
nextProps.focusedRouteKey == "ClassList" &&
(!nextProps.childrenSelection.selectedDependant ||
!nextProps.childrenSelection.selectedSchool)
) {
console.log("componentWillReceiveProps");
this._openChildSchoolSelector();
}
if (
this.props.isFocused &&
this.props.focusedRouteKey === "ClassList" &&
nextProps.focusedRouteKey !== "ClassList"
) {
console.log("reset");
this.props.resetClassList();
}
}
But not sure if this is an elegant way of doing this

React-Native SwitchNavigator don't provide new props in root

I have problem with receiving new props in root stack navigator. I have 2 screens in stack navigator: list and edit item. On list screen i click a edit button and dispatch data to store - it works. But in the edit screen i edit data and dispatch new list with new element (for test). List screen dont receive new list. Can you help me?
App.js
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'CATEGORIES_CHANGED':
return {
...state,
categories: action.data
};
case 'SET_CATEGORY_INFO':
return {
...state,
categoryInfo: action.data
};
default:
return state;
}
};
const store = createStore(reducer);
export default class App extends React.Component {
render() {
return (
<Provider store={store}>
<AppNavigator/>
</Provider>
);
}
}
AppNavigator.js
import React from 'react';
import { createSwitchNavigator } from 'react-navigation';
import MainTabNavigator from './MainTabNavigator';
import { connect } from 'react-redux';
const AppNavigator = createSwitchNavigator({
Main: MainTabNavigator
});
export default AppNavigator;
MainTabNavigator.js
import React from 'react';
import {Platform} from 'react-native';
import {createStackNavigator, createBottomTabNavigator} from 'react-navigation';
import { connect } from 'react-redux';
...
const CategoriesStack = createStackNavigator({
CategoriesListScreen: {
screen: CategoriesListScreen,
},
CategoryInfoScreen: {
screen: CategoryInfoScreen,
},
CategoryEditScreen: {
screen: CategoryEditScreen,
},
});
CategoriesStack.navigationOptions = {
tabBarLabel: 'Categories',
tabBarIcon: ({focused}) => (
<TabBarIcon
focused={focused}
name={Platform.OS === 'ios' ? 'ios-link' : 'md-link'}
/>
),
};
...
const bottomTabNavigator = createBottomTabNavigator({
CategoriesStack,
...
});
export default bottomTabNavigator;
CategoriesListScreen.js
import { connect } from 'react-redux';
class CategoriesListScreen extends React.Component {
render() {
const cats = this.state.categories;
return (
<ScrollView style={styles.container}>
{cats.map((category, i) => {
return (
<TouchableOpacity key={category.id} style={
(i === cats.length - 1) ?
{...styles.categoryItem, ...styles.categoryItemLast} :
styles.categoryItem
} onPress={()=>{this.onPressCategory(category)}}>
<View style={{
...styles.categoryLabel, ...{
backgroundColor: category.color
}
}}>
<Icon name={category.icon} size={25} style={styles.categoryIcon}
color={category.iconColor}/>
</View>
<Text>{category.title}</Text>
</TouchableOpacity>
)
})}
</ScrollView>
);
}
componentWillReceiveProps(nextProps) {
console.log(nextProps);
}
componentWillMount() {
const categories = this.props.categories;
this.setState({
categories: categories
});
}
onPressCategory(category) {
this.props.setCategoryInfo(category);
this.props.navigation.navigate('CategoryInfoScreen', {});
}
}
function mapStateToProps(state) {
return {
categories: state.categories
}
}
function mapDispatchToProps(dispatch) {
return {
setCategoryInfo: (category) => dispatch({ type: 'SET_CATEGORY_INFO', data: category })
}
}
export default connect(mapStateToProps, mapDispatchToProps)(CategoriesListScreen)
CategoryEditScreen.js
import { connect } from 'react-redux';
class CategoryEditScreen extends React.Component {
static navigationOptions = ({navigation}) => {
return {
title: 'Edit Category',
headerRight: <Button onPress={() => {navigation.getParam('categoryChangedDispatch')()}} title="Save"/>
}
};
render() {
const category = this.state.category;
...
}
componentWillMount() {
const category = this.props.categoryInfo;
this.setState({
category: category
});
}
componentDidMount() {
this.props.navigation.setParams({
categoryChangedDispatch: this.categoryChangedDispatch.bind(this)
});
}
categoryChangedDispatch() {
let cats = this.props.categories;
cats.push({
id: 3,
title: 'My third category',
color: '#7495e7',
icon: 'airplane',
iconColor: '#2980B9'
});
this.props.categoryChanged(cats);
this.props.navigation.navigate('CategoryInfoScreen', {});
}
}
function mapStateToProps(state) {
return {
categories: state.categories,
categoryInfo: state.categoryInfo
}
}
function mapDispatchToProps(dispatch) {
return {
categoryChanged: (categories) => dispatch({ type: 'CATEGORIES_CHANGED', data: categories }),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(CategoryEditScreen)
It seems it's related to the fact you update your local state (this.state.categories) based on the categories property, only during the componentWillMount phase but not when props are updated (which should happen after you dispatched the new data).
Try using this.props.categories directly within your CategoriesListScreen component.
before
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'CATEGORIES_CHANGED':
return {
...state,
categories: action.data
};
default:
return state;
}
};
after
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'CATEGORIES_CHANGED':
return {
...state,
categories: Object.assign([], action.data)
};
default:
return state;
}
};
Reducer has problem. I made the absolutely new array. It works! But I think it isn't normal :)

connect() does not re-render component

a component dispatches an action which modifies the Redux store and the other component should get the changed state to props and rerender.
The thing is, the component gets the props, and they are correct and modified, but the component is never rerendered.
Could someone help, been stuck too much..
Component who uses store:
on mount it does a http request,
and should rerender when the state is changed.
class CalendarView extends Component {
componentDidMount() {
axios.get('http://localhost:3000/api/bookings/get')
.then(foundBookings => {
this.props.getBookings(foundBookings);
})
.catch(e => console.log(e))
}
render() {
return (
<Agenda
items={this.props.items}
selected={this.props.today}
maxDate={this.props.lastDay}
onDayPress={this.props.setDay}
renderItem={this.renderItem}
renderEmptyDate={this.renderEmptyDate}
rowHasChanged={this.rowHasChanged}
/>
);
}
renderItem = (item) => {
return (
<View style={[styles.item, { height: item.height }]}>
<Text>Name: {item.name} {item.surname}</Text>
<Text>Time: {item.time}</Text>
</View>
);
}
renderEmptyDate = () => {
return (
<View style={styles.emptyDate}><Text>This is empty date!</Text></View>
);
}
rowHasChanged = (r1, r2) => {
console.log('hit')
return true;
}
}
const mapStateToProps = (state, ownProps) => {
return {
today: state.app.today,
lastDay: state.app.lastDay,
items: state.app.items
}
}
const mapDispatchToProps = (dispatch) => {
return {
setDay: date => dispatch(appActions.setSelectionDate(date.dateString)),
getBookings: data => dispatch(appActions.getBookings(data)),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(CalendarView);
Action Dispatching:
dispatches an action which modifies the state
onSubmit = (name, surname, selectionDate, selectionTime) => {
axios.post('http://localhost:3000/api/bookings/create', {
bookerName: name,
bookerSurname: surname,
bookerTime: selectionTime,
date: selectionDate
}).then(savedBookings => {
this.props.createBooking(savedBookings);
this.props.navigator.pop({
animationType: 'slide-down',
});
}).catch(e => console.log(e))
}
const mapStateToProps = state => {
//...
}
const mapDispatchToProps = (dispatch) => {
return {
createBooking: data => dispatch(appActions.createBooking(data))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(NewBookingScreen);
Reducer:
case types.CREATE_BOOKING: {
const { date , bookings } = action.savedBookings.data;
let dateArr = state.items;
// formatting a booking how needed
Object.keys(dateArr).forEach(key => {
if (key == date) {
dateArr[key] = [];
bookings.map(oneBooking => {
dateArr[key].push({
name: oneBooking.bookerName,
surname: oneBooking.bookerSurname,
time: oneBooking.bookerTime,
height: Math.max(50, Math.floor(Math.random() * 150))
});
})
}
});
return {
...state,
items: dateArr
};
}
full repo if needed: https://github.com/adtm/tom-airbnb/tree/feature/redux
Thank You in advance!
Your reducer is mutating the state, so connect thinks nothing has changed. In addition, your call to map() is wrong, because you're not using the result value.
Don't call push() on an array unless it's a copy. Also, please don't use any randomness in a reducer.
For more info, see Redux FAQ: React Redux ,Immutable Update Patterns, and Roll the Dice: Random Numbers in Redux .