React Native Redux - Limiting component rerenders caused when redux state changes (for array children) - react-native

I am passing redux state into my component using the following
const mapStateToProps = state => ({
rdx_myColorPreference: state.profile.mySelf.preferences[4],
});
My goal is that the component only rerenders when the 5th child in the preferences array is changed (i.e. preferences[4]....which relates to color preferences).
Problem is that when ANY child in the preferences array changes (e.g. preferences[0]....which relates to food preferences), the component rerenders.

Each child should be a pure component (use React.memo) and when you pass a function from the parent then make sure you don't pass a newly created function each time the parent renders so you need to do onChange={function} and not onChange={()=>newly created function}.
Here is an example:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { memo, useCallback } = React;
const initialState = {
items: [
{ id: 1, value: '' },
{ id: 2, value: '' },
{ id: 3, value: '' },
],
};
//action types
const CHANGE_ITEM = 'CHANGE_ITEM';
//action creators
const changeItem = (id, value) => ({
type: CHANGE_ITEM,
payload: { id, value },
});
const reducer = (state, { type, payload }) => {
if (type === CHANGE_ITEM) {
const { id, value } = payload;
return {
...state,
items: state.items.map((item) =>
item.id !== id ? item : { ...item, value }
),
};
}
return state;
};
//selectors
const selectItems = (state) => state.items;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
() => (next) => (action) => next(action)
)
)
);
//use React.memo to create a pure component
const Item = memo(function Item({
item: { id, value },
onChange,
}) {
console.log('render item wih id:', id);
return (
<ul>
{id}
<input
type="text"
value={value}
onChange={(e) => onChange(id, e.target.value)}
/>
</ul>
);
});
const App = () => {
const items = useSelector(selectItems);
const dispatch = useDispatch();
//create this onChange function on mount using React.useCallback
const onChange = useCallback(
(id, value) => dispatch(changeItem(id, value)),
//dependencies is dispatch but that will never change so it is
// only created on mount
[dispatch]
);
return (
<ul>
{items.map((item) => (
<Item
key={item.id}
item={item}
onChange={onChange}
/>
))}
</ul>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>

Related

How to use navigation types inside list child component?

I have a problem to set proper types on card component that I render from flat list which should navigate from list to details screen.
All this works but I am getting types errors.
This is my navigators types:
export type RootStackParams = {
Drawer: DrawerStackParams;
};
export type DrawerStackParams = {
AppTabStack: AppTabStackParams;
};
export type AppTabStackParams = {
AppTabStack: HomeStackParams;
Clients: ClientsStackParams;
};
export type ClientsStackParams = {
Clients: undefined;
ClientDetails: {
clientId: number;
};
};
This is app tab navigator which contain clients navigator:
<AppTabStack.Navigator>
<AppTabStack.Screen
name="Clients"
component={ClientsScreenStack}
}}
/>
</AppTabStack.Navigator>
export const ClientsScreenStack = () => {
return (
<ClientsStack.Navigator>
<ClientsStack.Screen name="Clients" component={ClientsScreen} />
<ClientsStack.Screen
name="ClientDetails"
component={ClientDetailsScreen}
/>
</ClientsStack.Navigator>
);
};
In flat list in clients screen I have renderItem() component:
<FlatList ...
renderItem={({ item }) => (
<ClientCard id={item.id} navigation={navigation} />
)}
/>
Which should navigate to another screen:
const onCardPress = () => {
navigation.navigate('ClientDetails', { clientId: id });
};
This card component has following props:
type ClientCardProps = {
id: number;
navigation: NativeStackScreenProps<ClientsStackParams, 'Clients'>;
};
const ClientCard: FC<ClientCardProps> = ({ id, navigation }) => {...
But if I try to call navigation.navigate('ClientDetails', { clientId: id }); I get:
Property 'navigate' does not exist on type 'NativeStackScreenProps<ClientsStackParams, "Clients", undefined>'

Cannot access global store redux when implementing isolated store for specific screen

I got this error when using useSelector to access variable from global store of Redux.
But Redux store for specific screen is still available.
Here is my code for specific screen:
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
productDetailReducer,
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(productDetailWatcher);
interface IProductDetailProps {}
const Product = () => {
const product = useSelector((state: IProductDetailState) => state.product);
const products = useSelector((state: IStoreState) => state.productsState.products);
const dispatch: Dispatch = useDispatch();
const handlePress = () => {
const product = {
id: 1,
thumbImage: 'https://aladin-today-bucket.s3.ap-southeast-1.amazonaws.com/sm/4bd144c7-896c-55db-b70d-7b5a0b2d4638.jpeg',
thumbHeight: 192,
productName: 'Váy xường xám mặc Trung Thu,Tết'
};
dispatch(GetProductDetailRequest(product));
}
console.log(`Product Detail 2 render with ${product}`);
console.log(`Products ${products}`);
return (
<View style={styles.container}>
<Button onPress={handlePress} title='Get Product Detail' />
{product && (
<View style={styles.productContainer}>
<Image style={styles.image} source={{ uri: product.thumbImage }} />
<Text style={styles.name}>{product.productName}</Text>
</View>
)}
</View>
);
};
const ProductDetail: React.FC<IProductDetailProps> = () => {
console.log("Product Detail rendering...");
return <Provider store={store}>
<Product />
</Provider>;
};
reducer for ProductDetail:
import { IProductDetailState, ProductDetailActions, ProductDetailActionType } from './types';
import { ProductModel } from 'models/Product';
import AsyncStorage from '#react-native-community/async-storage';
import { PersistConfig, persistReducer } from 'redux-persist';
const productDetailState: IProductDetailState = {
product: undefined,
loading: false,
}
export const productDetailReducer = (state = productDetailState, action: ProductDetailActions): IProductDetailState => {
switch(action.type) {
case ProductDetailActionType.GET_PRODUCT_DETAIL_REQUEST: {
console.log('Enter');
return {
...state,
loading: true
}
}
case ProductDetailActionType.GET_PRODUCT_DETAIL_SUCCESS: {
const productDetail: ProductModel = action.payload;
return {
...state,
product: productDetail,
loading: false
}
}
case ProductDetailActionType.GET_PRODUCT_DETAIL_FAILURE: {
return {
...state
}
}
default:
return {
...state
}
}
}
const persistConfig: PersistConfig<any> = {
key: 'ProductDetail',
whitelist: ['product'],
storage: AsyncStorage,
version: 1,
timeout: 0
};
export default persistReducer(persistConfig, productDetailReducer) as any;
Package I use:
"react-native": "0.61.4"
"react-redux": "^7.2.0"
"redux": "^4.0.5",
"redux-persist": "^6.0.0"
"redux-saga": "^1.1.3"
Does anyone have any solution? Thank a lot
It is not advised to use multiple stores but react redux connect can use a different store by providing a store prop when rendering the component that connect creates. Here is an example:
const { Provider, useSelector, connect } = ReactRedux;
const { createStore } = Redux;
const store1 = createStore((x) => x, {
message: 'store 1',
});
const store2 = createStore((x) => x, {
message: 'store 2',
});
const Messages = ({ message2 }) => {
//useSelector will use the prop value store from Provider
const message1 = useSelector((s) => s.message);
return (
<ul>
<li>{message1}</li>
<li>{message2}</li>
</ul>
);
};
//connect uses prop store={store2} or store={store1} when
// MessageContainer does not have a store prop
const MessageContainer = connect(({ message }) => ({
message2: message,
}))(Messages);
const App = () => {
return <MessageContainer store={store2} />;
};
ReactDOM.render(
<Provider store={store1}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>

react-native InputText onChange returns undefined or doesn't allow to change the value

I have the following code:
const { state, updateParentName, updateBabyName } = useContext(AuthContext);
const [isParentInputVisible, setParentInputVisible] = useState(false);
const toggleParentInput = () => {
setParentInputVisible(!isParentInputVisible);
};
<View style={styles.headerContainer}>
<Text style={styles.header}>Hello, </Text>
{isParentInputVisible
? (<TouchableOpacity onPress={toggleParentInput}>
<Text style={styles.headerLink}>{state.parentName}</Text>
</TouchableOpacity>)
: (<TextInput
value={state.parentName}
style={styles.parentInput}
onChangeText={(value) => {updateParentName(value);}}
onSubmitEditing={toggleParentInput}
/>)}
</View>;
And, as you can see, I use a Context to store my parentName value. I also set this var to parent as my default value.
Now, the problem that I am having and, that is driving me crazy, is I can't change the value in this Input field to anything. When I try to enter a new value there is returns undefined. When I remove onChangeText prop from that input for the sake of testing, it doesn't allow me to change the default value.
Can anyone point me to what should I do to fix it? I couldn't find anything useful that would help me.
UPDATE:
AuthContext file:
import createDataContext from './createDataContext';
const authReducer = (state, action) => {
switch (action.type) {
case 'update_parent_name':
return { ...state, parentName: action.payload };
default:
return state;
}
};
const updateParentName = (dispatch) => ({ parentName }) => {
dispatch({ type: 'update_parent_name', payload: parentName });
};
export const { Provider, Context } = createDataContext(
authReducer,
{
updateParentName,
},
{
parentName: 'parent',
}
);
and, just in case createDataContext code:
import React, { useReducer } from 'react';
export default (reducer, actions, defaultValue) => {
const Context = React.createContext(defaultValue);
const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, defaultValue);
boundActions = {};
for (let key in actions) {
boundActions[key] = actions[key](dispatch);
}
return (
<Context.Provider value={{ state, ...boundActions }}>
{children}
</Context.Provider>
);
};
return { Context, Provider };
};
The error is you've taken object as an argument in this function updateParentName but while calling the function through prop onChangeText you've passed the value as is without enclosing it in an object!
const updateParentName = (dispatch) => ({ parentName }) => {
dispatch({ type: 'update_parent_name', payload: parentName });
};
onChangeText={(value) => {updateParentName(value);}} //passed only value not in an object!
Just change the onChangeText prop as such
onChangeText={(value) => {updateParentName({parentName:value});}}

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 .

How to re render sub component on prop change with 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')
);