I've been working with React/React-Native for a while now, but I'm new to Redux and I cannot find the problem. I have a RESTFull API and two main modules: the service model and the price model. Once the admin user adds a new service the user can also associate a price for that service. The problema is that when I add a service (in the NewServiceScreen) my code dispatches an action to change the redux store and therefore update the service list on the NewPriceScreen for the user to associate a price with the service that was just added.
NewPriceScreen.js
function mapStateToProps(state){
return {
newPrice: state.newPriceReducer
}
}
// Exports the connected NewPriceScreen
export default connect(mapStateToProps)(NewPriceScreen);
NewServiceScreen
...
handleSubmit = async() => {
const { descricao } = this.props.newService;
const userToken = await AsyncStorage.getItem('token');
axios.post('/estabelecimento/servicos/',{descricao: descricao}, {
headers: {
"Authorization": `Token ${userToken}`
}
})
.then(res => {
Alert.alert(
'Deu tudo certo :)',
'Dados salvos com sucesso !',
);
console.log(res.data);
this.props.dispatch({type: 'addService', newService: res.data});
})
.catch(error =>{
Alert.alert(
'Ops ! Algo aconteceu :(',
error.message,
);
})
}
...
const mapStateToProps = state => ({
newService: state.newService
});
export default connect(mapStateToProps)(NewServiceScreen);
Reducers.js
const initialState = {
servicos: [],
// Database variables
servico: 0,
duracao: '',
custo: '',
comissao: ''
}
export default function newPriceReducer(state = initialState, action){
// console.log("My state")
// console.log(state);
switch(action.type){
case 'setState': {
return {
...state,
servico: action.descricao,
duracao: action.duracao,
custo: action.custo,
comissao: action.comissao
}
}
case "ADD_SERVICES": {
// console.log("Servicos");
const newState = {
...state,
servicos: action.servicos
}
// console.log(newState);
// console.log(newState === initialState)
return newState;
}
case 'addService': {
// console.log("ADD Service");
servicos = state.servicos;
servicos.push(action.newService);
const newState = {
...state,
servicos: servicos
}
// console.log(newState);
// console.log(newState === initialState);
return newState
}
default:
return state;
}
}
const initialState = {
descricao: ''
}
export default function newServiceReducer(state = initialState, action){
switch(action.type){
case 'setDescricao': {
return {
...state,
descricao: action.descricao
}
}
default:
return state;
}
}
App.js
import { createStore, applyMiddleware, combineReducers } from "redux";
import thunkMiddleware from 'redux-thunk'
import newServiceReducer from '../reducers/NewService';
import newPriceReducer from "../reducers/NewPrice";
import logger from 'redux-logger';
const mainReducer = combineReducers({
newService: newServiceReducer,
newPriceReducer
})
const store = createStore(mainReducer, applyMiddleware(logger));
export default store
import React from 'react';
import { Platform, StatusBar, StyleSheet, View, AsyncStorage, ImageBackground} from 'react-native';
import { AppLoading, Asset, Font, Icon } from 'expo';
import AppNavigator from './navigation/AppNavigator';
// Redux Stuff
import {Provider} from 'react-redux';
import AppStore from './store/App';
export default class App extends React.Component {
state = {
isLoadingComplete: false,
isLoggedIn: false,
};
render() {
if (!this.state.isLoadingComplete && !this.props.skipLoadingScreen) {
return (
<AppLoading
startAsync={this._loadResourcesAsync}
onError={this._handleLoadingError}
onFinish={this._handleFinishLoading}
/>
);
} else {
return (
<Provider store={AppStore}>
<ImageBackground source={require('./assets/images/back.png')} style={{width: '100%', height: '100%'}}>
{Platform.OS === 'ios' && <StatusBar barStyle="default" />}
<AppNavigator />
</ImageBackground>
</Provider>
);
}
}
}
In lines:
servicos = state.servicos;
servicos.push(action.newService);
I was making some sort mutation. I just changed it to:
const newState = {
...state,
servicos: [...state.servicos, action.newService]
}
Related
I have a problem with redux and react-native because I can't connect the state of the "login" view.
I would like to upload the user data and the token in the store.
In "AppState" View, I manage the actions :
import * as type from "../redux/actionTypes";
import {initialState} from "../redux/initialState";
export default function LoginStateReducer(state = initialState, action) {
switch( action.type ) {
case type.SET_LOGIN_STATE:
return {
...state,
user: action.user,
token: action.token,
isLoading: false,
isLoggedIn: true,
};
case type.SET_LOGOUT_STATE:
return {
...state,
user: null,
token: null,
isLoading: false,
isLoggedIn: false,
};
default:
return state;
}
};
In "AppView", I manage the state :
import React, { useState, useEffect, useReducer, useMemo } from 'react';
import { View, ActivityIndicator } from 'react-native';
import AsyncStorage from '#react-native-async-storage/async-storage';
import Login from './login/LoginViewContainer';
import { AuthContext } from './config/Context';
import * as type from '../redux/actionTypes'
import { initialState } from '../redux/initialState';
import LoginStateReducer from './AppState';
import Navigator from './navigation/Navigator';
const App = () => {
const [isLoading, setIsLoading] = useState(true);
const [loginState, dispatch] = useReducer(LoginStateReducer, initialState);
const authContext = useMemo(() => ({
signIn: async(data) => {
setIsLoading(false);
const token = String(data.token);
const user= data.user;
try {
await AsyncStorage .setItem('token', token);
} catch(e) {
console.log(e);
}
dispatch({ type: type.SET_LOGIN_STATE, user: user, token: token });
},
signOut: async() => {
setIsLoading(false);
try {
await AsyncStorage .removeItem('token');
} catch(e) {
console.log(e);
}
dispatch({ type: type.SET_LOGOUT_STATE });
},
}), []);
if( loginState.isLoading ) {
return(
<View style={{flex:1,justifyContent:'center',alignItems:'center'}}>
<ActivityIndicator size="large" />
</View>
);
}
return (
<AuthContext.Provider value={authContext}>
{ loginState.token !== null ? (
<Navigator onNavigationStateChange={() => {}} uriPrefix="/app" />
)
:
<Login />
}
</AuthContext.Provider>
);
}
export default App;
In "AppViewContainer", I connect the reducer with the state :
import { connect } from 'react-redux';
import { compose, lifecycle } from 'recompose';
import { Platform, UIManager } from 'react-native';
import AppView from './AppView';
import LoginStateReducer from './AppState';
export default compose(
connect(
state => ({
user: state.user,
token: state.token,
}),
dispatch => ({
LoginStateReducer: () => dispatch(LoginStateReducer()),
})
),
lifecycle({
componentDidMount() {
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental &&
UIManager.setLayoutAnimationEnabledExperimental(true);
}
this.props.LoginStateReducer();
},
}),
)(AppView);
In the "reducer", I have :
import { combineReducers } from 'redux';
// ## Generator Reducer Imports
import LoginStateReducer from '../modules/AppState';
export default combineReducers({
login: LoginStateReducer,
});
When I call the store once connected, it has not changed and I don't understand why :
"login": {"isLoading": true, "isLoggedIn": false, "token": "", "user": ""}
Can you tell me if i am missing something?
Thank you in advance for your feedback
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
I have Problem in dispatching an action and is suppose to update my state in redux.
This is my Homepage component. I am able to get the current state of the calculator balance.
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { Text, ScrollView, Button } from "react-native";
import * as reducerActions from "../../store/actions/calculator";
function Homepage() {
const balance = useSelector((state) => state.calculator.balance);
const dispatch = useDispatch();
console.log(balance);
return (
<ScrollView>
<Text>Balance:{balance}</Text>
<Button
title="Add to Cart"
onPress={() => {
dispatch(reducerActions.DepositMoney(10));
}}
/>
</ScrollView>
);
}
export default Homepage;
This is my action component: The issue is that it doesn't call my reducer. It logs the value of 10 when I press on the button.
export const DEPOSIT = "DEPOSIT";
export const DepositMoney = (amount) => {
return { type: DEPOSIT, payload: amount };
};
This is my reducer component:
import { DEPOSIT } from "../actions/calculator";
const initialState = {
balance: 0,
};
export default (state = initialState, action) => {
switch (action.Type) {
case DEPOSIT:
console.log("reducer");
console.log(action.payload);
return { balance: state.balance + action.payload };
case "WITHDRAW":
return { balance: state.balance - action.payload };
}
return state;
};
And this is how i set up the redux in my app.js
import React, { useState } from "react";
import { createStore, combineReducers, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import { AppLoading } from "expo";
import * as Font from "expo-font";
import ReduxThunk from "redux-thunk";
import productsReducer from "./store/reducers/products";
import ShopNavigator from "./navigation/ShopNavigator";
import calculatorReducer from "./store/reducers/calculator";
import { composeWithDevTools } from "redux-devtools-extension";
const rootReducer = combineReducers({
products: productsReducer,
calculator: calculatorReducer,
});
//composewithdevtools should be taken out for production
//const store = createStore(rootReducer, composeWithDevTools());
const store = createStore(
rootReducer,
composeWithDevTools(),
applyMiddleware(ReduxThunk)
);
const fetchFonts = () => {
return Font.loadAsync({
"open-sans": require("./assets/fonts/OpenSans-Regular.ttf"),
"open-sans-bold": require("./assets/fonts/OpenSans-Bold.ttf"),
});
};
export default function App() {
const [fontLoaded, setFontLoaded] = useState(false);
if (!fontLoaded) {
return (
<AppLoading
startAsync={fetchFonts}
onFinish={() => {
setFontLoaded(true);
}}
/>
);
}
return (
<Provider store={store}>
<ShopNavigator />
</Provider>
);
}
#Christian is right but one thing is missing.
Must copy state before making change in it.
You can also do it like.
export default (state = initialState, {type,payload}) => {
switch (type) {
case DEPOSIT:
console.log("reducer");
console.log(payload);
return { ...state, balance: state.balance + payload };
case "WITHDRAW":
return {...state, balance: state.balance - payload };
}
return state;
};
It was something silly. The following line was wrong:
switch (action.Type) {
It should be:
switch (action.type) {
I have a problem during the login my redirection is not done, I use a rail API for the back, I can recover the user, but when I click on login the page reloads and does not navigate on the home screen.
In my AuthActions.js after the dispatch I added NavigationActions.navigate ('Home'), this should allow me to go to the home page
AppNavigator.js
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { addNavigationHelpers, StackNavigator, TabNavigator } from 'react-navigation';
import { addListener } from '../../utils/redux';
import LoginScreen from '../login/LoginForm';
import RegisterScreen from '../register/RegisterForm';
import LogoutScreen from '../logout/Logout';
import Home from '../home/HomeList';
import Hair from '../hair/HairList';
export const Tabs = TabNavigator({
screen1: {
screen: Home
}
});
export const AppNavigator = StackNavigator({
Login: { screen: LoginScreen },
Main: { screen: Home },
Hair: { screen: Hair },
},{
initialRouteName : 'Login'
});
class AppWithNavigationState extends React.Component {
static propTypes = {
dispatch: PropTypes.func.isRequired,
nav: PropTypes.object.isRequired,
};
render() {
const { dispatch, nav } = this.props;
return (
<AppNavigator
navigation={addNavigationHelpers({
dispatch,
state: nav,
addListener,
})}
/>
);
}
}
const mapStateToProps = state => ({
nav: state.nav,
});
export default connect(mapStateToProps)(AppWithNavigationState);
AuthReducers.js
import {
EMAIL_CHANGED,
PASSWORD_CHANGED,
LOGIN_USER_SUCCESS,
LOGIN_USER_FAIL,
LOGIN_USER
} from '../actions/types';
const INITIAL_STATE = {
email: '',
password: '',
user: null,
error: '',
loading: false,
};
export default (state = INITIAL_STATE, action) => {
// console.log('action =>', action);
switch (action.type) {
case EMAIL_CHANGED:
return { ...state, email: action.payload };
case PASSWORD_CHANGED:
return { ...state, password: action.payload };
case LOGIN_USER:
return { ...state, loading: true, error: '' };
case LOGIN_USER_SUCCESS:
return { ...state, ...INITIAL_STATE, user: action.payload };
case LOGIN_USER_FAIL:
return { ...state, error: 'Authentication Failed.', password: '', loading: false };
default:
return state;
}
};
// newState === oldState ? nothing : update state, so component too
AuthActions.js
import { NavigationActions } from 'react-navigation';
import Api from '../api';
import User from '../models/User';
import {
EMAIL_CHANGED,
PASSWORD_CHANGED,
LOGIN_USER_SUCCESS,
LOGIN_USER_FAIL,
LOGIN_USER
} from './types';
export const emailChanged = (text) => {
return {
type: EMAIL_CHANGED,
payload: text
};
};
export const passwordChanged = (text) => {
return {
type: PASSWORD_CHANGED,
payload: text
};
};
export const loginUser = ({ email, password }) => {
return (dispatch) => {
dispatch({ type: LOGIN_USER });
Api.userSignUp(email, password)
.then((user) => loginUserSuccess(dispatch, user))
.catch((error) => {
console.log(error);
loginUserFail(dispatch);
});
};
};
const loginUserFail = (dispatch) => {
dispatch({
type: LOGIN_USER_FAIL
});
};
const loginUserSuccess = (dispatch, user) => {
// console.log('access token =>', User.getAccessToken());
// Api.createEmployee(null);
dispatch({
type: LOGIN_USER_SUCCESS,
payload: user,
});
NavigationActions.navigate('home'); //HERE NOT REDIRECT AFTER SUCCESS TO HOME
};
LoginForm.js
import React, { Component } from 'react';
import { Text } from 'react-native';
import { connect } from 'react-redux';
import { emailChanged, passwordChanged, loginUser } from '../../actions';
import { Card, CardSection, Input, Button, Spinner } from '../common';
class LoginForm extends Component {
onEmailChange(text) {
this.props.emailChanged(text);
}
onPasswordChange(text) {
this.props.passwordChanged(text);
}
onButtonPress() {
const { email, password } = this.props;
this.props.loginUser({ email, password });
}
renderButton() {
if (this.props.loading) {
return <Spinner size="large" />;
}
return (
<Button onPress={this.onButtonPress.bind(this)}>
Login
</Button>
);
}
render() {
return (
<Card>
<CardSection>
<Input
label="Email"
placeholder="email#gmail.com"
onChangeText={this.onEmailChange.bind(this)}
value={this.props.email}
/>
</CardSection>
<CardSection>
<Input
secureTextEntry
label="Password"
placeholder="password"
onChangeText={this.onPasswordChange.bind(this)}
value={this.props.password}
/>
</CardSection>
<Text style={styles.errorTextStyle}>
{this.props.error}
</Text>
<CardSection>
{this.renderButton()}
</CardSection>
</Card>
);
}
}
const styles = {
errorTextStyle: {
fontSize: 20,
alignSelf: 'center',
color: 'red'
}
};
const mapStateToProps = ({ auth }) => {
const { email, password, error, loading } = auth;
return { email, password, error, loading };
};
export default connect(mapStateToProps, {
emailChanged, passwordChanged, loginUser
})(LoginForm);
App.js
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import ReduxThunk from 'redux-thunk';
import AppReducer from './reducers';
import AppWithNavigationState from './components/navigator/AppNavigator';
import { middleware } from './utils/redux';
import User from './models/User.js';
import {
NavigationActions,
} from 'react-navigation';
const store = createStore(
AppReducer,
applyMiddleware(ReduxThunk),
);
class App extends Component {
state = { isLoggedIn: null }
componentWillMount() {
// Si le asyncstorage a changé (connexion, etc)
User.getCurrent().then((user) => {
console.log('GetCurrentUser =>', user);
if (user) {
this.setState({isLoggedIn: true});
this.props.navigation.navigate('Main', user);
}
else {
this.setState({isLoggedIn: false});
}
})
}
render() {
return (
<Provider store={store}>
<AppWithNavigationState />
</Provider>
);
}
}
export default App;
I'm blocking for some day on the worries, I still is not finding a solution
use this code NavigationActions.NAVIGATE("home") works for me
You have to use componentWillReceiveProps in your Home like this:
//Home
componentWillReceiveProps(nextProps) {
if(nextProps.user) {
this.props.navigation.navigate('Home');
}
}
LoginForm.js
const mapStateToProps = state => {
return {
error: state.auth.error,
loading: state.auth.loading,
user: state.auth.user,
}
}
And you edit your AuthReducers.js like this:
case LOGIN_USER_SUCCESS:
return { ...state, ...INITIAL_STATE, user: action.user};
It works for me!
i'm developing an android application whereby my screen has to depends on whether the user is logged in or not. The login data is stored inside the AsyncStorage.
By the time when the apps start, it should get the data from AsyncStorage and make it as the default state. How can i achieve that ?
Below is my redux structure
index.android.js
import {
AppRegistry,
} from 'react-native';
import Root from './src/scripts/Root.js';
AppRegistry.registerComponent('reduxReactNavigation', () => Root);
Root.js
import React, { Component } from "react";
import { Text, AsyncStorage } from "react-native";
import { Provider, connect } from "react-redux";
import { addNavigationHelpers } from 'react-navigation';
import getStore from "./store";
import { AppNavigator } from './routers';
const navReducer = (state, action) => {
const newState = AppNavigator.router.getStateForAction(action, state);
return newState || state;
};
const mapStateToProps = (state) => ({
nav: state.nav
});
const user = {};
class App extends Component {
constructor(){
super();
}
render() {
return (
<AppNavigator
navigation={addNavigationHelpers({
dispatch: this.props.dispatch,
state: this.props.nav
})}
/>
);
}
}
const AppWithNavigationState = connect(mapStateToProps)(App);
const store = getStore(navReducer);
export default function Root() {
return (
<Provider store={store}>
<AppWithNavigationState />
</Provider>
);
}
user reducers
import {
REGISTER_USER,
UPDATE_USER,
LOGIN_USER
} from '../../actions/actionTypes';
import { handleActions } from 'redux-actions';
**// here is the default state, i would like to get from asyncstorage**
const defaultState = {
isLoggedIn: false,
user:{},
};
export const user = handleActions({
REGISTER_USER: {
next(state, action){
return { ...state, user: action.payload, isLoggedIn: true }
}
},
LOGIN_USER: {
next(state, action){
return { ...state, user: action.payload, isLoggedIn: true }
}
},
}, defaultState);
You can use the component lifecycle method componentWillMount to fetch the data from AsyncStorage and when the data arrive you can change the state.