Search code examples
reactjsreduxreact-reduxreact-hooksredux-thunk

Redux, access data inside API object with useSelector


I'm new to React, Redux and hooks and I'm building a simple application. First component is a search bar, you type a pokemon name, it calls an API (pokeapi)(I also use react-thunk) and returns a new component with information regarding this pokemon.

I'm building the result page component, I can console.log the state and see the whole object, but I cannot manipulate anything inside of the object.

e.g: console.log(pokemonState) returns the whole object and it's nested properties // console.log(pokemonState.name) returns undefined.

Here is my code: App.js

import { Route, NavLink, Redirect } from 'react-router-dom';
import PokemonSearch from './container/PokemonSearch';
import PokemonResult from './container/PokemonResult';

function App() {
  return (
    <div className="App">
      <nav>
        <NavLink to={'/'}>Search</NavLink>
      </nav>
      <h1>TEST</h1>
      <Route path={'/'} exact component={PokemonSearch} />
      <Route path={'/pokemon/:pokemon'} exact component={PokemonResult} />
      <Redirect to={'/'} />
    </div>
  );
}

export default App;

I didn't paste it above, but the top-level index.js is wrapped by Provider as well. PokemonSearch.js

import React, { useState } from 'react';
import { getPokemonData } from '../actions/pokemonAction';
import { useDispatch } from 'react-redux';
import '../styles/PokemonSearch.css';

const PokemonSearch = (props) => {
  const [search, setSearch] = useState('');
  const dispatch = useDispatch();

  const FetchData = () => {
    dispatch(getPokemonData(search));
  };

  const handleChange = (e) => {
    setSearch(e.target.value);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    FetchData();
    props.history.push(`/pokemon/${search}`);
  };

  return (
    <div>
      <div className="bar">
        <form onSubmit={handleSubmit}>
          <input
            type="text"
            className="searchInput"
            placeholder="Search a Pokemon"
            onChange={handleChange}
          />
        </form>
      </div>
      <div></div>
    </div>
  );
};

export default PokemonSearch;

Action

import axios from 'axios';

export const getPokemonData = (pokemon) => async (dispatch) => {
  try {
    dispatch({
      type: 'POKEMON_DATA_LOADING',
    });

    const res = await axios.get(`https://pokeapi.co/api/v2/pokemon/${pokemon}`);

    dispatch({
      type: 'POKEMON_DATA_SUCCESS',
      payload: res.data,
      pokemonName: pokemon,
    });
  } catch (e) {
    dispatch({
      type: 'POKEMON_DATA_FAIL',
    });
  }
};

Reducer

const DefaultState = {
  loading: false,
  data: [],
  errorMsg: '',
};

const PokemonSearchReducer = (state = DefaultState, action) => {
  switch (action.type) {
    case 'POKEMON_DATA_LOADING':
      return {
        ...state,
        loading: true,
        errorMsg: '',
      };
    case 'POKEMON_DATA_FAIL':
      return {
        ...state,
        loading: false,
        errorMsg: "Error: cannot find the pokemon you're looking for.",
      };
    case 'POKEMON_DATA_SUCCESS':
      return {
        ...state,
        loading: false,
        errorMsg: '',
        data: {
          ...state.data,
          data: action.payload,
        },
      };

    default:
      return state;
  }
};

export default PokemonSearchReducer;

Root Reducer and Store

import { combineReducers } from 'redux';
import PokemonSearchReducer from './PokemonSearchReducer';

const rootReducer = combineReducers({
  PokemonSearch: PokemonSearchReducer,
});

export default rootReducer;

import { createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import { applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/rootReducer';

const Store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(thunk))
);

export default Store;

PokemonResult.js

import { useSelector } from 'react-redux';

const PokemonResult = (props) => {
  const pokemonState = useSelector((state) => state.PokemonSearch);
  console.log(pokemonState.name);

  return (
    <div>
      <h1>Title</h1>
    </div>
  );
};

export default PokemonResult;

console.log(PokemonState) output: what apis return when you type a valid pokemon name

So I'm wondering what's wrong and why can I console.log the whole state but not particular properties in my component (PokemonResult)?

Thanks for your help.


Solution

  • There is multiple wrong things in your code:

    you declare a default state with data = empty array

    // use lowerCamelCase for variable, it can be confusing as PascalCase is used for class / function component
    const DefaultState = {
      loading: false,
      data: [],
      errorMsg: '',
    };
    

    then in your reducer the array is now an object named data that contain a data object (?) { data: { data: { yourpayload }, ... }

        case 'POKEMON_DATA_SUCCESS':
          return {
            ...state,
            loading: false,
            errorMsg: '',
    
            data: {
              ...state.data,
              data: action.payload,
            },
          };
    

    then in your action getPokemonData you dispatch POKEMON_DATA_SUCCESS with pokemonName but you don't use it inside your reducer

    dispatch({
        type: 'POKEMON_DATA_SUCCESS',
        payload: res.data,
        pokemonName: pokemon,
    });
    
    

    and finally your useSelector select the whole state pokemonSearch so you get back { loading: boolean, errorMsg: string, data: { data: pokemon }}

    import { useSelector } from 'react-redux';
    
    const PokemonResult = (props) => {
      const pokemonState = useSelector((state) => state.PokemonSearch); // <== if you want to select pokemonData it should be state.PokemonSearch.data.data
      console.log(pokemonState.name); // <=== it should be pokemonState.data.data.name
    
      return (
        <div>
          <h1>Title</h1>
        </div>
      );
    };
    
    export default PokemonResult;
    

    to make it cleaner and work as expected you can change the way you handle your payload inside the POKEMON_DATA_SUCCESS in your reducer:

        case 'POKEMON_DATA_SUCCESS':
          // spreading state is useless here since you change all the state
          return {
            loading: false,
            errorMsg: '',
            data: action.payload,
          };
    

    so then your PokemonResult component will be able to read the pokemon name by selecting the data in your PokemonSearch state:

    import { useSelector } from 'react-redux';
    
    const PokemonResult = (props) => {
      const pokemonState = useSelector((state) => state.PokemonSearch.data);
      console.log(pokemonState.name);
    
      return (
        <div>
          <h1>Title</h1>
        </div>
      );
    };
    
    export default PokemonResult;
    

    don't forget to change your defaultState data value, it'll still work that way but it's ugly.