Migrating to redux-toolkit a switch multiply-case code - redux

Reading about redux-toolkit and thinking that it's a great tool. But how to deal if you have to migrate from a redux switch multi-cases written code? Is there way not to duplicate action methods that are described with cases as logical OR?
const someReducer = (state = {result:1, error: null, text: ""}, action) => {
switch(action.type){
case 'ADD':
case 'SUM':
case 'TOTAL':
return {
...state,
result: state.result + action.payload,
error: null
};
case 'DIVIDE':
return {
...state,
error: true,
text: "some error"
};
case 'MULTIPLY':
return {
...state,
result: state.result*ation.payload,
error: null
};
default:
return state
}
};
So after migrating to RTK it will looks not so handy with duplicating same code:
const someReducer = createSlice({
name: 'someReducer',
initialState: {result:1, error: null, text: ""},
reducers: {
add: (state, { payload }) => { result.state += payload, result.error=null }
total: (state, { payload }) => { result.state += payload, result.error=null }
sum: (state, { payload }) => { result.state += payload, result.error=null }
divide: (state, { payload }) => { result.error=null, result.tex t= "some error" }
multiply: (state, { payload }) => { result.state= result.state * payload, result.error=null }
}});

Related

converting to redux tool kit and getting "Unhandled Rejection (TypeError): state.push is not a function"

i'm stuck while converting an old project to redux tool kit getting an "Unhandled Rejection (TypeError): state.push is not a function" error. i haven't got to grips with the action/thunk and reducer immutability yet. The alerts are working but then the error msg.
import axios from 'axios';
import { setAlert } from '../alerts/alertSlice';
const slice = createSlice({
name: 'auth',
initialState: {
token: localStorage.getItem('token'),
isAuthenticated: null,
loading: true,
user: null,
},
reducers: {
registerSuccess: (state, action) => {
const { payload } = action.payload;
state.push({
payload,
isAuthenticated: true,
loading: false,
});
},
registerFail: (state, action) => {
localStorage.removeItem('token');
state.push({
token: null,
isAuthenticated: false,
loading: false,
user: null,
});
},
},
});
const { registerSuccess, registerFail } = slice.actions;
export default slice.reducer;
// Register User
export const register =
({ name, email, password }) =>
async (dispatch) => {
const config = {
headers: {
'comtent-Type': 'application/json',
},
};
const body = JSON.stringify({ name, email, password });
try {
const res = await axios.post('/api/users', body, config);
dispatch({
type: registerSuccess,
payload: res.data,
});
} catch (err) {
const errors = err.response.data.errors;
if (errors) {
errors.forEach((error) => dispatch(setAlert(error.msg, 'danger')));
}
dispatch(registerFail());
}
};
.push is an array function to add a new item at the end of an array - your state is not an array.
You probably wanted to do something along the lines of
state.token = null
state.isAuthenticated = false
state.loading = false
state.user = null

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.

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]

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.

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