littlechad
littlechad

Reputation: 1228

redux-observable TypeError: Cannot read property 'type' of undefined

I have been trying to implement react server-side-rendering using next, and i am using the with-redux-observable-app example, the example works fine, but i would like to improve the project a little bit by doing

  1. redux modular pattern
  2. fractal project structure
  3. If possible, i would like to implement stateless components
  4. Because #2, i can no longer use react state lifecycle, to solve that i usually took advantage of react router onEnter props, but this suggest that i should use componentWillMount, which doesn't meet my #2 condition

I have put the project on github, with this particular problem committed on this branch

Here's the summary of what i did so far

to achieve #1

// ./redux/index.js
...

import rootEpics from './root/epics'
import rootReducers from './root/reducers'

export default function initStore(initialState) {
  const epicMiddleware = createEpicMiddleware(rootEpics)
  const logger = createLogger({ collapsed: true })
  const middlewares = applyMiddleware(thunkMiddleware, epicMiddleware, logger)

  return createStore(rootReducers, initialState, middlewares)
}

// ./redux/root/epics.js
import { fetchCharacterEpic, startFetchingCharactersEpic } from '../ducks/Character/epics'

const rootEpics = combineEpics(
  fetchCharacterEpic,
  startFetchingCharactersEpic,
)

export default rootEpics

// ./redux/root/reducers.js
import { combineReducers } from 'redux'
import Character from '../ducks/Character'

const rootReducers = combineReducers({
  Character,
})

export default rootReducers


// ./redux/ducks/Character/index.js
import * as types from './types'

const INITIAL_STATE = {
  data: {},
  error: {},
  id: 1,
}

const Character = (state = INITIAL_STATE, { type, payload }) => {
  switch (type) {
    case types.FETCH_CHARACTER_SUCCESS:
      return {
        ...state,
        data: payload.response,
        id: state.id + 1,
      }
    case types.FETCH_CHARACTER_FAILURE:
      return {
        ...state,
        error: payload.error,
      }
    default:
      return state
  }
}

export default Character

// ./redux/ducks/Character/types.js
export const FETCH_CHARACTER = 'FETCH_CHARACTER'
export const FETCH_CHARACTER_SUCCESS = 'FETCH_CHARACTER_SUCCESS'
export const FETCH_CHARACTER_FAILURE = 'FETCH_CHARACTER_FAILURE'
export const START_FETCHING_CHARACTERS = 'START_FETCHING_CHARACTERS'
export const STOP_FETCHING_CHARACTERS = 'STOP_FETCHING_CHARACTERS'

// ./redux/ducks/Character/actions.js
import * as types from './types'

export const startFetchingCharacters = () => ({
  type: types.START_FETCHING_CHARACTERS,
})

export const stopFetchingCharacters = () => ({
  type: types.STOP_FETCHING_CHARACTERS,
})

export const fetchCharacter = id => ({
  type: types.FETCH_CHARACTER,
  payload: { id },
})

export const fetchCharacterSuccess = response => ({
  type: types.FETCH_CHARACTER_SUCCESS,
  payload: { response },
})

export const fetchCharacterFailure = error => ({
  type: types.FETCH_CHARACTER_FAILURE,
  payload: { error },
})

// ./redux/ducks/Character/epics.js
import 'rxjs'
import { of } from 'rxjs/observable/of'
import { takeUntil, mergeMap } from 'rxjs/operators'
import { ofType } from 'redux-observable'
import ajax from 'universal-rx-request'

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

export const startFetchingCharactersEpic = action$ => action$.pipe(
  ofType(types.START_FETCHING_CHARACTERS),
  mergeMap(() => action$.pipe(
    mergeMap(() => of(actions.fetchCharacter())),
    takeUntil(ofType(types.STOP_FETCHING_CHARACTERS)),
  )),
)

export const fetchCharacterEpic = (action$, id) => action$.pipe(
  ofType(types.FETCH_CHARACTER),
  mergeMap(() => ajax({
    url: 'http://localhost:8010/call',
    method: 'post',
    data: {
      method: 'get',
      path: `people/${id}`,
    },
  })
    .map(response => actions.fetchCharacterSuccess(
      response.body,
      true,
    ))
    .catch(error => of(actions.fetchCharacterFailure(
      error.response.body,
      false,
    )))),
)

