I've been trying to implement unit testing with react using the react-testing-library
I want to test my login component that use useSelector and useDispatch hooks from react-redux
The problem is that when I use this function to pass the store to in my login.test.js is not recognizing the reducer and show me this error:
An error occurred while selecting the store state: Cannot read property 'isLoading' of undefined.
const isLoadingAuth = useSelector(state => state.Auth.isLoading);
I use combineReducers in my store (the app has a lot of reducers) in order to access in that specific reducer "Auth" but I don't know how to use them in my login.test.js
How can I access to my Auth reducer in my login.test.js file?
This is my login.jsx
const LoginForm = () => {
const [values, setValues] = useState({ email: "", password: "" });
const dispatch = useDispatch();
function handleChange(e) {
const { name, value } = e.target;
setValues({ ...values, [name]: value });
}
function submitData(e) {
e.preventDefault();
dispatch(actions.AuthUser(values));
}
const isLoadingAuth = useSelector(state => state.Auth.isLoading);
const error = useSelector(state => state.Auth.err);
const isAuthSucess = useSelector(state => state.Auth.isAuthSuccess);
if (isAuthSuccess) {
<Redirect to="/dashboard" />;
}
return (
<>
<div>
<form onSubmit={submitData}>
<Input
label="Email"
name="email"
value={values.email}
change={handleChange}
/>
<Input
label="Password"
name="password"
type="password"
value={values.password}
change={handleChange}
/>
<div>
<button>Entrar</button>
</div>
</form>
</div>
</>
);
};
My AuthReducer.js
import * as actionTypes from "../actions/Auth/types";
import { updateObject } from "../store/utility";
export const initalState = {
authData: null,
isLoading: false,
isAuthSuccess: null,
err: null
};
const authStart = state => {
return updateObject(state, {
isLoading: true,
err: null
});
};
const authFail = (state, action) => {
return updateObject(state, {
isLoading: false,
err: action.err
});
};
const auth = (state, action) => {
return updateObject(state, {
isLoading: false,
authData: action.authData,
isAuthSuccess: true
});
};
export function reducer(state = initalState, action) {
switch (action.type) {
case actionTypes.START_AUTH_REQ: {
return authStart(state, action);
}
case actionTypes.FAIL_AUTH_REQ: {
return authFail(state, action);
}
case actionTypes.AUTH: {
return auth(state, action);
}
default:
return state;
}
}
export default reducer;
And my Login.test.js
import React from "react";
import { createStore, combineReducers } from "redux";
import { Provider } from "react-redux";
import { render, cleanup, fireEvent } from "#testing-library/react";
import rootReducer from "../../../../reducers";
import "#testing-library/jest-dom/extend-expect";
import LoginForm from "./Form";
function renderWithRedux(
ui,
{
initialState,
store = createStore(combineReducers(rootReducer, initialState))
} = {}
) {
return {
...render(<Provider store={store}>{ui}</Provider>),
// adding `store` to the returned utilities to allow us
// to reference it in our tests (just try to avoid using
// this to test implementation details).
store
};
}
test("can render with redux with custom initial state", () => {
const { getByTestId, getByText } = renderWithRedux(<LoginForm />, {
initialState: { isLoading: false }
});
});
Your initial state is for you entire store so needs to match the structure of your root reducer:
test("can render with redux with custom initial state", () => {
const { getByTestId, getByText } = renderWithRedux(<LoginForm />, {
initialState: { Auth: { isLoading: false } }
});
});
I know it is a late reply but might help someone.
The problem with the above code is that it is using combineReducer correctly but passing state of AuthReducer only.
The combineReducer is expecting a consolidated state. For example:
const state = {
auth: initialState,
app: {
temp: {
// Some state
}
}
}
function renderWithRedux(ui: any, state: any) {
const store = createStore(rootReducer, state)
return {
...render(<Provider store={ store } > { ui } < /Provider>),
store,
}
}
test('can render with redux with custom initial state', () => {
const { getByTestId, getByText } = renderWithRedux(<LoginForm />, {
...state,
auth: {
...initialState, loading: true
}
});
});
Related
I am trying to implement a simple login form, that gets username and password as input.
User.js
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
set,
reject,
verify,
isLoggedIn,
} from './userSlice';
export function User() {
const isUserLoggedIn = useSelector(isLoggedIn);
const dispatch = useDispatch();
const [loginError, setLoginError] = useState(false);
return (
<div>
<div> {loginError? 'Invalid credentials': ''}</div>
{/* Form elements here */}
<button
onClick={() => dispatch(verify())}
>
Verify User
</button>
</div>
);
}
userSlice.js
import { createSlice } from '#reduxjs/toolkit';
export const userSlice = createSlice({
name: 'user',
initialState: {
loggedIn: false,
email: '',
name: '',
token:''
},
reducers: {
set: (state, action) => {
return Object.assign({}, state, action.payload);
},
reject: (state, action) =>{
state.value = action.payload
}
},
});
export const { set, reject } = userSlice.actions;
export const verify = user => dispatch => { // For making an api call to verify the credentials are correct
axios.post('login', data).then(function(){
dispatch(set({loggedIn:true}))
}).catch(function(){
dispatch(reject({loggedIn:false}))
});
};
export const isLoggedIn = state => state.user.loggedIn;
export default userSlice.reducer;
All codes are working fine.
Now if the api call fails, i need to update the state loginError to true. How it can be done from userSlice.js file to User.js file.
Something like that I guess
User.js
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
set,
reject,
verify,
isLoggedIn,
isLoginError, //<-------------
} from './userSlice';
export function User() {
const isUserLoggedIn = useSelector(isLoggedIn);
const dispatch = useDispatch();
const isLoginError = useSelector(isLoginError); //<----------------
return (
<div>
<div> {isLoginError ? 'Invalid credentials': ''}</div> //<-------------
{/* Form elements here */}
<button
onClick={() => dispatch(verify())}
>
Verify User
</button>
</div>
);
}
userSlice.js
import { createSlice } from '#reduxjs/toolkit';
export const userSlice = createSlice({
name: 'user',
initialState: {
loggedIn: false,
email: '',
name: '',
token:''
},
reducers: {
set: (state, action) => {
return Object.assign({}, {...state, loginError:false}, action.payload); //<------
},
reject: (state, action) =>{
return Object.assign({}, {...state, loginError:true}, action.payload); //<-------
}
},
});
export const { set, reject } = userSlice.actions;
export const verify = user => dispatch => { // For making an api call to verify the credentials are correct
axios.post('login', data).then(function(){
dispatch(set({loggedIn:true}))
}).catch(function(){
dispatch(reject({loggedIn:false}))
});
};
export const isLoggedIn = state => state.user.loggedIn;
export const isLoginError = state => state.user.loginError; //<----------
export default userSlice.reducer;
I created an action creator that is simply supposed to make a get request to my API and return with a list of all projects. However, for some reason, my return dispatch in my thunk function is not firing at all. It gets to the console.log() statement and just ends. There are no consoles errors, and no network calls being made either as far as I can tell. Any ideas why it would do absolutely nothing?
Dashboard.js (component)
import ProjectItem from "../Project/ProjectItem";
import styles from "./Dashboard.module.css";
import CreateProjectButton from "../CreateProjectButton/CreateProjectButton";
import { connect } from "react-redux";
import { getProjects } from "../../Redux/getProjects/actions";
const Dashboard = props => {
useEffect(() => {
console.log("blah");
getProjects();
}, []);
return (
<div className={styles.dashboardContainer}>
<h1>Projects</h1>
<br />
<CreateProjectButton />
<br />
<hr />
<ProjectItem />
</div>
);
};
const mapStateToProps = state => {
return {
projects: state
};
};
const mapDispatchToProps = dispatch => {
return {
getProjects: () => dispatch(getProjects())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
action.js (action creator)
import { GET_PROJECTS_SUCCESS, GET_PROJECTS_ERROR } from "./constants";
export const getProjectsSuccess = payload => {
console.log("getProjectSuccess", payload);
return {
type: GET_PROJECTS_SUCCESS,
payload
};
};
export const getProjectsError = () => {
console.log("there was an error");
return {
type: GET_PROJECTS_ERROR
};
};
export function getProjects() {
console.log("getProject");
return dispatch => {
axios
.get("/project/all")
.then(res => dispatch(getProjectsSuccess(res.data)))
.catch(err => dispatch(getProjectsError(err)));
};
}
index.js (getProject reducer)
const initialState = {
projects: [], //array of projects
project: {}, // single project for update case
reRender: false
};
const getProjectsReducer = (state = initialState, action) => {
switch (action.type) {
case GET_PROJECTS_SUCCESS:
return { ...state, projects: action.payload }; // will need to change action.payload later on
default:
return state;
}
};
export default getProjectsReducer;
constants.js
export const GET_PROJECTS_SUCCESS = "GET_PROJECTS_SUCCESS";
export const GET_PROJECTS_ERROR = "GET_PROJECTS_ERROR";
rootReducer.js
import createProjectReducer from "./createProject/index";
import getProjectsReducer from "./getProjects/index";
const rootReducer = (state = {}, action) => {
return {
project: createProjectReducer(state.project, action),
projects: getProjectsReducer(state.projects, action)
};
};
export default rootReducer;
FIXED: After reading up on the use effect hook in functional components I realized I was missing props.getProjects in the useEffect function in dashboard.js!
I recently started using redux for a new personal project. It worked pretty well until I started using "combineReducers". Whenever I click "Fetch todos" both my user as well as my todo reducer get updated and even though they have different data field names both get the same data. Now I probably did some wrong encapsulation here. But no matter how often I went over the docs, I just cannot see what I am doing wrong.
My store initialization script:
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import toDoReducer from './todos/reducer';
import userReducer from './users/reducer';
const rootReducer = combineReducers({
todosSlice: toDoReducer,
usersSlice: userReducer
});
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)));
export default store;
gets injected into index:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './containers/app/App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import configureStore from './store/configureStore';
ReactDOM.render(<Provider store={ configureStore }><App /></Provider>, document.getElementById('root'));
serviceWorker.unregister();
My app hold the logic for the todo container
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as todoActions from '../../store/todos/actions';
import UserContainer from '../usersContainer/UserContainer';
class App extends Component {
componentDidMount() {
console.log(this.props);
}
render() {
let loading = '';
let error = '';
let todos = [];
// check whether the component is fetching data
this.props.loading === true ? loading = <p>Loading...</p> : loading = '';
// check if there was an error
this.props.error && this.props.loading === false ? error = <p>There was an error</p> : error = '';
// map the todos in the desired html markup.
todos = this.props.todos.map( todo => {
return <div key={todo.id}> name: {todo.title} </div>
});
return (
<div className="App">
{/* <UserContainer /> */}
{loading}
{error}
<p onClick={() => this.props.onFetchTodos()}>Fetch Todos</p>
{todos}
</div>
);
}
}
const mapStateToProps = state => {
return {
error: state.todosSlice.error,
loading: state.todosSlice.loading,
todos: state.todosSlice.todos
}
}
const mapDispatchToProps = dispatch => {
return {
onFetchTodos: () => dispatch(todoActions.fetchTodos())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
Which has the following actions:
import axios from 'axios';
export const FETCH_TODOS = 'FETCH_TODOS';
export const GET_TODOS_STARTED = 'GET_TODOS_STARTED';
export const FETCH_TODOS_SUCCESS = 'FETCH_TODOS_SUCCESS';
export const FETCH_TODOS_FAILURE = 'FETCH_TODOS_FAILURE';
export const fetchRequest = () => {
return dispatch => {
dispatch(getTodoStarted());
axios.get('https://one365-api-dev.azurewebsites.net/api/teams/')
.then(result => {
dispatch(fetchTodosSucces(result));
}).catch(error => {
dispatch(fetchTodoFailure(error));
});
}
}
const getTodoStarted = () => ({
type: GET_TODOS_STARTED
});
const fetchTodosSucces = todos => ({
type: FETCH_TODOS_SUCCESS,
payload: {
...todos
}
});
const fetchTodoFailure = error => ({
type: FETCH_TODOS_FAILURE,
payload: {
error
}
});
export const fetchTodos = () => {
return (dispatch => {
dispatch(fetchRequest());
});
}
And it's reducer
import * as actions from './actions';
const initialState = {
error: null,
loading: false,
todos: []
}
const todosReducer = (state = initialState, action) => {
switch(action.type) {
case actions.GET_TODOS_STARTED: {
console.log('fetch todo state', state)
return {
...state,
loading: state.loading = true
};
}
case actions.FETCH_TODOS_SUCCESS: {
const todos = action.payload.data;
return {
...state,
loading: false,
todos: state.todos = todos
};
}
case actions.FETCH_TODOS_FAILURE: {
const error = action.payload.error;
return {
...state,
loading: false,
error: state.error = error
};
}
default: {
return state;
}
}
}
export default todosReducer;
The Users Component
import React from 'react';
import { connect } from 'react-redux';
import * as userActions from '../../store/users/actions';
class UserContainer extends React.Component {
render () {
let loading = '';
let error = '';
let users = [];
// check whether the component is fetching data
this.props.usersLoading === true ? loading = <p>Loading...</p> : loading = '';
// check if there was an error
this.props.usersError && this.props.loading === false ? error = <p>There was an error</p> : error = '';
// map the users in the desired html markup.
users = this.props.users.map( user => {
return <div key={user.id}> name: {user.title} </div>
});
return (
<div className="Users">
{loading}
{error}
<p onClick={() => this.props.onFetchUsers()}>Fetch Users</p>
{users}
</div>
);
}
}
const mapStateToProps = state => {
return {
usersError: state.usersSlice.error,
usersLoading: state.usersSlice.loading,
users: state.usersSlice.users
}
}
const mapDispatchToProps= (dispatch) => {
return {
onFetchUsers: () => dispatch(userActions.fetchUsers())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(UserContainer);
the user actions:
import axios from 'axios';
export const FETCH_USERS = 'FETCH_TODOS';
export const FETCH_USERS_STARTED = 'GET_TODOS_STARTED';
export const FETCH_USERS_SUCCESS = 'FETCH_TODOS_SUCCESS';
export const FETCH_USERS_FAILURE = 'FETCH_TODOS_FAILURE';
export const fetchRequest = () => {
return dispatch => {
dispatch(fetchUsersStarted());
axios.get('https://one365-api-dev.azurewebsites.net/api/me')
.then(result => {
dispatch(fetchUsersSuccess(result));
}).catch(error => {
dispatch(fetchUsersFailure(error));
});
}
}
export const fetchUsersSuccess = (users) => {
return {
type: FETCH_USERS_SUCCESS,
payload: {
...users
}
}
}
export const fetchUsersStarted = () => ({
type: FETCH_USERS_STARTED
});
export const fetchUsersFailure = (error) => {
return {
type: FETCH_USERS_FAILURE,
payload: {
error
}
}
}
export const fetchUsers = () => {
return dispatch => {
dispatch(fetchRequest())
}
};
And it's reducer:
import * as actions from './actions';
const initialState = {
error: '',
loading: false,
users: []
}
const userReducer = (state = initialState, action) => {
switch(action.type) {
case actions.FETCH_USERS_STARTED: {
console.log('fetch users state', state)
return {
...state,
loading: state.loading = true
}
}
case actions.FETCH_USERS_SUCCESS: {
const users = action.payload.data;
return {
...state,
loading: false,
users: state.users = users
}
}
case actions.FETCH_USERS_FAILURE: {
const error = state.payload.error;
return {
...state,
loading: false,
error: state.error = error
}
}
default: {
return state;
}
}
}
export default userReducer;
Now when I run my DEV server I only see the fetch todo button. I commented out the users on click handler to see if it was an event bubble going up. Bu t this wasn't the case.
Once the app load redux dev tools shows the state as follows:
but once i click the fetch todo's handler. Both todos and users get filled.
I appreciate anyone who read though so much (boilerplate) code. I probably made a problem encapsulating my state. but again after reading many tutorials I still cannot find my issue.
You have a copy/paste issue. You changed the names of the constants for your "USERS" actions, but left the values the same as the "TODOS" actions.
export const FETCH_USERS = 'FETCH_TODOS';
export const FETCH_USERS_STARTED = 'GET_TODOS_STARTED';
export const FETCH_USERS_SUCCESS = 'FETCH_TODOS_SUCCESS';
export const FETCH_USERS_FAILURE = 'FETCH_TODOS_FAILURE';
I assume you meant to have:
export const FETCH_USERS = 'FETCH_USERS';
export const FETCH_USERS_STARTED = 'FETCH_USERS_STARTED';
export const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS';
export const FETCH_USERS_FAILURE = 'FETCH_USERS_FAILURE';
I have read through 100's of these threads on here, and I can't seem to understand why my component isn't updating. I am pretty sure it has something to do with the Immutability, but I can't figure it out.
The call is being made, and is returning from the server. The state is changing (based on the redux-Dev-Tools that I have installed).I have made sure to not mutate the state in any instance, but the symptoms seem to point that direction.
Code Sandbox of whole app https://codesandbox.io/s/rl7n2pmpj4
Here is the component.
class RetailLocationSelector extends Component {
componentWillMount() {
this.getData();
}
getData = () => {
this.props.getRetailLocations()
}
render() {
const {data, loading} = this.props;
return (
<div>
{loading
? <LinearProgress/>
: null}
<DefaultSelector
options={data}
placeholder="Retail Location"/>
</div>
);
}
}
function mapStateToProps(state) {
return {
loading: state.retaillocations.loading,
data: state.retaillocations.data,
osv: state.osv};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
getRetailLocations,
selectRetailLocation,
nextStep
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(RetailLocationSelector);
And here is my reducer :
import {REQUEST_RETAIL_LOCATIONS, SUCCESS_RETAIL_LOCATIONS,
ERR_RETAIL_LOCATIONS, SELECT_RETAIL_LOCATION} from
'../actions/RetailLocationsAction'
const initialState = {
data: [],
loading: false,
success: true,
selectedRetailLocation: undefined
}
function retailLocation(state = initialState, action) {
switch (action.type) {
case REQUEST_RETAIL_LOCATIONS:
return Object.assign({}, state, {
loading: true
}, {success: true})
case SUCCESS_RETAIL_LOCATIONS:
return Object.assign({}, state, {
loading: false
}, {
success: true
}, {
data: Object.assign([], action.payload.data)
})
case ERR_RETAIL_LOCATIONS:
return Object.assign({}, state, {
loading: false
}, {
success: false
}, {errorMsg: action.payload.message})
case SELECT_RETAIL_LOCATION:
return Object.assign({}, state, {
selectedRetailLocation: state
.data
.find((rec) => {
return rec.id === action.payload.id
})
})
default:
return state;
}
}
export default retailLocation
And finally, my Action file:
import axios from 'axios';
//import {api} from './APIURL'
export const REQUEST_RETAIL_LOCATIONS = 'REQUEST_RETAIL_LOCATIONS'
export const SUCCESS_RETAIL_LOCATIONS = 'SUCCESS_RETAIL_LOCATIONS'
export const ERR_RETAIL_LOCATIONS = 'ERR_RETAIL_LOCATIONS'
export const SELECT_RETAIL_LOCATION = 'SELECT_RETAIL_LOCATION'
const URL = 'localhost/api/v1/retail/locations?BusStatus=O&LocType=C'
export const getRetailLocations = () => (dispatch) => {
dispatch({ type: 'REQUEST_RETAIL_LOCATIONS' });
return axios.get(URL)
.then(data => dispatch({ type: 'SUCCESS_RETAIL_LOCATIONS', payload: data }))
.catch(error => dispatch({type : 'ERR_RETAIL_LOCATIONS', payload: error}));
}
Combined Reducer
import { combineReducers } from "redux";
import retailLocations from './RetailLocationsReducer'
import vendors from './VendorsReducer'
import receiptInformation from './ReceiptInfoReducer'
import osv from './OSVReducer'
import receiptDetail from './ReceiptDetailReducer'
const allReducers = combineReducers({
retaillocations: retailLocations,
vendors: vendors,
receiptInformation: receiptInformation,
receiptDetail: receiptDetail,
osv: osv
});
export default allReducers;
This answer doesn't solve your issue totally but provides some hints about what is not working. The broken part is your store definition. I don't have much experience with redux-devtools-extension or redux-batched-subscribe but if you define your store like that your thunk function works:
const store = createStore(reducer, applyMiddleware(thunk));
One of the configuration for the mentioned packages above brokes your thunk middleware.
I'm very beginner in React/Redux and I was trying to do something in an already existing code, just to understand how it works. The part I want to edit is the connection part. As it's now, if the login and password are OK, you go to a new page, and if not, it does nothing.
The only simple thing I'm trying to do is to show the user their login informations are wrong, by adding a red border on the fields for example.
So here is the code I added, I'll try not to show you useless code and not to forget useful code, but let me know if you need more.
The first thing I did is adding a constant for the error in actionTypes.js:
export const AUTH_REQUEST = 'AUTH_REQUEST';
export const AUTH_RECEIVE = 'AUTH_RECEIVE';
export const AUTH_ERROR = 'AUTH_ERROR';
Then in actions/auth.js, I added the authError function and called it after a fail response from the server:
function authRequest() {
return {
type: actionTypes.AUTH_REQUEST
};
}
function authReceive(authToken) {
return {
type: actionTypes.AUTH_RECEIVE,
authToken
};
}
function authError() {
return {
type: actionTypes.AUTH_ERROR
};
}
export function fetchLogin(email, password) {
return function (dispatch) {
dispatch(authRequest());
const urlApi = `//${AUTH_BACKEND_HOST}:${AUTH_BACKEND_PORT}/${AUTH_BACKEND_URL.login}`
fetch(urlApi, {
method: 'POST',
headers: {
'Accept': 'application/json',
'content-type': 'application/json'
},
body: JSON.stringify({
email,
password
})
})
.then((response) => {
if(response.ok) {
// SUCCESS
response.json().then(function(json) {
dispatch(authReceive(json.key));
dispatch(push('/'));
});
} else {
// FAIL
response.json().then(function(json) {
dispatch(authError());
});
}
})
.catch(function(ex) {
console.log(ex);
});
};
}
Now, in reducers/auth.js:
const initialState = {
authToken: '',
isFetching: false,
error: false,
errorMessage: ''
}
export default function (state=initialState, action) {
switch (action.type) {
case actionType.AUTH_REQUEST:
return {
...state,
isFetching: true
};
case actionType.AUTH_RECEIVE:
return authLogin(state, action);
case actionType.AUTH_ERROR:
return {
...state,
error: true,
errorMessage: 'Incorrect login or password!'
};
}
return state;
}
function authLogin(state, action) {
const { authToken } = action;
return {
...state,
isFetching: false,
authToken
};
}
Until now, it seems to work when I inspect it in Firefox. The state contains the error and errorMessage values.
So here is my components/Login/presenter.jsx which I thought was going to display the right HTML depending on the state:
import React from 'react';
const Login = React.createClass({
handleSubmit(event) {
event.preventDefault()
const email = this.refs.email.value
const password = this.refs.password.value
this.props.onAuth(email, password);
},
render() {
const { errorMessage } = this.props
return (
<form onSubmit={this.handleSubmit}>
<label>Email <input ref="email" placeholder="email" required /></label>
<label>Password <input ref="password" placeholder="password" type="password" required /></label><br />
<p>{errorMessage}</p>
<button type="submit">Login</button>
</form>
)
}
});
export default Login;
And here is components/Login/index.js which I think imports the presenter and do... things...:
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import Login from './presenter';
function mapDispatchToProps(dispatch) {
return {
onAuth: bindActionCreators(actions.fetchLogin, dispatch)
};
}
export default connect(null, mapDispatchToProps) (Login);
Edit : it seems that one of the problems is that I'm not mapping the state to props. I tried Mael Razavet and azium's answers, adding mapStateToProps in Login/index.js:
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import Login from './presenter';
function mapDispatchToProps(dispatch) {
return {
onAuth: bindActionCreators(actions.fetchLogin, dispatch)
};
}
function mapStateToProps (state) {
return {
errorMessage: state.errorMessage
};
}
export default connect(mapStateToProps, mapDispatchToProps) (Login);
But it seems that errorMessage is still undefined.
Thank you.
I think you forgot to map your state to props. In your case, you should add this content to your components/Login/index.js:
import * as actions from './actions/auth.js';
import Login from './presenter';
const mapStateToProps = (state) => {
return {
error: state.login.error,
errorMessage: state.login.errorMessage,
};
};
const mapDispatchToProps = (dispatch) => {
return {
onAuth: (email, password) => {
dispatch(actions.fetchLogin(email, password));
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Login); // maps your state to your component's props
In your reducers/auth.js:
const initialState = {
authToken: '',
isFetching: false,
error: false,
errorMessage: ''
}
export default function loginReducer(state=initialState, action) {
switch (action.type) {
case actionType.AUTH_REQUEST:
return {
...state,
isFetching: true
};
case actionType.AUTH_RECEIVE:
return authLogin(state, action);
case actionType.AUTH_ERROR:
return {
...state,
error: true,
errorMessage: 'Incorrect login or password!'
};
}
return state;
}
function authLogin(state, action) {
const { authToken } = action;
return {
...state,
isFetching: false,
authToken
};
}
Then, in your code, you should be combining your reducer like:
import { combineReducers, createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import Login from './components/Login';
import login from './reducers/auth.js'; //import your default reducer function
//COMBINE REDUCERS
const reducers = combineReducers({
//reducers go here
login, //name of your reducer => this is is why your access your state like state.login
});
//WRAP WITH STORE AND RENDER
const createStoreWithMiddleware = applyMiddleware()(createStore);
ReactDOM.render(
<Provider store={createStoreWithMiddleware(reducers)}>
<Login/>
</Provider>
, document.querySelector('.container'));
In Redux, you manage your state (setState) in a different layer (reducer) than your actual component. To do so, you need to map your state from the reducer to the component so you can use it as a props. This is why in your Login class, you are able to do :
const { errorMessage } = this.props; // now you can use errorMessage directly or this.props.errorMessage
This errorMessage comes from your state managed in your reducer and can be used in your component as this.props.errorMessage.
Here is the link to the tutorial which helped me understand Redux in React : https://github.com/happypoulp/redux-tutorial
It should help you understand better the workflow