Handling loading state of multiple async calls in an action/reducer based application - redux

I don´t think this issue is bound to a specific framework or library, but applies to all store based application following the action - reducer pattern.
For clarity, I am using Angular and #ngrx.
In the application I am working on we need to track the loading state of individual resources.
The way we handle other async requests is by this, hopefully familiar, pattern:
Actions
GET_RESOURCE
GET_RESOURCE_SUCCESS
GET_RESOURCE_FAILURE
Reducer
switch(action.type)
case GET_RESOURCE:
return {
...state,
isLoading = true
};
case GET_RESOURCE_SUCCESS:
case GET_RESOURCE_FAILURE:
return {
...state,
isLoading = false
};
...
This works well for async calls where we want to indicate the loading state globally in our application.
In our application we fetch some data, say BOOKS, that contains a list of references to other resources, say CHAPTERS.
If the user wants to view a CHAPTER he/she clicks the CHAPTER reference that trigger an async call. To indicate to the user that this specific CHAPTER is loading, we need something more than just a global isLoading flag in our state.
The way we have solved this is by creating a wrapping object like this:
interface AsyncObject<T> {
id: string;
status: AsyncStatus;
payload: T;
}
where AsyncStatus is an enum like this:
enum AsyncStatus {
InFlight,
Success,
Error
}
In our state we store the CHAPTERS like so:
{
chapters: {[id: string]: AsyncObject<Chapter> }
}
However, I feel like this 'clutter' the state in a way and wonder if someone has a better solution / different approach to this problem.
Questions
Are there any best practices for how to handle this scenario?
Is there a better way of handling this?

