I'm creating react native app with redux state management. I want to know what is the best practice of having connect(mapStateToProps, mapDispatchToProps).
I have several component classes i.e. ParentA, ChildA, ChildB. Currently I'm getting state properties for each parent and child classes independently.
eg:
const ParentA = (props) => {
return (
<View>
<Text>{props.item.name}</Text>
<ChildA />
<ChildB />
</View>
)
}
const mapStateToProps = (state) => {
const { item } = state
return {
item: item.item,
}
}
export default connect(mapStateToProps)(ParentA)
const ChildA = (props) => {
return (
<View>
<Text>{props.item.name}</Text>
</View>
)
}
const mapStateToProps = (state) => {
const { item } = state
return {
item: item.item,
}
}
export default connect(mapStateToProps)(ChildA)
const ChildB = (props) => {
return (
<View>
<Text>{props.item.age}</Text>
</View>
)
}
const mapStateToProps = (state) => {
const { item } = state
return {
item: item.item,
}
}
export default connect(mapStateToProps)(ChildB)
But rather having connect for each child component I could get item state from ParentA and pass it to Child components.
eg:
const ParentA = (props) => {
return (
<View>
<Text>{props.item.name}</Text>
<ChildA item={item}/>
<ChildB item={item}/>
</View>
)
}
const mapStateToProps = (state) => {
const { item } = state
return {
item: item.item,
}
}
export default connect(mapStateToProps)(ParentA)
const ChildA = (props) => {
return (
<View>
<Text>{props.item.name}</Text>
</View>
)
}
const mapStateToProps = (state) => {
const { item } = state
return {
item: item.item,
}
}
export default ChildA
const ChildB = (props) => {
return (
<View>
<Text>{props.item.age}</Text>
</View>
)
}
const mapStateToProps = (state) => {
const { item } = state
return {
item: item.item,
}
}
export default ChildB
My questions are,
What would be the best approach while considering the app performance ?
Can I use same approach for mapDispatchToProps as well ?
Yes, you can Use useSelector, useDispatch but the thing is you should use hooks. It can fix considering the app performance with this approach.
I think rather than using 'const', try another datatypes such as 'var' or 'let' as 'const' value once fixed cannot be changed.
Related
I'm building a React Native app.
My app has 5 Screens: Home (initialRouteName), DeckPage, QuestionPage, NewCardPage, NewDeckPage. (in this order)
I'm using Redux for state management. The state is updating from AsyncStorage.
The component that does the fetching is the class component "Home" by dispatching the "fetching" function in componentDidMount.
Component NewCardPage, NewDeckPAge are also updating the state with new content by dispatching the same fetching function as the Home when a button is pressed.
My problem appears when I want to delete a Deck component from inside DeckPage parent component. The function that does this job has this functionality: after removing the item from AsyncStorage, updates the STATE, and moves back to Screen HOME. The issue is that when I go back to HOME component the state doesn't update with the latest info from AsyncStorage.
This is not the case when I'm doing the same operation in the other 2 components NewCardPage, NewDeckPage.
I'll paste the code below:
import React, { Component } from "react";
import { connect } from "react-redux";
import { View, Text, StyleSheet, FlatList } from "react-native";
import Header from "../components/Header";
import AddDeckButton from "../components/AddDeckButton";
import DeckInList from "../components/DeckInList";
import { receiveItemsAction } from "../redux/actions";
class Home extends Component {
componentDidMount() {
this.props.getAsyncStorageContent();
}
renderItem = ({ item }) => {
return <DeckInList {...item} />;
};
render() {
const { items } = this.props;
// console.log(items);
const deckNumber = Object.keys(items).length;
return (
<View style={styles.container}>
<Header />
<View style={styles.decksInfoContainer}>
<View style={styles.deckNumber}>
<View style={{ marginRight: 50 }}>
<Text style={styles.deckNumberText}>{deckNumber} Decks</Text>
</View>
<AddDeckButton />
</View>
<View style={{ flex: 0.9 }}>
<FlatList
data={Object.values(items)}
renderItem={this.renderItem}
keyExtractor={(item) => item.title}
/>
</View>
</View>
</View>
);
}
}
const mapStateToProps = (state) => {
return {
items: state.items,
};
};
const mapDispatchToProps = (dispatch) => {
return {
getAsyncStorageContent: () => dispatch(receiveItemsAction()),
};
};
-----------DECKPAGE COMPONENT------------
import React from "react";
import { View, StyleSheet } from "react-native";
import Deck from "../components/Deck";
import { useSelector, useDispatch } from "react-redux";
import { removeItemAction, receiveItemsAction } from "../redux/actions";
import AsyncStorage from "#react-native-community/async-storage";
const DeckPage = ({ route, navigation }) => {
const { title, date } = route.params;
const questions = useSelector((state) => state.items[title].questions);
const state = useSelector((state) => state.items);
const dispatch = useDispatch();
// const navigation = useNavigation();
const handleRemoveIcon = async () => {
await AsyncStorage.removeItem(title, () => {
dispatch(receiveItemsAction());
navigation.goBack();
});
};
console.log(state);
return (
<View style={styles.deckPageContainer}>
<Deck
handleRemoveIcon={handleRemoveIcon}
title={title}
questions={questions}
date={date}
/>
</View>
);
};
-----------This is my ACTIONS file----------
import AsyncStorage from "#react-native-community/async-storage";
export const RECEIVE_ITEMS = "RECEIVE_ITEMS";
// export const REMOVE_ITEM = "REMOVE_ITEM";
export const receiveItemsAction = () => async (dispatch) => {
const objectValues = {};
try {
const keys = await AsyncStorage.getAllKeys();
if (keys.length !== 0) {
const jsonValue = await AsyncStorage.multiGet(keys);
if (jsonValue != null) {
for (let element of jsonValue) {
objectValues[element[0]] = JSON.parse(element[1]);
}
dispatch({
type: RECEIVE_ITEMS,
payload: objectValues,
});
} else {
return null;
}
}
} catch (e) {
console.log(e);
}
};
-----This is my REDUCERS file----
import { RECEIVE_ITEMS, REMOVE_ITEM } from "./actions";
const initialState = {
};
const items = (state = initialState, action) => {
switch (action.type) {
case RECEIVE_ITEMS:
return {
...state,
...action.payload,
};
// case REMOVE_ITEM:
// return {
// ...state,
// ...action.payload,
// };
default:
return state;
}
}
export default items;
-----This is my UTILS file----
import AsyncStorage from "#react-native-community/async-storage";
export const removeDeckFromAsyncStorage = async (title)=>{
try{
await AsyncStorage.removeItem(title);
}
catch(e){
console.log(`Error trying to remove deck from AsyncStorage ${e}`);
}
}
I have a function component UpdateCustomerScreen connect with redux store and use react-navigation navigate to SelectorGenderScreen.
selectedCustomer is my redux store data. I change the data on SelectorGenderScreen, when I use navigation.pop() to UpdateCustomerScreen. I have no idea how to re render the UpdateCustomerScreen.
Here is my UpdateCustomerScreen:
const UpdateCustomerScreen = ({ navigation, selectedCustomer }) => {
const gender = changeGenderOption(selectedCustomer.sex); // gender is an array.
const [sex, setSex] = useState(gender); // set array to my state.
console.log('sex', sex);
return (
<View>
<TouchableOpacity onPress={() => navigation.push('SelectorGenderScreen')}
<Text>Navigate to next screen</Text>
</TouchableOpacity>
</View>
);
const mapStateToProps = (state) => {
const { selectedCustomer } = state.CustomerRedux;
return { selectedCustomer };
};
export default connect(mapStateToProps, {})(UpdateCustomerScreen);
Here is my SelectorGenderScreen:
const SelectorGenderScreen = ({ navigation, selectedCustomer, changeGender }) => {
const gender = changeGenderOption(selectedCustomer.sex);
const [genderOption, setGenderOption] = useState(gender);
return (
<Header
title={Translate.chooseStore}
leftComponent={
<BackButton onPress={() => navigation.pop()} />
}
/>
<TouchableOpacity onPress={() => changeGender(selectedCustomer, genderOption)}>
<Text>Change the redux store data</Text>
</TouchableOpacity>
);
const mapStateToProps = (state) => {
const { selectedCustomer } = state.CustomerRedux;
return { selectedCustomer };
};
const mapDispatchToProps = dispatch => {
return {
changeGender: (selectedCustomer, genderOption) => {
dispatch(changeGender(selectedCustomer, genderOption));
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(SelectorGenderScreen);
I try to use useCallback() in UpdateCustomerScreen. When I navigation.pop(), It still doesn't re render.
// my state
const [sex, setSex] = useState(gender);
// It is not working
useCallback(() => {
console.log(sex);
},[sex]);
// It is not working
console.log('sex', sex);
return (
// my view
);
Any way to re render the UpdateCustomerScreen when redux store value has been changed ?
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);
}
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 .
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')
);