I am learning react native and i am building an application. For some concepts, I am not able to understand where the magic happens. I am using redux store for the managing the data.
I have a stateless login component.
export class Login extends Component {
onChangeText = (key, value) => {
this.props.user[key] = value
}
render() {
const { user, fetchUserDetails } = this.props
return (
<View style={styles.container}>
<Text style={styles.heading}>Login</Text>
<TextInput
placeholder='Email'
onChangeText={val => this.onChangeText('email', val)}
value={user.email}
/>
<TextInput
placeholder='Password'
onChangeText={val => this.onChangeText('password', val)}
value={user.password}
/>
<TouchableOpacity onPress={this.fetchUserDetails(user)}>
<View style={styles.button}>
<Text style={styles.buttonText}>Login</Text>
</View>
</TouchableOpacity>
</View>
)
}
}
This is my Login Container
class LoginContainer extends Component {
render () {
return (
<Login/>
)
}
}
const mapStateToProps = (state) => ({
user: state.loginReducer.user,
})
const mapDispatchToProps = {
...fetchUserDetails,
}
export default connect(mapStateToProps, mapDispatchToProps)(Login)
my reducer looks like this:
const initialState = {
user: {
email: '',
password: '',
}
}
const loginReducer = (state = initialState, action) => {
switch(action.type) {
case GET_USER:
return Object.assign({}, state, {
user: action.user
})
default:
return state
}
return state
}
export default loginReducer
My actions look something like this:
export const GET_USER = 'GET_USER'
export function fetchUserDetails (user) {
console.log("executing fetch user action")
if (user === '')
{
alert('please complete form')
}
return {
type: GET_USER,
user
}
}
My root reducer:
import { combineReducers } from 'redux';
import loginReducer from './loginReducer'
const rootReducer = combineReducers({
loginReducer
})
export default rootReducer
My configure Store:
import { createStore } from 'redux'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import rootReducer from './reducers'
const persistConfig = {
key: 'mykey',
storage,
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
const store = createStore(persistedReducer)
const persistedStore = persistStore(store)
export default store
I need to have a stateless component which updates directly the state of the user attributes in the redux store. I am not able to follow how the state or actions will be passed to the Login component. Any Explanation will be appreciated.
Managing redux store in react-native is basically same as you do in react.
From what I understand you are trying to store user details in redux store on every onChangeText event and reflect the updated state in Login component.
Firstly you should use a separate action reducer pair for setting user details in redux. Also You most probably want to call some API on form submission and store the response in redux, For that you might need another pair of action and reducer. I'll leave that to you
Here's how u can manage user-details in redux...
Your stateless login component.
export class Login extends Component {
onChangeText = (value, key) => {
this.props.setUserDetails({
...this.props.user,
[key]: value
})
}
render() {
const { user, onSubmitForm } = this.props
console.log('user===>', this.props);
return (
<View style={styles.container}>
<Text style={styles.heading}>Login</Text>
<TextInput
placeholder='Email'
onChangeText={val => this.onChangeText(val, 'email')}
placeholderTextColor={'rgba(0,40,70,0.5)'}
value={user.email}
/>
<TextInput
placeholder='Password'
onChangeText={val => this.onChangeText(val, 'password')}
placeholderTextColor={'rgba(0,40,70,0.5)'}
value={user.password}
/>
<TouchableOpacity onPress={() => onSubmitForm(user)}>
<View style={styles.button}>
<Text style={styles.buttonText}>Login</Text>
</View>
</TouchableOpacity>
</View>
)
}
}
...
Your Login Container.
class LoginContainer extends Component {
onSubmitForm = () => {
// Checking Validations
const { name, email } = this.props;
if (!name || !email) {
alert('Please fill the form')
return;
}
// call some API for verification and handle the response here
}
render () {
return (
<Login
user={this.props.user}
setUserDetails={this.props.setUserDetails}
onSubmitForm={this.onSubmitForm}
/>
)
}
}
const mapStateToProps = (state) => ({
user: state.userReducer.user,
})
const mapDispatchToProps = dispatch => ({
setUserDetails: payload => dispatch(setUserDetails(payload)),
})
export default connect(mapStateToProps, mapDispatchToProps)(LoginContainer)
...
Your Reducer for setting user details
const initialState = {
user: {
email: '',
password: '',
}
}
const userReducer = (state = initialState, action) => {
switch(action.type) {
case 'SET_USER_DETAILS':
return Object.assign({}, state, {
user: action.user
})
default:
return state
}
return state
}
export default userReducer
...
Your store will remain same and rootReducer should be
import { combineReducers } from 'redux';
import userReducer from './reducer'
const rootReducer = combineReducers({
userReducer
})
export default rootReducer
...
Finally your Action
export const SET_USER_DETAILS = 'SET_USER_DETAILS'
export function setUserDetails (user) {
return {
type: 'SET_USER_DETAILS',
user
}
}
...
Hope it helps.
Hope that helps:
Login:
You must NEVER update a component props inside the said component.
From the React documentation:
Props are Read-Only
If you want your state (the truth) to be stored in the login component, then store it as a proper state and send this local state on submit:
onChangeText = (key, value) => {
this.setState((state) => ({ ...state, [key] => value}))
}
However, if you want to store your state in redux, you will need to create an action that can be triggered to update the redux state. This action needs to be passed to your component props and called like this onChangeText={val => this.props.onChangeText('email', val)}
Also, your calling the fetchUserDetails function on render, where you should be passing a callback. this.fetchUserDetails does not exists, this.props.fetchUserDetails does. The login code becomes
<TouchableOpacity onPress={() => fetchUserDetails(user)}>
Login Container:
mapDispatchToProps must be a function that takes dispatch as first parameter OR an object where each function is an action creator. From the Redux documentation:
If an object is passed, each function inside it is assumed to be a Redux action creator. An object with the same function names, but with every action creator wrapped into a dispatch call so they may be invoked directly, will be merged into the component’s props.
So the code you wrote:
const mapDispatchToProps = {
...fetchUserDetails,
}
Is equivalent to this code
function mapDispatchToProps(dispatch) {
return {
fetchUserDetails: (user) => dispatch(fetchUserDetails(user))
},
}
The dispatch function is where the magic happens, every action that is dispatched is passed down to your reducers where you can create a new state based on the action.
Related
I have just for several reasons migrated from the old style of redux (pre 2019) which used case, switch etc. The redux store gets updated as it should which i can see in the TextInput for example but when trying to use the selected value from redux store elsewhere inside the screen function it seems to initial state and not the updated one. Shortened code below and any help would be greatly appriciated.
redux/slices/auth.js
import { createSlice } from "#reduxjs/toolkit"
const initialState = {
email: "",
}
const authSlice = createSlice({
name: 'userAuth',
initialState,
reducers: {
setEmail: (state, action) => {
state.email = action.payload.email;
}
}
});
export const { setEmail } = authSlice.actions;
export const selectEmail = (state) => state.userAuth.email;
export default authSlice.reducer;
redux/store.js
import { configureStore } from '#reduxjs/toolkit'
import authSlice from './slices/auth'
export const store = configureStore({
reducer: {
userAuth: authSlice,
},
})
screens/LoginScreen.js
import {useSelector, useDispatch} from 'react-redux';
import { selectEmail, setEmail } from '../../redux/slices/auth';
function LoginScreen({navigation}) {
const _email = useSelector(selectEmail);
const onEmailButtonPress = async () => {
console.log("Begin sign in: Email");
// GETS INITIAL STATE AND NOT UPDATED ONE
if (_email == null || _email == 0 || _email == ""){
console.log(_email);
return;
}
}
return (
<View>
<Text>Login with e-mail</Text>
<TextInput
placeholder="Enter e-mail address"
placeholderTextColor={'#000'}
keyboardType="email-address"
onChangeText={(value) => dispatch(setEmail(value))}
maxLength={128}
value={_email} // SHOWS UPDATED STATE
/>
<TouchableOpacity
onPress={() => onEmailButtonPress()}
>
<Text>Continue</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
});
export default LoginScreen;
App.js
class App extends React.Component {
render() {
LogBox.ignoreAllLogs(true);
return (
<Provider store={store}>
{/*<PersistGate persistor={persistor}>*/}
<LoadingScreen />
{/*</PersistGate>*/}
</Provider>
);
}
}
In the reducer you are reading action.payload.email but in the component you are dispatching setEmail(value).
That's not consistent, or you read in the reducer action.payload or you dispatch in the component setEmail({email: value})
moving from React Context to Redux probably I missed something
I just want to update an array with the ADD_FAV type action
databaseReducer.js
import FOOD from "../../data/db/food";
import { ADD_FAV, FETCH_FOOD, SET_LOADING } from "../actions/DatabaseActions";
const initialState = {
food: FOOD,
favorites: [],
loading: false,
};
export default (state = initialState, action) => {
switch (action.type) {
case ADD_FAV:
return {
...state,
favorites: [...state.favorites, action.payload],
loading: false,
};
databaseActions.js
export const ADD_FAV = "ADD_FAV";
export const FETCH_FOOD = "FETCH_FOOD";
export const SET_LOADING = "SET_LOADING";
export const addFav = (id) => {
console.log("fav", id); /// shows me that works
return { type: ADD_FAV, payload: id };
};
then I call the action like this:
<TouchableOpacity onPress={() => addFavBtn(id)}>
<MaterialIcons name="favorite-border" size={25} />
</TouchableOpacity>
and I see the call in the console
FavoriteScreen.js
const FavoritesScreen = () => {
// const { state, fecthFavorites, putInArray } = useContext(DatabaseContext);
const favorites = useSelector((state) => state.database.favorites);
console.log(favorites)
but I always get an empty array, so means that it doesn't update it
With REACT UseContext it worked.
Any idea?
thanks!
The issue is with the way to dispatch your action (As mentioned in the comments). First of all, you need to make sure that the component which dispatches the action has been bound to the redux store with the connect method from react-redux like so:
import React from 'react';
import { connect } from 'react-redux';
const ClickableComponent = ({ dispatch }) => {
// The code that defines the id variable...
return (
<TouchableOpacity onPress={() => dispatch(addFavBtn(id))}>
<MaterialIcons name="favorite-border" size={25} />
</TouchableOpacity>
);
};
export const connect()(ClickableComponent);
This way, the action gets dispatched to the store and the favorites list should update as expected. To verify this, you can use the browser debugger or use a console.log inside the reducer, to make sure you get the correct id with the action.
Another approach to this would be to call dispatch from within the action creator and return that Promise.
databaseActions.js
export const ADD_FAV = "ADD_FAV";
export const FETCH_FOOD = "FETCH_FOOD";
export const SET_LOADING = "SET_LOADING";
export const addFav = (id) => {
console.log("fav", id); /// shows me that works
return dispatch({ type: ADD_FAV, payload: id });
};
Example Component
import React from 'react';
import { connect } from 'react-redux';
import { addFav } from './databaseActions.js';
const ExampleComponent = ({ id, addFav }) => (
<button onPress={() => addFav(id)}>
Add Favorite
</button>
);
export default connect({ /*mapStateToProps*/ }, { addFav })(ExampleComponent);
Personally, I like this approach as it doesn't require you to include the dispatch everywhere that you are calling your action. And allows you to make calls post-state update using a .then() if needed.
I am trying to create a sample app using React Native & Redux. What I am not able to understand is that why my state object is getting wrapped into another object.
I have initial state as {email: 'test'}. I have to access email as this.props.email.email
Why do I have to do this.props.email.email instead of this.props.email
Any help will be appreciated.
Welcome.js
class Welcome extends Component {
render() {
return (
<View style={ styles.container }>
<View style = { styles.inputContainer }>
<Text>{JSON.stringify(this.props.email)}</Text>
<Button title = 'Update Email'
style = { styles.placeButton }
onPress={() => this.props.onChangeEmail('hello')}
/>
</View>
</View>
);
}
}
const mapStateToProps = state => {
return {
email: state.email
}
}
const mapDispatchToProps = dispatch => {
return {
onChangeEmail: (email) => { dispatch({type: 'CHANGE_EMAIL_INPUT', email: email}) }
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Welcome)
EmailReducer.js
const initialState = {
email: 'test',
};
const emailReducer = (state = initialState, action) => {
switch(action.type) {
case 'CHANGE_EMAIL_INPUT':
return Object.assign({}, state,
{ email: action.email }
);
default:
return state;
}
}
export default emailReducer;
Store.js
import { createStore, combineReducers } from 'redux';
import emailReducer from '../reducers/EmailReducer';
const rootReducer = combineReducers({
email: emailReducer
});
const configureStore = () => {
return createStore(rootReducer);
}
export default configureStore;
When you call combineReducers you are creating a store with the following shape
{
email: {
email: 'test'
}
}
That is, the keys in the object passed to combineReducers are the root keys of the state object. The initial state for the email reducer is inserted in the key "email" of the state object.
This is the reason why you need to write this.props.email.email: the former is the key in the root state object (that deduced from combineReducers), the latter is the prop of the state part managed by emailReducer
My Login Component:
import React, { Component } from 'react'
import {
View,
Text,
StyleSheet,
TextInput,
TouchableOpacity
} from 'react-native'
export class Login extends Component {
onChangeText = (key, value) => {
this.props.setUserDetails({
...this.props.user,
[key]: value
})
}
render() {
const { user, onSubmitForm } = this.props
console.log(this.props.user) // user undefined here
return (
<View style={styles.container}>
<Text style={styles.heading}>Login</Text>
<TextInput
placeholder='Email'
onChangeText={val => this.onChangeText('email', val)}
style={styles.input}
value={user.email}
/>
<TextInput
placeholder='Password'
onChangeText={val => this.onChangeText('password', val)}
style={styles.input}
value={user.password}
/>
<TouchableOpacity onPress={() => onSubmitForm(user)}>
<View style={styles.button}>
<Text style={styles.buttonText}>Submit</Text>
</View>
</TouchableOpacity>
</View>
)
}
}
My Login container:
import React, { Component } from 'react'
import { setUserDetails } from '../actions/loginActions'
import { connect } from 'react-redux'
import loginReducer from '../reducers/loginReducer'
import { Login } from '../components/login'
export class LoginContainer extends Component {
onSubmitForm = () => {
// Checking Validations
const { name, email } = this.props;
if (!name || !email) {
alert('Please fill the form')
console.log(this.props.user) // It says undefined
return;
}
}
render () {
return (
<Login
user={this.props.user}
setUserDetails={this.props.setUserDetails}
onSubmitForm={this.onSubmitForm}
/>
)
}
}
const mapStateToProps = (state) => ({
user: state.loginReducer.user,
})
const mapDispatchToProps = dispatch => ({
setUserDetails: payload => dispatch(setUserDetails(payload)),
})
export default connect(mapStateToProps, mapDispatchToProps)(Login)
My login Reducer:
const initialState = {
user: {
email: '',
password: '',
}
}
const loginReducer = (state = initialState, action) => {
switch(action.type) {
case SET_USER_DETAILS:
return Object.assign({}, state, {
user: action.user
})
default:
return state
}
return state
}
export default loginReducer
My root Reducer:
import { combineReducers } from 'redux';
import loginReducer from './loginReducer'
const rootReducer = combineReducers({
loginReducer
})
export default rootReducer
MY store configuration:
import { createStore } from 'redux'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import rootReducer from './reducers'
const persistConfig = {
key: 'mykey',
storage,
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
const store = createStore(persistedReducer)
const persistedStore = persistStore(store)
export default store
I am learning React native and trying to implement some features.
The problem is that I just can't access my this.props.user in the Login container when the submit is called. What am I missing in this scenario?
Any help is appreciated.
I've noticed some weird thing. Your default export of LoginContainer.js is connected Login component. I guess what you really meant is instead of this:
// ...imports
export class LoginContainer extends Component {
// ...
}
//...
export default connect(mapStateToProps, mapDispatchToProps)(Login)
to use this:
// ...imports
// no need to 'export class ...' here.
class LoginContainer extends Component {
// ...
}
// ...
export default connect(mapStateToProps, mapDispatchToProps)(LoginContainer)
In the container you used:
const mapStateToProps = (state) => ({
user: state.loginReducer.user,
})
But in your initial state there is not loginReducer. Maibe it works:
const mapStateToProps = (state) => ({
user: state.user
})
I'm working on setting up a user login screen in React Native using Recompose, with separate actions and reducer files, but my reducer is never being called. Currently, there is just a login button that triggers a doUserLogin() recompose handler:
loginScreen.js
import React from 'react';
import { Button, Text, View } from 'react-native';
import { connect } from 'react-redux';
import { withHandlers, compose } from 'recompose';
import { loginUser } from './user/userActions';
const LoginScreen = ({ user, doUserLogin }) => {
return (
<View style={styles.loginContainer}>
{user ? <Text>Hi, {user.name}!</Text> : <Text>NOT Logged in!</Text>}
<Button title="Log In" onPress={doUserLogin} />
</View>
);
};
export default compose(
connect((state, props) => ({
...state.user,
})),
withHandlers({
doUserLogin: props =>
(event) => {
console.log('LOGIN USER IN HANDLER'); // <-- THIS IS WORKING
loginUser();
},
}),
)(LoginScreen);
The doUserLogin() handler in turn calls loginUser() in my actions file:
userActions.js:
import { LOGIN_REQUEST } from './actionTypes';
export const loginUser = () => {
return (dispatch) => {
console.log('In action'); // <-- THIS IS WORKING
dispatch({ type: LOGIN_REQUEST });
};
};
So far, so good. However, when I dispatch(), my reducer is never called. But the reducer is picking up other actions (from navigation, etc.) - it simply isn't receiving the action from loginUser() above:
userReducer.js:
import { LOGIN_REQUEST } from './actionTypes';
const userReducer = (state = initialState, action) => {
console.log('In reducer'); <-- ** THIS IS NEVER CALLED **
switch (action.type) {
case LOGIN_REQUEST:
return Object.assign({}, state, {
isFetching: true,
});
case LOGOUT:
return initialState;
default:
return state;
}
};
export default userReducer;
Any suggestions would be greatly appreciated.
Ok, looks like I was able to figure this out. In a nutshell, in loginScreen.js I needed to add mapStateToProps and mapDispatchToProps functions, which are passed to connect. withHandlers can then dispatch the loginUser() function in my actions file as a prop.
updated loginScreen.js
import React from 'react';
import { Button, Text, View } from 'react-native';
import { connect } from 'react-redux';
import { withHandlers, compose } from 'recompose';
import { loginUser } from './user/userActions';
const LoginScreen = ({ user, doUserLogin }) => {
return (
<View style={styles.loginContainer}>
{user ? <Text>Hi, {user.name}!</Text> : <Text>NOT Logged in!</Text>}
<Button title="Log In" onPress={doUserLogin} />
</View>
);
};
const mapStateToProps = state => ({
...state.user,
});
const mapDispatchToProps = dispatch => ({
loginUser: () => {
dispatch(loginUser());
},
});
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withHandlers({
doUserLogin: props =>
() => {
console.log('LOGIN USER IN HANDLER');
props.loginUser();
},
}),
)(LoginScreen);
Any additional advice/suggestions would still be appreciated.
Actually, for this particular case, you can dismiss completely withHandlers helper.
You only need to pass the action creator to the react-redux connect function, in order to bind it to the dispatch function, just as you shown. Even more, check connect docs. You can access the props of the component, in the 3rd parameter of connect, and further create handlers that depend on props.
In your case it could be something like this
const mergeProps = (stateProps, dispatchProps, ownProps) => {
return Object.assign({}, ownProps, stateProps, dispatchProps, {
doUserLogin: () => {
console.log('LOGIN USER IN HANDLER');
console.log('accessing a prop from the component', ownProps.user);
dispatchProps.loginUser();
}
});
}
export default connect(mapStateToProps,
mapDispatchToProps,
mergeProps)(LoginScreen);
Notice how we can create new functions, that will be available as a new prop to the component, in a similar way to withHandler helper