1

Okay, caveat is that I'm very very new to redux. I'm doing a course on it atm and I'm trying to step outside the box a little and generate a fairly standard website using the wordpress API and Redux. I appreciate that redux is generally meant for larger things but this seems like a useful first step in the learning process.

I have a series of components which list out posts, pages and different types of custom posts taken from the wordpress API and I navigate between these using react-router-dom.

The problem is that every time I go back to a component/view the list of posts or pages is rendered again so, for example, the first time I go there the list might be: test post 1, test post 2, the second time it would be: test post 1, test post 2, test post 1, test post 2, the third time: test post 1, test post 2, test post 1, test post 2, test post 1, test post 2 etc etc etc.

The reason for this is obvious, each time the component is rendered the data gets pulled from the store and rendered, however, as the entire app doesn't rerender as it would be with plain old reactjs, it doesn't cleared.

My question, of course is what's the best way of going about fixing this. I've read some kind of related posts which advise attaching some kind of condition to the component to check whether the data is already present but I've no idea how to do this and can't find out how. My attempts haven't worked because it seems that any var returned from componentDidMount is not seen in the render method.

Thanks in advance.

Code is below:

src/index.js

import React from "react";
import { BrowserRouter as Router } from 'react-router-dom';
import { render } from "react-dom";
import { Provider } from "react-redux";
import store from "./js/store/index";
import App from "./js/components/App";
render(
  <Router>
    <Provider store={store}>
      <App />
    </Provider>
  </Router>,
  document.getElementById("root")
);

src/js/index.js

import store from "../js/store/index";
window.store = store;

src/js/store/index.js

import { createStore, applyMiddleware, compose } from "redux";
import rootReducer from "../reducers/index";
import thunk from "redux-thunk";
const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  rootReducer,
  storeEnhancers(applyMiddleware(thunk))
);
export default store;

src/js/reducers/index.js

import { POSTS_LOADED } from "../constants/action-types";
import { PAGES_LOADED } from "../constants/action-types";

const initialState = {
  posts: [],
  pages: [],
  banner_slides: [],
  portfolio_items: []
};

function rootReducer(state = initialState, action) {

    switch (action.type) {
      case 'POSTS_LOADED':
          return Object.assign({}, state, {
            posts: state.posts.concat(action.payload)
          });

      case 'PAGES_LOADED':
          return Object.assign({}, state, {
            pages: state.pages.concat(action.payload)
          });

      default: 
          return state;
    }
}

export default rootReducer;

src/js/actions/index.js

export function getWordpress(endpoint) {
    return function(dispatch) {
        return fetch("http://localhost/all_projects/react-wpapi/my_portfolio_site/wordpress/wp-json/wp/v2/" + endpoint )
            .then(response => response.json())
            .then(json => { 
            dispatch({ type: endpoint.toUpperCase() + "_LOADED", payload: json });
        });
    };
}

src/js/constants/action-types.js

export const ADD_ARTICLE = "ADD_ARTICLE";
export const POSTS_LOADED = "POSTS_LOADED";
export const PAGES_LOADED = "PAGES_LOADED";

src/js/components/app.js

import React from "react";
import { Route, Switch, Redirect } from 'react-router-dom';

import Header from "./Header/Header";
import Posts from "./Posts";
import Pages from "./Pages";
import BannerSlides from "./BannerSlides";
import PortfolioItems from "./PortfolioItems";

const App = () => (
    <div>
        <Header />
        <Route render = {({ location }) => (
            <Switch location={location}>
            <Route 
                exact path="/posts"
                component={Posts} 
            />   
            <Route 
                exact path="/pages"
                component={Pages} 
            />   
            </Switch>
        )} />
    </div>
);
export default App;

src/js/components/Posts.js

import React, { Component } from "react";
import { connect } from "react-redux";
import { getWordpress } from "../actions/index";
export class Posts extends Component {

  componentDidMount() {
      this.props.getWordpress('posts');
      let test = 1;
      return test;
  }

  render() {
    console.log("test: ", test); // not defined
      if (test !== 1) { 
          return (
            <ul>
              {this.props.posts.map(item => ( 
                <li key={item.id}>{item.title.rendered}</li>
              ))}
            </ul>
          );
      }
  }
}
function mapStateToProps(state) {
    return {
        posts: state.posts.slice(0, 10)
    };
}
export default connect(
  mapStateToProps,
  { getWordpress }
)(Posts);
Stef
  • 359
  • 1
  • 4
  • 21

3 Answers3

1

The problem was that, every time you were fetching data, you were adding it to previous data in the array. That's why it was duplicating over time. Just assign instead of adding it in your reducer

function rootReducer(state = initialState, action) {
switch (action.type) {
  case 'POSTS_LOADED':
      return {
        ...state,
        posts: action.payload
      };

  case 'PAGES_LOADED':
      return {
        ...state,
        pages: action.payload
      };

  default: 
      return state;
 }
}

Hope it helps :)

Max
  • 1,996
  • 1
  • 10
  • 16
  • Brilliant, thanks. Looking at it now, I realise that it was the 'concat' which was doing it but this method is most succinct and works well. Out of interest, the original reducer was doing an assignment (albeit using longer syntax). How does the method you showed me know that it's to be assigned. Is it just implicit in the code / default position? Thanks :-) – Stef Jan 25 '20 at 11:58
  • It so called "Spread syntax", it works almost like `Object.assign`, but with a few different behaviors. Probably 95% of the time you can use spread syntax, which's shorter and a bit more readable. You can checkout docs about it: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax, as well as, difference between these approaches there: https://stackoverflow.com/questions/32925460/object-spread-vs-object-assign – Max Jan 25 '20 at 12:26
  • Cheers buddy, appreciated ;-) – Stef Jan 25 '20 at 16:05
1

If I'm understanding, you want to only fetch initial posts on first mount instead of every time the component is mounted?

In src/js/components/Posts.js you can check if any posts are stored in Redux before fetching inside the CDM lifecycle method. Eg.

componentDidMount() {
  // access props.posts which you set inside mapDispatchToProps
  if (this.props.posts.length === 0) {
    this.props.getWordpress('posts');
  }
}

If you are okay with duplicate API calls on every mount, and you are ok with fetching all the posts at once, you can adjust your reducer to overwrite the posts array instead of concat. But overwriting it assumes you want to load all the posts in 1 API call, instead of loading say 25 posts per page or having a 'Load more posts' button.

thor83
  • 96
  • 4
1

You need to check your state before calling fetch. I like to put mst of my logic in the redux part of the application (fat action creators) and use my react components only for rendering the current state. I would recommend something like this:

    export function getWordpress(endpoint) {
        return function(dispatch, getState) {
            const currentState = getState();
            if (currentState.posts && currentState.posts.length) {
            // already initialized, can just return current state
                return currentState.posts;
            }
            return fetch("http://localhost/all_projects/react-wpapi/my_portfolio_site/wordpress/wp-json/wp/v2/" + endpoint )
                .then(response => response.json())
                .then(json => { 
                dispatch({ type: endpoint.toUpperCase() + "_LOADED", payload: json });
            });
        };
    }

Later you could separate the logic if posts are initialized into a selector and add some additional layers (like if posts are stale). This way your 'business' logic is easily testabale and separate from your UI.

Hope this helps :)

Kaca992
  • 2,211
  • 10
  • 14