to achieve #2

// ./pages/index/container/index.js
import React from 'react'
import { connect } from 'react-redux'
import { of } from 'rxjs/observable/of'

import rootEpics from '../../../redux/root/epics'
import { fetchCharacter } from '../../../redux/ducks/Character/actions'

import Index from '../component'

const mapStateToProps = state => ({
  id: state.Character.id,

})

const mapDispatchToProps = dispatch => ({
  async setInitialCharacter(id) {
    const epic = of(fetchCharacter({ id }))
    const resultAction = await rootEpics(
      epic,
      id,
    ).toPromise()
    dispatch(resultAction)
  },
})

export default connect(mapStateToProps, mapDispatchToProps)((props) => {
  props.setInitialCharacter(props.id)
  return (<Index />)
})


// ./pages/index/component/index.js
import React from 'react'
import Link from 'next/link'
import Helmet from 'react-helmet'

import Info from '../container/info'

const Index = () => (
  <div>
    <Helmet
      title="Ini index | Hello next.js!"
      meta={[
        { property: 'og:title', content: 'ini index title' },
        { property: 'og:description', content: 'ini index description' },
      ]}
    />
    <h1>Index Page</h1>
    <Info />
    <br />
    <nav>
      {/* eslint-disable jsx-a11y/anchor-is-valid */}
      <Link href="/other"><a>Navigate to other</a></Link><br />
      <Link href="/about"><a>Navigate to about</a></Link>
      {/* eslint-enable jsx-a11y/anchor-is-valid */}
    </nav>
  </div>
)

export default Index


// ./pages/index/container/info.js
import { connect } from 'react-redux'

import Info from '../../../components/Info'

const mapStateToProps = state => ({
  data: state.Character.data,
  error: state.Character.error,
})

export default connect(mapStateToProps)(Info)

with those above, the fetch works fine, but...

i don't want the fetch to keep running, i want it to run just once onEnter.

As an attempt to achieve that, i wrote an epic called startFetchingCharactersEpic(), and an action called startFetchingCharacters(), and lastly add mergeMap(() => of(actions.stopFetchingCharacters())), at the end of fetchCharacterEpic() pipe arguments, with the following scenario in mind

  1. dispatch actions.startFetchingCharacters(), in container
  2. that will trigger startFetchingCharactersEpic()
  3. that will do so until types.STOP_FETCHING_CHARACTERS
  4. that will dispatch actions.fetchCharacter()
  5. that will trigger fetchCharacterEpic()
  6. that will dispatch actions.stopFetchingCharacters()
  7. that will trigger #3

setInitialCharacter

// ./pages/index/container/index.js
const mapDispatchToProps = dispatch => ({
  async setInitialCharacter(id) {
    const epic = of(startFetchingCharacters())
    const resultAction = await rootEpics(
      epic,
      id,
    ).toPromise()
    dispatch(resultAction)
  },
})

but by doing that i got TypeError: Cannot read property 'type' of undefined, the console doesn't give me enough information than saying that the error is coming from setInitialCharacter

Tried googling the issue, but found nothing related to my problem

UPDATE

I manage to make it work again based on @jayphelps' answer below, which brought me back to some of my original problems, which are

  1. How to fully use stateless component without utilizing react state lifecycle, especially replacing onEnter
  2. How to just call the fetchCharacterEpic just once on page load

but i guess these 2 worth another post, as i realized i am asking too many question on this post

Upvotes: 0

Views: 1972

Answers (1)

jayphelps
jayphelps

Reputation: 15401

Totally guessing here, but it's possible that the error is coming from the fact that you're dispatching a Promise here:

const resultAction = await rootEpics(
  epic,
  id,
).toPromise()
dispatch(resultAction)

Your question doesn't mention, but that means you must have middleware that intercepts that promise since redux (and redux-observable) only expected POJOs { type: string }.

It's also possible that the promise isn't resolving to anything other than undefined, in which case the ofType operator in your epics will choke because it only works on those POJO actions { type: string }.


Sorry I can't help more specifically, it's tough to follow what the intent is.

e.g. this await rootEpics(epic, id) seems odd as rootEpics is the root epic and expects the arguments to be (action$, store) and UI components should not directly call epics?

Upvotes: 2

Related Questions