using dispatch on react - reactjs

I am new to react and I'm trying to create a register and login page with react-redux and dispatch using the mern stack.
When I am calling the method the function did not run.
I have a file for the login page:
import React from "react";
import {login} from '../../actions/authActions';
export class Login extends React.Component {
constructor(props) {
super(props);
}
checkIfElementIsEmpty = (element) => {
if (!element) {
return false
}
if (element.value.length === 0)
{
return false;
}
return true;
}
handleOnClickLogin = () =>
{
let usernameElement = document.getElementsByName("loginUsername")[0];
let passwordElement = document.getElementsByName("loginPassword")[0];
if (!this.checkIfElementIsEmpty(usernameElement))
{
usernameElement.style.backgroundColor = "#ff000042";
return;
}
if (!this.checkIfElementIsEmpty(passwordElement))
{
passwordElement.style.backgroundColor = "#ff000042";
return;
}
console.log("asd");
login(usernameElement.value, passwordElement.value);
}
setColorToDefault = (e) =>{
e.target.style.backgroundColor = "#f3f3f3";
}
render() {
return <div className="base-container" ref={this.props.containerRef}>
<div className="header">Login</div>
<div className="content">
<div className="image">
{/* <img src={loginImg}/> */}
</div>
<div className="form">
<div className="form-group">
<label htmlFor="username">Username:</label>
<input type="text" name="loginUsername" placeholder="username" onFocus={this.setColorToDefault}/>
</div>
<div className="form-group">
<label htmlFor="password">Password:</label>
<input type="password" name="loginPassword" placeholder="password" onFocus={this.setColorToDefault}/>
</div>
</div>
</div>
<div className="footer">
<button type="button" className="btn" onClick={this.handleOnClickLogin}>
Login
</button>
</div>
</div>
}
}
and a file called "authActions.js" with the function "login" that should send the request to the server and validate the login.
export const login = (email, password) => (dispatch: Function) => {
console.log("bbb");
// Headers
const config = {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': "*"
}
}
// Request body
const body = JSON.stringify({ email, password });
axios
.post('http://${HOST}:${PORT}/api/auth/login', body, config)
.then(res =>
dispatch({
type: LOGIN_SUCCESS,
payload: res.data
})
)
.catch(err => {
dispatch(
returnErrors(err.response.data, err.response.status, 'LOGIN_FAIL')
);
dispatch({
type: LOGIN_FAIL
});
});
}
When handleOnClickLogin is called, I only see the 'aaa' on the console. The 'bbb' is never being printed.
Why this is happening and how I need to use dispatch and react-redux correctly?

Your question needs more detail, but I'll guess and give you an overview of what it should look like.
Redux has a connect method that basically will call a function you pass to it with a dispatch (and getState) parameter. So, given: login = (email, password) => (dispatch: Function). You call login(email, pass); and it returns a function (dispatch, [getState]) => xxx. Redux will handle it by calling it with the store's dispatch.
For this to work, you'll also need to configure redux globally, a store, a provider, but I'm assuming your project already does that. Otherwise you'll need to go to the official docs which are really good https://react-redux.js.org/tutorials/connect
However, if you're new to Redux and don't have all the connect set up, it'll be easier (and recommended way also) to use the Hooks API (It's also recommended in react to use Hooks rather than class components). https://react-redux.js.org/introduction/getting-started#hooks
Back to your code, the important pieces you'll need:
import React from "react";
import { login } from '../../actions/authActions';
import { connect } from 'react-redux';
class MyLoginPage extends React.Component {
handleOnClickLogin = () => {
...
// calling the login bound by redux
this.props.doLogin(usernameElement.value, passwordElement.value);
}
}
const LoginPageHOC = connect(null, {
doLogin: login, // changing names for clarity (we could do login: login)
})(MyLoginPage);
export const LoginPage = LoginPageHOC; // use the HOC instead of the class

Related

How to save states between route change in React