I have faced several times this kind of situation but the solution differs according to the use case.
One of the solution would be to have nested reducers. It is not an antipattern but not advised because it is hard to maintain but it depends on the usecase.
The other one would be the one I detail below.
Based on what you described, your fetched data should look like this:
[
{
id: 1,
title: 'Robinson Crusoe',
author: 'Daniel Defoe',
references: ['chp1_robincrusoe', 'chp2_robincrusoe'],
},
{
id: 2,
title: 'Gullivers Travels',
author: 'Jonathan Swift',
references: ['chp1_gulliverstravels', 'chp2_gulliverstravels', 'chp3_gulliverstravels'],
},
]
So according to your data, your reducers should look like this:
{
books: {
isFetching: false,
isInvalidated: false,
selectedBook: null,
data: {
1: { id: 1, title: 'Robinson Crusoe', author: 'Daniel Defoe' },
2: { id: 2, title: 'Gullivers Travels', author: 'Jonathan Swift' },
}
},
chapters: {
isFetching: false,
isInvalidated: true,
selectedChapter: null,
data: {
'chp1_robincrusoe': { isFetching: false, isInvalidated: true, id: 'chp1_robincrusoe', bookId: 1, data: null },
'chp2_robincrusoe': { isFetching: false, isInvalidated: true, id: 'chp2_robincrusoe', bookId: 1, data: null },
'chp1_gulliverstravels': { isFetching: false, isInvalidated: true, id: 'chp1_gulliverstravels', bookId: 2, data: null },
'chp2_gulliverstravels': { isFetching: false, isInvalidated: true, id: 'chp2_gulliverstravels', bookId: 2, data: null },
'chp3_gulliverstravels': { isFetching: false, isInvalidated: true, id: 'chp3_gulliverstravels', bookId: 2, data: null },
},
}
}
With this structure you won't need isFetching and isInvalidated in your chapter reducers as every chapter is a separated logic.
Note: I could give you a bonus details later on on how we can leverage the isFetching and isInvalidated in a different way.
Below the detailed code:
Components
BookList
import React from 'react';
import map from 'lodash/map';
class BookList extends React.Component {
componentDidMount() {
if (this.props.isInvalidated && !this.props.isFetching) {
this.props.actions.readBooks();
}
}
render() {
const {
isFetching,
isInvalidated,
data,
} = this.props;
if (isFetching || (isInvalidated && !isFetching)) return <Loading />;
return <div>{map(data, entry => <Book id={entry.id} />)}</div>;
}
}
Book
import React from 'react';
import filter from 'lodash/filter';
import { createSelector } from 'reselect';
import map from 'lodash/map';
import find from 'lodash/find';
class Book extends React.Component {
render() {
const {
dispatch,
book,
chapters,
} = this.props;
return (
<div>
<h3>{book.title} by {book.author}</h3>
<ChapterList bookId={book.id} />
</div>
);
}
}
const foundBook = createSelector(
state => state.books,
(books, { id }) => find(books, { id }),
);
const mapStateToProps = (reducers, props) => {
return {
book: foundBook(reducers, props),
};
};
export default connect(mapStateToProps)(Book);
ChapterList
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import map from 'lodash/map';
import find from 'lodash/find';
class ChapterList extends React.Component {
render() {
const { dispatch, chapters } = this.props;
return (
<div>
{map(chapters, entry => (
<Chapter
id={entry.id}
onClick={() => dispatch(actions.readChapter(entry.id))} />
))}
</div>
);
}
}
const bookChapters = createSelector(
state => state.chapters,
(chapters, bookId) => find(chapters, { bookId }),
);
const mapStateToProps = (reducers, props) => {
return {
chapters: bookChapters(reducers, props),
};
};
export default connect(mapStateToProps)(ChapterList);
Chapter
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import map from 'lodash/map';
import find from 'lodash/find';
class Chapter extends React.Component {
render() {
const { chapter, onClick } = this.props;
if (chapter.isFetching || (chapter.isInvalidated && !chapter.isFetching)) return <div>{chapter.id}</div>;
return (
<div>
<h4>{chapter.id}<h4>
<div>{chapter.data.details}</div>
</div>
);
}
}
const foundChapter = createSelector(
state => state.chapters,
(chapters, { id }) => find(chapters, { id }),
);
const mapStateToProps = (reducers, props) => {
return {
chapter: foundChapter(reducers, props),
};
};
export default connect(mapStateToProps)(Chapter);
Book Actions
export function readBooks() {
return (dispatch, getState, api) => {
dispatch({ type: 'readBooks' });
return fetch({}) // Your fetch here
.then(result => dispatch(setBooks(result)))
.catch(error => dispatch(addBookError(error)));
};
}
export function setBooks(data) {
return {
type: 'setBooks',
data,
};
}
export function addBookError(error) {
return {
type: 'addBookError',
error,
};
}
Chapter Actions
export function readChapter(id) {
return (dispatch, getState, api) => {
dispatch({ type: 'readChapter' });
return fetch({}) // Your fetch here - place the chapter id
.then(result => dispatch(setChapter(result)))
.catch(error => dispatch(addChapterError(error)));
};
}
export function setChapter(data) {
return {
type: 'setChapter',
data,
};
}
export function addChapterError(error) {
return {
type: 'addChapterError',
error,
};
}
Book Reducers
import reduce from 'lodash/reduce';
import { combineReducers } from 'redux';
export default combineReducers({
isInvalidated,
isFetching,
items,
errors,
});
function isInvalidated(state = true, action) {
switch (action.type) {
case 'invalidateBooks':
return true;
case 'setBooks':
return false;
default:
return state;
}
}
function isFetching(state = false, action) {
switch (action.type) {
case 'readBooks':
return true;
case 'setBooks':
return false;
default:
return state;
}
}
function items(state = {}, action) {
switch (action.type) {
case 'readBook': {
if (action.id && !state[action.id]) {
return {
...state,
[action.id]: book(undefined, action),
};
}
return state;
}
case 'setBooks':
return {
...state,
...reduce(action.data, (result, value, key) => ({
...result,
[key]: books(value, action),
}), {});
},
default:
return state;
}
}
function book(state = {
isFetching: false,
isInvalidated: true,
id: null,
errors: [],
}, action) {
switch (action.type) {
case 'readBooks':
return { ...state, isFetching: true };
case 'setBooks':
return {
...state,
isInvalidated: false,
isFetching: false,
errors: [],
};
default:
return state;
}
}
function errors(state = [], action) {
switch (action.type) {
case 'addBooksError':
return [
...state,
action.error,
];
case 'setBooks':
case 'setBooks':
return state.length > 0 ? [] : state;
default:
return state;
}
}
Chapter Reducers
Pay extra attention on setBooks which will init the chapters in your reducers.
import reduce from 'lodash/reduce';
import { combineReducers } from 'redux';
const defaultState = {
isFetching: false,
isInvalidated: true,
id: null,
errors: [],
};
export default combineReducers({
isInvalidated,
isFetching,
items,
errors,
});
function isInvalidated(state = true, action) {
switch (action.type) {
case 'invalidateChapters':
return true;
case 'setChapters':
return false;
default:
return state;
}
}
function isFetching(state = false, action) {
switch (action.type) {
case 'readChapters':
return true;
case 'setChapters':
return false;
default:
return state;
}
}
function items(state = {}, action) {
switch (action.type) {
case 'setBooks':
return {
...state,
...reduce(action.data, (result, value, key) => ({
...result,
...reduce(value.references, (res, chapterKey) => ({
...res,
[chapterKey]: chapter({ ...defaultState, id: chapterKey, bookId: value.id }, action),
}), {}),
}), {});
};
case 'readChapter': {
if (action.id && !state[action.id]) {
return {
...state,
[action.id]: book(undefined, action),
};
}
return state;
}
case 'setChapters':
return {
...state,
...reduce(action.data, (result, value, key) => ({
...result,
[key]: chapter(value, action),
}), {});
},
default:
return state;
}
}
function chapter(state = { ...defaultState }, action) {
switch (action.type) {
case 'readChapters':
return { ...state, isFetching: true };
case 'setChapters':
return {
...state,
isInvalidated: false,
isFetching: false,
errors: [],
};
default:
return state;
}
}
function errors(state = [], action) {
switch (action.type) {
case 'addChaptersError':
return [
...state,
action.error,
];
case 'setChapters':
case 'setChapters':
return state.length > 0 ? [] : state;
default:
return state;
}
}
Hope it helps.

