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.
Related
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
I'm training React Hooks with a movie app and I now face with a problem. When I click in a button in the Header component, there should be a change of state by cleaning the itemsList array. The code is actually prepared for a Load More button, that will add more items to this array, when the API request used with other component (not present yet).
The problem is that the array is not been cleaned and when some button is cliche the items from que API Request are added to it.
This is the App.js file
import React, { useState } from "react";
import axios from "axios";
import Header from "./Containers/Header";
export default function App() {
const [values, setValues] = useState({
msdb: "API_CODE",
page: 1,
totalPages: 0,
listType: "popular",
mode: "movie",
itemsList: [],
searchFiled: "",
loading: false,
error: false
});
const { msdb, page, totalPages, listType, mode, itemsList, searchFiled, loading, error } = values;
const modeSelection = (event) => {
let modeType = "";
let selectedMode = event.target.innerText;
console.log(selectedMode);
if (selectedMode === "Movies") {
modeType = "movie";
} else if (selectedMode === "Series") {
modeType = "tv";
}
setValues((prevValues) => {
return { ...prevValues, mode: modeType, itemsList: [] };
});
let endPoint = `https://api.themoviedb.org/3/${mode}/${listType}?api_key=${msdb}&page=${page}`;
fetchItems(endPoint);
};
const fetchItems = (endPoint) => {
axios
.get(endPoint)
.then((response) => {
const newItemsList = [...itemsList];
const newItems = response.data.results;
if (newItems) {
setValues((prevValues) => {
return {
...prevValues,
page: response.data.page,
itemsList: [...newItemsList, ...newItems],
loading: false,
totalPages: response.data.total_pages
};
});
}
})
.catch((error) => {
setValues({ ...values, error: true });
console.log(error);
});
};
return (
<div className="App">
<Header mode={modeSelection} />
</div>
);
}
And this is the Header.js file
import React from "react";
import "./Header.css";
import { NavLink } from "react-router-dom";
export default function Header(props) {
return (
<div>
<div className="top-center">
<NavLink to="/movies" onClick={props.mode} className="ch-button">
Movies
</NavLink>
<NavLink to="/series" onClick={props.mode} className="ch-button">
Series
</NavLink>
</div>
</div>
);
}
So what I would like to be the result is, when clicking on Header component buttons, the itemsList array should be cleaned and the API request would populate it again. Remember that the axios method is already prepared for a Load More Button in another component and, in this case, it will add more Items to the array. Should there be a useEffect somewhere?
Thank you
The problem is about the asynchronous nature of setState. You correctly use prevState to set a new one, but in the 'fetchItems' when setting new items, you get old ones from current state and not prevState. This way the state is not yet updated with empty array when you use it for setting new items. You can try
if (newItems) {
setValues((prevValues) => {
return {
...prevValues,
page: response.data.page,
itemsList: [...prevState.itemsList, ...newItems],
loading: false,
totalPages: response.data.total_pages
};
});
}
I'm trying to use React-Redux to render the content of a small JSON.
I'm able to get the content via an Action :
(part of actions.js)
export const getUsersCount = () => {
return function (dispatch, getState) {
connectBack.get('users/count', {
headers: {
'Authorization': getState().current_user.token
}
})
.then( response => {
console.log(response.data)
dispatch(countUsers(response.data))
})
.catch( error => {
console.log('Count ', error)
})
}
}
export const countUsers= (users) => {
return {
type: 'COUNT_USERS',
payload: users
}
}
The console will correctly render the content of response.data (in this case : {users: 6}).
Then I want to render it on a simple page but it's undefined, I'm missing something...
import React from 'react'
import { connect } from 'react-redux'
import { getUnfulfilledCount, getUsersCount } from './../../tools/actions'
class Dashboard extends React.Component {
render () {
let statsUsers = this.props.usersCount()
console.log(statsUsers)
return(
<div className="container">
<div className="row">
Dashboard <br/>
Number of registered users: <br/>
{statsUsers}
</div>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
}
}
const mapDispatchToProps = (dispatch) => {
return {
usersCount: () => { dispatch(getUsersCount()) }
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Dashboard)
In addition to it, I've got the reducer set as follow:
import * as actionTypes from './actionTypes'
const initialState = {
users:[]
}
const reducer = (state = initialState, action ) => {
switch (action.type) {
case actionTypes.COUNT_USERS:
return {
...state,
users: action.payload
}
Where actionTypes.js is :
export const COUNT_USERS = 'COUNT_USERS'
The exact render from the console :
undefined Dashboard.js:12
{users: 6} actions.js:315
Obviously on the display the variable is just skipped as statsUsers is undefined:
"Number of registered users:"
I think your main problem lies in how you are mapping the resultant data back to the Dashboard component. You have a call to mapStateToProps that is empty, and a call to mapDispatchToProps that isn't necessary. I would remove the latter, and map the users property from the store in the props where you can access it in the Dashboard component, then render this.props.users instead.
Also note that by having your render function call usersCount, which maps to a call to dispatch(getUsersCount()), you're making an API call inside your render function. Making API calls while rendering is kind of a big no-no, and should be done in another place like componentDidMount.
See working Codepen here. It rewrites a little bit of your code, and adds a feature of an emulated delayed network response.
Code not enough for understand, Maybe this example will help you.
const myJsonData = [
{0: {name: "Amoos John Ghouri"}},
{1: {name: "GMK Hussain"}},
{2: {name: "Talib Hussain"}}
];
class DemoApp extends React.Component {
render() {
const items = this.props.myJsonData.map((d,i) => (<li>{d[i].name}</li>));
return (
<div>
<ul>
{items}
</ul>
</div>
)
}
}
ReactDOM.render(
<DemoApp myJsonData={myJsonData} />,
DemoAppDiv
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="DemoAppDiv"></div>
I'm trying to do an API call through an action. I am using an onchange event to do a call with also adding an ID.
I made the MapDispatchToProps where I bind my action through bindActionCreators. When I call the action, I see that he is doing the API call and gets the correct value. Only when it returns to the onchange event it is undefined.
I tried following several examples and pluralsights tutorials, but none of these work.
ACTION:
export function loadStanding(id) {
var url = "http://api.football-data.org/v2/competitions/" + id + "/standings";
return function (dispatch) {
return fetch(url,
{
mode: "cors"
})
.then(
response => response.json(),
error => console.log('An error occurred.', error)
)
.then((json) => {
console.log("=== LOADSTANDING ACTION ===");
console.log(json);
dispatch(loadStandingsSucces(json));
});
};
}
PAGE:
class HomePage extends React.Component {
constructor(props, context) {
super(props, context);
this.state = { standings: [], selectedId: 0 };
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
event.preventDefault();
this.props.actions.loadStanding(event.target.value).then(function(output) {
console.log("=== HANDLECHANGE ===");
console.log(output);
});
}
render() {
const { competitions = [] } = this.props.competitions;
const compIds = [2000,2001,2002,2003,2013,2014,2015,2016,2017,2018,2019,2021];
return (
<div className="flex-container">
<div className="row">
<div className="flex-item">
<h2>Kies een competitie:</h2>
</div>
<div className="flex-item">
<DropdownComponent onChange={this.handleChange} value="id" itemKey="id" text="name" competitions={competitions.filter(function(comp) { return compIds.includes(comp.id); })} />
</div>
<div className="flex-item">
{/* <TableComponent /> */}
</div>
</div>
</div>
);
}
}
HomePage.propTypes = {
competitions: PropTypes.any.isRequired,
actions: PropTypes.object.isRequired
};
function mapStateToProps(state) {
return {
competitions: state.competitions,
standings: state.standings
};
}
const mapDispatchToProps = (dispatch) => {
return {
actions: bindActionCreators(standingActions, dispatch)
};
};
export default connect(mapStateToProps, mapDispatchToProps)(HomePage);
First, I sugest you to check that call is working properly with Postman or similar tool.
In second place, I think that you have a little misconception about how to manage data with React and Redux.
The data you are fetching must be stored inside the redux store, when you call that action creator, the data received in the response should be dispatched to a reducer.
That reducer will store that information and then will cause your component will render again, and the fetched data will be available on the component props.
More information here: Redux data flow
I have a web app that fetches recipe from a backend API. When the feed component mounts, I set an axios get method to receive data from the API and update my redux store and then update the components state to the props matched to state of the redux store using mapPropsToState.
It works when the component is rendered initially, but moving to another component, say Create Recipe and then switching back to the Feed component, the content flashes for a mini second ad then disappears. And shows 'No Recipes To Show' which is what I set to display when there are no recipes.
I have tried using the setState in the componentDidMount method and then also in the .then method of axios, and also in both, simultaneously. Still same result. I have also tried logging the state to the console and it shows that it received the data well all the times that I switched back and forth between components, but the data wont display on screen.
FEED.JS
import React, {Component} from 'react';
import RecipeCard from './RecipeCard';
import {connect} from 'react-redux';
import {updateRecipes} from '../actions/recipeActions'
import axios from 'axios'
class Feed extends Component {
state = {
recipes: []
};
feedTitleStyle = {
color: 'rgba(230, 126, 34, 1)',
margin: '28px 0'
};
componentDidMount() {
axios.get('http://127.0.0.1:8000/api/recipes/')
.then(res =>{
console.log(res);
this.props.updateRecipesFromAPI(res.data);
this.setState({
recipes: this.props.recipes
})
})
.catch(err => {
console.log(err)
});
let recipes = [...this.state.recipes, this.props.recipes];
this.setState({
recipes
})
}
render() {
const {recipes} = this.state;
console.log(this.props.recipes);
console.log(recipes);
const recipesList = recipes.length ? (
recipes.map(recipe => {
return (
<div className="container" key={recipe.id}>
<div className='col-md-10 md-offset-1 col-lg-9 mx-auto'>
<div className="row">
<div className="col s12 m7">
<RecipeCard recipe={recipe}/>
</div>
</div>
</div>
</div>
)
})
) : (
<div className='center'>No recipes yet</div>
);
return (
<div>
<div className='container'>
<h4 style={this.feedTitleStyle} className='center feed-title'>Feed</h4>
{recipesList}
</div>
</div>
)
}
}
const mapStateToProps = (state) => {
return{
recipes: state.recipes
}
};
const mapDispatchToProps = (dispatch) => {
return {
updateRecipesFromAPI: (recipes) => {dispatch({
type: 'UPDATE_RECIPES',
recipes
}}
}
};
export default connect(mapStateToProps, mapDispatchToProps)(Feed)
Here is my reducer:
const initialState = {
recipes: [],
};
const recipeReducer = (state = initialState, action) => {
switch (action.type) {
case 'UPDATE_RECIPES':
let updatedRecipes = [...state.recipes, action.recipes];
console.log(updatedRecipes[0]);
return {
...state,
recipes: updatedRecipes[0]
};
default:
return state
}
};
export default recipeReducer
You are juggling between REDUX and State which is wrong, you should not be doing this, instead, the ideal solution would be to stick on with REDUX and let REDUX do the async call and fill in the store, and make use of the mapStateToProps to get it into props.
use Action Creators ( Async ) to solve this, you should be using middleware like thunk (Thunk) to do this.
Action creators:
export const updateRecipesFromAPI_Async = () => { // async action creator
return dispatch => {
axios.post('http://127.0.0.1:8000/api/recipes/')
.then(response => {
console.log(response.data);
dispatch(updateRecipesFromAPI_Success(response.data.name, orderData)); // calls a sync action creator
})
.catch(error => {
console.log(error);
});
}
}
export const updateRecipesFromAPI_Success = (recipes) => { // sync action creator
return {
type: 'UPDATE_RECIPES',
orderData: recipes
}
}