I have a simple application that shows the list of local hotels. Each list item has a <Link/> that redirects to another component, which displays the location on the map for that specific hotel. When switching routes, it seems that the <ProductList/> component gets destroyed and so do all the states in it. So every time when it makes new API calls and re-renders. I tried to save in local storage on each componentWillUnmount and retrieve it in useEffect() so that I can make API calls conditionally, and it works but sometimes doesn't work.
import React, { useState, useEffect} from "react";
import ProductItem from "../Components/ProductItem";
import axios from "axios";
const ProductList = () => {
const [hotelList, setHotelList] = useState([]);
// Get user location by IP
const getCurrentLocation = () => {
return fetch("https://ipinfo.io/json?token=MyToken").then(
(response) => response.json()
);
};
// Get list of hotels in specific location
const getHotelsInLocation = (destInfo) => {
console.log('destInfo is: ', destInfo)
const options = {
method: "GET",
url: "https://booking-com.p.rapidapi.com/v1/hotels/search",
params: {
checkout_date: "2022-10-01",
units: "metric",
dest_id: destInfo.destId,
dest_type: destInfo.destType,
locale: "en-gb",
adults_number: 2,
order_by: "popularity",
filter_by_currency: "USD",
checkin_date: "2022-09-30",
room_number: 1,
},
headers: {
"X-RapidAPI-Host": "booking-com.p.rapidapi.com",
"X-RapidAPI-Key": "MyApiKey",
},
};
axios
.request(options)
.then(function (response) {
console.log(response.data.result);
setHotelList(response.data.result);
})
.catch(function (error) {
console.error(error);
});
};
useEffect(() => {
getCurrentLocation().then((currentLocation) => {
console.log("Current city ", currentLocation.city);
const options = {
method: "GET",
url: "https://booking-com.p.rapidapi.com/v1/hotels/locations",
params: { locale: "en-gb", name: currentLocation.city },
headers: {
"X-RapidAPI-Host": "booking-com.p.rapidapi.com",
"X-RapidAPI-Key":
"MyApiKey",
},
};
axios
.request(options)
.then(function (response) {
console.log(response.data);
let destId = response.data[0].dest_id;
let destType = response.data[0].dest_type;
const destInfo = { destId, destType };
getHotelsInLocation(destInfo);
})
.catch(function (error) {
console.error(error);
});
});
}, []);
return (
<>
{hotelList.map((hotel) => (
<ProductItem key={hotel.hotel_id} hotel={hotel} />
))}
</>
);
};
export default ProductList;
How could I do so when coming back to <ProductList/> component, it doesn't make new API calls but just display the hotelList from the previous call.
In this case, You need to keep the datas in the centralized store. For this, you have 2 options. One is using context api which is default react feature. Another one is using redux which is seperate package.
My opinion is, you can go with context api.
A simple example of context api is given below,
Filecontext.jsx
import { createContext } from 'react'
export const Filecontext = createContext({});
Formcomponent.jsx
import { Filecontext } from '../Contexts/Filecontext';
import { useContext } from 'react'
export default function Formcomponent() {
const { setName, setEmail, setMobileno, showAlert } = useContext(Filecontext)
return (
<>
<div className="form-group">
<label>Name : </label>
<input type="text" onChange={(e) => { setName(e.target.value) }} />
</div>
<div className="form-group">
<label>Email : </label>
<input type="email" onChange={(e) => { setEmail(e.target.value) }} />
</div>
<div className="form-group">
<label>Mobile No : </label>
<input type="number" onChange={(e) => { setMobileno(e.target.value) }} />
</div>
<div className="form-group">
<input type="submit" value="submit" onClick={() => { showAlert() }} />
</div>
</>
)
}
Listcomponent.jsx
import { Filecontext } from '../Contexts/Filecontext';
import { useContext } from 'react'
export default function Listcomponent() {
const { name, email, mobileno } = useContext(Filecontext)
return (
<>
<p>Name :</p>{name}
<p>Email :</p>{email}
<p>Mobile No :</p>{mobileno}
</>
)
}
App.js
import logo from './logo.svg';
import './App.css';
import Formcomponent from './Components/Formcomponent';
import Listcomponent from './Components/Listcomponent';
import { Filecontext } from './Contexts/Filecontext';
import { useState } from 'react'
function App() {
const [name, setName] = useState("")
const [email, setEmail] = useState("")
const [mobileno, setMobileno] = useState("")
return (
<div className="App">
<Filecontext.Provider value={{ name, setName, email, setEmail, mobileno, setMobileno, showAlert }}>
<Formcomponent />
<Listcomponent />
</Filecontext.Provider>
</div>
);
}
export default App;
From the above example, The Formcomponent can be displayed in different route and Listcomponent can be displayed in different route. But even then, the datas can be retained with the help of context.
Although caching with useEffect is possible, My recommendation is to consider using one of the query caching libraries such as:
RTK Query
react-query
Apollo (if you're using GraphQL)
From my experience if you're already using redux toolkit RTK Query will be the fit for you.
The short answer is that
You need the list of hotels in a global state instead of local.
Use context / redux along with a cache policy in API calls and track your state changes.
Skip the API calls based on your application logic and memoize the query result when needed hence in effect memoize the global state.
First set up a global state manager. you can use redux, or context API. I prefer to use zustand.
second config local storage to set visited component data.
third when you navigate to a new component retrieve a list of hotels visited and check if the ID exists or not. if yes no need for an API call and if no call it and save to zustand again.

Persisting data without redux-persist or localStorage

So, once again, I've been facing this issue of persisting the state tree. In login, for the user to persist, I dispatched an action from my main App.js and got the current logged in user like this:
App.js
componentDidMount() {
const authToken = localStorage.getItem("authToken")
if (authToken) {
this.props.dispatch({ type: "TOKEN_VERIFICATION_STARTS" })
this.props.dispatch(getCurrentUser(authToken))
}
}
Now, I have a form and when it is submitted I'm redirecting the user to the feed where I will show the post title, description in a card form. But as usual, the postData is disappearing after refresh.
It means do I have to make another route, similar to the /me route that I made for getting the current logged in user? And dispatch an action again from the componentDidMount() in App.js?
NewPostForm.js
import React, { Component } from "react"
import { connect } from "react-redux"
import { addpost } from "../actions/userActions"
class NewpostForm extends Component {
constructor(props) {
super(props)
this.state = {
postTitle: "",
postDescription: "",
maxLength: 140
}
}
handleChange = (event) => {
const { name, value } = event.target
this.setState({
[name]: value
})
}
handleSubmit = () => {
const postData = this.state
this.props.dispatch(addpost(postData, () => {
this.props.history.push("/feed")
})
)
}
render() {
const charactersRemaining = (this.state.maxLength - this.state.postDescription.length)
return (
<div>
<input
onChange={this.handleChange}
name="postTitle"
value={this.state.postTitle}
className="input"
placeholder="Title"
maxLength="100"
/>
<textarea
onChange={this.handleChange}
name="postDescription"
value={this.state.postDescription}
className="textarea"
maxLength="140">
</textarea>
<button onClick={this.handleSubmit}>Submit</button>
<div>
Characters remaining: {charactersRemaining}
</div>
</div>
)
}
}
const mapStateToProps = (store) => {
return store
}
export default connect(mapStateToProps)(NewpostForm)
addPost action
export const addpost = (postData, redirect) => {
console.log("inside addpost action")
return async dispatch => {
dispatch({
type: "ADD_post_STARTS"
})
try {
const res = await axios.post("http://localhost:3000/api/v1/posts/new", postData, {
headers: {
"Content-Type": "application/json",
"Authorization": `${localStorage.authToken}`
}
})
dispatch({
type: "ADD_post_SUCCESS",
data: { post: res.data.post },
})
redirect()
} catch (err) {
dispatch({
type: "ADD_post_ERROR",
data: { error: "Something went wrong" }
})
}
}
}
Feed.js
import React from "react";
import { connect } from "react-redux";
const Feed = (props) => {
// const postTitle = (props.post && props.post.post.post.postTitle)
return (
<div className="card">
<header className="card-header">
<p className="card-header-title">
{/* {postTitle} */}
</p>
</header>
<div className="card-content">
<div className="content">
The text of the post written by the user.
</div>
</div>
<footer className="card-footer">
<a href="#" className="card-footer-item">
Edit
</a>
<a href="#" className="card-footer-item">
Delete
</a>
</footer>
</div>
);
};
const mapStateToProps = state => {
return state;
};
export default connect(mapStateToProps)(Feed);
I know you want without redux-persist but the redux normal behavior force to initialize store again from scratch. If you want to persist your state even refresh your page, I would recommend the following package:
https://github.com/rt2zz/redux-persist
If you are losing your state on a page redirect or traveling to a different route using react-router you will want to use:
https://github.com/reactjs/react-router-redux
If I understand correctly it looks like you are using response of /api/v1/posts/new in your feed page however trying to access local state of NewPostForm.js
this.state = {
postTitle: "",
postDescription: "",
maxLength: 140
}
Instead of using local state to save form data which cannot be shared to another component(unless passed as props which is not the case here) you may need to save data to redux store so that it can be shared across different route
handleChange = (event) => {
const { dispatch } = this.props;
const { name, value } = event.target;
dispatch(setPostData(name, value));
}
You action may look like:-
export const setPostData = (name, value) => ({
type: "SET_POST_DATA",
name,
value,
});
After that you can use this.props.postTitle on feed page
Edit: in order to keep state between page reload (full browser reload), you may need to either fetch all data on mount(higher order components are helpful) or use local storage.

Cant get data from api

I trying to make request geocoding to yandex maps.
ymaps.geocode(cityname) returning a promise.
I using somthing like that
action>index.js
export function addWay(text) {
return async dispatch => {
try {
const request = await window.ymaps.geocode(text)
debugger
dispatch({
type: 'ADD_WAY',
payload: request
})
}
catch (e) {}
}
}
MarkAdd.js
import React, { Component} from 'react';
import {addWay} from '../actions/index';
import { connect } from 'react-redux';
class MarkerAdd extends Component {
constructor(props) {
super(props);
this.state = {value:''}
}
onInputChange = e => {
this.setState({ value: e.target.value})
}
keyPress = e => {
if(e.keyCode === 13){
console.log('enter', e.target.value);
this.props.addWay(this.state.value);
this.setState({ value: ''})
}
}
render() {
return(
<div className="field">
<div className="control">
<input className="input is-medium"
type="text"
placeholder="Add mark"
onKeyDown={this.keyPress}
onChange={this.onInputChange}
value={this.state.value}
>
</input>
</div>
</div>
)
}
}
export default connect(null, {addWay})(MarkerAdd);
But error say: Actions must be plain objects. Use custom middleware for async actions.
(Redux Thunk is installed and connected)
Whats wrong?
If i launch it via console it actually return promise.
If you have redux-thunk installed then you can dispatch actions from component this way:
import {addWay} from '../actions/index';
...
keyPress = e => {
if(e.keyCode === 13){
this.props.dispatch(addWay(this.state.value)); // <-- dispatch action
this.setState({ value: ''})
}
}
The action itslef must return a function that accepts dispatch:
export function addWay(text) {
return async dispatch => {
try {
const request = await window.ymaps.geocode(text)
dispatch({
type: 'ADD_WAY',
payload: request
})
}
catch (e) {}
}
}

React Redux - get user after login and auth token

I have created a rails API deployed on Heroku and I want to code the front using reactJS and redux, to learn this framework. Right now, I have a login page and a register page. The register page is working without redux (for now) and the login page use redux. When a user logged in, there is a token stored in sessionStorage, but I don't understand how I can access the user data (like username, email and more).
When the user logged in, he is redirected to /dashboard/:id page, and I want to make an API call using this :id, but I don't want to use url params to do that, I want to pass state/props between my login components and the dashboard components. And I don't know how to do that using react/redux.
Here is the login form code :
import React, { Component } from 'react';
import { Link, Redirect } from 'react-router-dom';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as authActions from '../../actions/authActions';
import axios from 'axios';
class LoginForm extends Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
errors: {},
isLoading: false,
};
}
onChange(e) {
this.setState({
[e.target.name]: e.target.value
})
}
onSubmit(e) {
e.preventDefault();
this.setState({ errors: {}, isLoading: true });
this.props.actions.logInUser( { data: { user: { email: this.state.email, password: this.state.password }}})
}
render() {
return(
<div>
<form onSubmit={this.onSubmit.bind(this)}>
<div className="field">
<label className="label"> Email </label>
<div className="control">
<input type="email"
name="email"
value={this.state.email}
onChange={this.onChange.bind(this)}
className="input" />
</div>
</div>
<div className="field">
<label className="label"> Mot de passe </label>
<div className="control">
<input type="password"
ref="password"
name="password"
value={this.state.password}
onChange={this.onChange.bind(this)}
className="input" />
</div>
</div>
<div className="form-group">
<input type="submit" value="Signup" className="button is-primary" />
</div>
<Link to={{ pathname: '/register' }}>Inscription</Link>
</form>
</div>
);
}
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(authActions, dispatch)
};
}
export default connect(null, mapDispatchToProps)(LoginForm);
Here is the authActions code :
import axios from 'axios';
import setAuthorizationToken from '../utils/setAuthorizationToken';
import jwtDecode from 'jwt-decode';
import { browserHistory } from 'react-router';
import * as types from './types';
import sessionApi from '../api/SessionApi';
export function loginSuccess() {
return {
type: types.LOG_IN_SUCCESS
}
}
export function loginFailed() {
return {
type: types.LOG_IN_FAILED
}
}
export function logInUser(credentials) {
return function(dispatch) {
return sessionApi.login(credentials)
.then(response => {
console.log(response);
if(response.data) {
sessionStorage.setItem('jwt', response.data.authentication_token);
dispatch(loginSuccess());
browserHistory.push('/dashboard/' + response.data.id);
} else {
dispatch(loginFailed());
}
})
.catch(error => {
throw(error);
})
}
}
The dashboard component code right now :
import React, { Component } from 'react';
import axios from 'axios';
import * as authActions from '../../actions/authActions';
class Dashboard extends Component {
constructor(props) {
super(props);
}
render() {
return(
<div>
<h1> Hello user </h1>
</div>
);
}
}
export default Dashboard;
The sessions reducer code :
import * as types from '../actions/types';
import initialState from './initialState';
import { browserHistory } from 'react-router';
export default function sessionReducer(state = initialState.session, action) {
switch(action.type) {
case types.LOG_IN_SUCCESS:
return !!sessionStorage.jwt;
case types.LOG_IN_FAILED:
console.log('login failed');
browserHistory.push('/login');
default:
return state;
}
}
and the sessionApi with the API call :
import axios from 'axios';
class SessionApi {
static login(credentials) {
return axios.post('https://spendin-api.herokuapp.com/api/v1/sessions', credentials)
.then(response => {
console.log(response);
return response;
})
.catch(error => {
return error;
});
}
}
export default SessionApi;
I want to pass the data that I received when I do the API call, like username, id, and more..
Thank you !
EDIT
The rootReducer :
import { combineReducers } from 'redux';
import session from './sessionReducer';
const rootReducer = combineReducers({
session,
});
export default rootReducer;
Have loginSuccess take a data object parameter:
export function loginSuccess(userData) {
return {
type: types.LOG_IN_SUCCESS,
payload: userData
}
}
When you dispatch that action, pass in the data from your API call. Note, this assumes your API returns a userData object in the JSON payload, so you'll need modify it to match what your API returns:
if(response.data) {
sessionStorage.setItem('jwt', response.data.authentication_token);
dispatch(loginSuccess(response.data.userData));
Then in your reducer, put that data into the redux store:
case types.LOG_IN_SUCCESS:
return {
...state,
userData: action.payload
};
BTW your reducer doesn't look right. Two things I suggest:
Don't do anything with side effects (e.g. navigate to another page!!) in your reducer code. This will definitely cause unexpected behavior and bugs. Do anything with side effects in the action creator instead.
When modifying state in a reducer, always return a modified version of the state that was passed in. Having a reducer return a boolean is not the Redux pattern; though it might work if your reducer has only one value, it's going to confuse anyone who is used to reading Redux code. With the changes above obviously your state will no longer consist of a single boolean value.
To access user data in your dashboard component, wrap it in a call to connect(). Your new export will look something like this:
function mapStateToProps(state) {
const userData = state.sessionReducer.userData;
return { userData };
}
export default connect(mapStateToProps)(Dashboard);
This assumes you have multiple reducers that you combined with combineReducers. Your dashboard component will then receive a userData prop.

Post request with axios and redux-thunk in a redux-form

I can't figure how to make a post request with axios and redux-thunk so the action is dispatched after the query.
Here's my request module
export default {
get: function (action, url) {
return (dispatch) => {
axios.get(`${ROOT_URL}${url}`)
.then(({ data }) => {
dispatch({
type: action,
payload: data
});
});
};
},
post: function (action, url, props) {
return (dispatch) => {
axios.post(`${ROOT_URL}${url}`, props)
.then(({ data }) => {
return (dispatch) => {
dispatch({
type: action,
payload: data
});
};
});
};
}
}
The GET works. When I call the post function, it enters the function, but never run the returned function.
I tried modifying the order of the functions. I ended up with a working post request, but the action was never dispatched.
post: function (action, url, props) {
//POST works but action is not dispatched to reducer
axios.post(`${ROOT_URL}${url}`, props)
.then(({ data }) => {
return (dispatch) => {
dispatch({
type: action,
payload: data
});
};
});
}
Any idea how I can achieve a working post request that gets send to my api and then the response is dispatched to the reducer?
Thank you!
UPDATE
After extensive testing and back and forth, I think the problem is in redux-form. As pointed by Michael, the dispatch should work. I tested my call in my component with the get method and it doesn't work. Here's my component
const form = reduxForm({
form: 'LoginPage',
validate
});
const renderField = ({ input, label, type, meta: { touched, error } }) => (
<div className="form-group">
<label>{label}</label>
<div>
<input {...input} placeholder={label} type={type} className="form-control" />
{touched && ((error && <span>{error}</span>))}
</div>
</div>
)
class LoginPage extends Component {
displayName: 'LoginPage';
onSubmit(props) {
login(props.email, props.password);
}
render() {
const {handleSubmit} = this.props;
return (
<div className='row'>
<div className='center-block'>
<form onSubmit={handleSubmit(this.onSubmit.bind(this))}>
<Field name="email" type="email" label='Courriel :' component={renderField} />
<Field name="password" type="password" label='Mot de passe :' component={renderField} />
<div className="form-group text-center">
<button type="submit" className='btn btn-primary'>Se connecter</button>
</div>
</form >
</div>
</div>
);
};
};
const validate = values => {
const errors = {}
if (!values.email) {
errors.email = 'Required'
} else if (!/^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
errors.email = 'Invalid email address'
}
if (!values.password) {
errors.password = 'Required'
} 4
return errors
}
export default reduxForm({
form: 'LoginPage',
validate
})(LoginPage);
The post request is in the onSubmit method. The login method is called but the return value is never dispatched.
Again, thank you for your time and help
I found my problem. Thanks to Michael for his comment that helped me look in another direction.
The problem was that my form wasn't connected to redux. I ended up adding the connect function to my export statement.
export default connect(null, { login })
(reduxForm({
form: 'LoginPage',
validate
})(LoginPage));
And I also had to change the call to the login function in onSubmit
onSubmit(props) {
this.props.login(props.email, props.password);
}
Can't add a comment, but I'm glad you figured it out. I'd like to warn you however that on newer versions of redux-form you'll have to decorate your form outside of the connect function to redux. I ran into this problem and it drove me nuts trying to figure it out.
it would be a two step process to connect redux-form after the update. for example, it would look like this.
LoginPage = reduxForm({form: 'LoginPage', validate})(LoginPage)
and then afterwards your standard connect function
export default connect(null, actions)(LoginPage)
hope this saves you the headache in the future
your function shouldnt 'return (dispatch)' again, because what the action does is return a function. it should just be
function myFunc(action, url) {
return function (dispatch) {
axios.post(`${ROOT_URL}${url}`, props)
.then(response => {
dispatch({
type: action,
payload: response.data
})
})
}
}
edited with full function.

Resources