I am trying to update state each time I type into an input. The action creator is firing but the reducer is not and I am not sure why. Below is the component and the reducer file. I have commented where the console.log works and where is does not. Some suggestions are greatly appreciated, thanks in advance.
//Component
import React, { Component } from "react";
import { Link, Route } from "react-router-dom";
import { connect } from "react-redux";
import {
updateName,
updateAddress,
updateCity,
updateState,
updateZip
} from "../../redux/reducers/reducer";
class StepOne extends Component {
render() {
return (
<div>
<div className="name">
<h3>Property Name</h3>
<input
name="name"
placeholder="Name"
onChange={e => updateName(e.target.value)}
/>
</div>
<div className="address">
<h3>Address</h3>
<input
name="address"
placeholder="Address"
onChange={e => updateAddress(e.target.value)}
/>
</div>
<div className="city">
<h3>City</h3>
<input
name="city"
placeholder="City"
onChange={e => updateCity(e.target.value)}
/>
</div>
<div className="state">
<h3>State</h3>
<input
name="state"
placeholder="State"
onChange={e => updateState(e.target.value)}
/>
</div>
<div className="zip">
<h3>Zip</h3>
<input
name="zip"
placeholder="Zip"
onChange={e => updateZip(e.target.value)}
/>
</div>
<Link to="/wizard/step_two">
<button onClick={console.log(this.props)}>Next Step</button>
</Link>
</div>
);
}
}
const mapStateToProps = state => {
return {
state
};
};
export default connect(
mapStateToProps,
{
updateName,
updateAddress,
updateCity,
updateState,
updateZip
}
)(StepOne);
//Reducer
const initialState = {
name: "",
address: "",
city: "",
state: "",
zip: 0,
image: "",
monthly_mortgage: 0,
desired_rent: 0,
houses: []
};
const NAME_INPUT = "NAME_INPUT";
const ADDRESS_INPUT = "ADDRESS";
const CITY_INPUT = "CITY_INPUT";
const STATE_INPUT = "STATE_INPUT";
const ZIP_INPUT = "ZIP_INPUT";
const IMAGE_INPUT = "IMAGE_INPUT";
const MORTGAGE_INPUT = "MORTGAGE_INPUT";
const DESIRED_RENT_INPUT = "DESIRED_RENT_INPUT";
function reducer(state = initialState, action) {
console.log("REDUCER HIT: Action ->", action); // console.log does not work
switch (action.type) {
case NAME_INPUT:
return { ...state, name: action.payload };
case ADDRESS_INPUT:
return { ...state, address: action.payload };
case CITY_INPUT:
return { ...state, city: action.payload };
case STATE_INPUT:
return { ...state, state: action.payload };
case ZIP_INPUT:
return { ...state, zip: action.payload };
case IMAGE_INPUT:
return { ...state, image: action.payload };
case MORTGAGE_INPUT:
return { ...state, monthly_mortgage: action.payload };
case DESIRED_RENT_INPUT:
return { ...state, desired_rent: action.payload };
default:
return state;
}
}
export function updateName(name) {
console.log(name); // console.log is working
return {
type: NAME_INPUT,
payload: name
};
}
export function updateAddress(address) {
return {
type: ADDRESS_INPUT,
payload: address
};
}
export function updateCity(city) {
return {
type: CITY_INPUT,
payload: city
};
}
export function updateState(state) {
return {
type: STATE_INPUT,
payload: state
};
}
export function updateZip(zip) {
return {
type: ZIP_INPUT,
payload: zip
};
}
export default reducer;
//Store
import { createStore } from "redux";
import reducer from "./reducers/reducer";
export default createStore(reducer);
You are using your action creators directly from import, but for them to have the full redux flow, you need to use them from this.props e.g:
this.props.updateName()
The second parameter of the connect() function maps dispatch to your action creators, so your actions can be dispatched to the root reducer.
Related
I'm trying to learn the MERN stack, and I'm going through this tutorial on YouTube. I'm getting stuck in Ep. 7. The issue I think I'm having is my ADD_ITEM action is never triggered, and so the state is never updated, and I have to reload the page in order to see any items added. The DELETE_ITEM action works properly, so I suspect there may be an issue with the ADD_ITEM action being called from a form in a modal, but I'm unsure.
Picture of my Redux DevTools after refreshing the page, deleting 2 items, and trying to add 1:
itemReducer.js
import { GET_ITEMS, ADD_ITEM, DELETE_ITEM, ITEMS_LOADING } from '../actions/types';
const initialState = {
items: [],
loading: false
};
export default function(state = initialState, action) {
console.log(action.type);
switch(action.type) {
case GET_ITEMS:
return { ...state, items: action.payload, loading: false };
case ADD_ITEM:
return { ...state, items: [action.payload, ...state] };
case DELETE_ITEM:
return { ...state,
items: state.items.filter(item => item._id !== action.payload)
};
case ITEMS_LOADING:
return {
...state,
loading: true
};
default:
return state;
}
}
ItemModal.js
import React, { Component } from 'react';
import { Button, Modal, ModalHeader, ModalBody, Form, FormGroup, Label, Input } from 'reactstrap';
import { connect } from 'react-redux';
import { addItem } from '../actions/itemActions';
class ItemModal extends Component {
state = {
modal: false,
name: ''
}
toggle = () => {
this.setState({
modal: !this.state.modal
});
}
onChange = (e) => {
this.setState({ [e.target.name]: e.target.value })
}
onSubmit = (e) => {
e.preventDefault();
const newItem = {
name: this.state.name
}
this.props.addItem(newItem);
this.toggle();
}
render() {
return(
<div>
<Button
color="dark"
style={{marginBotton: '2rem'}}
onClick={this.toggle}
>Add Item</Button>
<Modal
isOpen={this.state.modal}
toggle={this.toggle}
>
<ModalHeader
toggle={this.toggle}
>
Add To Shopping List
</ModalHeader>
<ModalBody>
<Form onSubmit={this.onSubmit}>
<FormGroup>
<Label for="item">Item</Label>
<Input
type="text"
name="name"
id="item"
placeholder="Add shopping item"
onChange={this.onChange}
/>
</FormGroup>
<Button
color="dark"
style={{marginTop: '2rem'}}
block
>Submit</Button>
</Form>
</ModalBody>
</Modal>
</div>
)
}
}
const mapStateToProps = state => ({
item: state.item
});
export default connect(mapStateToProps, { addItem })(ItemModal);
itemActions.js
import axios from 'axios';
import { GET_ITEMS, ADD_ITEM, DELETE_ITEM, ITEMS_LOADING } from './types';
export const getItems = () => dispatch => {
dispatch(setItemsLoading());
axios.get('/api/items').then(res => dispatch({
type: GET_ITEMS,
payload: res.data
}))
};
export const deleteItem = (id) => dispatch => {
axios.delete(`/api/items/${id}`).then(res => dispatch({
type: DELETE_ITEM,
payload: id
}))
};
export const addItem = (item) => dispatch => {
axios.post('/api/items', item).then(res => dispatch({
type: ADD_ITEM,
payload: res.data
}))
};
export const setItemsLoading = () => {
return {
type: ITEMS_LOADING
};
};
I am using redux for simple POC. I am passing props to component. However when I assign it to input box, the input box is not updating.
My component code:
CurrenctConverter.js
handleAmountchange(e) {
debugger;
var payload = e.target.value
store.dispatch({ type: "Add", val: payload })
}
render() {
return (
<div>
Currency Converter
USD <input type="text" onChange={this.debounceEventHandler(this.handleAmountchange, 1000)} value={this.props.globalstate.val}></input> **this inputbox not working**
INR <input type="text"></input>
<input type="button" value="Convert"></input>
</div>
)
}
redux store:
I am getting props from this store
import React from 'react'
import { createStore } from 'redux'
var initialstate = {
val: 100
}
const MyReducer = (state = initialstate, action) => {
if (action.type = "Add") {
return {
...state,
val: action.val
}
}
return state;
}
var mystore = createStore(MyReducer);
export default mystore;
In the reducer, please update the code as
var initialstate = {
val: 100
}
const MyReducer = (state = initialstate, action) => {
switch (action.type) {
case 'Add':
let updatedState={
...state,
val: action.val
}
return updatedState;
default:
return state;
}
var mystore = createStore(MyReducer);
export default mystore;
After clicking on submit button in form with addBook action nested inside, data is passed into DB, but not outputting imidiately to the screen (i have to refresh page each time to output newly added data from DB).
I tried to put my getBooks function into componentDidUpdate() lifecycle hook, but it causes infinite loop.
getBooks action
export const getBooks = () => dispatch => {
axios.get('https://damianlibrary.herokuapp.com/library')
.then(res => dispatch({
type: GET_BOOKS,
payload: res.data
}))
};
addBook action
export const addBook = book => dispatch => {
axios.post('https://damianlibrary.herokuapp.com/library', book)
.then(res => dispatch({
type: ADD_BOOK,
payload: res.data
}))
};
bookReducer
const initialState = {
books: []
}
export default function(state = initialState, action) {
switch(action.type) {
case GET_BOOKS:
return {
...state,
books: action.payload
};
case DELETE_BOOK:
return {
...state,
books: state.books.filter(book => book.book_id !== action.payload)
};
case ADD_BOOK:
return {
...state,
eventDetails: [action.payload, ...state.books]
};
default:
return state;
}
}
Form.js component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addBook, getBooks } from '../../actions/bookActions';
import './Form.css';
class Form extends Component {
state = {
name: '',
author: '',
isbn: ''
}
componentDidMount () {
this.props.getBooks();
}
onChangeHandler = (e) => {
this.setState({ [e.target.name]: e.target.value })
};
onSubmitHandler = (e) => {
const {name, author, isbn} = this.state
const newBook = {
name: name,
author: author,
isbn: isbn
}
this.props.addBook(newBook);
this.setState({
name: '',
author: '',
isbn: ''
})
e.preventDefault();
}
render() {
const { name, author, isbn } = this.state;
return (
<div className='formContainer'>
<div className='form'>
<form className='bookForm' onSubmit={this.onSubmitHandler.bind(this)}>
<div className='inputs'>
<input
type='text'
name='name'
placeholder='Book name'
onChange={this.onChangeHandler}
value={name}/>
<input
type='text'
name='author'
placeholder='Book author'
onChange={this.onChangeHandler}
value={author}/>
<input
type='text'
name='isbn'
placeholder='ISBN'
onChange={this.onChangeHandler}
value={isbn}/>
</div>
<div className='buttonSpace'>
<button>Add book</button>
</div>
</form>
</div>
</div>
)
}
}
const mapStateToProps = (state) => ({
book: state.book
});
export default connect(mapStateToProps, { addBook, getBooks })(Form);
In the reducer you should return an updated reducer object. In the ADD_BOOK you add new property eventDetails. Do you use it somewhere?
Your new reducer look that: { books: [ initial book list ], eventDetails: [initial book list and new book]}. When you change eventDetails to books in ADD_BOOK, your book list will update without additional requests.
I have a redux-form which is used to receive data from users and store it to an API.My redux-form is as shown below:
import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { createPosts } from '../actions/posts_action';
const uuidv1 = require('uuid/v1'); //For generating unique id for each post
class CreatePost extends Component {
constructor() {
super();
this.state = {
selectValue : ''
};
this.handleChange = this.handleChange.bind(this);
this.renderCategory = this.renderCategory.bind(this);
}
renderField(field) {
return(
<div className="title-design">
<label className="label-design"> {field.label} </label>
<input
type="text"
className="title-input"
{...field.input}
/>
<div className="text-help has-danger">
{field.meta.touched ? field.meta.error : ''}
</div>
</div>
);
}
handleChange(e) {
const value=e.target.value;
this.props.change("categories",value);
this.setState({selectValue: value}, () => {
console.log(value)
});
}
renderCategory(field) {
return(
<div className="title-design">
<label className="label-design">{field.label} </label>
<Field name="category" className="title-input" component="select">
<option></option>
<option value="react">React</option>
<option value="redux">Redux</option>
<option value="udacity">Udacity</option>
</Field>
<div className="text-help has-danger">
{field.meta.touched ? field.meta.error : ''}
</div>
</div>
);
}
onSubmit(values) {
this.props.createPosts(values, () => {
uuidv1();
console.log(uuidv1());
this.props.history.push('/');
});
}
render() {
const { handleSubmit } = this.props;
return (
<form onSubmit={handleSubmit(this.onSubmit.bind(this))}>
<Field
label="Title for Post"
name="title"
component={this.renderField}
/>
<Field
label="Post Content"
name="body"
component={this.renderField}
/>
<Field
label="Category"
name="category"
component={this.renderCategory}
/>
<button type="submit" className="btn btn-primary">Submit</button>
<Link to="/">
<button className="cancel-button">Cancel</button>
</Link>
</form>
);
}
}
function validate(values) {
const errors = {} ;
if (!values.title) {
errors.title = "Enter a title";
}
if (!values.body) {
errors.body = "Enter some content";
}
if(!values.category) {
errors.category = "Please select a category";
}
return errors;
}
export default reduxForm({
validate : validate, //validate
form : 'CreatePostForm'
})(
connect(null,{ createPosts })(CreatePost)
);
Action creator for creating the post:
//Action Creator for creating posts
export function createPosts(values, callback) {
return dispatch => {
return axios.post(`${API}/posts`,values,{headers})
.then((data) => {
callback();
console.log(data)
dispatch({
type: CREATE_POST,
payload: data
})
})
}
}
Reducer for the posts is:
import _ from 'lodash';
import { FETCH_POSTS, FETCH_POST, CREATE_POST } from '../actions/posts_action';
export default function(state = {}, action) {
switch (action.type) {
case FETCH_POST:
return {...state, [action.payload.id]: action.payload};
case FETCH_POSTS:
return {posts: { ...state.posts, ...action.payload }};
case CREATE_POST:
return {posts: { ...state, ...action.payload}};
default:
return state;
}
}
So,now what happens is when I try to post data to the server,I am able to see the data set if I console.log, but if I want to add a second set of data to the API,then the first set of data is deleted and only the second set remains.So,how to save the records for each set of data to the API?
Edit 1:
My index.js file with all the reducers is:
import { combineReducers } from 'redux';
import PostReducer from './PostsReducer';
import { reducer as formReducer} from 'redux-form';
import CategoriesReducer from './CategoriesReducer';
const rootReducer = combineReducers({
posts: PostReducer,
categories: CategoriesReducer,
form : formReducer
});
export default rootReducer;
EDIT 2: My updated Reducer:
import _ from 'lodash';
import { FETCH_POSTS, FETCH_POST, CREATE_POST } from '../actions/posts_action';
export default function(state = {posts: [] }, action) {
switch (action.type) {
case FETCH_POST:
return {...state, [action.payload.id]: action.payload};
case FETCH_POSTS:
return {posts: { ...state.posts, ...action.payload }};
case CREATE_POST:
return {...state, posts: [...posts, ...action.payload]};
default:
return state;
}
}
Edit 3: Screenshot of "data" content
First set your posts default value in your default state, so later we can use the spread operator on the array to generate the mutation.
export default function(state = { posts: [] }, action) {
...
}
Then, currently you are replacing your posts slice of the redux store with the payload:
case CREATE_POST:
{posts: { ...state, ...action.payload}};
The following happens in your store:
{
posts: {
posts: action.payload
},
categories,
form
}
You should instead just add the new post:
case CREATE_POST:
{...state, posts: [...state.posts, action.payload]};
The following happens in your store:
{
posts: {
posts: [post1, post2, ... , action.payload]
},
categories,
form
}
I've created a container called siteEdit.js. It handles creating & editing "sites".
I've setup actionCreators that handles taking the form data and submitting it to the API. This works perfectly.
But when you visit the container using a route that contains an ID it will run an actionCreator that will fetch for the "Site" data based on the id param.
This all works as expected, but since I'm using redux, I'm setting the Input value with the props. for example, this.props.title
I'm trying to stay away from using the redux-form package for now.
Container:
import React, {Component} from 'react';
import { connect } from 'react-redux';
import {createSite, getSite} from '../../actions/siteActions';
class SiteEdit extends Component {
constructor(props) {
super(props)
this.state = {
title: '',
url: '',
description: '',
approvedUsers: []
}
this.handleSubmit = this.handleSubmit.bind(this)
this.handleInputChange = this.handleInputChange.bind(this)
}
componentWillMount() {
if(this.props.params.id) {
this.props.dispatch(getSite(this.props.params.id))
}
}
handleInputChange(e) {
const target = e.target
const value = target.type === 'checkbox' ? target.checked : target.value
const name = target.name
this.setState({
[name]: value
})
}
handleSubmit(e) {
e.preventDefault()
this.props.dispatch(createSite(this.state))
}
render() {
const {title, url, description, approvedUsers} = this.props
return (
<div className="SiteEdit">
<h1>NEW SITE</h1>
<form onSubmit={this.handleSubmit}>
<div className="block">
<label>Site Name</label>
<input
className="input"
type="text"
value={title ? title : this.state.title}
onChange={this.handleInputChange}
name="title" />
</div>
<div className="block">
<label>Site URL</label>
<input
className="input"
type="text"
value={this.state.url}
onChange={this.handleInputChange}
name="url" />
</div>
<div className="block">
<label>Description</label>
<input
className="textarea"
type="textarea"
value={this.state.description}
onChange={this.handleInputChange}
name="description" />
</div>
<div className="block">
<label>Approved Users</label>
<input
className="input"
type="text"
value={this.state.approvedUsers}
onChange={this.handleInputChange}
name="approvedUsers" />
</div>
<button className="button--action">Create</button>
</form>
</div>
)
}
}
const mapStateToProps = (state) => ({
title: state.sites.showSite.title,
url: state.sites.showSite.url,
description: state.sites.showSite.description,
approvedUsers: state.sites.showSite.approvedUsers
})
SiteEdit = connect(mapStateToProps)(SiteEdit)
export default SiteEdit
ActionCreators:
import config from '../config'
import { push } from 'react-router-redux'
const apiUrl = config.api.url
// List all sites
export const LIST_SITES_START = 'LIST_SITES_START'
export const LIST_SITES_SUCCESS = 'LIST_SITES_SUCCES'
export const LIST_SITES_ERROR = 'LIST_SITES_ERROR'
export function sitesListStart(data) {
return { type: LIST_SITES_START, data }
}
export function sitesListSuccess(data) {
return { type: LIST_SITES_SUCCESS, data }
}
export function sitesListError(data) {
return { type: LIST_SITES_ERROR, data }
}
export function listSites() {
return (dispatch) => {
dispatch(sitesListStart())
fetch(`${apiUrl}/listSites`)
.then(res => res.json())
.then(json => {
dispatch(sitesListSuccess(json))
})
.catch(error => {
dispatch(sitesListError)
})
}
}
// Create & Edit Sites
export const CREATE_SITE_START = 'CREATE_SITE_START'
export const CREATE_SITE_SUCESS = 'CREATE_SITE_SUCCESS'
export const CREATE_SITE_ERROR = 'CREATE_SITE_ERROR'
export function siteCreateStart(data) {
return { type: CREATE_SITE_START, data}
}
export function siteCreateSuccess(data) {
return { type: CREATE_SITE_SUCCESS, data}
}
export function siteCreateError(error) {
return { type: CREATE_SITE_ERROR, error}
}
export function createSite(data) {
return (dispatch) => {
dispatch(siteCreateStart())
fetch(`${apiUrl}/createSite`, {
method: 'post',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(res => res.json())
.then(json => {
dispatch(push('/'))
dispatch(siteCreateSuccess())
})
.catch(error => {
dispatch(siteCreateError())
})
}
}
// Get Single Site
export const GET_SITE_START = 'GET_SITE_START'
export const GET_SITE_SUCCESS = 'GET_SITE_SUCCESS'
export const GET_SITE_ERROR = 'GET_SITE_ERROR'
export function getSiteStart(data) {
return { type: GET_SITE_START, data}
}
export function getSiteSuccess(data) {
return { type: GET_SITE_SUCCESS, data}
}
export function getSiteError(error) {
return { type: GET_SITE_ERROR, error}
}
export function getSite(id) {
return (dispatch) => {
dispatch(getSiteStart())
fetch(`${apiUrl}/getSite/${id}`)
.then(res => res.json())
.then(json => {
dispatch(getSiteSuccess(json))
})
.catch(error => {
dispatch(getSiteError())
})
}
}
Reducers:
import {push} from 'react-router-redux'
import {
LIST_SITES_START,
LIST_SITES_SUCCESS,
LIST_SITES_ERROR,
GET_SITE_START,
GET_SITE_SUCCESS,
GET_SITE_ERROR
} from '../actions/siteActions'
const initialState = {
sitesList: {
sites: [],
error: null,
loading: true
},
showSite: {
title: '',
url: '',
description: '',
approvedUsers: [],
loading: true
}
}
export default function (state = initialState, action) {
switch (action.type) {
// List Sites
case LIST_SITES_START:
return Object.assign({}, state, {
sitesList: Object.assign({}, state.sitesList, {
loading: true
})
})
case LIST_SITES_SUCCESS:
return Object.assign({}, state, {
sitesList: Object.assign({}, state.sitesList, {
sites: action.data,
loading: false
})
})
case LIST_SITES_ERROR:
return Object.assign({}, state, {
error: action.error,
loading: false
})
case GET_SITE_START:
return Object.assign({}, state, {
showSite: Object.assign({}, state.showSite, {
loading: true
})
})
case GET_SITE_SUCCESS:
return Object.assign({}, state, {
showSite: Object.assign({}, state.showSite, {
...action.data,
loading: false
})
})
case GET_SITE_ERROR:
return Object.assign({}, state, {
showSite: Object.assign({}, state.showSite, {
error: action.error,
loading: false
})
})
default:
return state
}
}
You are setting the ternary for the value with the props.title taking precedent, like so just to re-iterate -
const { title } = this.props;
...
value={title ? title : this.state.title}
Your onChange logic is correct and is probably updating your components local state correctly, however you still have this.props.title, so it will take precedent in that ternary.
There are a bunch of ways you could handle this, it will depend on order of operations really (that is when props.title will be truthy or not). Assuming you have the title when the component mounts you can do something in the constructor like:
constructor(props) {
super(props)
this.state = {
title: props.title, << set default here
url: '',
description: '',
approvedUsers: []
}
then in the input you only need to set the value to the state title
value={this.state.title}
This will depend on when the props.title value comes into your component of course, if it is not there for mount, this will not work as intended.
You could also pass a function to evaluate all of this for the value of the input as well - inside of which you would more verbosely check the props.title vs the state.title and decide which one to return as your value.
<input value={this.returnTitleValue} .. << something like so
Hope this helps!