Why does mapping state to props give undefined?

2019-09-17 15:54发布

问题:

I'm having a problem with my setup of Redux. I didn't have a problem with single file of posts actions and reducers, but as soon as added a searchQueries sections, it shows only undefined values for the searchQueries props.

I've tried copying it as far as I can and modifying it for the second set of actions/reducers, but I'm still ending up with undefined props in the case of searchQueries. I'm getting all the props, including the default values in the case of posts. Here's the code for each of these:

/actions/posts.js:

import axios from 'axios'

export function postsHasErrored(bool) {
  return {
    type: 'POSTS_HAS_ERRORED',
    hasErrored: bool
  }
}

export function postsIsLoading(bool) {
  return {
    type: 'POSTS_IS_LOADING',
    isLoading: bool
  }
}

export function postsFetchDataSuccess(posts) {
  return {
    type: 'POSTS_FETCH_DATA_SUCCESS',
    posts
  }
}

export function totalPagesFetchDataSuccess(totalPages) {
  return {
    type: 'TOTAL_PAGES_FETCH_DATA_SUCCESS',
    totalPages
  }
}

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

    axios.get(url)
      .then(res => {
        if (res.status !== 200) throw Error(res.statusText)
        dispatch(postsIsLoading(false))
        return res
      })
      .then(res => {
        dispatch(postsFetchDataSuccess(res.data))
        dispatch(totalPagesFetchDataSuccess(res.headers['x-wp-totalpages']))
      })
      .catch(() => dispatch(postsHasErrored(true)))
  }
}

/actions/searchQueries.js:

const readLocation = (name) => {
    let parameter = getParameterByName(name);
    if (name === 'categories') {
      if (parameter) {
        parameter = parameter.split(',')
        for (let i = 0; i < parameter.length; i++) parameter[i] = parseInt(parameter[i], 10)
      }
      else parameter = []
    }

    if (parameter === null) {
      if (name === 'search') parameter = ''
      if (name === 'page') parameter = 1
    }

    console.log(parameter)

    return parameter
  }

export function setSearchString(searchString) {
  return {
    type: 'SET_SEARCH_STRING',
    searchString
  }
}

export function setSearchCategories(searchCategories) {
  return {
    type: 'SET_SEARCH_CATEGORIES',
    searchCategories
  }
}

export function setSearchPage(searchPage) {
  return {
    type: 'SET_SEARCH_PAGE',
    searchPage
  }
}

export function searchQueriesSetting() {
  return (dispatch) => {
    dispatch(setSearchString(readLocation('search')))
    dispatch(setSearchCategories(readLocation('categories')))
    dispatch(setSearchPage(readLocation('page')))
  }
}

/reducers/posts.js:

export function postsHasErrored(state = false, action) {
  switch (action.type) {
    case 'POSTS_HAS_ERRORED':
      return action.hasErrored

    default:
      return state
  }
}

export function postsIsLoading(state = false, action) {
  switch (action.type) {
    case 'POSTS_IS_LOADING':
      return action.isLoading

    default:
      return state
  }
}

export function posts(state = [], action) {
  switch (action.type) {
    case 'POSTS_FETCH_DATA_SUCCESS':
      return action.posts

    default:
      return state
  }
}

export function totalPages(state = 1, action) {
  switch (action.type) {
    case 'TOTAL_PAGES_FETCH_DATA_SUCCESS':
      return parseInt(action.totalPages, 10)

    default:
      return state
  }
}

/reducers/searchQueries.js:

export function searchString(state = '', action) {
  switch (action.type) {
    case 'SET_SEARCH_STRING':
      return action.searchString

    default:
      return state
  }
}

export function searchCategories(state = [], action) {
  switch (action.type) {
    case 'SET_SEARCH_CATEGORIES':
      return action.searchCategories

    default:
      return state
  }
}

export function searchPage(state = 1, action) {
  switch (action.type) {
    case 'SET_SEARCH_PAGE':
      return action.searchPage

    default:
      return state
  }
}

/reducers/index.js:

import { combineReducers } from 'redux'
import { posts, totalPages, postsHasErrored, postsIsLoading } from './posts'
import { searchString, searchCategories, searchPage } from './searchQueries'

export default combineReducers({
  posts,
  postsHasErrored,
  postsIsLoading,
  totalPages,

  searchString,
  searchCategories,
  searchPage
})

/components/PostsList.js

