I'm trying to make an api call in action with axios and pass the results of it to the reducer.
Though action is triggered, reducer isn't. And I can't understand why.
Here's the component that should make api call before mounting
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
//actions
import { getPost } from '../actions/';
class PostShow extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
getPost(this.props.params.id);
}
render() {
console.log(this.props.activePost);
return (
<div>
<h1> hello from a post</h1>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
activePost: state.posts.activePost
}
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
getPost
}, dispatch);
};
export default connect(mapStateToProps, mapDispatchToProps)(PostShow);
Here's my action
import axios from 'axios';
import { FETCH_POSTS, SEND_POST, FETCH_POST } from './types';
const ROOT_URL = 'http://reduxblog.herokuapp.com/api';
const API_KEY = '?key=qwerty';
export function fetchPosts() {
const req = axios.get(`${ROOT_URL}/posts${API_KEY}`);
return {
type: FETCH_POSTS,
payload: req
}
}
export function sendPost(props) {
const req = axios.post(`${ROOT_URL}/posts${API_KEY}`, props);
return {
type: SEND_POST,
payload: req
}
}
export function getPost(id) {
console.log('action triggered');
const req = axios.get(`${ROOT_URL}/posts/${id}${API_KEY}`);
return {
type: FETCH_POST,
payload: req
}
}
And here's my reducer
import { FETCH_POSTS, FETCH_POST } from '../actions/types';
const INITIAL_STATE = {
allPosts: [],
activePost: null
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case FETCH_POSTS:
return {
...state,
allPosts: action.payload.data
};
case FETCH_POST:
console.log('reducer triggered');
return {
...state,
activePost: action.payload.data
};
default:
return state;
}
}
As a result I see 'action triggered' coming from console.log in action, and null coming from console.log in component, and no console.log from the reducer, so it's not triggered and I have no data to render in my component.
Though I make a request and get a response from server with the data, it doesn't go to the reducer. (moreover case FETCH_POSTS works fine and I can render a list of posts, but not a particular one).
"axios": "^0.17.0"
"redux-promise": "^0.5.3"
You need to use this.props.getPost in componentDidMount instead of getPost.
Connect sends bound action creator to component as a prop
Related
I am pretty new to Redux and the whole Redux-Saga thing and wanted to use React-Boilerplate to try a small project that basically just makes an API call and iterates over the data. And I currently have a problem I've been stuck at for hours. Maybe you have an idea?
My React Component looks like this:
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { compose } from 'redux';
import { useInjectSaga } from 'utils/injectSaga';
import { useInjectReducer } from 'utils/injectReducer';
import {
makeSelectDevices,
makeSelectLoading,
makeSelectError
} from './selectors';
import reducer from './reducer';
import { fetchDevices } from './actions';
import saga from './saga';
export function LeafletMap(props) {
const {devices, loading, error, fetchDevices } = props;
useInjectReducer({ key: 'leafletMap', reducer });
useInjectSaga({ key: 'leafletMap', saga });
useEffect(() => {
fetchDevices();
}, [fetchDevices]);
if (loading) return(<div>Loading...</div>)
return (
<div>
{ !error ?
<Map center={[47.3, 9.9]} zoom={9} style={{height: '500px'}}>
<TileLayer
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
attribution='© OpenStreetMap contributors'
/>
{ devices && devices.map((device)=> {
let coordinates = [device.latitude, device.longitude];
return (
<Marker key={device.id} position={coordinates}></Marker>
);
})}
</Map>
: ''
}
</div>
);
};
LeafletMap.propTypes = {
devices: PropTypes.array,
loading: PropTypes.bool,
error: PropTypes.any,
};
const mapStateToProps = createStructuredSelector({
devices: makeSelectDevices(),
loading: makeSelectLoading(),
error: makeSelectError(),
});
function mapDispatchToProps(dispatch) {
return {
fetchDevices: () => dispatch(fetchDevices())
};
}
const withConnect = connect(
mapStateToProps,
mapDispatchToProps,
);
export default compose(withConnect)(LeafletMap);
When my component mounts I use the useEffect Hook to dispatch an action that I bound to my props using mapDispatchToProps. The actions file looks like this:
import {
FETCH_DATA,
FETCH_DATA_ERROR,
FETCH_DATA_SUCCESS,
CLICK_DEVICE
} from './constants';
export function fetchDevices() {
return {
type: FETCH_DATA,
};
}
export function fetchDevicesSuccess(devices) {
return {
type: FETCH_DATA_SUCCESS,
devices
};
}
export function fetchDevicesError(error) {
return {
type: FETCH_DATA_ERROR,
error
};
}
My saga then reacts to the FETCH_DATA action and calls a generator to fetch the data from my local API:
import { all, call, put, takeEvery } from 'redux-saga/effects';
import request from 'utils/request';
import { fetchDevicesSuccess, fetchDevicesError } from './actions';
import { FETCH_DATA } from './constants';
function* fetchDevicesAsync() {
yield takeEvery(FETCH_DATA, fetchAllDevices);
}
function* fetchAllDevices() {
try {
const requestUrl = '/api/devices';
const devices = yield call(request, requestUrl);
yield put(fetchDevicesSuccess(devices));
} catch (error) {
yield put(fetchDevicesError(error.toString()));
}
}
export default function* rootSaga() {
yield all([fetchDevicesAsync()]);
}
This in return should trigger my reducer which looks as follows:
import produce from 'immer';
import {
FETCH_DATA,
FETCH_DATA_ERROR,
FETCH_DATA_SUCCESS,
} from './constants';
export const initialState = {
devices: [],
loading: true,
error: false,
};
/* eslint-disable default-case, no-param-reassign */
const leafletMapReducer = (state = initialState, action) =>
produce(state, () => {
switch (action.type) {
case FETCH_DATA:
state.loading = true;
state.error = false;
break;
case FETCH_DATA_ERROR:
state.loading = false
state.error = action.error;
break;
case FETCH_DATA_SUCCESS:
state.loading = false;
state.error = false;
state.devices = action.devices;
break;
}
});
export default leafletMapReducer;
My problem here is that everything seems to work but my action is neither being displayed in Redux DevTools nor does my component update after the initial render. It seems as if the action is being dispatched before the ##INIT event.
Any idea why this happens?
Thanks in advance!
EDIT:
Just in case it has something to do with my selectors:
import { createSelector } from 'reselect';
import { initialState } from './reducer';
/**
* Direct selector to the leafletMap state domain
*/
const selectLeafletMapDomain = state => state.leafletMap || initialState;
/**
* Other specific selectors
*/
const makeSelectDevices = () =>
createSelector(
selectLeafletMapDomain,
leafletMapState => leafletMapState.devices
);
const makeSelectLoading = () =>
createSelector(
selectLeafletMapDomain,
leafletMapState => leafletMapState.loading,
);
const makeSelectError = () =>
createSelector(
selectLeafletMapDomain,
leafletMapState => leafletMapState.error,
);
/**
* Default selector used by LeafletMap
*/
const makeSelectLeafletMap = () =>
createSelector(selectLeafletMapDomain, leafletMapState => leafletMapState.toJS());
export default makeSelectLeafletMap;
export {
selectLeafletMapDomain,
makeSelectDevices,
makeSelectLoading,
makeSelectError
};
Found the problem myself :)
The problem was in my reducer:
const leafletMapReducer = (state = initialState, action) =>
produce(state, () => { // <-- here
switch (action.type) {
case FETCH_DATA:
state.loading = true;
state.error = false;
break;
I here wrongly mutated my state which leads to the error. The correct solution is:
const leafletMapReducer = (state = initialState, action) =>
produce(state, draftState => { // use draftState instead of normal state
switch (action.type) {
case FETCH_DATA:
draftState.loading = true; //<------
draftState.error = false; //<------
break;
I'm new to React/Redux. I'm making an app using an API but the code doesn't work. When I run the code it says "this.props.recipes.map is not a function" and doesn't render anything.
If I change payload to: "payload: response.data.recipes" then the error changes to "Given action "FETCH_RECIPE", reducer "recipes" returned undefined." but no errors on screen (only in console). I thought writing "(state = [], action)" would solve the problem but it seems not. What's the problem and how do I fix this error?
Action Creator
import recipe from '../apis/recipe';
export const fetchRecipe = () => async dispatch => {
const response = await recipe.get('');
dispatch({ type: 'FETCH_RECIPE', payload: response.data })
};
Reducer
import { combineReducers } from 'redux';
const recipeReducer = (state = [], action) => {
switch(action.type) {
case 'FETCH_RECIPE':
return action.payload;
default:
return state;
}
};
export default combineReducers({
recipes: recipeReducer
});
import React from 'react';
import { connect } from 'react-redux';
import { fetchRecipe } from '../actions';
class Recipe extends React.Component {
componentDidMount() {
this.props.fetchRecipe();
console.log("This doesn't work", this.props.recipes)
}
renderList() {
return this.props.recipes.map(recipe => {
return (
<div>
<p>{recipe.publisher}</p>
</div>
)
})
}
render() {
console.log("First loaded: empty, second time: data fetched", this.props.recipes)
return (
<div>
{this.renderList()}
</div>
);
}
}
const mapStateToProps = (state) => {
return { recipes: state.recipes }
};
export default connect(mapStateToProps,{
fetchRecipe
})(Recipe);
API Request
import axios from 'axios';
import { key } from './config';
export default axios.create({
baseURL: `https://cors-anywhere.herokuapp.com/https://www.food2fork.com/api/search?key=${key}&q=pizza`
});
**Hello! my problem is my state is not uploading, is always empty altough my actions brings data correct. Can anyone give me some help of what am I doing wrong ?
I think is something with the name or the combine reducers part.
Maybe I am not accesing data correct with my reducer or something like that **
The object I receive from the api call has this format {categories: Array(4), items: Array(50)}
Component
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import ItemList from '../components/ItemList/ItemList';
import { getItems } from './actions'
class ItemListContainer extends PureComponent {
async componentDidMount() {
const { getItems } = this.props;
await getItems()
console.log(this.props)
}
render() {
return <ItemList />;
}
}
const mapStateToProps = state => (
{
items: state.items.items,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
getItems,
},
dispatch,
);
export default connect(mapStateToProps, mapDispatchToProps)(ItemListContainer);
actions.js
export const GET_ITEMS = 'GET_ITEMS';
export const GET_ITEM = 'GET_ITEM';
export const GET_ITEM_DESCRIPTION = 'GET_ITEM_DESCRIPTION';
export function getItems(query) {
return async function (dispatch) {
// const res = await fetch(`http://localhost:3000/api/items?q=${query}`)
const res = await fetch(`http://localhost:3000/api/items?q=ipad`)
const items = await res.json()
return dispatch({
type: 'GET_ITEMS',
items: items.items,
})
}
}
reducer.js
import { GET_ITEMS } from './actions';
const initialState = {
items: [],
itemsLoaded: false,
};
export default function(state = initialState, action) {
const { type, data } = action;
switch (type) {
case GET_ITEMS:
return {
...state,
items: data,
itemsLoaded: true,
};
default: {
return {
...state
}
}
}
}
I was accessing { data} in the reducer which of course it was empty. The correnct action was items.
I am learning react-redux, so I decided to implement what I have been learning. But I am have a bug challenge. So I console.logged this.props.users from mapStateToProps function.
I believe there's something I not doing right which I don't understand. Please an explanation in other to move on. Thanks you so much for helping out.
Here is my code.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchUsers } from '../actions/userAction';
import UserList from '../components/UserList';
class UserPage extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
this.props.fetchUsers();
}
componentDidMount() {
console.log(this.props.users);
}
render() {
return (
<div>
<h2>Users Page</h2>
<UserList users={this.props.users} />
</div>
);
}
}
const mapStateToProps = state => {
return {
users: state.userReducer.users
};
};
const mapDispatchToProps = dispatch => {
return {
fetchUsers: () => dispatch(fetchUsers())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(UserPage);
So this is what I get from the chrome console - Empty arrays.
props showing empty arrays
But when I check the React DevTool and Redux DevTool, they display the expected Props and States respectively. Below are the snapshot of the dev tools
React devtool shows the correct Props
Redux devtool show the correct States and Actions
userAction.js
import axios from 'axios';
import * as types from './actionTypes';
export let fetchingUser = () => {
return {
type: types.FETCHING_USERS
};
};
export let fetchedUser = payload => {
return {
type: types.FETCHED_USER,
payload
};
};
export let fetchUser_error = () => {
return {
type: types.FETCH_USER_ERROR
};
};
export let fetchUsers = () => {
let url = 'https://eventcity.herokuapp.com/api/v1/users';
return dispatch => {
dispatch(fetchingUser());
return axios
.get(url)
.then(response => {
const users = response.data.data;
dispatch(fetchedUser(users));
})
.catch(err => {
dispatch(fetchUser_error());
});
};
};
userReducer.js
import * as types from '../actions/actionTypes';
import initialState from './initialState';
const userReducer = (state = initialState, action = {}) => {
switch (action.type) {
case types.FETCHING_USERS:
return { ...state, users: [], error: null, loading: true };
case types.FETCHED_USER:
return { ...state, users: action.payload, error: null, loading: false };
case types.FETCH_USER_ERROR:
return {
...state,
users: [],
error: { message: 'Error loading data from the API' },
loading: false
};
default:
return state;
}
};
export default userReducer;
configureStore.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from '../reducer/rootReducer';
const configureStore = () => {
return createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)));
};
export default configureStore;
rootReducer.js
import { combineReducers } from 'redux';
import userReducer from './userReducer';
const rootReducer = combineReducers({
userReducer
});
export default rootReducer;
I think you might want to check this
https://github.com/reactjs/react-redux/issues/129. Your problem is using componentDidMount and componentWillMount without having a better understanding of what they are used for.
The problem is not with redux, all you need to understand is that your fetchUsers request is async and componentDidMount function is only executed once after the component has rendered and it may so happen that the data is not present by the time componentDidMount function is executed and hence your console.log(this.props.users); return empty array, Log it in the render method and you will see the correct data
class UserPage extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
this.props.fetchUsers();
}
render() {
console.log(this.props.users);
return (
<div>
<h2>Users Page</h2>
<UserList users={this.props.users} />
</div>
);
}
}
Here is my code:
The action creator
export function fetchHead() {
const url = HEAD_URL;
const request = axios.get(url);
return {
type: FETCH_HEAD,
payload: request
};
}
The Reducer
import { FETCH_HEAD } from '../actions';
import _ from 'lodash';
export default function(state = {}, action) {
switch (action.type) {
case FETCH_HEAD:
return _.mapKeys(action.payload.data.articles, 'id');
default:
return state;
}
}
Reducer keys, promise
import { combineReducers } from 'redux';
import HeadReducer from './head_reducer';
const rootReducer = combineReducers({
heads: HeadReducer
});
export default rootReducer;
Component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchHead } from '../actions';
class HeadNews extends Component {
componentDidMount() {
this.props.fetchHead();
console.log(this.props.heads);
}
render() {
return <div>Hello</div>;
}
}
function mapStateToProps(state) {
return { heads: state.heads };
}
export default connect(mapStateToProps, { fetchHead })(HeadNews);
You are passing a deffered object to the reducer and not the data returned from the ajax request.
You should use .then:
axios.get(url)
.then(function (response) {
return {
type: FETCH_HEAD,
payload: response
}
})
.catch(function (error) {
console.log(error);
});
EDIT
I don't know if you are using redux-thunk middleware but in order to dispatch actions that returns a function instead of a plain object like an action should be, you need to use redux-thunk.
Just but console.log under rendering funcnction :
componentDidMount() {
this.props.fetchHead();
}
render() {
console.log(this.props.heads);
return <div>Hello</div>;
}