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

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!

Related

Refactoring with createSlice reduxtoolkit

I'm having trouble refactoring with createSlice, I'm a beginner with redux-toolkit and have looked through the documentation but still having problems.if someone could point me in the right direction that would be fantastic. This is the working code
const SET_ALERT = 'setAlert';
const REMOVE_ALERT = 'alertRemoved';
export const setAlert =
(msg, alertType, timeout = 5000) =>
(dispatch) => {
const id = nanoid();
dispatch({
type: SET_ALERT,
payload: { msg, alertType, id },
});
setTimeout(() => dispatch({ type: REMOVE_ALERT, payload: id }), timeout);
};
const initialState = [];
export default function alertReducer(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case SET_ALERT:
return [...state, payload];
case REMOVE_ALERT:
return state.filter((alert) => alert.id !== payload);
default:
return state;
}
}
Your current setAlert action creator creates a thunk action (an action which takes dispatch as an argument) so it cannot be an action creator that is automatically generated by createSlice.
createSlice
You can keep the setup very similar to what you have now. You would have two separate actions for setting and removing an alert and a thunk for dispatching both. The underlying basic actions can be created with createSlice.
import { createSlice, nanoid } from "#reduxjs/toolkit";
const slice = createSlice({
name: "alerts",
initialState: [],
reducers: {
addAlert: (state, action) => {
// modify the draft state and return nothing
state.push(action.payload);
},
removeAlert: (state, action) => {
// replace the entire slice state
return state.filter((alert) => alert.id !== action.payload);
}
}
});
const { addAlert, removeAlert } = slice.actions;
export default slice.reducer;
export const setAlert = (msg, alertType, timeout = 5000) =>
(dispatch) => {
const id = nanoid();
dispatch(addAlert({ msg, alertType, id }));
setTimeout(() => dispatch(removeAlert(id)), timeout);
};
CodeSandbox
createAsyncThunk
This next section is totally unnecessary and overly "tricky".
We can make use of createAsyncThunk if we consider opening the alert as the 'pending' action and dismissing the alert as the 'fulfilled' action. It only gets a single argument, so you would need to pass the msg, alertType, and timeout as properties of an object. You can use the unique id of the thunk which is action.meta.requestId rather than creating your own id. You can also access the arguments of the action via action.meta.arg.
You can still use createSlice if you want, though there's no advantage over createReducer unless you have other actions. You would respond to both of the thunk actions using the extraReducers property rather than reducers.
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
export const handleAlert = createAsyncThunk( "alert/set", (arg) => {
const { timeout = 5000 } = arg;
return new Promise((resolve) => {
setTimeout(() => resolve(), timeout);
});
});
export default createReducer(initialState, (builder) =>
builder
.addCase(handleAlert.pending, (state, action) => {
const { alertType, msg } = action.meta.arg;
const id = action.meta.requestId;
// modify the draft state and don't return anything
state.push({ alertType, msg, id });
})
.addCase(handleAlert.fulfilled, (state, action) => {
const id = action.meta.requestId;
// we are replacing the entire state, so we return the new value
return state.filter((alert) => alert.id !== id);
})
);
example component
import { handleAlert } from "../store/slice";
import { useSelector, useDispatch } from "../store";
export const App = () => {
const alerts = useSelector((state) => state.alerts);
const dispatch = useDispatch();
return (
<div>
{alerts.map((alert) => (
<div key={alert.id}>
<strong>{alert.alertType}</strong>
<span>{alert.msg}</span>
</div>
))}
<div>
<button
onClick={() =>
dispatch(
handleAlert({
alertType: "success",
msg: "action was completed successfully",
timeout: 2000
})
)
}
>
Success
</button>
<button
onClick={() =>
dispatch(
handleAlert({
alertType: "warning",
msg: "action not permitted"
})
)
}
>
Warning
</button>
</div>
</div>
);
};
export default App;
CodeSandbox

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 action payload being ignored when dispatching some other action inside axios interceptor

