I have a react native app using redux and immutable js. When i dispatch an action from my main screen, it goes through my actions, to my reducer and then back to my container, however, the view doesn't update and componentWillReceieveProps is never called. Furthermore, the main screen is a list whose items are sub components Item. Here's the relevant code for the issue, if you want to see more let me know.
Render the row with the data:
renderRow(rowData) {
return (
<Item item={ rowData } likePostEvent={this.props.likePostEvent} user={ this.props.user } removable={ this.props.connected } />
)
}
The part of Item.js which dispatches an action, and shows the result:
<View style={{flex: 1, justifyContent:'center', alignItems: 'center'}}>
<TouchableOpacity onPress={ this.changeStatus.bind(this, "up") }>
<Image source={require('../img/up-arrow.png')} style={s.upDownArrow} />
</TouchableOpacity>
<Text style={[s.cardText,{fontSize:16,padding:2}]}>
{ this.props.item.starCount }
</Text>
<TouchableOpacity onPress={ this.changeStatus.bind(this, "down") }>
<Image source={require('../img/up-arrow.png')} style={[s.upDownArrow,{transform: [{rotate: '180deg'}]}]} />
</TouchableOpacity>
</View>
The action dispatched goes to firebase, which has an onChange handler that dispatches another action.
The reducer:
const initialState = Map({
onlineList: [],
offlineList: [],
filteredItems: [],
connectionChecked: false,
user: ''
})
...
...
case ITEM_CHANGED:
list = state.get('onlineList')
if(state.get('onlineList').filter((e) => e.id == action.item.id).length > 0){
let index = state.get('onlineList').findIndex(item => item.id === action.item.id);
list[index] = action.item
list = list.sort((a, b) => b.time_posted - a.time_posted)
}
return state.set('onlineList', list)
.set('offlineList', list)
The container:
function mapStateToProps(state) {
return {
onlineItems: state.items.get('onlineList'),
offlineItems: state.items.get('offlineList'),
filteredItems: state.items.get('filteredItems'),
connectionChecked: state.items.get('connectionChecked'),
connected: state.items.get('connected'),
user: state.login.user
}
}
Where I connect the onChange:
export function getInitialState(closure_list) {
itemsRef.on('child_removed', (snapshot) => {
closure_list.removeItem(snapshot.val().id)
})
itemsRef.on('child_added', (snapshot) => {
closure_list.addItem(snapshot.val())
})
itemsRef.on('child_changed', (snapshot) => {
closure_list.itemChanged(snapshot.val())
})
connectedRef.on('value', snap => {
if (snap.val() === true) {
closure_list.goOnline()
} else {
closure_list.goOffline()
}
})
return {
type: GET_INITIAL_STATE,
connected: true
}
}
Calling get initial state:
this.props.getInitialState({
addItem: this.props.addItem,
removeItem: this.props.removeItem,
goOnline: this.props.goOnline,
goOffline: this.props.goOffline,
itemChanged: this.props.itemChanged
})
Any suggestions are welcome, thanks so much!
The source of your issue could be with the call to Firebase. If it is an asynchronous call, it's return callback might not be returning something that can be consumed by your action.
Do you know if it is returning a Promise? If that is the case, middleware exists that handle such calls and stops the calling of an action until a correct response is received. One such middleware is Redux-Promise.
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore,combineReducers } from 'redux' //Redux.createStore
import { Provider,connect } from 'react-redux';
//Функція яка змінює store
const hello = (state= {message:'none'}, action) => {
switch (action.type) {
case 'HELLO':
return Object.assign({}, state, {message:"hello world"});
break
case 'buy':
return Object.assign({}, state, {message:"buy"});
break;
case 'DELETE':
return Object.assign({}, state, {message:"none"});
break;
default :
return state;
}
};
const price = (state= {value:0}, action) => {
switch (action.type) {
case 'HELLO':
return Object.assign({}, state, {value: state.value + 1 });
break;
default :
return Object.assign({}, state, {value:0});
}
};
const myApp = combineReducers({
hello,price
});
//створюємо store
let store = createStore(myApp);
let unsubscribe = store.subscribe(() => console.log(store.getState()))
//VIEW
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<p>value: {this.props.price}</p>
<a href="#" onClick={this.props.onClick}>click</a><b>{this.props.message}</b>
</div>
)
}
}
//mapStateToProps() для чтения состояния и mapDispatchToProps() для передачи события
const mapStateToProps = (state, ownProps) => {
return {
message: state.hello.message,
price: state.price.value
}
};
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onClick: () => {
var items= ['HELLO','buy','DELETE','error']
var item = items[Math.floor(Math.random()*items.length)];
dispatch({ type: item })
}
}
}
const ConnectedApp = connect(
mapStateToProps,
mapDispatchToProps
)(App);
ReactDOM.render(
<Provider store={store}>
<ConnectedApp />
</Provider>,
document.getElementById('app')
);
Related
need some serious help here. I am currently using a class component to add my items to cart but it looks like i keep getting undefined, can someone point me in the right direction please?
Homepage :
const mapStateToProps = state => ({
cartItems: state.cart.cartItems
});
const mapDispatchToProps = dispatch => {
return {
addToCartHandler: item => {
dispatch(addToCart(item))
}
}
}
renderRow =({item}) =>{
console.log(this.state.cartItems)
return(
<TouchableHighlight >
<View style ={styles.card}>
<Image source={{uri:item.image}} style={styles.image} />
<Icon name="add" size ={20} style= {styles.addToCartBtn} onPress ={() => this.props.addToCartHandler(item)} />
</TouchableHighlight>
)
}
reducers
import { ADD_TO_CART } from '../constants'
const initialState = {
cartItems: [],
totalPrice :0
}
//add item
export const cart = (state = initialState, action) => {
switch (action.type) {
case ADD_TO_CART:
console.log("reducer",action)
var {item} =action.payload;
var newState =Object.assign({},{...state});
for (var i = 0; i < state.cartItems.length; i++) {
newState.cartItems = newState.cartItems.concat(item);
console.log(newState)
return newState
}
default:
return state
}
}
action
import {ADD_TO_CART} from '../constants'
import {REMOVE_FROM_CART} from '../constants'
export const addToCart =(item) =>{
// console.log(item)
return{
type: ADD_TO_CART,
payload : item,
};
}
Dont think anything is wrong with my action or rootreducers. I am not sure why i keep getting undefined whenever i add to cart when i can console.log the item
You are not creating the new state correctly, I think the following will work:
case ADD_TO_CART:
console.log('reducer', action);
var item = action.payload;
return {
...state,
cartItems:state.cartItems.concat(item)
}
Here is some useful information on how to create a new state.
I have deleted an item from the redux store. but it does not re-render the application. if I manually rerender the app, then deleted item went. I am doing this method for reading and un-reading the message. but it works fine. deleting also work, but it is not re-render the app automatically. if I trigger the re-render manually then the message was gone.
Reducer.js
import { combineReducers } from 'redux'
import {
DELETE_MESSAGE,
ADD_MESSAGE,
READ_MESSAGE,
UN_READ_MESSAGE,
ARCHIVED_MESSAGE,
UN_ARCHIVED_MESSAGE
} from './Actions'
// reducers
const messageReducer = (state = [], action) => {
switch (action.type) {
case DELETE_MESSAGE:
return state.filter(value => value.key != action.payload.key)
case ADD_MESSAGE:
return [...state, action.payload]
case READ_MESSAGE:
return state.map(value => {
if (value.key == action.payload.key) {
value.read = 'true'
return value
}
return value
})
case UN_READ_MESSAGE:
return state.map(value => {
if (value.key == action.payload.key) {
value.read = 'false'
return value
}
return value
})
case ARCHIVED_MESSAGE:
return state.map(value => {
if (value.key == action.payload.key) {
value.archived = 'true'
return value
}
return value
})
case UN_ARCHIVED_MESSAGE:
return state.map(value => {
if (value.key == action.payload.key) {
value.archived = 'false'
return value
}
return value
})
default:
return state
}
}
// combine reducer
const reducer = combineReducers({
message: messageReducer
})
export default reducer
Actions.js
// action types
export const DELETE_MESSAGE = 'DELETE_MESSAGE'
export const ADD_MESSAGE = 'ADD_MESSAGE'
export const READ_MESSAGE = 'READ_MESSAGE'
export const UN_READ_MESSAGE = 'UN_READ_MESSAGE'
export const ARCHIVED_MESSAGE = 'ARCHIVED_MESSAGE'
export const UN_ARCHIVED_MESSAGE = 'UN_ARCHIVED_MESSAGE'
// action creators
export const deleteMessage = (message) => ({
type: DELETE_MESSAGE,
payload: message
})
export const addMessage = (message) => ({
type: ADD_MESSAGE,
payload: message
})
export const readMessage = (message) => ({
type: READ_MESSAGE,
payload: message
})
export const unReadMessage = (message) => ({
type: UN_READ_MESSAGE,
payload: message
})
export const archivedMessage = (message) => ({
type: ARCHIVED_MESSAGE,
payload: message
})
export const unArchivedMessage = (message) => ({
type: UN_ARCHIVED_MESSAGE,
payload: message
})
deleting page .js
import React from 'react'
import { StyleSheet, View, TouchableHighlight, Alert } from 'react-native'
import { readMessage } from '../../../Redux/Actions'
import { unReadMessage } from '../../../Redux/Actions'
import { deleteMessage } from '../../../Redux/Actions'
import { connect } from 'react-redux'
import Icons from './Icon' // trash-alt
class HiddenRight extends React.Component {
delete = (data) => {
Alert.alert(
'Would you like to Delete?',
'You will permanently remove this message from your mobile local storage',
[
{text: 'Cancel', onPress: () => this.props.delete({})},
{text: 'Delete', onPress: () => this.props.delete(data)}
]
)
}
render () {
return (
<View style={styles.container}>
<TouchableHighlight onPress={() => this.delete(this.props.data) }>
<Icons iconName='trash' title='Delete' backgroundColor='#f80101' />
</TouchableHighlight>
{this.props.data.read == 'false'
? <TouchableHighlight onPress={() => this.props.read(this.props.data)}>
<Icons iconName='envelope' title='Read' backgroundColor='#007AFF' />
</TouchableHighlight>
: <TouchableHighlight onPress={() => this.props.unRead(this.props.data)}>
<Icons iconName='envelope-open' title='UnRead' backgroundColor='gray' />
</TouchableHighlight>
}
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end'
}
})
export default connect(null, { delete: deleteMessage, read: readMessage, unRead: unReadMessage})(HiddenRight)
Store.js
import { createStore } from 'redux'
import reducer from './Reducer'
const DEFAULT_STATE = {
message: [{
'key': '0',
'title': 'Hello, It about my self. Keep watch',
'date': '02/89/3456',
'body': 'Nive to here you all, I am not crazy, Well will you find. yes Of course. I will be there on 56-78-2',
'read': 'false',
'archived': 'false'
}, {
'key': '1',
'title': 'Hello, It about my self. Keep watch',
'date': '02/89/3456',
'body': 'Nive to here you all, I am not crazy, Well will you find. yes Of course. I will be there on 56-78-2',
'read': 'false',
'archived': 'false'
}]
}
// store
const store = createStore(reducer, DEFAULT_STATE)
export default store
MessageScreen.js
...
...
function mapStateToProps(state){
return {
listViewData:state
}
}
export default connect(mapStateToProps)(MessageScreen)
I am expecting that if press delete button, then the message should remove.
Note: read and unread are working fine
I have solved this issue. The mistake I did is, I was storing the prop value which comes from the store directly inside the component state. So it's not re-render because when props change only the render & componentPropsWillChange run. so storing the value in the state means data not change the state first time when a prop changes. Thus the case I have faced.
I'am creating a simple application with authentication. To change a state using redux with react-native-navigation (v1). For example, index.js
...
import { Navigation, } from 'react-native-navigation';
import { Provider, } from 'react-redux';
import store from './src/store';
import registerScreens from './src/screens';
registerScreens(store, Provider);
class App {
constructor () {
this.auth = false;
store.subscribe(this.onStoreUpdate.bind(this));
this.start();
}
onStoreUpdate () {
const state = store.getState();
if (this.auth != state.auth) {
this.auth = state.auth;
this.start();
}
}
start () {
switch (this.auth) {
case false:
Navigation.startTabBasedApp({
tabs: [{
screen: 'navigation.AuthScreen',
}, {
screen: 'navigation.RegisterScreen',
},],
});
break;
case true:
Navigation.startSingleScreenApp({
screen: {
screen: 'navigation.MainScreen',
},
});
break;
}
}
}
const application = new App();
Store is listening an update and change an application layout if need.
AuthScreen show a simple ActivityIndicator, when server request is perform. For example, auth.js
...
import { bindActionCreators, } from 'redux';
import { connect, } from 'react-redux';
import * as actions from './../actions';
...
class AuthScreen extends Component {
constructor (props) {
super(props);
this.state = {
loading: false,
...
};
this.handlePressEnter = this.handlePressEnter.bind(this);
}
handlePressEnter () {
...
this.loadingState(true);
jsonFetch(url, {
method: 'POST',
body: JSON.stringify({...}),
}).then((value) => {
this.loadingState();
this.props.actions.auth(true);
}).catch((errors) => {
this.loadingState();
console.log('Error while auth', errors);
});
}
...
loadingState (state = false) {
this.setState({
loading: state,
});
}
render () {
return (<View>
...
<Modal visible={this.state.loading} transparent={true} animationType="none" onRequestClose={() => {}}>
<View>
<ActivityIndicator size="large" animating={this.state.loading} />
</View>
</Modal>
</View>);
}
}
function mapStateToProps (state, ownProps) {
return {};
}
function mapDispatchToProps (dispatch) {
return {
actions: bindActionCreators(actions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps) (AuthScreen);
I'am starting application with iOS simulator and try to authenticate. It show me activity indicator, then indicator disappear, but layout does not change. And strange behavior, if I comment this.loadingState(true); and this.loadingState(); in auth.js layout changes with success.
Can someone explain to me, why layout does not change from auth to main when activity indicator using?
I think that you can use dispatch props for loading.
For example When you call this.props.actions.auth(true);
You can return loading reducers.
handlePressEnter () {
...
dispatch({ type:'loading', loading: true });
jsonFetch(url, {
method: 'POST',
body: JSON.stringify({...}),
}).then((value) => {
dispatch({ type:'loading', loading: false });
this.props.actions.auth(true);
}).catch((errors) => {
this.loadingState();
console.log('Error while auth', errors);
});
}
And than you can use
<ActivityIndicator size="large" animating={this.props.loading} />
But dont forget the reducers return
In my react native android app, when I try to dispatch an action in BoardsScreen or in the root of the app, the following error pops up:
However, when I remove it, the app doesn't crashes.
BoardsScreen.js
import React from 'react';
import { connect } from 'react-redux';
import { Container, Content, Text, List, Button, Icon, ListItem } from 'native-base';
import { ListView, StatusBar } from 'react-native';
import { ConfirmDialog } from 'react-native-simple-dialogs';
import ActionButton from 'react-native-action-button';
import { removeBoard } from '../actions/configurationActions';
class BoardsScreen extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
boardDeleteDialog: false,
secId: null,
rowId: null,
rowMap: null,
};
}
deleteRow(secId, rowId, rowMap) {
rowMap[`${secId}${rowId}`].props.closeRow();
const newData = [...this.props.boards];
newData.splice(rowId, 1);
this.props.removeBoard(newData);
this.setState({
rowId: null,
secId: null,
rowMap: null,
boardDeleteDialog: false,
});
}
dataSource = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
render() {
console.log(this.props.boards);
return (
<Container>
<StatusBar backgroundColor="#00C853" />
<ConfirmDialog
title="Delete board?"
animationType="fade"
visible={this.state.boardDeleteDialog}
positiveButton={{
title: 'Delete',
titleStyle: {
color: '#2ecc71',
},
onPress: () => this.deleteRow(this.state.secId, this.state.rowId, this.state.rowMap),
}}
negativeButton={{
titleStyle: {
color: '#2ecc71',
},
title: 'Cancel',
onPress: () =>
this.setState({
boardDeleteDialog: false,
secId: null,
rowId: null,
rowMap: null,
}),
}}
/>
<Content>
{this.props.boards.length >= 1 ? (
<List
style={{ backgroundColor: '#D9534F' }}
dataSource={this.dataSource.cloneWithRows(this.props.boards)}
renderRow={data => (
<ListItem
style={{ paddingLeft: 14, backgroundColor: 'transparent' }}
button
onPress={() =>
this.props.navigation.navigate('Board', {
board: data.board,
boardName: data.boardName,
})
}
>
<Text>{data.boardName}</Text>
</ListItem>
)}
renderRightHiddenRow={(data, secId, rowId, rowMap) => (
<Button
full
danger
onPress={() =>
this.setState({
boardDeleteDialog: true,
secId,
rowId,
rowMap,
})
}
>
<Icon active name="trash" />
</Button>
)}
disableRightSwipe
rightOpenValue={-75}
/>
) : (
<Text>No boards added.</Text>
)}
</Content>
<ActionButton
buttonColor="#2ecc71"
fixNativeFeedbackRadius
onPress={() => this.props.navigation.navigate('AddBoard')}
/>
</Container>
);
}
}
const mapStateToProps = state => ({
boards: state.configurationReducer.boards,
});
const mapDispatchToProps = dispatch => ({
removeBoard: (board) => {
dispatch(removeBoard(board));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(BoardsScreen);
App.js
import React from 'react';
import { connect } from 'react-redux';
import MainNavigator from './src/config/Router';
import { addBoardToList } from './src/actions/configurationActions';
import { Board } from './src/API';
class App extends React.PureComponent {
componentDidMount() {
Board.getList(true).then(response => this.parseDataFromJSONResponse(response));
}
parseDataFromJSONResponse(response) {
for (let i = 0; i < response.length; i += 1) {
this.props.addBoardToList(response[1]);
}
}
render() {
return <MainNavigator />;
}
}
const mapStateToProps = state => ({
boards: state.configurationReducer.boards,
});
const mapDispatchToProps = dispatch => ({
addBoardToList: (board) => {
dispatch(addBoardToList(board));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
configurationReducer.js
const initialState = {
theme: 1,
obscure: false,
boards: [],
boardsList: [],
};
const configurationReducer = (state = initialState, action) => {
let newState = { ...state };
switch (action.type) {
case 'ADD_BOARD':
newState = {
boards: [...state.boards, action.payload],
};
return newState;
case 'REMOVE_BOARD':
newState = {
boards: action.payload,
};
return newState;
case 'ADD_BOARD_TO_LIST':
newState = {
boardsList: [...state.boardsList, action.payload],
};
return newState;
default:
return state;
}
};
export default configurationReducer;
configurationActions.js
function addBoard(board) {
return {
type: 'ADD_BOARD',
payload: board,
};
}
function removeBoard(board) {
return {
type: 'REMOVE_BOARD',
payload: board,
};
}
function addBoardToList(board) {
return {
type: 'ADD_BOARD_TO_LIST',
payload: board,
};
}
export { addBoard, removeBoard, addBoardToList };
I really don't have a clue what is causing this, maybe it's a bug but I don't know if is react-redux fault or react native itself.
When you remove the board, it looks like in you reducer, you return a strange new state:
case 'REMOVE_BOARD':
newState = {
boards: action.payload,
};
return newState;
Should the boards to be an array always? I think you missed something, for example:
boards: state.boards.filter ((it) => it.id !== action.payload.id),
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 .