Related

React-Redux useSelector has trouble passing data

I am using React with Redux. The Redux devtool console shows that data exists in the state (redux devtools console), but the webpage displays an error saying that the object is undefined (error).
This is my code for my screen:
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { listProductDetails } from "../redux/actions/productActions";
const ProductScreen = ({ match }) => {
const dispatch = useDispatch();
const productDetails = useSelector((state) => state.productDetails);
const { loading, error, product } = productDetails;
useEffect(() => {
dispatch(listProductDetails(match.params.id));
}, [dispatch, match]);
return <div>{product.name}</div>;
};
export default ProductScreen;
This is the code for my redux reducer:
import {
PRODUCT_DETAILS_FAIL,
PRODUCT_DETAILS_REQUEST,
PRODUCT_DETAILS_SUCCESS,
} from "../constants";
export const productDetailsReducer = (state = { product: {} }, action) => {
switch (action.type) {
case PRODUCT_DETAILS_REQUEST:
return { loading: true };
case PRODUCT_DETAILS_SUCCESS:
return { loading: false, product: action.payload };
case PRODUCT_DETAILS_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
This is the code for my action:
import axios from "axios";
import {
PRODUCT_DETAILS_FAIL,
PRODUCT_DETAILS_REQUEST,
PRODUCT_DETAILS_SUCCESS,
} from "../constants";
export const listProductDetails = (id) => async (dispatch) => {
try {
dispatch({
type: PRODUCT_DETAILS_REQUEST,
});
const { data } = await axios.get(`/api/products/${id}`);
dispatch({
type: PRODUCT_DETAILS_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: PRODUCT_DETAILS_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
I really cannot find the issue here, any help would be greatly appreciated!
I think the problem is when you dispatch PRODUCT_DETAILS_REQUEST action, reducer will override the state value with { loading: true }, and so product will be undefined instead of empty object {}.
So you should return merged object with the previous state in the reducer. e.g. return { ...state, loading: true };
Hope it could help you.
export const productDetailsReducer = (state = { product: {} }, action) => {
switch (action.type) {
case PRODUCT_DETAILS_REQUEST:
return { ...state, loading: true };
case PRODUCT_DETAILS_SUCCESS:
return { ...state, loading: false, product: action.payload };
case PRODUCT_DETAILS_FAIL:
return { ...state, loading: false, error: action.payload };
default:
return state;
}
};

Redux: Uncaught Error: Actions may not have an undefined "type" property. Have you misspelled a constant?

I have decided to break up my redux store—to now represent logical buckets i.e. users, ui etc.
These are the files which each contain the initial state, action types and reducers for each category:
ui reducer file:
/*./reducers/ui/index' reducer for ui */
/* initial state */
export const uiStartState = { ui: { modalActive: false } }
/* action types */
export const actionTypes = {
ui: { MODAL_ACTIVE: 'MODAL_ACTIVE' },
ui: { MODAL_INACTIVE: 'MODAL_INACTIVE' },
}
/* reducer(s) */
export default function ui(state = uiStartState, action) {
switch (action.type) {
case actionTypes.MODAL_ACTIVE:
return Object.assign({}, state, { ui: { modalActive: true } });
case actionTypes.MODAL_INACTIVE:
return Object.assign({}, state, { ui: { modalActive: false } });
default:
return state
}
};
/* actions */
export const modalStateOn = () => {
return { type: actionTypes.ui.MODAL_ACTIVE }
}
export const modalStateOff = () => {
return { type: actionTypes.ui.MODAL_INACTIVE }
}
users reducer file:
/*./reducers/users/index' reducer for ui */
/* initial state */
export const usersStartState = { users: { isLoggedIn: false } }
/* action types */
export const actionTypes = {
users: { IS_LOGGED_IN: 'IS_LOGGED_IN' },
users: { IS_LOGGED_OUT: 'IS_LOGGED_OUT' },
}
/* reducer(s) */
export default function users(state = usersStartState, action) {
switch (action.type) {
case actionTypes.users.IS_LOGGED_IN:
return Object.assign({}, state, {
users: { isLoggedIn: true }
});
case actionTypes.users.IS_LOGGED_OUT:
return Object.assign({}, state, {
users: { isLoggedIn: false }
});
default:
return state
}
};
/* actions */
export const logInUser = () => {
return { type: actionTypes.users.IS_LOGGED_IN }
}
export const logOutUser = () => {
return { type: actionTypes.users.IS_LOGGED_OUT }
}
And this is my store:
import { applyMiddleware, combineReducers, createStore } from 'redux'
/* imported reducers */
import ui from './reducers/ui/index'
import users from './reducers/users/index'
import { composeWithDevTools } from 'redux-devtools-extension'
import { persistStore } from 'redux-persist';
import { createLogger } from 'redux-logger'
import thunkMiddleware from 'redux-thunk'
var rootReducer = combineReducers({
ui,
users
})
export default () => {
let store;
const isClient = typeof window !== 'undefined';
if (isClient) {
const { persistReducer } = require('redux-persist');
const storage = require('redux-persist/lib/storage').default;
const persistConfig = {
key: 'primary',
storage,
whitelist: ['isLoggedIn', 'modalActive'], // place to select which state you want to persist
}
store = createStore(
persistReducer(persistConfig, rootReducer), {
ui: { modalActive: false },
users: { isLoggedIn: false }
},
composeWithDevTools(applyMiddleware(
thunkMiddleware,
createLogger({ collapsed: false })
))
);
store.__PERSISTOR = persistStore(store);
} else {
store = createStore(
rootReducer, {
ui: { modalActive: false },
users: { isLoggedIn: false }
},
composeWithDevTools(applyMiddleware(
thunkMiddleware,
createLogger({ collapsed: false })
))
);
}
return store;
};
So taking my actions from users:
export const logInUser = () => {
return { type: actionTypes.users.IS_LOGGED_IN }
}
export const logOutUser = () => {
return { type: actionTypes.users.IS_LOGGED_OUT }
}
Not sure why the error is saying I don't have a key with the name type, I assume it's a matter of restructuring.
Thanks in advance!
UPDATE
I am wondering if the problem is I am merging the new state incorrectly?
From my reducer:
case actionTypes.users.IS_LOGGED_IN:
return Object.assign({}, state, {
users: { isLoggedIn: true }
});
My state feedback from redux tools:
You can see the next state the users object gets another users object nested in the orignal with the correct payload!
Your user action types must be in this format:
export const actionTypes = {
users: { IS_LOGGED_IN: "IS_LOGGED_IN", IS_LOGGED_OUT: "IS_LOGGED_OUT" }
};
With your code, actionTypes.users.IS_LOGGED_IN will be undefined, because you have the same key in the same object, and it will be replaced. This is the reason why redux complains.
Also ui action types must be:
export const actionTypes = {
ui: { MODAL_ACTIVE: "MODAL_ACTIVE", MODAL_INACTIVE: "MODAL_INACTIVE" }
};
May be you can keep all your action types in a single object like this:
export const actionTypes = {
users: { IS_LOGGED_IN: "IS_LOGGED_IN", IS_LOGGED_OUT: "IS_LOGGED_OUT" },
ui: { MODAL_ACTIVE: "MODAL_ACTIVE", MODAL_INACTIVE: "MODAL_INACTIVE" }
};
Update: about your question merging state:
Can you try like this?
export default function users(state = usersStartState, action) {
switch (action.type) {
case actionTypes.users.IS_LOGGED_IN:
return {
...state,
users: {
...state.users,
isLoggedIn: true
}
};
case actionTypes.users.IS_LOGGED_OUT:
return {
...state,
users: {
...state.users,
isLoggedIn: false
}
};
default:
return state;
}
}

Understanding JSON.stringify() on Redux Action?

I was trying to reset the data, and want to go to initial state ,I know that the immutability playing major role in this part.
Below is my store data (Flow Completed data)
animalSense: {
selectedVision: 'dayLight',
selectedState: 'california',
viewedVisions: ['dayLightcalifornia', 'dayLightsouthAfrica', 'nightVisioncalifornia'],
viewedAnimals: ['dog', 'cat']
},
I want to replace it with the below data
animalSense: {
selectedVision: '',
selectedState: '',
viewedVisions: [''],
viewedAnimals: []
},
I know the below action is the Straight and proper way to add initial data is
export const RESET_ANIMAL_SENSES = 'actions/reset_animal_senses';
export default () => ({
type: RESET_ANIMAL_SENSES,
payload: {
selectedVision: '',
selectedState: '',
selectedAnimal: '',
viewedVisions: [''],
viewedAnimals: []
}
});
But the above action maintaining the same state
Below action is Working Solution but I don't know is this a Proper way
export const RESET_ANIMAL_SENSES = 'actions/reset_animal_senses';
const data = JSON.stringify({
selectedVision: '',
selectedState: '',
selectedAnimal: '',
viewedVisions: [''],
viewedAnimals: []
});
export default () => ({
type: RESET_ANIMAL_SENSES,
payload: JSON.parse(data)
});
When we are using stringify the connectivity has been ended and the new state has been added but i don't know why this is not working without JSON.stringify()?
Reducer
import { SELECT_VISION } from '../actions/select_vision_type';
import { CHANGE_ANIMAL_VIDEO_STATE } from '../actions/change_animal_video_state';
import { UPDATE_ANIMALS } from '../actions/update_animals';
import { RESET_ANIMAL_SENSES } from '../actions/reset_animal_senses';
export default (state = {}, action) => {
let newState = state;
switch (action.type) {
case SELECT_VISION:
newState = { ...state, ...action.payload };
break;
case CHANGE_ANIMAL_VIDEO_STATE:
newState = { ...state, ...action.payload };
break;
case UPDATE_ANIMALS:
newState = { ...state, ...action.payload };
break;
case RESET_ANIMAL_SENSES:
newState = { ...state, ...action.payload };
break;
default:
break;
}
return newState;
};
Spread Operator in payload Solved this issue
export const RESET_ANIMAL_SENSES = 'actions/reset_animal_senses';
const data = {
selectedVision: '',
selectedState: '',
selectedAnimal: '',
viewedVisions: [''],
viewedAnimals: []
};
export default () => ({
type: RESET_ANIMAL_SENSES,
payload: { ...data } // here is the solution
});
Try this out, I'd do good amount of refactors to your reducer.
import { SELECT_VISION } from '../actions/select_vision_type';
import { CHANGE_ANIMAL_VIDEO_STATE } from '../actions/change_animal_video_state';
import { UPDATE_ANIMALS } from '../actions/update_animals';
import { RESET_ANIMAL_SENSES } from '../actions/reset_animal_senses';
const initialState = {
selectedVision: '',
selectedState: '',
selectedAnimal: '',
viewedVisions: [''],
viewedAnimals: []
}
export default (state = initialState, action) => {
switch (action.type) {
// since all the cases have common code.
case SELECT_VISION:
case CHANGE_ANIMAL_VIDEO_STATE:
case UPDATE_ANIMALS: {
return { ...state, ...action.payload }
}
case RESET_ANIMAL_SENSES: {
return { ...initialState }
}
default: {
return state;
}
}
};
Try this reducer once. However, currently I don't have a clarity on why would it work with stringify in place.

Redux store reset when navigating (after I've refactored reducer)

I am trying the refactor my reducer into two "sub" reducers and combine them before exporting to store.js. However, when I am navigating in my app the state of my notificationReducer gets reseted, and not the other reducers. I am unsure of what might be the problem and I've followed the guide (sort of) from redux.js.org =>
Separating Data Handling by Domain
Any thoughts or tips on how you've refactored reducers?
notificationReducer.js
import {
FETCHING_NOTIFICATION_STATUS, // NOTIFICATION
FETCHING_NOTIFICATION_STATUS_SUCCESS,
FETCHING_NOTIFICATION_STATUS_FAILURE,
FETCHING_NOTIFICATION_DATA, // NOTIFICATION
FETCHING_NOTIFICATION_DATA_SUCCESS,
FETCHING_NOTIFICATION_DATA_FAILURE,
FETCHING_MARK_NOTIFICATION_AS_UNSEEN, // NOTIFICATION
FETCHING_MARK_NOTIFICATION_AS_UNSEEN_SUCCESS,
FETCHING_MARK_NOTIFICATION_AS_UNSEEN_FAILURE
} from '../Actions/actionTypes'
const fetchingData = {
isFetching: false,
dataFetched: false,
error: false,
errorMsg: '',
}
const initialState = {
notificationStatus: {
...fetchingData,
hasNotifications: false,
},
notificationData: {
...fetchingData,
data: [],
},
markNotification: {
...fetchingData,
isUnseen: false,
},
}
const { notificationStatus, notificationData, markNotification } = initialState
const notificationStatusReducer = (state = notificationStatus, action) => {
switch(action.type) {
case FETCHING_NOTIFICATION_STATUS:
return {
...state,
isFetching: true,
}
case FETCHING_NOTIFICATION_STATUS_SUCCESS:
return {
...state,
isFetching: false,
dataFetched: true,
hasNotifications: action.data,
}
case FETCHING_NOTIFICATION_STATUS_FAILURE:
return {
...state,
isFetching: false,
error: true,
errorMsg: action.errorMsg,
}
default:
return state
}
}
const notificationDataReducer = (state = notificationData, action) => {
switch(action.type) {
case FETCHING_NOTIFICATION_DATA:
return {
...state,
isFetching: true
}
case FETCHING_NOTIFICATION_DATA_SUCCESS:
return {
...state,
isFetching: false,
dataFetched: true,
data: action.data,
}
case FETCHING_NOTIFICATION_DATA_FAILURE:
return {
...state,
isFetching: false,
error: true,
errorMsg: action.errorMsg,
}
default:
return state
}
}
const markNotificationReducer = (state = markNotification, action) => {
switch(action.type) {
case FETCHING_MARK_NOTIFICATION_AS_UNSEEN:
return {
...state,
isFetching: true
}
case FETCHING_MARK_NOTIFICATION_AS_UNSEEN_SUCCESS:
return {
...state,
isFetching: false,
dataFetched: true,
isUnseen: true,
}
case FETCHING_MARK_NOTIFICATION_AS_UNSEEN_FAILURE:
return {
...state,
isFetching: false,
error: true,
errorMsg: action.errorMsg,
}
default:
return state
}
}
const notificationReducer = (state = initialState, action) => {
return {
notificationStatusReducer : notificationStatusReducer(state.notificationStatus, action),
notificationDataReducer : notificationDataReducer(state.notificationStatus, action),
markNotificationReducer : markNotificationReducer(state.markNotification, action),
}
}
export default notificationReducer
You should use combineReducers for such things. So your notificationReducer should be the combination of yours three reducers.
const notificationReducer = combineReducers({
notificationStatusReducer,
notificationDataReducer,
markNotificationReducer
})
Hope it will help

Action creators handling axios get.request with state access for param

I'm trying to pass some value from a component to a action creators which is doing a get request with axios. I'm trying to follow this pattern from Dan Abramov :
export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
return (dispatch, getState) => {
const {items} = getState().otherReducer;
dispatch(anotherAction(items));
}
}
However I can't make it work. I think I have trouble on two level : my component and my action creator. Would be great to have some helps.
my component :
const timeR = ({
selectedTimeRange,
timeRange = [],
onTimeChange }) => {
return (
<div>
<div>
Filters:
<div>
Year:
<select
defaultValue={selectedTimeRange}
onChange={onTimeChange}>
<option value="all" >All</option>
{timeRange.map((y, i) =>
<option key={i} value={y}>{y}</option>
)}
</select>
</div>
</div>
</div>
);
}
function mapStateToProps(state) {
var range = ['30daysAgo', '15daysAgo', '7daysAgo'];
return {
selectedTimeRange: state.timeReducer.timerange[0],
timeRange: range
};
};
const mapDispachToProps = (dispatch) => {
return {
onTimeChange: (e) => {dispatch (onSetTimeRange(e.target.value));},
};
};
const TimeRange = connect(mapStateToProps, mapDispachToProps)(timeR);
export default TimeRange;
This component give me a dropdown menu. When selecting a timerange, for example '30daysAgo', it update my redux store state so I can access the value from my reducer.
Here is the action associated to my dropdown menu :
export function onSetTimeRange(timerange) {
return {
type: 'SET_TIME_RANGE',
timerange
}
}
and here is the action dealing with axios.get :
export const fetchgadata = () => (dispatch) => {
dispatch({
type: 'FETCH_DATA_REQUEST',
isFetching:true,
error:null
});
var VIEW_ID = "ga:80820965";
return axios.get("http://localhost:3000/gadata", {
params: {
id: VIEW_ID
}
}).then(response => {
dispatch({
type: 'FETCH_DATA_SUCCESS',
isFetching: false,
data: response.data.rows.map( ([x, y]) => ({ x, y }) )
});
})
.catch(err => {
dispatch({
type: 'FETCH_DATA_FAILURE',
isFetching:false,
error:err
});
console.error("Failure: ", err);
});
};
My question :
How do I bring these two actions together. At the end I would like to be able, when doing onChange on my drop-down menu, to call a action with the value selected from my menu as a param for my axios.get request.
I feel like I need to nest two actions creators. I've tried this but doesn't work ("fetchgadata" is read-only error in my terminal)
export const SET_TIME_RANGE = 'SET_TIME_RANGE';
export function onSetTimeRange() {
return (dispatch, getState) => {
const {VIEW_ID} = getState().timerange;
dispatch(fetchgadata = (VIEW_ID) => (dispatch) => {
dispatch({
type: 'FETCH_DATA_REQUEST',
isFetching:true,
error:null,
id:VIEW_ID,
});
});
return axios.get("http://localhost:3000/gadata", {
params: {
id: VIEW_ID
}
}).then(response => {
dispatch({
type: 'FETCH_DATA_SUCCESS',
isFetching: false,
data: response.data.rows.map( ([x, y]) => ({ x, y }) )
});
})
.catch(err => {
dispatch({
ype: 'FETCH_DATA_FAILURE',
isFetching:false,
error:err
});
console.error("Failure: ", err);
});
}
}
Edit:
reducers for API call :
const initialState = {data:null,isFetching: false,error:null};
export const gaData = (state = initialState, action)=>{
switch (action.type) {
case 'FETCH_DATA_REQUEST':
case 'FETCH_DATA_FAILURE':
return { ...state, isFetching: action.isFetching, error: action.error };
case 'FETCH_DATA_SUCCESS':
return Object.assign({}, state, {data: action.data, isFetching: action.isFetching,
error: null });
default:return state;
}
};
reducers for Drop-down :
const items = [{timerange: '30daysAgo'},{timerange: '15daysAgo'},{timerange: '7daysAgo'}]
const timeReducer = (state = {
timerange: items
}, action) => {
switch (action.type) {
case 'SET_TIME_RANGE':
console.log(state,action);
return {
...state,
timerange: action.timerange,
};
default:
return state;
}
}
I see a little typo in the catch of your axios.get request, it reads ype: FETCH_DATA_FAILURE. Otherwise, can you add in your reducer for me, I don't see it up there? If I understand correctly, you want one action to update two different pieces of state, in which case you would simply dispatch an action and add it to both reducers. Really it's best to just demonstrate:
//axios call
axios.get("some route", { some params } )
.then(response => {
dispatch({
type: UPDATE_TWO_THINGS,
payload: some_value
})
}) .... catch, etc
//reducer 1
import { UPDATE_TWO_THINGS } from 'types';
const INITIAL_STATE = { userInfo: '' };
export default function (state = INITIAL_STATE, action) {
switch(action.type) {
case UPDATE_TWO_THINGS:
return {...state, userInfo: payload };
}
return state;
}
//reducer 2
import { UPDATE_TWO_THINGS } from 'types';
const INITIAL_STATE = { businessInfo: '' };
export default function (state = INITIAL_STATE, action) {
switch(action.type) {
case UPDATE_TWO_THINGS:
return {...state, businessInfo: payload };
}
return state;
}
Hopefully this helps, but let me know if not, I'll do my best to get this working with you! Thanks for asking!

Resources