Adrian Grzywaczewski
Adrian Grzywaczewski

Reputation: 888

Problems with getting data from API in React-redux and Redux-Thunk

I am struggling to understand how the react-redux and thunks library work.

What I want to achieve is to get all the posts by using some API when accessing a page.

I am making my call to the API in the componentDidMount() function. From what I noticed my code gets executed exactly 3 times of which the last one gets the posts.

Here is my postReducer.js

import * as types from "../actions/actionTypes";
import initialState from "../reducers/initialState";

export function postsHaveError(state = false, action) {
  switch (action.type) {
    case types.LOAD_POSTS_ERROR:
      return action.hasError;

    default:
      return state;
  }
}

export function postsAreLoading(state = false, action) {
  switch (action.type) {
    case types.LOADING_POSTS:
      return action.isLoading;

    default:
      return state;
  }
}

export function posts(state = initialState.posts, action) {
  switch (action.type) {
    case types.LOAD_POSTS_SUCCESS:
      return action.posts;

    default:
      return state;
  }
}
// export default rootReducer;

postAction.js

import * as types from "./actionTypes";
import axios from "axios";

    export function postsHaveError(bool) {
      return {
        type: types.LOAD_POSTS_ERROR,
        hasError: bool
      };
    }

    export function postsAreLoading(bool) {
      return {
        type: types.LOADING_POSTS,
        isLoading: bool
      };
    }

    export function postsFetchDataSuccess(posts) {
      return {
        type: types.LOAD_POSTS_SUCCESS,
        posts
      };
    }

    export function postsFetchData(url) {
      return dispatch => {
        dispatch(postsAreLoading(true));

        axios
          .get(url)
          .then(response => {
            if (response.status !== 200) {
              throw Error(response.statusText);
            }

            dispatch(postsAreLoading(false));

            return response;
          })
          .then(response => dispatch(postsFetchDataSuccess(response.data)))
          .catch(() => dispatch(postsHaveError(true)));
      };
    }

and the component in which i am trying to get the posts.

import React from "react";
import PostItem from "./PostItem";
import { connect } from "react-redux";
import { postsFetchData } from "../../actions/postActions";

class BlogPage extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      data: null
    };
  }

  componentDidMount() {
   this.props.fetchData("http://localhost:3010/api/posts");
  }

  render() {
    if (this.props.hasError) {
      return <p>Sorry! There was an error loading the items</p>;
    }

    if (this.props.isLoading) {
      return <p>Loading…</p>;
    }

    console.log(this.props);
    return (
      <div>
        <div className="grid-style">
          <PostItem <<once i have posts they should go here>> />
        </div>
      </div>
    );
  }
}

const mapStateToProps = state => {
  return {
    posts: state.posts,
    hasError: state.postsHaveError,
    isLoading: state.postsAreLoading
  };
};

