Redux action payload being ignored when dispatching some other action inside axios interceptor - redux

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]

Related

RTK Query - update all injectedEndpoints cache with one WebSocket connection (Best Practice)

I am new to RTK query and need only one WebSocket connection for my entire application as you can see below I implemented it like an example in GitHub.
I need to somehow send my payload to this WebSocket by subscribing to it.
and then whenever the message comes in I update the other injected Endpoints' cache.
import { ApiSlice } from 'api';
import { instrumentsAdapter } from './marketSlice';
const socket = new WebSocket(process.env.REACT_APP_SOCKET_BASE_URL);
const socketConnected = new Promise((resolve, reject) => {
// Connection opened
try {
socket.addEventListener('open', (event) => {
resolve(event);
});
} catch (err) {
console.log('err', err);
reject(err);
}
});
export const socketApi = ApiSlice.injectEndpoints({
endpoints: (builder) => ({
socketChannel: builder.mutation({
async queryFn(arg) {
await socketConnected;
const { type, topic } = arg;
const sendPayload = { type, path: topic };
socket.send(JSON.stringify(sendPayload));
return { data: { messages: [] } };
},
async onCacheEntryAdded(arg, { cacheDataLoaded, cacheEntryRemoved }) {
console.log('arg', arg);
await cacheDataLoaded;
// Listen for messages
socket.onmessage = (res) => {
const message = JSON.parse(res.data);
try {
// ApiSlice.util.updateQueryData('getInstrumentByRefId', arg, (draft) => {
// console.log('arg', arg);
// draft = { ...message.value, baseVolume: 3 };
// });
} catch (err) {
console.log('err', err);
}
};
await cacheEntryRemoved;
socket.close();
}
})
})
});
export const { useSocketChannelMutation } = socketApi;
after so much reading docs and researching I finally find this solution working but I do not know if this is a best practice or not.
Here is my not-empty ApiSlice.
/* eslint-disable import/prefer-default-export */
// Or from '#reduxjs/toolkit/query' if not using the auto-generated hooks
import { createApi } from '#reduxjs/toolkit/query/react';
import axiosBaseQuery from './axiosBaseQuery';
export const socket = new WebSocket(process.env.REACT_APP_SOCKET_BASE_URL);
const socketConnected = new Promise((resolve, reject) => {
try {
socket.addEventListener('open', (event) => {
resolve(event);
});
} catch (err) {
reject(err);
}
});
// initialize an empty api service that we'll inject endpoints into later as needed
export const ApiSlice = createApi({
reducerPath: 'api',
baseQuery: axiosBaseQuery(),
endpoints: (builder) => ({
subscribeSocket: builder.mutation({
async queryFn(arg) {
await socketConnected;
const sendPayload = { type: 'SUBSCRIBE', path: arg };
socket.send(JSON.stringify(sendPayload));
return { data: { messages: [] } };
}
}),
unsubscribeSocket: builder.mutation({
async queryFn(arg) {
await socketConnected;
const sendPayload = { type: 'UNSUBSCRIBE', path: arg };
socket.send(JSON.stringify(sendPayload));
return { data: { messages: [] } };
}
}),
channel: builder.mutation({
async queryFn(onMessage) {
await socketConnected;
socket.addEventListener('message', onMessage);
return { data: { messages: [] } };
}
})
})
});
export const { useUnsubscribeSocketMutation, useSubscribeSocketMutation, useChannelMutation } =
ApiSlice;
and this is my enhanced Api slice
import { createEntityAdapter } from '#reduxjs/toolkit';
import { ApiSlice } from 'api';
export const instrumentsAdapter = createEntityAdapter({
selectId: (item) => item?.state?.symbol
});
export const marketApi = ApiSlice.injectEndpoints({
overrideExisting: false,
endpoints: (builder) => ({
getMarketMap: builder.query({
query: (type) => ({
url: `/market/map?type=${type}`,
method: 'get'
})
}),
getInstruments: builder.query({
query: (type) => ({
url: `/market/instruments?type=${type}`,
method: 'get'
})
}),
getInstrumentByRefId: builder.query({
query: (refId) => ({
url: `/market/instruments/${refId}/summary`,
method: 'get'
}),
transformResponse: (res) => {
return instrumentsAdapter.addOne(instrumentsAdapter.getInitialState(), res);
},
async onCacheEntryAdded(
arg,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved, dispatch }
) {
await cacheDataLoaded;
const payload = `instruments.${arg}.summary`;
// subs to socket
dispatch(ApiSlice.endpoints.subscribeSocket.initiate(payload));
// Listen for messages
const onMessage = (res) => {
const message = JSON.parse(res.data);
try {
updateCachedData((draft) => {
instrumentsAdapter.setOne(draft, message.value);
});
} catch (err) {
// eslint-disable-next-line no-console
console.log('err', err);
}
};
dispatch(ApiSlice.endpoints.channel.initiate(onMessage));
await cacheEntryRemoved;
// unsubs to socket
dispatch(ApiSlice.endpoints.unsubscribeSocket.initiate(payload));
}
}),
getCandles: builder.query({
query: ({ refId, bucket, end, limit = 1 }) => ({
url: `/market/instruments/${refId}/candles?bucket=${bucket}&end=${end}&limit=${limit}`,
method: 'get'
})
})
})
});
export const {
useGetMarketMapQuery,
useGetInstrumentByRefIdQuery,
useGetInstrumentsQuery,
useGetCandlesQuery
} = marketApi;
and I try to dispatch my socket endpoints from ApiSlice inside of onCacheEntryAdded.
async onCacheEntryAdded(
arg,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved, dispatch }
) {
await cacheDataLoaded;
const payload = `instruments.${arg}.summary`;
// subs to socket
dispatch(ApiSlice.endpoints.subscribeSocket.initiate(payload));
// Listen for messages
const onMessage = (res) => {
const message = JSON.parse(res.data);
try {
updateCachedData((draft) => {
instrumentsAdapter.setOne(draft, message.value);
});
} catch (err) {
// eslint-disable-next-line no-console
console.log('err', err);
}
};
dispatch(ApiSlice.endpoints.channel.initiate(onMessage));
await cacheEntryRemoved;
// unsubs to socket
dispatch(ApiSlice.endpoints.unsubscribeSocket.initiate(payload));
}
}),
```

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 Toolkit: How to test actions with uid prepare callback

In the docs for testing incrementing todo ids, this assumes a predictable response.
In an example such as below, a unique id is generated.
How could this be tested?
This test passes, but I'm not sure if it's correct, shouldn't the id be defined based on what's in the prepare callback?
slice.js
add: {
reducer: (state, {payload}: PayloadAction<{id: string, item: Item}>) => {
state[payload.id] = payload.item
},
prepare: (item: Item) => ({
payload: {id: cuid(), item}
})
}
slice.test.js
it('should handle add', () => {
expect(
reducer(
{},
{
type: actions.add,
payload: {
id: 'id-here?',
item: {
other: 'properties...'
}
},
}
)
).toEqual({
'id-here?': {
other: 'properties...'
},
})
})
You can pull out the prepare function and also the reducer function into it's own constant and then test prepare in isolation:
todosSlice.js:
[...]
let nextTodoId = 0;
export const addTodoPrepare = (text) => {
return {
payload: {
text,
id: nextTodoId++
}
}
}
export const addTodoReducer = (state,
action) => {
const {id, text} = action.payload;
state.push({
id,
text,
completed: false
});
};
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: {
prepare: addTodoPrepare,
reducer: addTodoReducer,
},
}
})
[...]
todosSlice.spec.js:
import todos, {addTodo, addTodoPrepare} from './todosSlice'
describe('addTodoPrepare',
() => {
it('should generate incrementing IDs',
() => {
const action1 = addTodoPrepare('a');
const action2 = addTodoPrepare('b');
expect(action1.payload).toEqual({
id: 0,
text: 'a'
})
expect(action2.payload).toEqual({
id: 1,
text: 'b'
})
})
})
describe('todos reducer',
() => {
[...]
})
For unit testing, NO, just test each reducer independently.
For integration testing and e2e testing, Yes.

setInterval props don't update

i dispatch action in the code below. And in the action i have loggingIn === false, but this.props.loggingIn === true.
How to fix it?
component.js
updateProfile() {
return { user: Meteor.user(), loggingIn: Meteor.loggingIn() }
}
componentWillMount() {
this.props.loadUser(this.updateProfile());
debugger;
this.data = setInterval(() => {
if(this.props.loggingIn === true) {
this.props.loadUser(this.updateProfile());
}
}, 1000);
}
actions.js
export const loadUser = ({user, loggingIn}) => dispatch => {
dispatch({
type: 'USER_DATA',
payload: user
})
dispatch({
type: 'USER_LOGGING_IN',
payload: loggingIn
});
};
You should be using redux-thunk for async calls. Here is the tutorial for using redux-thunk
and an example code for handling user authentication in Meteor

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