I need to call checkConnection before any other action so I thought of using axios interceptors:
axios.interceptors.request.use(
async config => {
await store.dispatch(checkConnection())
const { requestTime, hash } = intro(store.getState())
return {
...config,
headers: {
'Request-Time': requestTime,
'Api-Hash-Key': hash
}
}
}
)
intro is a reselect selector used to do some 'heavy' computing on serverTime (serverTime is the result of checkConnection)
checkConnection is a redux thunk action:
export const checkConnection = () => async (dispatch, _, {
Intro
}) => {
dispatch(introConnectionPending())
try {
const { data: { serverTime } } = await Intro.checkConnection()
dispatch(introConnectionSuccess(serverTime))
} catch ({ message }) {
dispatch(introConnectionFailure(message))
}
}
So, now every time I dispatch an action that calls for an API the checkConnection runs first.
The problem is when the reducer responsible for type that main action dispatched (not the checkConnection) gets called it doesn't even see the payload.
Here is an example of a action:
export const getData = () => async (dispatch, getState, {
API
}) => {
dispatch(dataPending())
const credentials = getUsernamePasswordCombo(getState())
try {
const { data } = await API.login(credentials)
dispatch(dataSuccess(data))
} catch ({ message }) {
dispatch(dataFailure())
}
}
and its reducer:
export default typeToReducer({
[SET_DATA]: {
PENDING: state => ({
...state,
isPending: true
})
},
SUCCESS: (state, { payload: { data } }) => ({
...state,
isPending: false,
...data
}),
FAILURE: state => ({
...state,
isPending: false,
isError: true
})
}, initialValue)
The reducer is totally wrong. It should be:
export default typeToReducer({
[SET_DATA]: {
PENDING: state => ({
...state,
isPending: true
}),
SUCCESS: (state, { payload: { data } }) => ({
...state,
isPending: false,
...data
}),
FAILURE: state => ({
...state,
isPending: false,
isError: true
})
}
}, initialValue)
Note the SUCCESS and FAILURE parts are now inside [SET_DATA]

Redux action not dispatched. TypeError: Invalid attempt to spread non-iterable instance

