I have a form in react where I'm asking for the last 8 of the VIN of a car. Once I get that info, I want to use it to get all the locations of the car. How do I do this? I want to call the action and then display the results.
Added reducer and actions...
Here is what I have so far...
class TaglocaByVIN extends Component {
constructor(props){
super(props);
this.state={
searchvin: ''
}
this.handleFormSubmit=this.handleFormSubmit.bind(this);
this.changeText=this.changeText.bind(this);
}
handleFormSubmit(e){
e.preventDefault();
let searchvin=this.state.searchvin;
//I want to maybe call the action and then display results
}
changeText(e){
this.setState({
searchvin: e.target.value
})
}
render(){
return (
<div>
<form onSubmit={this.handleFormSubmit}>
<label>Please provide the last 8 characters of VIN: </label>
<input type="text" name="searchvin" value={this.state.searchvin}
onChange={this.changeText}/>
<button type="submit">Submit</button>
</form>
</div>
);
}
}
export default TaglocaByVIN;
Here are my actions:
export function taglocationsHaveError(bool) {
return {
type: 'TAGLOCATIONS_HAVE_ERROR',
hasError: bool
};
}
export function taglocationsAreLoading(bool) {
return {
type: 'TAGLOCATIONS_ARE_LOADING',
isLoading: bool
};
}
export function taglocationsFetchDataSuccess(items) {
return {
type: 'TAGLOCATIONS_FETCH_DATA_SUCCESS',
items
};
}
export function tagformsubmit(data){
return(dispatch) =>{
axios.get(`http://***`+data)
.then((response) => {
dispatch(taglocationsFetchDataSuccess);
})
};
}
reducer:
export function tagformsubmit(state=[], action){
switch (action.type){
case 'GET_TAG_FORM_TYPE':
return action.taglocations;
default:
return state;
}
}
This is an easy fix but it will take a few steps:
Set up an action
Set up your reducer
Fetch and Render data in component
Creating the Action
The first thing, you need to set up an action for getting data based on a VIN. It looks like you have that with your tagformsubmit function. I would make a few adjustments here.
You should include a catch so you know if something went wrong, change your function param to include dispatch, add a type and a payload to your dispatch, and fix the string literal in your api address. Seems like a lot but its a quick fix.
Update your current code from this:
export function tagformsubmit(data){
return(dispatch) =>{
axios.get(`http://***`+data)
.then((response) => {
dispatch(taglocationsFetchDataSuccess);
})
};
}
to this here:
//Get Tag Form Submit
export const getTagFormSubmit = vin => dispatch => {
dispatch(loadingFunctionPossibly()); //optional
axios
.get(`/api/path/for/route/${vin}`) //notice the ${} here, that is how you use variable here
.then(res =>
dispatch({
type: GET_TAG_FORM_TYPE,
payload: res.data
})
)
.catch(err =>
dispatch({
type: GET_TAG_FORM_TYPE,
payload: null
})
);
};
Creating the Reducer
Not sure if you have already created your reducer. If you have you can ignore this. Creating your reducer is also pretty simple. First you want to define your initial state then export your function.
Example
const initialState = {
tags: [],
tag: {},
loading: false
};
export default (state=initialState, action) => {
if(action.type === GET_TAG_FORM_TYPE){
return {
...state,
tags: action.payload,
loading: false //optional
}
}
if(action.type === GET_TAG_TYPE){
return {
...state,
tag: action.payload,
}
}
}
Now that you have your action and reducer let's set up your component.
Component
I'm going to assume you know all of the necessary imports. At the bottom of your component, you want to define your proptypes.
TaglocaByVIN.propTypes = {
getTagFormSubmit: PropTypes.func.isRequired,
tag: PropTypes.object.isRequired
};
mapStateToProps:
const mapStateToProps = state => ({
tag: state.tag
});
connect to component:
export default connect(mapStateToProps, { getTagFormSubmit })(TaglocaByVIN);
Update your submit to both pass data to your function and get the data that is returned.
handleFormSubmit = (e) => {
e.preventDefault();
const { searchvin } = this.state;
this.props.getTagFormSubmit(searchvin);
const { tags } = this.props;
tags.map(tag => {
//do something with that tag
}
Putting that all together your component should look like this (not including imports):
class TaglocaByVIN extends Component {
state = {
searchvin: ""
};
handleFormSubmit = e => {
e.preventDefault();
const { searchvin } = this.state;
this.props.getTagFormSubmit(searchvin);
const { tags } = this.props.tag;
if(tags === null){
//do nothing
} else{
tags.map(tag => {
//do something with that tag
});
};
}
changeText = e => {
this.setState({
searchvin: e.target.value
});
};
render() {
return (
<div>
<form onSubmit={this.handleFormSubmit}>
<label>Please provide the last 8 characters of VIN: </label>
<input
type="text"
name="searchvin"
value={this.state.searchvin}
onChange={this.changeText}
/>
<button type="submit">Submit</button>
</form>
</div>
);
}
}
TaglocaByVIN.propTypes = {
getTagFormSubmit: PropTypes.func.isRequired,
tag: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
tag: state.tag
});
export default connect(
mapStateToProps,
{ getTagFormSubmit }
)(TaglocaByVIN);
That should be it. Hope this helps.
Related
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 parent react component containing 3 children:
<ChildComponent category="foo" />
<ChildComponent category="bar" />
<ChildComponent category="baz" />
The child component calls an api depending on the prop category value:
http://example.com/listings.json?category=foo
In my action the data is returned as expected. However, when the child component renders the data. The last child baz is overwriting its value in foo and bar as well.
A solution to this problem seems to be given here. But I would like this to be dynamic and only depend on the category prop. Is this not possible to do in Redux?
My child component looks like this:
class TweetColumn extends Component {
componentDidMount() {
this.props.fetchTweets(this.props.column)
}
render() {
const { tweets, column } = this.props
if (tweets.length === 0) { return null }
const tweetItems = tweets[column].map(tweet => (
<div key={ tweet.id }>
{ tweetItems.name }
</div>
)
return (
<div className="box-content">
{ tweetItems }
</div>
)
}
}
TweetColumn.propTypes = {
fetchTweets: PropTypes.func.isRequired,
tweets: PropTypes.array.isRequired
}
const mapStateToProps = state => ({
tweets: state.tweets.items
})
export default connect(mapStateToProps, { fetchTweets })( TweetColumn )
reducers:
export default function tweetReducer(state = initialState, action) {
switch (action.type) {
case FETCH_TWEETS_SUCCESS:
return {
...state,
[action.data[0].user.screen_name]: action.data
}
default:
return state;
}
}
export default combineReducers({
tweets: tweetReducer,
})
action:
export const fetchTweets = (column) => dispatch => {
dispatch({ type: FETCH_TWEETS_START })
const url = `${ TWITTER_API }/statuses/user_timeline.json?count=30&screen_name=${ column }`
return axios.get(url)
.then(response => dispatch({
type: FETCH_TWEETS_SUCCESS,
data: response.data
}))
.then(response => console.log(response.data))
.catch(e => dispatch({type: FETCH_TWEETS_FAIL}))
}
You are making an api call every time TweetColumn is mounted. If you have multiple TweetColumn components and each one makes an api call, then whichever one's response is last to arrive is going to set the value of state.tweets.items. That's because you are dispatching the same action FETCH_TWEETS_SUCCESS every time (the last one overrides the previous one). To solve that issue, assuming the response has a category (foo, bar, baz), I would write the reducer in the following way:
export default function tweetReducer(state = initialState, action) {
switch (action.type) {
case FETCH_TWEETS_SUCCESS:
return {
...state,
[action.data.category]: action.data
}
default:
return state;
}
}
You can then do the following in your TweetColumn component:
class TweetColumn extends Component {
componentDidMount() {
this.props.fetchTweets(this.props.column)
}
render() {
const { column } = this.props;
const tweetItems = this.props.tweets[column].map(tweet => (
<div key={ tweet.id }>
{ tweet.name }
</div>
)
return (
<div className="box-content">
{ tweetItems }
</div>
)
}
}
const mapStateToProps = state => ({
tweets: state.tweets
})
const mapDispatchToProps = dispatch => ({
fetchTweets: column => dispatch(fetchTweets(column))
})
export default connect(
mapStateToProps,
mapDispatchToProps,
)( TweetColumn )
You will have to do some validation to make sure tweets[column] exists, but you get the idea.
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.
I created component and connected it using connect() like this:
function mapStateToProps(state) {
return { users: state.users.users }
}
function mapDispatchToProps(dispatch) {
return { userActions: bindActionCreators(UserActions, dispatch) }
}
export default connect(mapStateToProps, mapDispatchToProps)(Test)
Sadly when I open React tools in chrome I get this:
Changing the state isn't forcing component to update. Why it isn't subscribing to it?
EDIT:
I'm changing state through action creator and reducer. This is how it looks like:
export function addUser(user) {
return function (dispatch) {
axios.post('http://localhost:4200/users/add/user', {user:user})
.then(() => dispatch({
type: USER_ADD,
user: user
})).catch(err => console.log(err));
}
}
export function getUsers() {
return function (dispatch) {
axios.get('http://localhost:4200/users')
.then((response) => dispatch({
type: REQUEST_USERS,
users:response.data
})).catch(err => console.log(err));
}
}
and reducer:
export function users(state = initialState, action) {
switch (action.type) {
case 'USER_ADD':
{
return {
...state,
users: [
...state.users,
action.user
]
}
break;
}
case 'REQUEST_USERS':
{
return {
...state,
users: [
action.users
]
}
break;
}
.........
and this is my full component:
class Test extends Component {
constructor(props) {
super(props);
this.state = {
login: "",
password: ""
}
}
handleLoginInputChange = (e) => {
this.setState({login: e.target.value})
}
handlePasswordInputChange = (e) => {
this.setState({password: e.target.value})
}
handleSubmit = (e) => {
e.preventDefault()
let user = {login:this.state.login,
password:this.state.password,userId:Math.floor(Math.random()*(100000))};
this.props.userActions.addUser(user);
}
render() {
return (
<div className="test">
<form>
<input type="text" onChange={this.handleLoginInputChange} value=
{this.state.login} placeholder="login"/>
<input type="text" onChange={this.handlePasswordInputChange}
value={this.state.password} placeholder="pass"/>
<button onClick={this.handleSubmit}>send</button>
</form>
<UsersList users = {this.props.users} />
</div>
)
}
componentDidMount() {
this.props.userActions.getUsers();
}
}
function mapStateToProps(state) {
return { users: state.users.users }
}
function mapDispatchToProps(dispatch) {
return { userActions: bindActionCreators(UserActions, dispatch) }
}
export default connect(mapStateToProps, mapDispatchToProps)(Test)
I've added everything to github so you could see it github.
Things that could interest you are in reducers/users.js, actions/useractions and in components/Test.js.
My state after getting data from server looks like this:.
I tried many different approaches. Right now I gave up changing the state after adding new user. Instead of that I've made button which can get data from the server - it's reloading the page so I'm not pleased with that solution
Are you sure that this is correct?
function mapStateToProps(state) {
return { users: state.users.users }
}
how are you binding your reducer to the store? The state might be different from what you expect in connect. Can you post what the store contains after the component gets mounted?
For some reason react is not re-rendering when the (redux) state changed. Might it be because it is nested 1 level? Also in the redux inspector I can see that the state has changed correctly and as expected. On manually refreshing the page, I can also see that it worked. But I am confused on why it is not re-rendering automatically.
Simplified Class Component
class Users extends Component {
render() {
const something = this.props.users.users;
return (
<div>
<div className='MAIN_SECTION'>
<SearchBar handleChange={(value) => this.setState({searchQuery: value})} />
<div className='LIST'>
{something.mapObject((user) => <div onClick={() => this.props.deleteUser(user.id)}>{user.name}</div>)}
</div>
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
users: state.users
};
}
export default connect(mapStateToProps, { deleteUser })(Users);
Action Creator
export function deleteUser(userhash) {
return function (dispatch, getState) {
return new Promise(resolve => setTimeout(resolve, 1000)).then(() => {
const data = {
status: 200,
data: {
status: 200
}
};
if (data.status === 200) {
const newState = getState().users.users;
delete newState[userhash];
dispatch({
type: DELETE_USER,
payload: {
data: newState
}
});
}
});
};
}
Reducer
const INITIAL_STATE = {
isFetching: true,
users: {}
};
case DELETE_USER:
return {
...state,
users: action.payload.data
};
Once you dispatch action DELETE_USER,
const something = this.props.users.users;
doesn't look right.
You are doing this in action which means something must be this.props.user.
const newState = getState().users.users;
delete newState[userhash];
dispatch({
type: DELETE_USER,
payload: {
data: newState
}
});
In your render, this.props.users itself is the array of users.
To have a dirty fix, changing mapStateToProps to:
function mapStateToProps(state) {
return {
users: state.users.users || state.users
};
}
Change in render:
const something = this.props.users;
The code doesn't really jive to me. In one spot you are you are using .map() on users.users and in another spot you are using delete[newHash] on it. So is it an array, or an object?
But hypothetically, let's say it did work. You say that that the state has changed correctly.
Let's assume the initial state is
{
isFetching: true,
users: { users: ['12345', '67890'] }
}
Let say user '12345' gets deleted, in the action, we have these sequence of events
const newState = getState().users.users; // ['12345', '67890']
delete newState[userhash]; // ['67890'] if you use your imagination
dispatch({
type: DELETE_USER,
payload: {
data: newState // ['67890']
})
Finally, in the reducer
case DELETE_USER:
return {
...state,
users: action.payload.data // ['67890']
};
So the final state is
{
isFetching: true,
users: ['67890']
}
It's not the same shape as the original state - users no longer has a users.users.