It's been 2 days that I'm stuck with an issue implementing redux in my react native application. I checked many topics but I can't fix my problem.
The problem is that mapStateToProps is called only once but it's not called anymore after executing action. My reducer is called but the state is not updating.
I test my work on android with android studio and emulator.
Here is my code :
App.js:
<Provider store={createStore(reducers)}>
<View>
<MainScreen />
</View>
</Provider>
HeaderReducer:
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case SET_HEADER:
return action.payload;
default:
return state;
}
};
reducers/index.js
import { combineReducers } from 'redux';
import HeaderReducer from './HeaderReducer';
export default combineReducers({
headerText: HeaderReducer,
});
actions/index.js
export const SET_HEADER = 'set_header';
export const setHeaderText = (title) => {
return {
type: SET_HEADER,
payload: title,
};
};
MainScreen.js
import * as actions from '../actions';
...
<Text> {this.props.headerText} </Text>
<Button
onPress={() => {
this.props.setHeaderText(`screen: ${SELECTED_PAGE.profile}`);
this.forceUpdate();
}}
>
...
const mapStateToProps = (state) => {
const { headerText } = state;
return { headerText };
};
export default connect(mapStateToProps, actions)(MainScreen);
I know that I don't need to forceUpdate in the onPress callback but it's just to check the state after call the function.
So when the application start the reducers are called and my mapStateToProps is working. But when I click on my button, my reducers are called, the good action is executed in the switch but my mapStateToProps is not called anymore. When I click a second time if I console.log the state in my reducer I can see that the state is not updated but stay the same than the INITIAL_STATE.
I don't mutate the state in my reducers, I connected everything in my MainScreen.js and I wrapped my app in Provider with store.
I follow a tutorial on Udemy to implement reduc in my app and even I check the code of the tuto on github, I can't find out where the issue is from (https://github.com/StephenGrider/ReactNativeReduxCasts/tree/master/tech_stack)
I think I miss something really obvious but I can't find out what's wrong since 2 days :/
If someone can help, I would appreciate :)
Thanks.
EDIT :
I didn't figure out the problem, so I just restart from scratch, I did exactly the same (THE SAAME!!) and it's working now... Thanks for your help :)
The problem is in your HeaderReducer, reducer is expecting to return a state object, but you're returning a string instead.
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case SET_HEADER:
return action.payload;
default:
return state;
}
During the case of SET_HEADER, what you really want is to overwrite a field instead of overwrite the whole state object, so change to below instead
const INITIAL_STATE = { customizeText: '' };
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case SET_HEADER:
return { ...state, customizeText: action.payload }; // <<=== HERE
default:
return state;
}
}
Then in your MainScreen.js, you have to update your mapStateToProps function
<Text> {this.props.someText} </Text>
...
const mapStateToProps = ({ headerText }) => {
return { someText: headerText.customizeText };
};
...
I've updated the field name to avoid confusion
First make an actual action as this.props.setHeaderText() and pass title if you wish to and make sure your INITIAL_STATE is an object or [].
const mapStateToProps = (state) => {
return{
headerText: state.headerText
}
};
export default connect(mapStateToProps, actions)(MainScreen);
Related
I am new to react native and redux.
In this file friends.js, I have already made it so that the app adds a friend when someone taps the "Add Friend" button. I am now also trying to make a form that adds a new name to a list of names. Here is the form:
import React from 'react';
import { StyleSheet, Text, View, Button, TextInput, KeyboardAvoidingView } from 'react-native';
import { connect } from 'react-redux';
import {bindActionCreators} from 'redux';
import {addFriend} from './FriendActions';
import {addFriendFromForm} from './FriendActions';
class Friends extends React.Component {
constructor(){
super();
this.formFieldValue = "";
}
render() {
return (
<KeyboardAvoidingView behavior="padding" enabled>
<Text>Add friends here!</Text>
{this.props.friends.possible.map((friend,index) => (
<Button
title={`Add ${friend}`}
key={friend}
onPress = { () =>
this.props.addFriend(index)
}
/ >
)
)}
<Button
title="Back to Home"
onPress={() => this.props.navigation.navigate('Home')}
/>
<TextInput
style={{height: 40, width: 200}}
placeholder="Type in a friend's name"
onChangeText={(text) => {
this.formFieldValue = text;
console.log(this.formFieldValue);
}
}
/>
<Button
title="Add Friend From Form"
onPress={() => this.props.addFriendFromForm(this.formFieldValue)}
/ >
</KeyboardAvoidingView>
);
}
}
const mapStateToProps = (state) => {
const friends = state;
console.log("friends", JSON.stringify(friends));
return friends
};
const mapDispatchToProps = dispatch => (
bindActionCreators({
addFriend,
addFriendFromForm
}, dispatch)
);
export default connect(mapStateToProps, mapDispatchToProps)(Friends);
Here is the action that the form triggers:
export const addFriend = friendIndex => ({
type: 'ADD_FRIEND',
payload: friendIndex
})
export const addFriendFromForm = friendNameFromForm => ({
type: 'ADD_FRIEND',
payload: friendNameFromForm
})
And here is the reducer that calls the actions . (WHERE I THINK THE PROBLEM IS):
import {combineReducers} from 'redux';
const INITIAL_STATE = {
current: [],
possible: [
'Allie',
'Gator',
'Lizzie',
'Reptar'
]
};
const friendReducer = (state = INITIAL_STATE, action) => {
switch(action.type){
case 'ADD_FRIEND':
const {
current,
possible
} = state;
const addedFriend = possible.splice(action.payload, 1);
current.push(addedFriend);
const newState = {current, possible};
return newState;
case 'ADD_FRIEND_FROM_FORM':
const {
currents,
possibles
} = state;
currents.push(action.payload);
const newState2 = {current: currents, possible: possibles};
return newState2;
default:
return state;
}
};
export default combineReducers({
friends: friendReducer
});
As I mentioned, pressing the button to automatically add a name from the initial state works. However, when adding a name through the form, it seems that the form will just add allie to possible friends instead of the name typed into the form because the console.log in friends.js shows this: friends {"friends":{"current":[["Allie"]],"possible":["Gator","Lizzie","Reptar"]}}.
How do I get the form to work?
I used this.formFieldValue in friends.js as it seems like overkill to change the state whenever the form field text changes when we really only need it when the submit button is pressed. Is this the proper way to go about sending the form data to the action?
While it may seem liike I'm asking 2 questions, I'm pretty sure it's one answer that will tie them both togethor. Thanks!
The issue might be that you're directly mutating the state with push, try using array spread instead:
const friendReducer = (state = INITIAL_STATE, action) => {
switch(action.type){
case 'ADD_FRIEND':
const {
current,
possible
} = state;
const addedFriend = possible.splice(action.payload, 1);
current.push(addedFriend);
const newState = {current, possible};
return newState;
case 'ADD_FRIEND_FROM_FORM':
return {...state, current: [...state.current, action.payload]}
default:
return state;
}
};
Also it's a good idea to be consistent with the property names, it seems you're using current and currents interchangeably.
I am developing an application with react-native and redux. I want to save my favorite movies in the store. Thus I set up my actions, reducers, store, Provider, connect...
When I dispatch an action, the reducer is called and the state changes (I check with (nextState === state) however mapStateToProps is never called.
For information, the dispatch action and mapStateToProps come from the same component. This component is in a StackNavigator. Even though I only use the component, the problem remains the same.
I guessed that I was changing the state, but it is not the case I think. I have been stuck with this problem for a while, I checked all existing problems, none of the solutions solve mine.
The dispatcher:
_toggleFavorite() {
const action = { type: "TOGGLE_FAVORITE", value: this.state.film }
this.props.dispatch(action)
}
The reducer:
const initialState = { favoritesFilm: [] }
function toggleFavorite(state = initialState, action) {
let nextState
switch (action.type) {
case 'TOGGLE_FAVORITE':
const favoriteFilmIndex = state.favoritesFilm.findIndex(item => item.id === action.value.id)
if (favoriteFilmIndex !== -1) {
nextState = {
...state,
favoritesFilm: state.favoritesFilm.filter( (item, index) => index !== favoriteFilmIndex)
}
}
else {
nextState = {
...state,
favoritesFilm: [...state.favoritesFilm, action.value]
}
}
console.log('state: ', state)
console.log('nextState: ', nextState)
console.log(nextState === state)
return nextState
default:
return state
}
}
export default toggleFavorite
The connection to the store:
const mapStateToProps = (state) => {
console.log('changed')
return {
favoritesFilm: state.favoritesFilm
}
}
export default connect(mapStateToProps)(FilmDetail)
The navigator:
const SearchStackNavigator = createStackNavigator({
SearchBar: {
screen: SearchBar,
navigationOptions: {
title: 'Rechercher'
}
},
FilmDetail: {
screen: FilmDetail
}
})
export default createAppContainer(SearchStackNavigator)
App.js :
import Navigation from './src/Navigation/Navigation';
import store from './src/Store/configureStore'
import { Provider } from 'react-redux'
export default class App extends Component<Props> {
render() {
return (
<Provider store={store}>
<Navigation/>
</Provider>
);
}
}
The log from reducers works. Here an example:
state: – {favoritesFilm: []}
nextstate: – {favoritesFilm: Array}
false
The log ('changed') never appears nor componentDidUpdate(). In addition, nothing is re-rendered.
Thank you in advance.
As I understand that you didn't see console.log('changed') in mapStateToProps after dispatch action.
Actually mapStateToProps just run once, after dispatch change in to reducer it will send new props to component so you can track by hook
static getDerivedStateFromProps(props, state)
getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates. It should return an object to update the state, or null to update nothing.
example:
static getDerivedStateFromProps(props, state) {
console.log('change', props);
}
For tracking Redux changes when develop I recommend use Redux devtool
https://github.com/zalmoxisus/redux-devtools-extension
This tool will save many time for you.
Solution found....... After days of looking around .....
Problem of versions:
Have react-native 0.57.4 and had react-redux 6.0.1
I changed react-redux to 5.1.0.
Below are the relevant files.
In the reducer, when it runs...
return {
loggedIn: action.loggedIn
};
I was expecting it to replace the state with that information.
When I run this code in LoginForm I get the old state output.
this.props.onLogin();
console.log(this.props.loggedIn);
I'm hoping I'm overlooking something simple here. Everything else seem to work the way I was expecting it to. I can change the state directly in the
switch using...
state.loggedIn = action.loggedIn;
And it works as expected. Can anyone shed some light on what I am doing wrong?
Action
import { LOGGED_IN } from './actionTypes';
export const loggedIn = () => {
return {
type: LOGGED_IN,
loggedIn: true,
};
};
Reducer
import {
LOGGED_IN
} from "../actions/actionTypes";
const initialState = {
loggedIn: false,
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case LOGGED_IN:
return {
loggedIn: action.loggedIn
};
default:
return state;
}
};
export default reducer;
import React, { Component } from 'react';
import { Text } from 'react-native';
import { Button, Card, CardSection, Input, Spinner } from './common';
import { Actions } from 'react-native-router-flux';
import firebase from '../Fire';
import { connect } from "react-redux";
import {
loggedIn
} from "../store/actions";
LoginForm
class LoginForm extends Component {
onButtonPress() {
this.onLoginSuccess();
}
onLoginSuccess() {
this.props.onLogin();
console.log(this.props.loggedIn);
Actions.main({});
}
renderButton() {
return (
<Button onPress={this.onButtonPress.bind(this)}>
Log in
</Button>
);
}
render() {
return (
<Card>
<CardSection>
<Input
placeholder="user#gmail.com"
label="Email"
</CardSection>
<CardSection>
<Input
secureTextEntry
placeholder="password"
label="Password"
/>
</CardSection>
<CardSection>
{this.renderButton()}
</CardSection>
</Card>
);
}
}
const styles = {
errorTextStyle: {
fontSize: 20,
alignSelf: 'center',
color: 'red'
}
};
const mapStateToProps = state => {
return {
loggedIn: state.loggedIn
};
};
const mapDispatchToProps = dispatch => {
return {
onLogin: () => dispatch(loggedIn()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm);
configureStore
import { createStore, combineReducers } from 'redux';
import prolinkReducer from './reducers/prolink';
const rootReducer = combineReducers({
loggedIn: prolinkReducer
});
const configureStore = () => {
return createStore(rootReducer);
};
export default configureStore;
The reason that you are getting the previous value is that you are console logging the previous value.
When the onLoginSuccess is called the current value for this.props.loggedIn will be passed to the console.log. I imagine if you set a long enough timeout on it then it would show that it is being updated, but that is not exactly the best way to check.
componentDidUpdate
If you want to check that your redux state is updating you should check what is happening in the componentDidUpdate https://reactjs.org/docs/react-component.html#componentdidupdate
As you subscribe to loggedIn in your mapStateToProps in your LoginForm.js, that means your component will receive the new value for loggedIn once it is updated.
In your LoginForm.js add the following:
componentDidUpdate(prevProps, prevState) {
console.warn('previous', prevProps.loggedIn, 'current', this.props.loggedIn)
}
This will allow you to see the values for loggedIn as it changes.
react-native-debugger
You could use react-native-debugger which includes redux inspection tools. https://github.com/jhen0409/react-native-debugger. This allows you to see your redux store in real-time, meaning you can easily track the changes without having to resort to checking in the componentDidUpdate. However, at this time there is currently an issue with react-native-debugger that means it is not working with react-native 0.58.+, though there is an open pull request that fixes the issue.
middleware
Alternatively you could add a middleware to your redux setup that logs each event to your console. https://redux.js.org/advanced/middleware, I have previously used redux-logger it is quite customisable, and depending on your use cases you may find it suits your needs.
I am pretty new to redux and am having trouble parsing JSON data, when I mapStateToProps inside my react component. For instance, if I console.log(this.props.chartData[0]) in my react component, the console will display the array I am trying to access, however, when I try to access a specific element in the array by console logging (this.props.ChartData[0].title), I get an error:
[enter image description here][1]
class ChartContainer extends Component {
componentWillMount(){
this.props.chartChanged();
}
render(){
console.log(this.props.chartData[0]);
return(
<Text style={styles.textStyle}>
test
</Text>
);
}
}
const mapStateToProps = state => {
return {
chartData: state.chart
}
};
export default connect (mapStateToProps, {chartChanged}) (ChartContainer);
Interestingly, I have no problem accessing(this.props.ChartData[0].title) inside my reducer.
import {CHART_CHANGED} from '../actions/types';
const INITIAL_STATE = { chartData: [] };
export default (state = INITIAL_STATE, action) => {
console.log(action);
switch (action.type) {
case CHART_CHANGED:
console.log("action");
console.log(action.payload[0].title);
return{...state, chartData: action.payload};
default:
return state;
}
};
Here is the api call in my action file:
export const chartChanged = (chartData) => {
return (dispatch) => {
axios.get('https://rallycoding.herokuapp.com/api/music_albums')
.then((chartData) =>{
dispatch({type: CHART_CHANGED, payload: chartData.data});
});
};
};
If someone can explain why this is happening, I would be super grateful.
So the problem is that you shouldn't assign any value during fetching, what you need to do is use lodash and try doing something like this
import _ from 'lodash'
const title = _.get(this.props.ChartData, 'chartData', [])
if(!isFetching){
//do something
}
I need some help with my app and Redux! (Currently, i hate it aha)
So, i have a notification page component which fetch some datas and i need to put the data length into my redux store to put badge on my icon in my tabbar!
My Main Reducer :
import { combineReducers } from "redux";
import NotificationReducer from "./NotificationReducer";
export default function getRootReducer(navReducer) {
return combineReducers({
nav: navReducer,
notificationReducer: NotificationReducer
});
}
My Notification reducer
const initialState = {
NotificationCount: 0
};
export default function notifications(state = initialState, action = {}) {
switch (action.type) {
case 'SET_COUNT' :
console.log('REDUCER NOTIFICATION SET_COUNT',state)
return {
...state,
NotificationCount: action.payload
};
default:
return state;
}
};
My Action :
export function setNotificationCount(count) {
return function (dispatch, getState) {
console.log('Action - setNotificationCount: '+count)
dispatch( {
type: 'SET_COUNT',
payload: count,
});
};
};
My Component :
import React, { Component } from 'react';
import { View, Text, StyleSheet, ScrollView, Dimensions, TouchableOpacity, SectionList, Alert } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import { Notification } from '#Components';
import { ORANGE } from '#Theme/colors';
import { NotificationService } from '#Services';
import Style from './style';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as Actions from '#Redux/Actions';
const width = Dimensions.get('window').width
const height = Dimensions.get('window').height
export class NotificationsClass extends Component {
constructor(props) {
super(props);
this.state = {
dataSource: [],
NotificationCount: undefined
};
}
async componentWillMount() {
this.updateNotifications();
}
componentWillReceiveProps(nextProps){
console.log('receive new props',nextProps);
}
async updateNotifications() {
this.props.setNotificationCount(10); <---
let data = await NotificationService.get();
if (data && data.data.length > 0) {
this.setState({ dataSource: data });
console.log(this.props) <-- NotificationCount is undefined
}
}
render() {
if (this.state.dataSource.length > 0) {
return (
<SectionList
stickySectionHeadersEnabled
refreshing
keyExtractor={(item, index) => item.notificationId}
style={Style.container}
sections={this.state.dataSource}
renderItem={({ item }) => this.renderRow(item)}
renderSectionHeader={({ section }) => this.renderSection(section)}
/>
);
} else {
return this.renderEmpty();
}
}
renderRow(data) {
return (
<TouchableOpacity activeOpacity={0.8} key={data.notificationId}>
<Notification data={data} />
</TouchableOpacity>
);
}
}
const Notifications = connect(
state => ({
NotificationCount: state.NotificationCount
}),
dispatch => bindActionCreators(Actions, dispatch)
)(NotificationsClass);
export { Notifications };
(I've removed some useless code)
Top Level :
const navReducer = (state, action) => {
const newState = AppNavigator.router.getStateForAction(action, state);
return newState || state;
};
#connect(state => ({
nav: state.nav
}))
class AppWithNavigationState extends Component {
render() {
return (
<AppNavigator
navigation={addNavigationHelpers({
dispatch: this.props.dispatch,
state: this.props.nav,
})}
/>
);
}
}
const store = getStore(navReducer);
export default function NCAP() {
return (
<Provider store={store}>
<AppWithNavigationState />
</Provider>
);
}
React : 15.6.1
React-Native : 0.46.4
Redux : 3.7.2
React-Redux : 5.0.5
React-Navigation : 1.0.0-beta.11
Node : 6.9.1
So if you've an idea! It will be great :D !
Thanks !
There's three issues.
First, React's re-rendering is almost always asynchronous. In updateNotifications(), you are calling this.props.setNotificationCount(10), but attempting to view/use the props later in that function. Even with the await in there, there's no guarantee that this.props.NotificationCount will have been updated yet.
Second, based on your reducer structure and mapState function, props.NotificationCount will actually never exist. In your getRootReducer() function, you have:
return combineReducers({
nav: navReducer,
notificationReducer: NotificationReducer
});
That means your root state will be state.nav and state.notificationReducer. But, in your mapState function, you have:
state => ({
NotificationCount: state.NotificationCount
}),
state.NotificationCount will never exist, because you didn't use that key name when you called combineReducers.
Third, your notificationReducer actually has a nested value. It's returning {NotificationCount : 0}.
So, the value you actually want is really at state.notificationReducer.NotificationCount. That means your mapState function should actually be:
state => ({
NotificationCount: state.notificationReducer.NotificationCount
}),
If your notificationReducer isn't actually going to store any other values, I'd suggest simplifying it so that it's just storing the number, not the number inside of an object. I'd also suggest removing the word Reducer from your state slice name. That way, you could reference state.notification instead.
For more info, see the Structuring Reducers - Using combineReducers section of the Redux docs, which goes into more detail on how using combineReducers defines your state shape.