Redux - state does not update correctly after typing something in a TextInput component

Viewed 372

I did all the instructions for a redux tutorial correctly. But after running the program, when I try to enter a value in the email or password TextInput, the value is not placed in it and the email input is emptied immediately. I do not know where I did the wrong thing. But I think the action is not working properly. I hope you will guide me.


In Action directory > index.js

import { EMAIL_CHANGED, PASS_CHANGED, USER_LOGIN_ATEMT, USER_LOGIN_SUCCESS, USER_LOGIN_FAILED } from "./types";
export const emailChanged = (text) => {
   return {
      type: EMAIL_CHANGED,
      payload: text
   }
}
export const passChanged = (text) => {
   return {
      type: PASS_CHANGED,
      payload: text
   }
}
export const loginUser = ({ email, pass }) => {
   return (dispatch) => {
      dispatch({ type: USER_LOGIN_ATEMT });
      fetch('', {
         method: 'POST',
         headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
         },
         body: JSON.stringify({
            email: email,
            password: pass
         })
      }).then((response) => response.json()).
         then((responseJson) => {
            if (responseJson === 'Data Matched') {
               loginUserSuccess(dispatch);
            } else {
               loginUserFailed(dispatch);
            }
         }).catch((error) => { alert(error) });
   }
}
const loginUserSuccess = (dispatch) => {
   dispatch({ type: USER_LOGIN_SUCCESS });
}

const loginUserFailed = (dispatch) => {
   dispatch({ type: USER_LOGIN_FAILED });
}

In Action directory > types.js

export const EMAIL_CHANGED = 'EMAIL_CHANGED';
export const PASS_CHANGED = 'PASS_CHANGED';
export const USER_LOGIN_ATEMT = 'USER_LOGIN_ATEMT';
export const USER_LOGIN_SUCCESS = 'USER_LOGIN_SUCCESS';
export const USER_LOGIN_FAILED = 'USER_LOGIN_FAILED';

In components directory > LoginForm.js

import React, { Component } from 'react';
import { View, TextInput, Text, FlatList, ActivityIndicator, TouchableOpacity } from 'react-native';
import { connect } from 'react-redux';
import { emailChanged, passChanged, loginUser } from '../actions/index';

class LoginForm extends Component {
  onEmailChange(text) {
    this.props.emailChanged(text)
  }
  onPassChange(text) {
    this.props.passChanged(text)
  }
  onLoginUser() {
    const { email, pass } = this.props;
    this.loginUser({ email, pass });
  }
  renderButton() {
    if (this.props.loading) {
      return (<ActivityIndicator size="large" color="#0000ff" />);
    } else {
      return (
        <TouchableOpacity onPress={() => this.onLoginUser.bind(this)} >
          <Text>Continue...</Text>
        </TouchableOpacity>
      )
    }
  }
  render() {
    return (
      <View>
        <TextInput
          placeholder="Email"
          onChangeText={() => this.onEmailChange.bind(this)}
          value={this.props.email}
        />
        <TextInput
          placeholder="Pass"
          onChangeText={() => this.onPassChange.bind(this)}
          secureTextEntry={true}
          value={this.props.pass}
        />
        <Text>{this.props.error}</Text>
        {this.renderButton()}
      </View>
    );
  }
}


const mapStateToProps = state => {
  return {
    email: state.auth.email,
    pass: state.auth.pass,
    loading: state.auth.loading,
    error: state.auth.error
  }
}


export default connect(mapStateToProps, { emailChanged, passChanged, loginUser })(LoginForm);

In reducers directory > AuthReducer.js

import { EMAIL_CHANGED, PASS_CHANGED, USER_LOGIN_ATEMT, USER_LOGIN_SUCCESS, USER_LOGIN_FAILED } from '../actions/types';
const initialState = {
   email: '',
   pass: '',
   loading: false,
   error: ''
}
const AuthReducer = (state = initialState, action) => {
   console.log(action);
   switch (action.type) {
      case EMAIL_CHANGED:
         return { ...state, 
            email: state.email.concat({
               key: Math.random(),
               value: action.payload
            })
          }
      case PASS_CHANGED:
         return { ...state, pass: action.payload }
      case USER_LOGIN_ATEMT:
         return { ...state, loading: true }
      case USER_LOGIN_SUCCESS:
         return { ...state, ...initialState }
      case USER_LOGIN_FAILED:
         return { ...state, loading: false, pass: '', error: 'اشتباه وارد شده است' }
      default:
         return state;
   }
}
export default AuthReducer;

In reducers directory > index.js

import { combineReducers } from 'redux';
import AuthReducer from '../reducers/AuthReducer';
const reducers = combineReducers({
  auth: AuthReducer
});
export default reducers;

In App.js

import React, { Component } from 'react';
import { View, TextInput, Text, FlatList, Pressable } from 'react-native';
import { Provider } from 'react-redux';
import { applyMiddleware, createStore } from 'redux';
import reducers from './reducers/index';
import LoginForm from './components/LoginForm';
import ReduxThunk from 'redux-thunk';

export default class App extends Component {
  render() {
    return (
      <Provider store={createStore(reducers, {}, applyMiddleware(ReduxThunk))}>
        <LoginForm />
      </Provider>
    );
  }
}

In index.js

import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';

AppRegistry.registerComponent(appName, () => App);

My package.json

"react": "16.13.1",
"react-native": "0.63.4",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0"
1 Answers

Issue

email is initially an empty string, when you dispatch EMAIL_CHANGED you attempt to concatenate an object with keys key and value to it. Your component is accessing only state.auth.email. I'm absolutely sure there should be an error being thrown somewhere. The onChangeText value from the text input is the entire value, not the delta, so you shouldn't concatenate or append anything, just fully replace the existing email state (You got this part right for the password, actually).

There is also an issue with how you are attaching your handler.

onChangeText={() => this.onEmailChange.bind(this)}

This drops the onChangeText value and passes undefined to your handler.

Solution

Update the reducer case to correctly update the email value.

case EMAIL_CHANGED:
  return {
    ...state, 
    email: action.payload,
  }

Update the handler to consume the changed value.

onChangeText={this.onEmailChange.bind(this)}

Hint: to save the binding of this you can either do that in a constructor or use an arrow function.

onEmailChange = (text) => {
  this.props.emailChanged(text)
}

...

onChangeText={this.onEmailChange}

Second hint: You can directly attach the passed handler to the input and save a function declaration and this binding.

onChangeText={this.props.onEmailChange}

If for some reason you need the random key value (random values are poor keys/ids as they aren't guaranteed to be unique) then update as follows:

Provide valid initial state

const initialState = {
  email: {}, // <-- empty object, provides object to destructure from in UI
  ...
}

Fully replace the email object

case EMAIL_CHANGED:
  return { ...state, 
    email: {
      key: Math.random(),
      value: action.payload
    }
  }

Access the email value correctly in the component

const mapStateToProps = state => {
  return {
    email: state.auth.email.value, // <-- get the email value
    ...
  }
}
Related