// dependencies
import React, { Component } from 'react'
import axios from 'axios'
import { connect } from 'react-redux'
// components
import PostsListItem from './PostsListItem'
import PostsPages from './PostsPages'
// actions
import { postsFetchData } from '../actions/posts'
import { searchQueriesSetting } from '../actions/searchQueries'
// styles
import '../styles/css/postsList.css'
// shared modules
import { createSearchUrl } from '../sharedModules/sharedModules'

class PostsList extends Component {

  componentWillReceiveProps(nextProps) {
    if (nextProps.searchPage !== this.props.searchPage) this.componentDidMount()
  }

  componentDidMount() {
    this.props.searchQueriesSetting()
    this.props.fetchData(createSearchUrl(
      'http://localhost/wordpress-api/wp-json/wp/v2/posts?per_page=1',
      this.props.searchCategories,
      this.props.searchString,
      this.props.searchPage
    ))
  }


  render() {
    console.log(this.props)
    const { isLoading, hasErrored, posts } = this.props
    if (isLoading) return <div className='posts-list'><h2 className='loading'>Loading...</h2></div>

    const postsList = posts.map(post => <PostsListItem post={post} key={post.id} />)

    return (
      <div className='posts-list'>
        {postsList}
        <PostsPages />
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    posts: state.posts,
    hasErrored: state.postsHasErrored,
    isLoading: state.postsIsLoading,
    searchCategories: state.searchCategories,
    searchString: state.searchString,
    searchPage: state.searchPage
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
    fetchData: (url) => dispatch(postsFetchData(url)),
    searchQueriesSetting: () => dispatch(searchQueriesSetting())
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(PostsList)

/components/PostsPages.js

// dependencies
import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
// actions
import { setSearchPage } from '../actions/searchQueries'
// shared modules
import { createSearchUrl } from '../sharedModules/sharedModules'

class PostsPages extends Component {
  isLinkEdgy = (pageNumber) => {
    if (parseInt(pageNumber, 10) <= 1) return ''
    if (parseInt(pageNumber, 10) >= parseInt(this.props.totalPages, 10)) return this.props.totalPages
    return pageNumber
  }

  render() {
    const { totalPages, currentPage, searchCategories, searchString, setSearchPage } = this.props

    const previousUrl = createSearchUrl('/blog', searchCategories, searchString, this.isLinkEdgy(parseInt(currentPage, 10) - 1))
    const nextUrl = createSearchUrl('/blog', searchCategories, searchString, this.isLinkEdgy(parseInt(currentPage, 10) + 1))

    return (
      <div className='posts-pages'>
        <ul className='posts-pages-list'>
          <li><Link to={previousUrl} onClick={() => setSearchPage(this.isLinkEdgy(parseInt(currentPage, 10) - 1))}>Prev page</Link></li>
          <li><Link to={nextUrl} onClick={() => setSearchPage(this.isLinkEdgy(parseInt(currentPage, 10) + 1))}>Next page</Link></li>
        </ul>
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    totalPages: state.totalPages,
    currentPage: state.searchPage,
    searchCategories: state.searchCategories,
    searchString: state.searchString
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
    setSearchPage: (searchPage) => dispatch(setSearchPage(searchPage))
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(PostsPages)

回答1:

That's because you're accessing the wrong par of state. Take a look at your combineReducers call:

export default combineReducers({
  posts,
  postsHasErrored,
  postsIsLoading,
  totalPages,

  setSearchString,
  setSearchCategories,
  setSearchPage
})

Per the Redux documentation:

combineReducers(reducers)

The shape of the state object matches the keys of the passed reducers.

Thus your state object actually looks like this:

{
  posts: ...,
  postsHasErrored: ...,
  postsIsLoading: ...,
  totalPages: ...,
  setSearchString: ...,
  setSearchCategories: ...,
  setSearchPage: ...
}

In your mapDispatchToProps, you're trying to access the wrong part of state:

currentPage: state.searchPage,
searchCategories: state.searchCategories,
searchString: state.searchString

Since state.searchPage and the other two don't exist in the state object, you get undefined. Instead, make sure you access the keys which have the same name as the reducers:

currentPage: state.setSearchPage,
searchCategories: state.setSearchCategories,
searchString: state.setSearchString

Or just rename your reducers (which would be preferable as they are misnomers right now). Get rid of the set prefix on the reducers, they are not actions.