I am trying to learn react and got an issue in redux.
The code is as follows.
import * as postActions from '../../redux/actions/postActions';
class PostForm extends Component {
handleSubmit = (e) => {
this.props.getBooks()
}
render() {
return (
<div>
<h1>Create Post</h1>
<form onSubmit={this.handleSubmit}>
<input required type="text" ref={(input)=>this.getTitle = input}
placeholder="Enter Post Title"/>
<br /><br />
<textarea required rows="5" ref={(input)=>this.getMessage = input} cols="28"
placeholder="Enter Post" />
<br /><br />
<button>Post</button>
</form>
</div>
);
}
}
export default connect(state => ({
...state.books,
}),{
...postActions,
})(PostForm);
As you can see, when the form is submitted, this.props.getBooks() action is called.
The action is defined as follows.
import * as types from '../constants/actionTypes';
export function getBooks(obj={}) {
const api = types.API_URL_BOOKS;
return dispatch => {
return dispatch({
type: types.ACTION_BOOK_LIST,
promise: client => client.get(api).then((data) => {
return data;
}),
});
};
}
I am using axios for making api calls. The issue is that I am not getting the server response in reducer. The reducer is as follows.
import * as types from '../constants/actionTypes';
export default function reducer(state = {}, action = {}) {
switch (action.type) {
case types.ACTION_BOOK_LIST:
return {
...state,
books : action.result.data.response.books
};
default:
return state;
}
}
On debugging, i found that the action is having only the following
{type: "BOOK_LIST"}
After that, in the apiMiddlewareCreator (which is defined in clientMiddleware.js), i am getting the server response
function apiMiddlewareCreator(client) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, client);
}
const { promise, type, hideLoader, ...rest } = action;
if (!promise) {
return next(action);
}
next({ ...rest, type: `${type}` });
const actionPromise = promise(client);
actionPromise
.then(result => {
debugger
if(result.data.success === false) throw result.data.message;
if (result && result.data && result.data.response) {
switch(action.type) {
default:
//nothing
}
}
return next({ ...rest, result, type: `${type}_SUCCESS`, originalType: type })
})
return actionPromise;
};
}
reducers/index.js
import { combineReducers } from 'redux';
//import { routerReducer as routing } from 'react-router-redux';
import postReducer from './postReducer';
const appReducer = combineReducers({
// routing,
books: postReducer,
});
const rootReducer = (state, action) => {
return appReducer(state, action)
};
export default rootReducer;
actionTypes.js
export const ACTION_BOOK_LIST = 'BOOK_LIST';
I need the data to be available in the reducer. That is, action.result.data.response.books should contain the response from server.
I am not sure on how to fix this.
Any help would be appreciated.
I think it's because you're dispatching the action before the promise is resolved.
Only dispatch the action once the promise has been resolved, e.g:
import * as types from '../constants/actionTypes';
export function getBooks(obj={}) {
const api = types.API_URL_BOOKS;
return dispatch => {
client.get(api).then(data => {
dispatch({
type: types.ACTION_BOOK_LIST,
books: data.response.books
});
});
};
}
Your reducer will need to be updated to:
case types.ACTION_BOOK_LIST:
return { ...state,
books: action.books
};
Related
I am making a React app where I need to add Redux using Hooks. Currently, I am stuck in making a POST request and can't figure out after going through the internet, how to make it work. I am on my way to understand how the Redux works and I will be happy for any help on this to make it work, so I can understand what is missing and how to send the data. My components:
App.js:
import { useState } from "react";
import { connect } from 'react-redux';
import "./App.css";
import Posts from "./components/posts";
import { addPost } from "./store/actions/postAction";
function App() {
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
const post = {
title: title,
body: body,
}
addPost(post);
setTitle('');
setBody('');
alert("Post added!");
};
return (
<div className="App">
<Posts />
<form onSubmit={handleSubmit}>
<label>
Mew post:
<input
type="text"
name="title"
placeholder="Add title"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<input
type="text"
name="body"
placeholder="Add body"
value={body}
onChange={e => setBody(e.target.value)}
/>
</label>
<button type="submit">Add</button>
</form>
</div>
);
}
export default connect()(App);
postAction.js
import axios from "axios";
import { GET_POSTS, ADD_POST, POSTS_ERROR } from "../types";
const url = "http://localhost:8002/";
export const getPosts = () => async (dispatch) => {
try {
const response = await axios.get(`${url}posts`);
dispatch({
type: GET_POSTS,
payload: response.data,
});
} catch (error) {
dispatch({
type: POSTS_ERROR,
payload: error,
});
}
};
export const addPost = (post) => (dispatch) => {
try {
const response = axios.post(`${url}`, {post});
dispatch({
type: ADD_POST,
payload: response.data,
});
} catch (error) {
dispatch({
type: POSTS_ERROR,
payload: error,
});
}
};
postReducer.js
import { ADD_POST, GET_POSTS, POSTS_ERROR } from "../types";
const initialState = {
posts: []
};
const postReducer = (state = initialState, action) => {
switch (action.type) {
case GET_POSTS:
return {
...state,
posts: action.payload
};
case ADD_POST:
return {
...state,
posts: action.payload
};
case POSTS_ERROR:
return {
error: action.payload
};
default:
return state;
}
};
export default postReducer;
posts.js
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getPosts } from "../store/actions/postAction";
const Posts = () => {
const dispatch = useDispatch();
const postsList = useSelector((state) => state.postsList);
const { loading, error, posts } = postsList;
useEffect(() => {
dispatch(getPosts());
}, [dispatch]);
return (
<>
{loading
? "Loading..."
: error
? error.message
: posts.map((post) => (
<div className="post" key={post.id}>
<h4>{post.title}</h4>
<p>{post.body}</p>
</div>
))}
</>
);
};
export default Posts;
App.js -> change to export default connect(null, {addPost})(App);
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'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
}
});
});
I'm trying to make api call with fetch method from Redux, i create some action types like fetch_start, fetch_success and fetch_failed.
but i cant my reducer nothing return to me. When i check redux dev tool there is 3 action types too working. Where i mistake?
i'm using thunk, redux
here is my component:
class SignInComponent extends Component {
signIn = () => {
this.props.signIn()
}
render() {
return (
<Row className="justify-content-md-center">
<Col lg={4}>
<button type="button" onClick={this.signIn}>
</button>
</Col>
</Row>
)
}
}
const mapStateToProps = state => ({
users: state.users
})
function mapDispatchToProps(dispatch) {
return {
signIn: bindActionCreators(signIn, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SignInComponent)
here is my reducer:
import { SIGNIN_START, SIGNIN_SUCCESS, SIGNIN_FAILED } from '../../actions/Auth/SignIn'
let initialState = []
export default (state = initialState, action) => {
switch (action.type) {
case SIGNIN_START:
return console.log('start')
case SIGNIN_SUCCESS:
return console.log("success")
case SIGNIN_FAILED:
return console.log("fail")
default:
return state
}
}
here is my action:
export const SIGNIN_START = 'SIGNIN_START';
export const SIGNIN_SUCCESS = 'SIGNIN_SUCCESS';
export const SIGNIN_FAILED = 'SIGNIN_FAILED';
export const signIn = () => {
return(dispatch) => {
dispatch({
type: SIGNIN_START
})
fetch('https://api.com/signIn')
.then((response) => {
dispatch({
type: SIGNIN_SUCCESS
})
})
.catch((err) => {
dispatch({
type: SIGNIN_FAILED
})
})
}
}
you have to return the new state in the reducer for each action
return console.log();
will simply returns undefined.
change it to
switch (action.type) {
case SIGNIN_START:
console.log('start')
return [...state];
case SIGNIN_SUCCESS:
console.log("success")
return [...state];
case SIGNIN_FAILED:
console.log("fail");
return [...state];
default:
return state
}
I have been working on authentication with my project. I have a REST api backend that serves JWT tokens. My front end stack is ReactJS, Redux, Axios and Redux Thunk.
My question is why when I submit my form it does not send any credentials?
But when I console log the action and payload on credChange it seems to be correct. Am I not setting the state somewhere?
Also, axios does not catch the 400 Bad Request error.
Here is my code:
AuthActions.js
export const credChange = ({ prop, value }) => {
return {
type: CRED_CHANGE,
payload: { prop, value },
};
};
export const logoutUser = () => {
return (dispatch) => {
dispatch({ type: LOGOUT_USER });
};
};
const loginSuccess = (dispatch, response) => {
dispatch({
type: LOGIN_USER_SUCCESS,
payload: response.data.token,
});
};
const loginError = (dispatch, error) => {
dispatch({
type: LOGIN_USER_ERROR,
payload: error.response.data,
});
};
export const loginUser = ({ empNum, password }) => {
return (dispatch) => {
dispatch({ type: LOGIN_USER });
axios({
method: 'post',
url: 'http://127.0.0.1:8000/profiles_api/jwt/authTK/',
data: {
emp_number: empNum,
password,
},
})
.then(response => loginSuccess(dispatch, response))
.catch(error => loginError(dispatch, error));
};
};
AuthReducer.js
const INITIAL_STATE = {
empNum: '',
password: '',
empNumErr: null,
passwordErr: null,
authTK: null,
loading: false,
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case CRED_CHANGE:
return { ...state, [action.payload.prop]: action.payload.value };
case LOGIN_USER:
return {
...state,
...INITIAL_STATE,
loading: true,
};
case LOGOUT_USER:
return {
...state,
INITIAL_STATE,
};
case LOGIN_USER_SUCCESS:
return {
...state,
...INITIAL_STATE,
authTK: action.payload,
};
case LOGIN_USER_ERROR:
return {
...state,
...INITIAL_STATE,
empNumErr: action.payload.emp_number,
passwordErr: action.payload.password,
};
default:
return state;
}
};
LoginForm.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
credChange,
loginUser,
logoutUser,
} from '../Actions';
class LoginForm extends Component {
constructor() {
super();
this.onFormSubmit = this.onFormSubmit.bind(this);
this.renderEmpNumErr = this.renderEmpNumErr.bind(this);
this.empNumChange = this.empNumChange.bind(this);
this.passwordChange = this.passwordChange.bind(this);
}
onFormSubmit() {
const { empNum, password } = this.props;
this.props.loginUser({ empNum, password });
}
empNumChange(text) {
this.props.credChange({ prop: 'empNum', value: text.target.value });
}
passwordChange(text) {
this.props.credChange({ prop: 'password', value: text.target.value });
}
renderEmpNumErr() {
if (this.props.empNumErr) {
return (
<p>
{this.props.empNumErr}
</p>
);
}
return null;
}
render() {
return (
<div>
<form onSubmit={this.onFormSubmit}>
<label htmlFor="numberLabel">Employee Number</label>
<input
id="numberLabel"
type="password"
value={this.props.empNum}
onChange={this.empNumChange}
/>
<label htmlFor="passLabel">Password</label>
<input
id="passLabel"
type="password"
value={this.props.password}
onChange={this.passwordChange}
/>
<button type="submit">Login</button>
</form>
{this.renderEmpNumErr()}
</div>
);
}
}
const mapStateToProps = ({ counter }) => {
const {
empNum,
password,
loading,
empNumErr,
passwordErr,
authTK,
} = counter;
return {
empNum,
password,
loading,
empNumErr,
passwordErr,
authTK,
};
};
export default connect(mapStateToProps, { credChange, loginUser, logoutUser })(LoginForm);
After Submitting form with credentials
The console says:
POST XHR http://127.0.0.1:8000/profiles_api/jwt/authTK/ [HTTP/1.0 400 Bad Request 5ms]
And the POST request Raw Data is blank, therefore no credentials were sent.
{"emp_number":["This field is required."],"password":["This field is required."]}
EDIT
If there is any other information I can provide please say so but I think this should be sufficient.
Looks like empNum and password aren't getting set in the state. This is because the action object returned by credChange doesn't get dispatched, so the reducer never get called:
// dispatch calls the reducer which updates the state
dispatch(actionCreator())
// returns an action object, doesn't call reducer
actionCreator()
You can dispatch actions automatically by calling a bound action creator:
// calls the reducer, updates the state
const boundActionCreator = () => {dispatch(actionCreator())}
// call boundActionCreator in your component
boundActionCreator()
mapDispatchToProps can be used to define bound action creators (to be passed as props):
const mapDispatchToProps = (dispatch) => {
return {
credChange: ({ prop, value }) => {dispatch(credChange({prop, value})},
loginUser: ({ empNum, password }) => {dispatch(loginUser({empNum, password})},
logoutUser: () => {dispatch(logoutUser()},
}
}
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm);
This should solve the state update issue, allowing props that read from state (empNumber, password, etc.) to update as well.