Ruud Luijten
Ruud Luijten

Reputation: 167

App re-renders when redux store is updated

Everytime I dispatch an action and update my store, my entire app re-renders. I assume I'm doing anything wrong w/ my connect/mapDispatchToProps function? Is it right to pass { ...actions } as a 2nd argument to my connect function in App.js?

Here's my code:

class App extends Component {
  componentDidMount() {
    this.props.fetchPages(api.API_PAGES);
    this.props.fetchPosts(api.API_POSTS);

    window.addEventListener('resize', () => {
      this.props.resizeScreen(window.innerWidth);
    });
  }

  render() {
    return (
      <div>
        {this.props.app.showIntro && <Intro {...this.props} endIntro={this.props.endIntro} />}

        {!this.props.pages.isFetching && this.props.pages.data &&
          <div>
            <Navbar {...this.props} />

            <Layout {...this.props}>
              <Switch location={this.props.location}>
                <Route
                  path={routes.HOME}
                  exact
                  component={() => (
                    <Home {...this.props} />
                  )}
                />
                <Route
                  path={routes.ABOUT}
                  component={() => (
                    <About {...this.props} />
                  )}
                />
                <Route
                  path={routes.NEWS}
                  exact
                  component={() => (
                    <News {...this.props} />
                  )}
                />
                <Route
                  component={NotFound}
                />
              </Switch>
            </Layout>
          </div>
        }
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
    app: state.app,
    pages: state.pages,
    posts: state.posts
  };
}

export default withRouter(connect(
  mapStateToProps,
  { ...actions }
)(App));

actions/index.js

export function resizeScreen(screenWidth) {
  return {
    type: types.RESIZE_SCREEN,
    screenWidth
  };
}

export function endIntro() {
  return {
    type: types.END_INTRO,
    showIntro: false
  };
}

export function toggleNav(bool) {
  return {
    type: types.TOGGLE_NAV,
    navOpen: bool
  };
}

export function toggleVideoPlayer(bool) {
  return {
    type: types.TOGGLE_VIDEO_PLAYER,
    videoIsPlaying: bool
  };
}

export function toggleScroll(bool) {
  return {
    type: types.TOGGLE_SROLL,
    disableScroll: bool
  };
}


// pages

function requestPages() {
  return {
    type: types.REQUEST_PAGES
  };
}

function receivePages(data) {
  return {
    type: types.RECEIVE_PAGES,
    data
  };
}


// posts

function requestPosts() {
  return {
    type: types.REQUEST_POSTS
  };
}

function receivePosts(data) {
  return {
    type: types.RECEIVE_POSTS,
    data
  };
}


// creators

export function fetchPages(path) {
  return (dispatch, getState) => {

    const { pages } = getState();

    if (pages.isFetching) return;

    dispatch(requestPages());
    fetch(`${process.env.API_URL}${path}`)
      .then(response => response.json())
      .then(json => dispatch(receivePages(json)));
  };
}

export function fetchPosts(path) {
  return (dispatch, getState) => {

    const { posts } = getState();

    if (posts.isFetching) return;

    dispatch(requestPosts());
    fetch(`${process.env.API_URL}${path}`)
      .then(response => response.json())
      .then(json => dispatch(receivePosts(json)));
  };
}

reducers/app.js:

const initialState = {
  screenWidth: typeof window === 'object' ? window.innerWidth : null,
  showIntro: true,
  navOpen: false,
  videoIsPlaying: false,
  disableScroll: false
};

export default function app(state = initialState, action) {
  switch (action.type) {

    case RESIZE_SCREEN: {
      return {
        ...state,
        screenWidth: action.screenWidth
      };
    }

    case TOGGLE_NAV: {
      return {
        ...state,
        navOpen: !state.navOpen
      };
    }

    case END_INTRO: {
      return {
        ...state,
        showIntro: false
      };
    }

    case TOGGLE_VIDEO_PLAYER: {
      return {
        ...state,
        videoIsPlaying: !state.videoIsPlaying
      };
    }

    case TOGGLE_SCROLL: {
      return {
        ...state,
        disableScroll: !state.disableScroll
      };
    }

    default: {
      return state;
    }
  }
}

reducers/posts.js is similar to reducers/pages.js:

const initialState = {
  isFetching: false
};

export default function posts(state = initialState, action) {
  switch (action.type) {

    case REQUEST_POSTS: {
      return {
        ...state,
        isFetching: true
      };
    }

    case RECEIVE_POSTS: {
      return {
        ...state,
        isFetching: false,
        data: action.data
      };
    }

    default: {
      return state;
    }
  }
}

Upvotes: 3

Views: 4052

Answers (2)

keul
keul

Reputation: 7819

This is how redux should work: props are changing so the connected componentin re-redered.

You can:

  • implements your shouldComponentUpdate to limit rerender (note: this will also prevent subcomponents)
  • use PureComponent instead of Component base class so you'll switch to shallow compare
  • Limit numbers of connected props, maybe you can connect subcomponents instead.

Upvotes: 1

Cory Danielson
Cory Danielson

Reputation: 14501

If you have an issue with too much of your app re-rendering with each redux update, it helps to use more connected components and limit the amount of state being passed to each one. I see that you're spreading props down into each page, this is convenient, but a common cause of inefficient re-renders.

<Home  {...this.props} />
<About {...this.props} />
<News  {...this.props} />

This could result in too much data being passed to each of these components, and each redux action causing the entire page to re-render.

Another potential issue that I see is that you're using an inline anonymous function as the component callback for your routes

<Route
    path={routes.ABOUT}
    component={() => (
        <About {...this.props} />
    )}
/>

I'm not exactly sure how React Router is working here, but a potential issue is that each time the router re-renders, those anonymous functions are created brand new again. React will see them as a new component and force a re-render. You can resolve this by making each of these a connected component that pulls in their own props, and then update the router like so

<Route
    path={routes.ABOUT}
    component={ConnectedAbout}
/>

Upvotes: 3

Related Questions