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
Related
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>
In my app component i have list of posts that contains user id, i want to display the user name and details against that user id, here's my app component's jsx:
App Component JSX:
render() {
const posts = [...someListOfPosts];
return posts.map((post) => {
return (
<div className="item" key={post.id}>
<div className="content">
<User userId={post.userId} />
</div>
</div>
);
});
}
User Component
import React from 'react';
import { connect } from 'react-redux';
import { fetchUser } from '../actions';
class UserHeader extends React.Component {
componentDidMount() {
this.props.fetchUser(this.props.userId); // getting correct userId
}
render() {
const { user } = this.props;
// Not displaying correct user i.e. showing the last resolved user for each post
return (
<div>
{user && <div className="header">{user.name}</div>}
</div>
);
}
}
const mapStateToProps = (state, props) => {
return {
user: state.user
};
}
export default connect(mapStateToProps, { fetchUser })(UserHeader);
I'm getting correct props for userId but for every post it displays the last resolved user from the api. It should be relevant user for every post.
Reducer and Action Creator
// action
export const fetchUser = (id) => {
return async (dispatch) => {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);
dispatch({
type: 'FETCH_USER',
payload: (response.status === 200 && response.data) ? response.data : null; // it returns single user not array of user
});
}
}
// reducer
export default (state = null, action) => {
switch (action.type) {
case 'FETCH_USER':
return action.payload; // i know it can be fixed by defaulting state to empty array and returning like so [...state, action.payload] but why should i return complete state why not just a single user object here?
default:
return state;
}
}
The fetchUser action creator returns single payload of a user not an array then why it's required to return the state like [...state, action.payload] why can't it be done by returning action.payload only? I've tried it by returning only action.payload but in my user component it displays the last resolved user from the api every time for each post. I'm confused regarding this.
You are subscribing to the store using mapStateToProps which rerenders when ever there is a change in the store. As you are trying to render via props in User component, the application retains the last value of user and re-renders all the old User Components as well. If you want to ignore the props updates make the result local to the component.
You can possibly try this:
import React from 'react';
import { connect } from 'react-redux';
import { fetchUser } from '../actions';
class UserHeader extends React.Component {
constructor(props){
super(props);
this.state={
userDetails:{}
}
}
componentDidMount() {
fetch(https://jsonplaceholder.typicode.com/users/${this.props.userId})
.then(res => res.json())
.then(
(result) => {
this.setState({
userDetails: result.data
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
this.setState({
isLoaded: false
});
}
)
}
render() {
return (
<div>
{this.state.userDetails && <div className="header">{this.state.userDetails.name}</div>}
</div>
);
}
}
const mapStateToProps = (state, props) => {
return {
};
}
export default connect(mapStateToProps, { fetchUser })(UserHeader);
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.
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
}
}
I'm trying to learn react-redux-saga; so i'm building a simple app which calls a random user profile api and just displays it. Basically when the user hits the button "next image" it should make a REST call and retrieve the next image. I was able to make the API call and display the information but it keeps constantly calling the API infinitely and the data keeps changing despite not clicking anything. Here is my code:
App.js (Parent component)
class App extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="App">
<p className="App-intro">
{ this.props.user !== undefined ? <ImageGenerator user={this.props.user}></ImageGenerator> : <span></span>}
</p>
</div>
);
}
}
const mapStateToProps = state => {
return { user: state.value };
};
const mapDispatchToProps = dispatch => {
return {
getNewImage: () =>
dispatch({
type: NEXT_IMAGE
})
}
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
This component is the one that makes the API call and passes the information onto the ImageGenerator component via the user prop. I noticed when I comment out the ImageGenerator line the API calls stop. I also put a log in mapDispatchToProps to verify that it's only dispatching the NEXT_IMAGE action once.
Here are my actions:
export const NEXT_IMAGE = function() { return { type: "NEXT_IMAGE" } };
export const fetchFailed = function(error) { return { type: "FETCH_FAILED", value: error } };
export const setImage = function(data) { return {type: "SET_IMAGE", value: data} };
Here is my saga:
export function* fetchImage() {
try {
const response = yield call(fetch, 'https://randomuser.me/api/');
const responseBody = yield response.json();
console.log("QWERT", responseBody.results);
yield put(setImage(responseBody.results[0]));
} catch (e) {
yield put(fetchFailed(e));
}
return;
}
export function* watchNextImage() {
yield takeEvery(NEXT_IMAGE, fetchImage);
}
export default function* rootSaga() {
yield all([
fetchImage(),
watchNextImage()
])
}
I have a suspicion it has to do with my watchNextImage function in my saga. That's the thing that puts a watch on the NEXT_IMAGE action and then calls fetchImage if it occurs. However, I don't understand why it would keep calling fetchImage if I only dispatched the NEXT_IMAGE action once...
Heres my reducer and my ImageGenerator - Probably nothing to interesting here:
reducer.js:
const rootReducer = (state = {}, action) => {
switch(action.type) {
case NEXT_IMAGE: {
return Object.assign({}, state, action)
}
case "FETCH_FAILED": {
return state;
}
case "SET_IMAGE": {
return Object.assign({}, state, action)
}
default: {
return state;
}
}
};
export default rootReducer;
ImageGenerator.js:
class ImageGenerator extends Component {
constructor(props) {
super(props);
}
render() {
let user = this.props.user;
console.log("USer", user);
return (
<div>
Name: {user.name.first} {user.name.last} <br />
Phone: {user.phone} <br />
Date of Birth: {user.dob.date} <br/>
Age: {user.dob.age} <br/>
Email: {user.email} <br />
Gender: {user.gender} <br/>
City: {user.location.city } <br />
State: {user.location.State } <br />
Street: {user.location.street } <br />
<img src={user.picture.medium} alt="No Image Found"/>
<button onClick={NEXT_IMAGE}>New Image</button>
<button>Add to Favorites</button>
</div>
)
}
}
export default ImageGenerator
Change rootSaga to only yield watchNextImage (the saga). You do not need to yield fetchImage (the side effect).
export default function* rootSaga() {
yield all([
spawn(watchNextImage)
])
}
After that, you just need to wire up your action correctly in your component.