Update: I'm using React-Boilerplate, unmodified from the original except in the containers / components. The reducers run multiple times, sometimes more than twice, when a new action is dispatched for the first time, but not when the same action is dispatched subsequently. The actions themselves are not fired on the repeated reducer calls, but the state is updated and re-renders the component.
For example: If I dispatch action1 which updates reducerCase1, but not action2 which updates reducerCase2, action1 will run once, and reducerCase1 will run twice. action2 and reducerCase2 will not run. If I then dispatch action3 which updates reducerCase3, reducerCase1 will be called multiple times, but action1, action2, and reducerCase2 will not be called.
If I dispatch action2 in the same manner as action1, it will be treated the same way as action1 and reducerCase1, running the reducer multiple times without firing the action.
If after all this I dispatch action3 a second time, reducerCase1 will not be run at all (as should be the case).
Here I'm interested GET_CATEGORIES and GET_CATS_COMPLETED actions:
here is the console log inside the reducer:
export default function Categories(state = initialState, action) {
switch (action.type) {
case GET_CATEGORIES:
debugger;
console.log('getting categories...');
return state.set('isLoading', true);
case GET_CATEGORIES_COMPLETED:
debugger;
console.log('setting categories...');
return state
.set('categories', fromJS(action.cats))
.set('isLoading', false);
Since this is happening with all of my reducers, I assume it has something to do with mapDispatchToProps:
const mapStateToProps = createStructuredSelector({
categories: makeSelectCategories(),
ownerId: makeSelectProfileId(),
selectedCategoryId: makeSelectSelectedCategoryId(),
isLoading: makeSelectIsLoading(),
});
const mapDispatchToProps = {
getCategories,
setCategory,
};
const withConnect = connect(
mapStateToProps,
mapDispatchToProps
);
const withReducer = injectReducer({ key: 'CategoryContainer', reducer });
const withSaga = injectSaga({ key: 'CategoryContainer', saga });
export default compose(
withSaga,
withReducer,
withConnect
)(CategoryContainer);
and here are my actions:
export function getCategories() {
console.log('inside getCategories()');
return {
type: GET_CATEGORIES,
};
}
export function getCategoriesCompleted(cats) {
return {
type: GET_CATEGORIES_COMPLETED,
cats,
};
}
and finally the saga:
export default function* CategoryContainerSaga() {
yield takeLatest(GET_CATEGORIES, getCategories);
}
function* getCategories() {
try {
const plidParam = yield call(getPlParam);
const profileId = yield call(getProfileId);
const url = getUrl();
const { categories2, playlist } = yield call(
getCatsRequest,
url,
profileId,
plidParam
);
yield put(getCategoriesCompleted(categories2));
if (playlist) yield put(setPlaylist(playlist));
} catch (error) {
yield put(getCategoriesCompleted([]));
yield put(setError(error.message));
}
}
Thanks to lecstor's comment, I was able to determine that this is expected behavior of Redux devtools.
Related
im writing a react app who has a default state management: View dispatch an action than change reducer state. I was able to test the view and the reducer but didn't find a way to test my actions file because return a dispatch function
Action File that need to be tested:
import {Dispatch} from 'redux'
import {AuthAction, AuthActionTypes, SetUserAction} from "../actions-types/auth-actions-types";
export const setUserAction = (user: User) => {
return async (dispatch: Dispatch<SetUserAction>) => {
dispatch({
type: AuthActionTypes.SET_USER,
payload: user
})
}
}
reducer
import {AuthAction, AuthActionTypes} from "../actions-types/auth-actions-types";
export const initialAuthState = {
auth: {},
user: null
};
const reducer = (state = initialAuthState, action: AuthAction) => {
switch(action.type) {
case AuthActionTypes.SET_USER:
return {
...state,
user: action.payload,
};
default:
return state
}
}
export default reducer
reducer Test working ok.
import authReducer, {initialAuthState} from "./auth-reducer";
import {AuthActionTypes} from "../actions-types/auth-actions-types";
describe('Auth Reducer', ()=>{
test('should return user correclty ', ()=>{
const mockPayload = {
name: 'any_name',
emaiL: 'any_email',
accessToken: 'any_tokem'
}
const newState = authReducer(initialAuthState, {
type: AuthActionTypes.SET_USER,
payload: mockPayload
})
expect(newState.user).toEqual(mockPayload);
})
})
Action File test with problems
describe('AuthAction', ()=>{
test('setUserAction', ()=>{
const user = {
name: 'any_user',
email: 'any_email',
token: 'any_token'
}
const result = setUserAction();
expect(result).toEqual(user);
})
})
Expected: {"email": "any_email", "name": "any_user", "token": "any_token"}
Received: [Function anonymous]
Writing an action creator
Here is the official documentation that shows how to create an action creator
I do not see the benefit for your action creator to do a dispatch, you can simply write it and use it in the following way:
// action.ts
import { Dispatch } from 'redux'
import { AuthAction, AuthActionTypes, SetUserAction } from "../actions-types/auth-actions-types";
export const setUser = (user: User) => ({
type: AuthActionTypes.SET_USER,
payload: user
})
// somewhere.ts
dispatch(setUser(user))
Now the redux team recommends using redux-toolkit and they provide a simple tool called createAction
And if you want to create your reducer and action creator at the same time in the easier possible way you can use createSlice
How to test a reducer and an action?
To avoid an opinionated response to this answer you have two paths:
testing reducer with your action creator
a test for the reducer and a test for the action
Testing a reducer with your action creator
The reducer test should confirm that the triggered action has the expected impact.
Here is an example of using your reducer and your action creator together:
describe('Auth Reducer', ()=>{
test('should set user correctly', ()=> {
const newState = authReducer(initialAuthState, setUser(mockPayload))
expect(newState.user).toEqual(mockPayload);
})
})
The benefit of this is that you just write one test and you assert that both action creator and reducer work well together.
How to test an action creator alone?
You do not need to test your action creator if you test your reducer with it.
An action is just an object with a type and payload basically, so you can test it in the following way
describe('AuthAction', () => {
test('setUserAction', () => {
const user = {
name: 'any_user',
email: 'any_email',
token: 'any_token'
}
const result = setUser(user);
expect(result).toEqual({ type: AuthActionTypes.SET_USER, user });
})
})
Im trying to comprehend the art of redux saga, but faced this situation:
I have useEffect hook that works correctly(works one time when changing url params). This hook dispatches action(created by redux-saga-routines) only one time.
const params = useParams().params;
useEffect(() => {
if (urlTriggers.some(item => item === params)) {
dispatch(setItemsCollection({ itemType: params }));
toggleVisibleMode(true);
} else {
toggleVisibleMode(false);
}
}, [params]);
Saga watcher reacts to the dispatched action
export function* setItemsCollectionWatcher() {
yield takeEvery(setItemsCollection.TRIGGER, setItemsCollectionWorker);
}
And then calls saga worker
function* setItemsCollectionWorker(action) {
const { itemType } = action.payload;
try {
yield put(toggleIsFetching({ isFetching: true }));
const itemsCollection = yield call(() => {
return axios.get(`http://localhost:60671/api/items/${itemType}/?page=1&count=2`).then(response => response.data.items);
});
yield put(setItemsCollection.success({ itemsCollection }));
yield put(toggleIsFetching({ isFetching: false }));
} catch (error) {
console.log(error);
} finally {
yield put(setItemsCollection.fulfill());
}
}
This saga listens all saga watchers
export default function* saga() {
yield all([
setBackgroundWatcher(),
setItemsCollectionWatcher(),
])
}
saga running
sagaMiddleware.run(saga);
export const setItemsCollection = createRoutine('showcase/SET_ITEMS_COLLECTION');
export const toggleIsFetching = createRoutine('showcase/TOGGLE_IS_FETCHING');
const showcase = createReducer(
{
itemsCollection: [],
isFetching: false,
},
{
[setItemsCollection.SUCCESS]: (state, action) => {
state.itemsCollection = action.payload.itemsCollection;
},
[toggleIsFetching.TRIGGER]: (state, action) => {
state.isFetching = action.payload.isFetching;
},
}
);
But I have 2 axios requests instead of just one.
Your dispatch from the client is type setItemsCollection which should be fine (though I generally 'USE_HUGE_OBVIOUS_TEXT_LIKE_THIS'). The response from the promise in your saga is the same: setItemsCollection whereas, depending on what you're trying to render, you may want to call your reducer with something entirely different.
At a glance, I'd suggest changing this line to something else (and matching what the reducer is listening for). I wonder if it's causing a crossed wire somewhere.
I am using Redux for state management and saga as a middleware. For some reason my app is in some infinite loop state of calling API endpoint.
This is my actions:
export const GET_USERS = "GET_USERS";
export const getUsers = () => ({
type: GET_USERS,
});
export const GET_USERS_SUCCESS = `${GET_USERS}_SUCCESS`;
export const getUsersSuccess = (data) => ({
type: GET_USERS_SUCCESS,
payload: data,
});
export const GET_USERS_FAIL = `${GET_USERS}_FAIL`;
export const getUsersFail = (error) => ({
type: GET_USERS_FAIL,
payload: error,
});
This is saga:
export function* getUsers$() {
try {
const users = yield getUsersAPI();
yield put(actions.getUsersSuccess(users.data));
} catch (error) {
yield put(actions.getUsersFail(error));
}
}
export default function* () {
yield all([takeLatest(actions.getUsers, getUsers$)]);
}
This is a reducer:
export default (state = initialState(), action) => {
const { type, payload } = action;
switch (type) {
case actions.GET_USERS:
return {
...state,
users: {
...state.users,
inProgress: true,
},
};
case actions.GET_USERS_SUCCESS:
return {
...state,
users: {
inProgress: false,
data: payload,
},
};
case actions.GET_USERS_FAIL:
return {
...state,
users: {
...state.users,
inProgress: false,
error: payload,
},
};
default:
return state;
}
};
And this is a component connected with redux:
const Home = (props) => {
useEffect(() => {
props.getUsers();
console.log('props', props.data);
}, []);
return(
<h1>Title</h1>
);
}
const mapStateToProps = ({
users: {
users: {
data
}
}
}) => ({data})
export default connect(mapStateToProps, {getUsers})(Home);
Why is this happening?
This is due to the fact that you misused the sagas in your example. As with any other effect creator as the first parameter must pass a pattern, which can be read in more detail in the documentation. The first parameter can also be passed a function, but in a slightly different way. View documentation (block take(pattern)).
In your case, you are passing a function there that will return an object
{
type: 'SOME_TYPE',
payload: 'some_payload',
}
Because of this, your worker will react to ALL events that you dispatch.
As a result, you receive data from the server, dispatch a new action to save data from the store. And besides the reducer, your getUsers saga will be called for this action too. And so on ad infinitum.
Solution
To solve this problem, just use the string constant actions.GET_USERS that you defined in your actions.
And your sagas will look like this:
export function* getUsers$() {
try {
const users = yield getUsersAPI();
yield put(actions.getUsersSuccess(users.data));
} catch (error) {
yield put(actions.getUsersFail(error));
}
}
export default function* () {
yield all([takeLatest(actions.GET_USERS, getUsers$)]);
}
This should fix your problem.
I have declared the following saga api.
export function* watchSaveProducts() {
yield takeLatest(ProductActionTypes.PRODUCT_SAVE_REQUEST, saveProducts);
}
export const saga = function* productSagasContainer() {
yield all([watchGetProducts(), watchSaveProducts()]);
};
When I dispatch an action from the container, both the saga watches triggered. But in that I am just calling only getProducts. Even If I do save products, then getProducts triggered before save products.
componentDidMount() {
this.props.getProducts();
}
The dispatch props like follow
const mapDispatchToProps = (dispatch: any) => ({
getProducts: () => dispatch(productActions.getProducts()),
saveProduct: (ids: number[]) => dispatch(productActions.saveProduct(ids)),
});
You're throwing both methods when you do this:
export const saga = function* productSagasContainer() {
yield all([watchGetProducts(), watchSaveProducts()]);
};
Therefore, both will always be running.
I'll explain my structure when I work with redux and sagas:
First, create a sagaMiddleware to connect redux-store with sagas (extracted from the redux-saga documentation) :
import createSagaMiddleware from 'redux-saga'
import reducer from './path/to/reducer'
export default function configureStore(initialState) {
// Note: passing middleware as the last argument to createStore requires redux#>=3.1.0
const sagaMiddleware = createSagaMiddleware()
return {
...createStore(reducer, initialState, applyMiddleware(/* other middleware, */sagaMiddleware)),
runSaga: sagaMiddleware.run(rootSaga)
}
}
Define rootSaga with sagas divided into application parts:
export function* rootSaga() {
yield all([fork(sample1Watcher), fork(sample2Watcher)]);
}
Create the set of sagas that will be launched in this part depending on the action that is dispatched
export function* sample1Watcher() {
yield takeLatest(ProductActionTypes.GET_PRODUCT_REQUEST, getProduct);
yield takeLatest(ProductActionTypes.PRODUCT_SAVE_REQUEST, saveProduct);
}
Define each of the methods, for example the get:
function* getProduct() {
try {
yield put({ type: actionTypes.SET_PRODUCTS_LOADING });
const data = yield call(products.fetchProductsAsync);
yield put({ type: actionTypes.GET_PRODUCTS_COMPLETE, payload: data });
} catch (e) {
console.log("Error");
}
}
Finally, define action method in dispatchToProps and launch it wherever you want:
const mapDispatchToProps = (dispatch: any) => ({
getProducts: () => dispatch(productActions.getProducts()),
saveProduct: (ids: number[]) => dispatch(productActions.saveProduct(ids)),
});
componentDidMount() {
this.props.getProducts();
}
I'm developing React/Redux application and I've got problem with getting one particular state from redux store after dispatching an action. I don't have any idea why is that happening, because I haven't experienced such issue with other states. Here is my code:
Reducer
import {SET_CURRENT_USER, SET_LECTURES} from '../actions/actionTypes';
import isEmpty from 'lodash/isEmpty';
const initialState = {
isAuthenticated: false,
user: {},
lectures: []
}
export default (state = initialState, action = {}) => {
switch(action.type) {
case SET_CURRENT_USER:
return {
isAuthenticated: !isEmpty(action.user),
user: action.user
};
case SET_LECTURES:
return {
lectures: action.lectures
}
default: return state;
}
}
Action creator and dispatching action
import { SET_LECTURES } from './actionTypes';
export const setLectures = (lectures) => {
return {
type: SET_LECTURES,
lectures
}
}
export const lecture = (lectures) => {
return dispatch => {
console.log(lectures);
dispatch(setLectures(lectures));
}
}
The problem is with SET_LECTURES action type, in particular lectures property of action object. In the component from which I want to get state lectures, I do mapStateToProps as follows:
const mapStateToProps = function(state) {
return {
notifications: state.notifications,
lectures: state.lectures
}
}
/*
*Code of the class
*/
export default connect(mapStateToProps, null)(AddQuestionForm);
I've skipped code which triggers dispatching action type SET_LECTURES, because it's working fine. I've also used React Developer Tools for tracking states, and there is lectures state. I just can't get this state from my component, when I do console.log(this.props.lectures) from ComponentDidMount(), it shows undefined. Could you explain what am I doing wrong here? I would appreciate any help.
You forgot about dispatch:
export const lectureAction = lectures => dispatch => {
return dispatch => {
dispatch(setLectures(lectures));
}
}
In Component:
import { bindActionCreators } from 'redux';
const mapStateToProps = function(state) {
return {
notifications: state.notifications
}
}
// use map dispatch for actions:
const mapDispatchToProps = dispatch =>
bindActionCreators({
lectures: lectureAction
}, dispatch);
/*
*Code of the class
*/
// connect map dispatch here:
export default connect(mapStateToProps, mapDispatchToProps)(AddQuestionForm);
Now you have an access to this.props.lectures(someParams) function in your Component which dispatch an action.