const mapDispatchToProps = dispatch => {
  return {
    fetchData: url => dispatch(postsFetchData(url))
  };
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(BlogPage);

index.js

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import registerServiceWorker from "./registerServiceWorker";
import { BrowserRouter } from "react-router-dom";
import configureStore from "./store/configureStore";
import { Provider } from "react-redux";

const store = configureStore();

ReactDOM.render(
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>,
  document.getElementById("root")
);

registerServiceWorker();

app.js

import React, { Component } from "react";
import "./App.css";
import Header from "./components/common/header.js";
import Footer from "./components/common/footer.js";
import Main from "./components/common/main.js";
import "./layout.scss";

class App extends Component {
  render() {
    return (
      <div className="App">
        <Header />
        <Main />
        <Footer />
      </div>
    );
  }
}

export default App;

and main.js in which BlogPage resides.

import React from 'react';
import BlogPage from '../blog/BlogPage';
import AboutPage from '../about/AboutPage';
import { Route, Switch } from 'react-router-dom';
import LoginPage from '../authentication/LoginPage';

const Main = () => {
  return (
    <div>
      <section id="one" className="wrapper style2">
        <div className="inner">
          <Switch>
            <Route path="/about" component={AboutPage} />
            <Route path="/login" component={LoginPage} />
            <Route path="/" component={BlogPage} />
          </Switch>
        </div>
      </section>
    </div>
  );
};

export default Main;

Upvotes: 1

Views: 1141

Answers (1)

Matt Carlotta
Matt Carlotta

Reputation: 19762

Your problem is very similar to this question (I also include a codesandbox to play around with). Please read through it and follow the working example and, most importantly, read the 7 tips (some may not apply to your project; however, I highly recommend you install prop-types to warn you when you're deviating from a 1:1 redux state).

The problem you're facing is related to this function postsFetchData not returning the axios promise (you also have an unnecessary .then() that has been removed -- this example flows with the example provided below):

actions/blogActions.js

import * as types from '../types';

export const postsFetchData = () => dispatch => {
  // dispatch(postsAreLoading(true)); // <== not needed

  return axios
    .get("http://localhost:3010/api/posts") // API url should be declared here
    .then(({ data }) => { // es6 destructuring, data = response.data
       /* if (response.status !== 200) {
          throw Error(response.statusText);
        } */ // <== not needed, the .catch should catch this

       // dispatch(postsAreLoading(false)); // <== not needed
       // dispatch(postsFetchDataSuccess(response.data)) // <== not needed, just return type and payload

       dispatch({ type: types.LOAD_POSTS_SUCCESS, payload: data })
     })
    .catch(err => dispatch({ type: types.LOAD_POSTS_ERROR, payload: err.toString() }));
}

As mentioned in the linked question, you don't need an isLoading with Redux connected container-components. Since the props are coming from redux's store, React will see the prop change and update the connected component accordingly. Instead, you can either use local React state OR simply just check to see if the data is present.

The example below checks if data is present, otherwise it's loading...

BlogPage.js

import isEmpty from "lodash/isEmpty";
import React, { PureComponent } from "react";
import { connect } from "react-redux";
import PostItem from "./PostItem";
import { postsFetchData } from "../../actions/blogActions";

class BlogPage extends PureComponent {

  componentDidMount => () => this.props.postsFetchData(); // the API url should be placed in action creator, not here, especially if it's static 

  render = () => (
    this.props.hasError // if this was an error...
      ? <p>Sorry! There was an error loading the items: {this.props.hasError}</p> // then an show error
      : isEmpty(this.props.posts) // otherwise, use lodash's isEmpty to determine if the posts array exists AND has a length of 0, if it does...
         ? <p>Loading…</p> // then show loading...
         : <div className="grid-style"> // otherwise, if there's no error, and there are posts in the posts array...
             <PostItem posts={this.props.posts} /> // then show PostItem
           </div>
   )
}

export default connect(state => ({ 
  // this is just inline mapStateToProps
  posts: state.blog.posts 
  hasError: state.blog.hasError
}),
  { postsFetchData } // this is just an inline mapDispatchToProps
)(BlogPage);

reducers/index.js

import { combineReducers } from 'redux';
import * as types from '../types';

const initialState = {
  posts: [], // posts is declared as an array and should stay that way
  hasError: '' // hasError is declared as string and should stay that way
}

const blogPostsReducer = (state = initialState, { type, payload }) => {
  switch (type) {
    case types.LOAD_POSTS_SUCCESS:
      return { ...state, posts: payload, hasError: '' }; // spread out any state, then update posts with response data and clear hasError
    case types.LOAD_POSTS_ERROR:
      return { ...state, hasError: payload }; // spread out any state, and update hasError with the response error
    default:
      return state;
  }
}

export default combineReducers({
  blog: blogPostReducer
  // include any other reducers here
})

BlogPage.js (with isLoading local React state)

import isEqual from "lodash/isEqual";
import isEmpty from "lodash/isEmpty";
import React, { Component } from "react";
import { connect } from "react-redux";
import PostItem from "./PostItem";
import { postsFetchData } from "../../actions/blogActions";

class BlogPage extends Component {
  state = { isLoading: true };

  componentDidUpdate = (prevProps) => { // triggers when props have been updated
    const { posts } =  this.props; // current posts
    const prevPosts = prevProps.posts; // previous posts
    const { hasError } = this.props; // current error
    const prevError = prevProps.hasError // previous error

    if (!isEqual(posts,prevPosts) || hasError !== prevError) { // if the current posts array is not equal to the previous posts array or current error is not equal to previous error...
      this.setState({ isLoading: false }); // turn off loading
    }
  }

  componentDidMount => () => this.props.postsFetchData(); // fetch data

  render = () => (
    this.state.isLoading // if isLoading is true...
      ? <p>Loading…</p> // then show loading...          
      : this.props.hasError // otherwise, if there was an error...
          ? <p>Sorry! There was an error loading the items: {this.props.hasError}</p> // then an show error
          : <div className="grid-style"> // otherwise, if isLoading is false and there's no error, then show PostItem
              <PostItem posts={this.props.posts} />
            </div>
   )
}

export default connect(state => ({ 
  // this is just inline mapStateToProps
  posts: state.blog.posts 
  hasError: state.blog.hasError
}),
  { postsFetchData } // this is just an inline mapDispatchToProps
)(BlogPage);

Upvotes: 2

Related Questions