In my application I want to add a 'ticket' to an array in the 'event' object. In the action I post the new ticket to the database, and after that I dispatch the action to the reducer. By using the Redux logger, I am able to retrieve the error:
The action of 'createTicket' is this:
// actions/tickets.js
export const TICKET_CREATE_SUCCESS = 'TICKET_CREATE_SUCCESS';
const ticketCreateSuccess = tickets => ({
type: TICKET_CREATE_SUCCESS,
tickets
});
export const createTicket = (eventId, data) => (dispatch, getState) => {
const jwt = getState().currentUser.token;
const id = getState().currentUser.userId;
const email = getState().currentUser.email;
const name = getState().currentUser.name;
request
.post(`${baseUrl}/events/${eventId}/tickets`)
.set('Authorization', `Bearer ${jwt}`)
.send(data)
.then(response => {
dispatch(ticketCreateSuccess({ ...response.body, user: { id, email, name } }));
})
.catch(error => error);
};
The reducer
// reducers/events.js
import { EVENT_FETCHED } from '../actions/events';
import { TICKET_EDIT_SUCCESS, TICKET_CREATE_SUCCESS } from '../actions/tickets';
export default (state = null, action = {}) => {
switch (action.type) {
case EVENT_FETCHED:
return action.event;
case TICKET_EDIT_SUCCESS:
return {
...state,
tickets: state.tickets.map(ticket => {
if (ticket.id === action.ticket.id) {
return action.ticket;
}
return ticket;
})
};
case TICKET_CREATE_SUCCESS:
console.log({ ...state, tickets: [...state.tickets, action.tickets] });
return { ...state, tickets: [...state.tickets, action.tickets] };
default:
return state;
}
};
The reducers are combined into :
import { combineReducers } from 'redux';
import currentUser from './currentUser';
import events from './events';
import event from './event';
import ticket from './ticket';
import tickets from './tickets';
import numberOfTickets from './numberOfTickets';
export default combineReducers({ currentUser, events, event, ticket, tickets, numberOfTickets });
Could it be that you're trying to spread your reducer state when its value is null:
export default (state = null, action = {}) => {
return {
...state, // Here
// rest
}
Your default state should probably be an object, e.g.:
const InitialState = {
tickets: []
};
export default (state = InitialState, action) => {
// Some code
case TICKET_CREATE_SUCCESS:
return {
...state,
tickets: [
...state.tickets,
action.tickets
]
}
}
just add this ...state || []
and you are good to go.
the problem is value of ...state equals null with empty array and when you try to iterate over null it creates an error.
so use and "OR" operator and it will work fine.

State does not change in my reducer when action is dispatched

I am not able to retrieve the state in the reducer
MyComponent looks like this
const MyComponent = ({name, features, onClick}) => {
return (
<div>
Hello! {name}
<Button onClick={() => { onClick(features); }}> Weight</Button>
</div>
);
const mapDispatchToProps = (dispatch: any) => {
return {
onClick: (features) => {
dispatch(weightSort(features));
}
};
};
const mapStateToProps = (state: any, ownProps: any) => {
console.log(state); //Displays the state
return {
name: "John Doe",
features: ownProps.features,
};
};
export const FeatureBlock = connect(mapStateToProps, mapDispatchToProps)(MyComponent);
My actions and reducers looks like below:
// Action Creator
export const weightSort = (features) => {
console.log("inside the weight sort action creator!!!");
return {
type: "SET_WEIGHT_FILTER",
filter: "DESC",
features,
};
};
// Reducer
export const weightFilter = (state = [], action) => {
switch (action.type) {
case "SET_WEIGHT_FILTER":
console.log(state); // Gives me empty state
console.log("++inside weight filter+++++", action); //Displays action
return state;
default:
return state;
}
};
export const FeatureBlock = connect(
mapStateToProps,
mapDispatchToProps,
)(MyComponent);
What am I missing here? Any help will be appreciated!
In your reducer, when you console.log(state), it is correct in returning an empty array because you haven't done anything to modify it.
// Reducer
export const weightFilter = (state = [1,2,3], action) => {
switch (action.type) {
case "SET_WEIGHT_FILTER":
console.log(state); // This will show [1,2,3] because [1,2,3] is the initial state.
console.log("++inside weight filter+++++", action); //Displays action
return state;
default:
return state;
}
};
My guess is that you want something like this for your reducer:
// Action Creator
export const weightSort = (name, features) => {
console.log("inside the weight sort action creator!!!");
return {
type: "SET_WEIGHT_FILTER",
name,
features,
};
};
// Reducer
export const weightFilter = (
state = {
name: '',
features: [],
},
action
) => {
switch (action.type) {
case "SET_WEIGHT_FILTER":
return {...state, name: action.name, features: action.features}
default:
return state;
}
};
and then in your mapStateToProps you would map out the attributes like so:
const mapStateToProps = (state: any, ownProps: any) => {
console.log(state); //Displays the state
return {
name: state.weightFilter.name,
features: state.weightFilter.features,
};
};
and your button would have a name prop passed into the function like so:
<Button onClick={() => { onClick(name, features); }}> Weight</Button>
If you would like to sort your data, you can do so either in the reducer or inside the container. I prefer to do it in the container and like to use the lodash sortBy function. It works like this:
import { sortBy } from 'lodash' //be sure to npm install lodash if you use this utility
...
...
function mapStateToProps(state) {
return {
name: state.weightFilter.name,
features: sortBy(features, ['nameOfPropertyToSortBy'])
};
}
Here is the lodash documentation on sortBy: https://lodash.com/docs/4.17.4#sortBy
Hope that helps!

Resources