I have this class that shows a list of cars:
render() {
return (
<div>
{this.props.cars.map((car) => <Car key={car.Id} car={car} />)}
</div>
);
}
I'm making a http request to get the cars in my api:
componentDidMount() {
axios.get(`http://localhost:8000/api/cars`)
.then(res => {
const cars= res.data.records;
this.props.dispatch({
type:'GET_CARS',
cars});
})
}
}
export default connect(mapStateToProps)(ListCars);
How i can make a reducer that add to props.cars the return of axios get? my actually reducer is don't working:
const CarReducer= (state = [], action) => {
switch(action.type) {
case 'ADD_CAR':
return state.concat([action.data]);
case 'GET_CARS':
return state.map(car =>
car
)
default:
return state;
}
}
export default carReducer
You dispatched this plain object to Redux:
this.props.dispatch({ type:'GET_CARS', cars })
So, you will be able to receive your data from action.cars in your reducer and add it to your Redux store:
const CarReducer = (state = [], action) => {
switch(action.type) {
case 'ADD_CAR':
return state.concat([action.data]);
case 'GET_CARS':
// Receive data from action.cars
// I am assuming your data is in an array
return [...state, ...action.cars];
default:
return state;
}
}
Then, in your component, you should be able to receive the data from the API by using connect() from react-redux:
import { connect } from 'react-redux';
class App extends React.Component {
render() {
return (
<div>
{this.props.cars.map((car) =>
<Car key={car.Id} car={car} />
)}
</div>
);
}
}
const mapStateToProps = state => {
return { cars: state.cars }
}
export default connect(mapStateToProps)(App)
Related
My problem is that mapStateToProps returns undefined. Maybe I have some problems with dispatching in the state or maybe app rendering before data comes from the server? I can't understand. So app works right without redux with just componentDidMount, but I have some problems with redux
So I have a top-level component App:
const App = () => {
return (
<Provider store={store}>
<Screen />
</Provider>
)
}
I have store with thunk meddleware:
const store = createStore(reducer, applyMiddleware(ReduxThunk));
Two types of action:
export const fetchData = (newPhotos) => async (dispatch) => {
function onSuccess(success) {
dispatch({
type: FETCH_DATA,
payload: success})
return success
}
function onError(error) {
dispatch({type: FETCH_FAILED, error})
}
try {
const URL = 'https://api.unsplash.com/photos/?client_id=cf49c08b444ff4cb9e4d126b7e9f7513ba1ee58de7906e4360afc1a33d1bf4c0';
const res = await fetch(URL);
const success = await res.json();
console.log(success);
return onSuccess(success);
} catch (error) {
return onError(error)
}
};
reducer:
const initialState = {
data: []
}
export default dataReducer = (state = initialState, action) => {
console.log(action);
switch (action.type) {
case FETCH_DATA:
return {
data: action.payload
}
case FETCH_FAILED:
return {
state
}
default: return state;
}
}
combine reducers:
export default combineReducers({
fetchedData: dataReducer
});
and my rendering component:
class HomeScreen extends Component {
render() {
console.log(this.props)
const {navigation, data} = this.props;
return (
<ScrollView>
<Header />
<ImageList navigation={navigation} data={data}/>
</ScrollView>
)
}
}
const mapStateToProps = state => {
return {
data: state.fetchedData.data
}
}
export default connect(mapStateToProps, {fetchData})(HomeScreen);
fetchData action will not be called on its own. You need to call that explicitly(in componentDidMount and, probably, componentDidUpdate) like
class HomeScreen extends Component {
componentDidMount() {
this.props.fetchData(/* I don't know where you are going to take newPhotos argument */);
}
render() {
//...
}
}
I have a parent react component containing 3 children:
<ChildComponent category="foo" />
<ChildComponent category="bar" />
<ChildComponent category="baz" />
The child component calls an api depending on the prop category value:
http://example.com/listings.json?category=foo
In my action the data is returned as expected. However, when the child component renders the data. The last child baz is overwriting its value in foo and bar as well.
A solution to this problem seems to be given here. But I would like this to be dynamic and only depend on the category prop. Is this not possible to do in Redux?
My child component looks like this:
class TweetColumn extends Component {
componentDidMount() {
this.props.fetchTweets(this.props.column)
}
render() {
const { tweets, column } = this.props
if (tweets.length === 0) { return null }
const tweetItems = tweets[column].map(tweet => (
<div key={ tweet.id }>
{ tweetItems.name }
</div>
)
return (
<div className="box-content">
{ tweetItems }
</div>
)
}
}
TweetColumn.propTypes = {
fetchTweets: PropTypes.func.isRequired,
tweets: PropTypes.array.isRequired
}
const mapStateToProps = state => ({
tweets: state.tweets.items
})
export default connect(mapStateToProps, { fetchTweets })( TweetColumn )
reducers:
export default function tweetReducer(state = initialState, action) {
switch (action.type) {
case FETCH_TWEETS_SUCCESS:
return {
...state,
[action.data[0].user.screen_name]: action.data
}
default:
return state;
}
}
export default combineReducers({
tweets: tweetReducer,
})
action:
export const fetchTweets = (column) => dispatch => {
dispatch({ type: FETCH_TWEETS_START })
const url = `${ TWITTER_API }/statuses/user_timeline.json?count=30&screen_name=${ column }`
return axios.get(url)
.then(response => dispatch({
type: FETCH_TWEETS_SUCCESS,
data: response.data
}))
.then(response => console.log(response.data))
.catch(e => dispatch({type: FETCH_TWEETS_FAIL}))
}
You are making an api call every time TweetColumn is mounted. If you have multiple TweetColumn components and each one makes an api call, then whichever one's response is last to arrive is going to set the value of state.tweets.items. That's because you are dispatching the same action FETCH_TWEETS_SUCCESS every time (the last one overrides the previous one). To solve that issue, assuming the response has a category (foo, bar, baz), I would write the reducer in the following way:
export default function tweetReducer(state = initialState, action) {
switch (action.type) {
case FETCH_TWEETS_SUCCESS:
return {
...state,
[action.data.category]: action.data
}
default:
return state;
}
}
You can then do the following in your TweetColumn component:
class TweetColumn extends Component {
componentDidMount() {
this.props.fetchTweets(this.props.column)
}
render() {
const { column } = this.props;
const tweetItems = this.props.tweets[column].map(tweet => (
<div key={ tweet.id }>
{ tweet.name }
</div>
)
return (
<div className="box-content">
{ tweetItems }
</div>
)
}
}
const mapStateToProps = state => ({
tweets: state.tweets
})
const mapDispatchToProps = dispatch => ({
fetchTweets: column => dispatch(fetchTweets(column))
})
export default connect(
mapStateToProps,
mapDispatchToProps,
)( TweetColumn )
You will have to do some validation to make sure tweets[column] exists, but you get the idea.
I want to list posts in PostList.js component from JSON file
I use react-redux for state managment and redux-saga to get json file
My components are Post.js and PostList.js:
Post.js
const Post = ({ post }) => {
<li>
{post}
</li>
}
export default Post
PostList.js
class PostList extends React.Component {
componentDidMount() {
console.log('did mount');
this.props.fetchPosts();
}
render() {
return (
<div>
<ul>
{this.state.posts(post => (
<Post key={post.id} {...post} />
))}
</ul>
</div>
)
}
}
export default PostList
Reducer
export default (state = [], action) => {
switch (action.type) {
case "FETCH_POSTS":
return {
...state,
loading: true,
posts: []
}
case "FETCH_FAILD":
return {
...state,
loading: false,
posts: []
}
case "FETCH_SUCCESS":
return Object.assign({}, state, {
posts: action.posts
})
default:
return state;
}
}
Actions.js
export const fetchPosts = () => {
return {
type: 'FETCH_POSTS'
}
}
export const fetchSuccess = data => ({
type: "FETCH_SUCCESS",
posts: data
})
export const fetchFaild = () => {
return {
type: 'FETCH_FAILD'
}
}
GetPosts.js (Container)
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import PostList from '../components/PostList'
import { fetchPosts } from '../actions'
const mapStateToProps = state => ({ posts: state.posts });
const mapDispatchToProps = dispatch => bindActionCreators({fetchPosts}, dispatch);
const GetPosts = connect(
mapStateToProps,
mapDispatchToProps
)(PostList)
export default GetPosts
Saga.js
export function* fetchProducts() {
try {
console.log('saga')
const posts = yield call(api_fetchPost);
yield put({ type: "FETCH_SUCCESS", posts});
} catch (e) {
yield put({ type: "FETCH_FAILD", e});
return;
}
}
export function* watchFetchProducts() {
yield takeEvery("FETCH_POSTS", fetchProducts)
}
You are fetching posts from the state of your postlist component. Redux mapStateToProps map the redux state to connected component's props and not state
class PostList extends React.Component {
componentDidMount() {
console.log('did mount');
this.props.fetchPosts();
}
render() {
return (
<div>
<ul>
{this.props.posts && this.props.posts.map(post => {
return ( <Post key={post.id} {...post} /> );
})}
</ul>
</div>
)
}
}
export default PostList
Change this.state.posts to this.props.posts
First of all my code is working (everything is exported correctly etc ) but it's not waiting for the async return of data.
I'm using redux-thunk for my async middleware
I have an action named async.js
export function itemsHasErrored(bool) {
return {
type: 'ITEMS_HAS_ERRORED',
hasErrored: bool
};
}
export function itemsIsLoading(bool) {
return {
type: 'ITEMS_IS_LOADING',
isLoading: bool
};
}
export function itemsFetchDataSuccess(items) {
return {
type: 'ITEMS_FETCH_DATA_SUCCESS',
items
};
}
export function itemsFetchData(url) {
return (dispatch) => {
dispatch(itemsIsLoading(true));
fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
dispatch(itemsIsLoading(false));
return response;
})
.then((response) => response.json())
.then((items) => dispatch(itemsFetchDataSuccess(items)))
.catch(() => dispatch(itemsHasErrored(true)));
};
}
My reducer
export function itemsHasErrored(state = false, action) {
switch (action.type) {
case 'ITEMS_HAS_ERRORED':
return action.hasErrored;
default:
return state;
}
}
export function itemsIsLoading(state = false, action) {
switch (action.type) {
case 'ITEMS_IS_LOADING':
return action.isLoading;
default:
return state;
}
}
export function items(state = [], action) {
switch (action.type) {
case 'ITEMS_FETCH_DATA_SUCCESS':
return action.items;
default:
return state;
}
}
I have a container component, asyncContainer.js
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'
import {itemsFetchData} from '../../../actions/async';
import AsyncUI from './asyncUI'
class AsyncContainer extends Component {
componentDidMount() {
this.props.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
}
render() {
if (this.props.hasErrored) {
return <p>Sorry! There was an error loading the items</p>;
}
if (this.props.isLoading) {
return <p>Loading…</p>;
}
//This fails to wait
return (
<AsyncUI />
);
}
}
AsyncContainer.propTypes = {
fetchData: PropTypes.func.isRequired,
items: PropTypes.array.isRequired,
hasErrored: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired
};
const mapStateToProps = (state) => {
return {
items: state.items,
hasErrored: state.itemsHasErrored,
isLoading: state.itemsIsLoading
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (url) => dispatch(itemsFetchData(url))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(AsyncContainer);
And finally I have a simple UI component named asyncUI.js written in a functional way
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'
const AsyncUI = (items) => {
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.label}
</li>
))}
</ul>
);
}
const mapStateToProps = (state) => {
return {
items: state.items
};
};
export default connect(mapStateToProps)(AsyncUI);
In asyncContainer.js you can see the call to my simple UI component
return (
<AsyncUI />
);
But on calling the property of the redux store items in asyncUI.js an empty array, therefore the items.map fails
However, if I remove the code from asyncUI.js and place it in asyncContainer.js it works
This is the code that works in asyncContainer.js
class AsyncContainer extends Component {
componentDidMount() {
this.props.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
}
render() {
if (this.props.hasErrored) {
return <p>Sorry! There was an error loading the items</p>;
}
if (this.props.isLoading) {
return <p>Loading…</p>;
}
//THIS IS WHERE I HAD <Async />
return (
<ul>
{this.props.items.map((item) => (
<li key={item.id}>
{item.label}
</li>
))}
</ul>
);
}
}
AsyncContainer.propTypes = {
fetchData: PropTypes.func.isRequired,
items: PropTypes.array.isRequired,
hasErrored: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired
};
const mapStateToProps = (state) => {
return {
items: state.items,
hasErrored: state.itemsHasErrored,
isLoading: state.itemsIsLoading
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (url) => dispatch(itemsFetchData(url))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(AsyncContainer);
I think the problem is that the component is rendering before the items data is ready. This is normal react behavior. So how do I "hold off" the rendering. As you can see I'm trying to use a Container /Component style of architecture. I can always use the solution that works as mentioned above, but I'd like to stick with this Container /Component.
Am I going to have to delve deeper into Promises etc ?
Should I not use the functional way of writing for asyncUI.js ?
I'm a little confused.
Try:
const AsyncUI = ({items}) => {
/* ^ see ^ */
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.label}
</li>
))}
</ul>
); }
This pulls the items value off the props you reacted in the mapStateToProps function, which is an object, not an array (hence no map function).
NOTE: This should fix your issue, but it is still technically trying to render the items before they are ready in 2 instances:
The first time the component renders. The initial state for itemsIsLoading is false, so the first render will fail all the safety checks. The default value for items is [] so it should just render <ul></ul> for a very brief moment until the itemsIsLoading(true) action is dispatched. You can set the initial state to true for stop this, or change the loading check to be
if (this.props.isLoading || this.props.items.length != 0) {
return <p>Loading…</p>;
}
An argument can be made for how necessary either of those solutions actually are.
After the fetch returns the order the actions is dispatched in will result in another brief render of <ul></ul> as the loading state is set to false before the items are set. See dgrijuela's answer for one way to fix this. Another way would be to not dispatch seperate actions and have the ITEMS_FETCH_DATA_SUCCESS and ITEMS_HAS_ERRORED actions also change the itemsIsLoading value back to false (multiple reducers can act on the same action type).
You call dispatch(itemsIsLoading(false)) before dispatch(itemsFetchDataSuccess(items))
Try like this:
// async.js
...
export function itemsFetchData(url) {
return (dispatch) => {
dispatch(itemsIsLoading(true));
fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
})
.then((response) => response.json())
.then((items) => {
dispatch(itemsFetchDataSuccess(items));
dispatch(itemsIsLoading(false));
})
.catch(() => dispatch(itemsHasErrored(true)));
};
}
see Michael Peyper for a good answer
It turns out that the problem was with the functional style of coding of my asyncUI component. I converted it back to the 'standard' stateful component and bingo it worked.
asyncContainer.js
class AsyncContainer extends Component {
componentDidMount() {
this.props.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
}
render() {
if (this.props.hasErrored) {
return <p>Sorry! There was an error loading the items</p>;
}
if (this.props.isLoading) {
return <p>Loading…</p>;
}
return (
<AsyncUI />
)
}
}
asyncUI.js
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'
class AsyncUI extends Component {
render() {
return (
<ul>
{this.props.items.map((item) => (
<li key={item.id}>
{item.label}
</li>
))}
</ul>
)
}
}
const mapStateToProps = (state) => {
return {
items: state.items,
};
};
export default connect(mapStateToProps)(AsyncUI);
Hope this helps anyone :-)
I am calling an async function in componentDidMount(), I expect after the state got updated with fetched data, the component should re-render, but no.
component code:
function mapStateToProps(state){
return {
posts: state.posts
}
}
function mapDispatchToProps(dispatch){
return bindActionCreators(actionCreators, dispatch)
}
export default class Main extends React.Component{
constructor(props){
super(props)
}
componentDidMount(){
this.fetchData()
}
fetchData(){
this.props.getAllPosts().then(() => {
console.log('props: ' + JSON.stringify(this.props))
this.props.posts.data.map( post => {
console.log(post.content)
})
})
}
render(){
return(
<div>
{!this.props.loaded
? <h1>loading...</h1>
:
<div>
{this.props.posts.data.map(post => {
return(
<div>
<h2>{post.title}</h2>
<p>{post.content}</p>
</div>
)
})}
</div>
}
</div>
)
}
}
const Home = connect(mapStateToProps, mapDispatchToProps)(Main)
action:
export function fetchAllPosts(){
return{
type: 'FETCH_ALL_POSTS'
}
}
export function receivedAllPosts(posts){
return{
type: 'RECEIVED_ALL_POSTS',
post_list: posts
}
}
export function getAllPosts(){
return (dispatch) => {
dispatch(fetchAllPosts())
return fetch('/api/posts')
.then(response => response.json())
.then(json => {
dispatch(receivedAllPosts(json.data))
})
.catch(error => {
})
}
}
reducer:
export function posts(state = {loaded: false}, action){
switch(action.type){
case 'FETCH_ALL_POSTS':
return Object.assign({}, state, {
'loaded': false
})
case 'RECEIVED_ALL_POSTS':
return Object.assign({}, state, {
'data': action.post_list,
'loaded': true
})
default:
return state
}
}
in the console.log() in the componentDidMount(), I do see the data got fetched, so it means it is in the state, but not applied into the render(), i don't know why.
It is because of a simple reason: you should use this.props.posts.loaded, instead of this.props.loaded.
When you set your state to props:
function mapStateToProps(state){
return {
posts: state.posts
}
}
Here state.posts is actually the object from your reducer:
{
'data': action.post_list,
'loaded': true
}
So similar to use access your posts list via this.props.posts.data, you should use this.props.posts.loaded. I believe you can debug through debugger or console.log easily.
A live code: JSFiddle
If you're using multiple reducers with a root reducer, then you should also provide your reducer's name to the mapStateToProps function.
e.g:
rootreducer.js:
import posts from './posts';
const rootReducer = combineReducers({ posts });
component:
function mapStateToProps(state){
return {
posts: state.posts.posts
}
}