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
}
}
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'm wanting to update my trending array with the results calling the tmdb api. I'm not sure if im going about this the wrong way with calling the api or if im messing up somewhere else along the way. So far I've really been going in circles with what ive tried. Repeating the same things and not coming to a real solution. Havent been able to find another question similar to mine.
my actions
export const getTrending = url => dispatch => {
console.log("trending action");
axios.get(url).then(res =>
dispatch({
type: "TRENDING",
payload: res.data
})
);
};
my reducer
const INITIAL_STATE = {
results: [],
trending: []
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case "SEARCH_INFO":
return {
results: [action.payload]
};
case "TRENDING":
return { trending: action.payload };
default:
return state;
}
};
and my component im trying to get the results from
import React, { Component } from "react";
import Trending from "./Treding";
import "../App.css";
import { getTrending } from "../actions/index";
import { connect } from "react-redux";
export class Sidebar extends Component {
componentDidMount = () => {
const proxy = `https://cors-anywhere.herokuapp.com/`;
getTrending(`${proxy}https://api.themoviedb.org/3/trending/all/day?api_key=53fbbb11b66907711709a6f1e90fc884
`);
};
render() {
return (
<div>
<h3 className="trending">Trending</h3>
{
this.props.trending ? (
<Trending movies={this.props.trending} />
) : (
<div>Loading</div>
)}
</div>
);
}
}
const mapStateToProps = state => {
return {
trending: state.trending
};
};
export default connect(mapStateToProps)(Sidebar);
Since you are directly calling the getTrending without passing it to connect method, it might be the issue.
Instead that you can pass getTrending to connect method so it can be available as props in the component. After that it can be dispatched and it will be handled by redux/ redux-thunk.
export default connect(mapStateToProps, { getTrending })(Sidebar);
And access it as props in the component.
componentDidMount = () => {
// const proxy = `https://cors-anywhere.herokuapp.com/`;
this.props.getTrending(`https://api.themoviedb.org/3/trending/all/day?api_key=53fbbb11b66907711709a6f1e90fc884
`);
};
I try to read my articles from my firebase DB but I can't make it work.
Here is my DB:
myDB-myid
articles
1
body: "Lorem Ipsum Dolor si damet blablabla"
createdAt: 12345
subtitle: "ça promet beaucoup d'autres supers articles"
title: "Mon premier article"
2
body: "Encore du Lorem Ipsum et bla et bla et bla..."
createdAt: 34567
subtitle: "Vous avez aimé le premier ? Le deuxième va vous..."
title: "Et voici le deuxième article"
my component that should render the list of all articles, ArticlePage:
import React from 'react';
import { connect } from 'react-redux';
import Article from './Article';
import { startSetArticles } from '../../actions/articles';
const ArticlesPage = props => (
<div className="articles-wrapper">
<div className="container">
{console.log(props.articles)}
{
props.articles.length === 0 ? (
<p>No article</p>
) : (
props.articles.map(a => {
return <Article key={a.id} {...a}/>
})
)
}
</div>
</div>
);
const mapDispatchToProps = dispatch => ({
articles: () => dispatch(startSetArticles())
});
export default connect(undefined, mapDispatchToProps)(ArticlesPage);
my store:
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import authReducer from '../reducers/auth';
import articlesReducer from '../reducers/articles';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
export default () => {
const store = createStore(
combineReducers({
auth: authReducer,
articles: articlesReducer
}),
composeEnhancers(applyMiddleware(thunk))
);
return store;
};
my actions in action/articles.js:
import database from '../firebase/firebase';
// SET_ARTICLES
export const setArticles = articles => ({
type: 'SET_ARTICLES',
articles
});
export const startSetArticles = () => {
return dispatch => {
return database.ref('articles').once('value').then(snapshot => {
const articles = [];
snapshot.forEach(a => {
articles.push({
id: a.key,
...a.val()
});
});
dispatch(setArticles(articles));
});
};
};
my reducer in reducers/articles.js:
const articlesReducerDefaultState = [];
export default (state = articlesReducerDefaultState, action) => {
switch (action.type) {
case 'SET_ARTICLES':
return action.articles;
default:
return state;
}
};
But I can't make it work. Here is the console output of the console.log(props.articles) from the component:
ƒ articles() {
return dispatch((0, _articles.startSetArticles)());
}
Any Idea ? It should render the two posts but it only returns the function itself. (I get a 'no article' from the ternary operator)
articles is a function because that's exactly what you do:
const mapDispatchToProps = dispatch => ({
articles: () => dispatch(startSetArticles())
});
you map dispatch to props and you mapped it to a prop called articles which is a function that dispatches an action. No surprise here. But I guess what you also want is to map state to props which you can do with a first argument of connect:
const mapStateToProps = state => ({ articlesList: state.articles });
export default connect(mapStateToProps, mapDispatchToProps)(ArticlesPage);
Now if you do console.log(props) you should get Object with two properties:
{
articles, // a function from mapDispatchToProps
articlesList // a function from mapStateToProps
}
EDIT:
You still however need to call your articles function to fetch data with firebase and populate the redux store. Best way to start asynchronous data is to use componentDidMount() hook:
class ArticlesPage extends React.Component {
componentDidMount() {
this.props.articles(); // use mapped function to dispatch redux action
}
render() {
// render data with array from the store
return (
<div className="articles-wrapper">
<div className="container">
{this.props.articlesList.length === 0 ? (
<p>No article</p>
) : (
this.props.articlesList.map(a => {
return <Article key={a.id} {...a} />;
})
)}
</div>
</div>
);
}
}
I think you only connected to the store, but never called the function startSetArticles(). Try add(this will work with your current mapDispatchToProps)
componentDidMount() {
this.props.articles()
}
This should call the function, which should update your store.
And I would recommend instead of:
const mapDispatchToProps = dispatch => ({
articles: () => dispatch(startSetArticles())
});
You should just do this:
const mapDispatchToProps = dispatch => ({
startSetArticles: () => dispatch(startSetArticles())
});
Then you would have to do this, which makes more sense.
componentDidMount() {
this.props.startSetArticles()
}
I think your confusion is about how to use mapDispatchToProps. When you mapDispatchToProps, it DOES NOT call that function right away. You will still need to call that function somewhere in the component. In your case componenetDidMount makes sense.
And then you need to add mapStateToProps, which will connect to the store and actually get the data back.
Here is a working example:
export class Morphology extends React.Component {
componentDidMount() {
document.title = 'Eflows | Morphology';
this.props.fetchGeoSites();
}
render() {
return (
<div>
<div style={styles.banner} />
<Layout geoSites={this.props.geoSites} />
</div>
);
}
}
Morphology.propTypes = {
fetchGeoSites: PropTypes.func,
geoSites: PropTypes.array,
};
const mapStateToProps = state => {
return {
geoSites: state.geoSite.geoSites,
};
};
const mapDispatchToProps = dispatch => {
return {
fetchGeoSites: () => dispatch(fetchGeoSites()),
};
};
I am using the same method, you can look into it more with this repo:
https://github.com/leogoesger/eflow-client
You should use the first parameter of connect to get the articles state.
connect(state => ({ articles: state.articles }), ...)
To set the articles state, you need to call startSetArticles somewhere like componentDidMount or in the file where you declare your store (if you want it to be called when the app loads) with store.dispatch(startSetArticles()).