I have an application that uses redux for state management and sagas for async calls, and I am trying to figure out the correct structure for pagination. I have a reducer like this:
function articles(
state = {
isFetching: false,
didInvalidate: false,
page: 1,
items: []
},
action
) {
switch (action.type) {
case 'INVALIDATE_ARTICLE':
return Object.assign({}, state, {
didInvalidate: true,
page: 1
})
case 'REQUEST_ARTICLES':
return Object.assign({}, state, {
isFetching: true,
didInvalidate: false
})
case 'RECEIVE_ARTICLES':
let updatedPage = 'max';
if (action.response.result.length == 10) {
updatedPage = state.page + 1
};
return Object.assign({}, state, {
categoryId: action.category,
isFetching: false,
didInvalidate: false,
items: action.response.result,
lastUpdated: action.receivedAt,
page: updatedPage
})
default:
return state
}
}
And the saga that calls those related actions like this:
function* fetchArticles(action) {
const { domain, category, page } = action.payload;
try {
yield put(requestArticles(category))
const response = yield fetch(`${domain}/url?categories=${category}&page=${page}`)
const stories = yield response.json();
yield all(stories.map(story => {
return call(fetchFeaturedImage, `${url][0]}`, story)
}))
const normalizedData = normalize(stories, articleListSchema);
yield put(receiveArticles(category, normalizedData))
}
catch (err) {
console.log('error fetching articles in saga', err)
}
}
Currently I am reading the redux state from the components that call the saga and passing the page in to the saga as the payload; but I am thinking this might get messy as I will eventually have to read the state to get the page from multiple different components that want to call this saga, so I am wondering if using sagas select() method might be the correct approach to read the redux state and get the page directly from within the saga so I don't have to worry about passing it?
Any opinions on how to correctly structure this?
Related
I have a saga that yields a put. One of the arguments to that put is a function object.
I've copied the saga function only as far as it is relevant:
function* updateTaskTimeCancelledTimeRejectedSuccess(action) {
const {payload} = action.data
const tasks = yield select(getTasksSelector);
const parent = yield call(findExistingTaskParent, tasks, action.data.taskUUID);
if (action.type === taskActions.updateTaskCancelledTimeActions.success) {
const currentValue = parent.taskGroup[action.data.taskUUID].time_cancelled;
if (currentValue === null) {
// only notify if marking rejected for the first time
const restoreActions = yield () => [taskActions.updateTaskCancelledTimeRequest(
action.data.taskUUID,
{time_cancelled: null}
)];
const viewLink = `/task/${encodeUUID(action.data.taskUUID)}`
yield put(displayInfoNotification("Task marked cancelled", restoreActions, viewLink))
This is the test:
it("updates the task state with cancelled time and sends a notification", () => {
const action = {type: taskActions.updateTaskCancelledTimeActions.success, data: {taskUUID: "someUUID", time_cancelled: new Date().toISOString()}};
const restoreActions = () => [taskActions.updateTaskCancelledTimeRequest(
action.data.taskUUID,
{time_cancelled: null}
)];
const viewLink = `/task/${encodeUUID(action.data.taskUUID)}`
return expectSaga(testable.updateTaskTimeCancelledTimeRejectedSuccess, action)
.provide([
[select(getTasksSelector), {
tasksNew: {
1: {
someUUID: {
time_cancelled: null,
parent_id: 1
}
}
}
}]])
.put(displayInfoNotification("Task marked cancelled", restoreActions, viewLink))
.run()
})
})
and this is the result:
SagaTestError:
put expectation unmet:
Expected
--------
{ '##redux-saga/IO': true,
combinator: false,
type: 'PUT',
payload:
{ channel: undefined,
action:
{ type: 'DISPLAY_INFO_NOTIFICATION',
message: 'Task marked cancelled',
restoreActions: [Function: restoreActions],
viewLink: '/task/0000000000000000000000' } } }
Actual:
------
1. { '##redux-saga/IO': true,
combinator: false,
type: 'PUT',
payload:
{ channel: undefined,
action:
{ type: 'DISPLAY_INFO_NOTIFICATION',
message: 'Task marked cancelled',
restoreActions: [Function],
viewLink: '/task/0000000000000000000000' } } }
at node_modules/redux-saga-test-plan/lib/expectSaga/expectations.js:48:13
at node_modules/redux-saga-test-plan/lib/expectSaga/index.js:544:7
at Array.forEach (<anonymous>)
at checkExpectations (node_modules/redux-saga-test-plan/lib/expectSaga/index.js:543:18)
It's obvious to me that the equality is failing because of the restoreActions object which is a function of a different reference. I can't figure out what I should do instead to make the test pass. What do I need to do to verify the saga is flowing as I expected? I don't necessarily need to verify the result of the function, just that the displayInfoNotification put took place.
Thank you.
Make a factory function for your restoreActions lambda and then you can mock it for your test.
e.g.
// somewhere in your module
const actionRestoreFactory = (action) => () => [taskActions.updateTaskCancelledTimeRequest(action.data.taskUUID, {time_cancelled: null})]
//In your saga
yield put(displayInfoNotification("Task marked cancelled", actionRestoreFactory(action), viewLink))
//In your test
import actionRestoreFactory from './your/module'
jest.mock('./your/module')
actionRestoreFactory.mockReturnValue(jest.fn())
//...
return expectSaga(testable.updateTaskTimeCancelledTimeRejectedSuccess, action)
.provide([ ... ])
.put(displayInfoNotification("Task marked cancelled", actionRestoreFactory(action), viewLink))
.run()
Redux documentation gives an example of storage that keeps posts and comments. To reduce complicity of nested objects it suggests to keep comments as array of IDs.
So the code of posts reducer will look like this:
function postsById(state = {}, action) {
switch (action.type) {
case 'ADD_COMMENT':
var { commentId, postId, commentText } = action.payload;
var post = state[postId];
return {
...state,
[postId]: {
...post,
comments: post.comments.concat(commentId)
}
}
default:
return state
}
}
But why not keep the whole objects there:
...
return {
...state,
[postId]: {
...post,
comments: post.comments.concat(
{commentId, commentText} // <=
)
}
}
...
If we do so we don't need to use complex selectors and computations to get required data:
// keeping IDs
function getPostComments(state, postId) {
return state.postsById[postId].comments.map(
commentId => state.commentsById[commentId]
)
}
// keeping objects
function getPostComments(state, postId) {
return state.postsById[postId].comments
}
Though this example is very simple, in other cases keeping the whole objects will make complex selectors much easier.
I have a loading object, with boolean values for each loading. My state looks like this:
state: {
loading: {
itemA: false,
itemB: false,
itemC: false
}
}
I want to update my state using setLoading({ itemA: true }) but having this only update itemA while keeping itemB and itemC the same as whatever the current state is.
return {
...state,
loading: {
...state.loading,
itemA,
itemB,
itemC
}
};
Here is the full reducer (condensed):
setLoading: state => ({ itemA, itemB, itemC }) => {
return {
...state,
loading: {
...state.loading,
itemA,
itemB,
itemC
}
};
}
Unfortunately, if I setLoading({ itemC: true }), A and B are now undefined.
How do I ensure they are not undefined, but rather whatever is in the state?
Please note - I have tried passing just anything and doing object assign or spreading loading.
However, to increase readability, I am wondering if I can destructure the props and not have to define each loading (I have like 12 things on this page that load separately - a dashboard).
Thanks
Use Object.assign()
const state = {
loading: {
itemA: false,
itemB: false,
itemC: false
}
}
Object.assign(state.loading, {itemA: true});
console.log(state.loading);
Just pass a variable, that you unfold
setLoading: state => (newLoadingState) => {
return {
...state,
loading: {
...state.loading,
...newLoadingState
}
};
}
You could provide default values in your destructure
const fn = state => ({
itemA = state.loading.itemA,
itemB = state.loading.itemB,
itemC = state.loading.itemC
}) => {
return {
...state,
loading: {
...state.loading,
itemA,
itemB,
itemC
}
};
};
Now when the variables are missing, they're taken from the state passed in;
const fn2 = fn({loading: {itemA: false, itemB: false, itemC: false}});
fn2({itemB: true});
// => {loading: {itemA: false, itemB: true, itemC: false}}
I have been working on the feature of comment deleting and came across a question regarding a mutation for an action.
Here is my client:
delete_post_comment({post_id, comment_id} = {}) {
// DELETE /api/posts/:post_id/comments/:id
return this._delete_request({
path: document.apiBasicUrl + '/posts/' + post_id + '/comments/' + comment_id,
});
}
Here is my store:
import Client from '../client/client';
import ClientAlert from '../client/client_alert';
import S_Helper from '../helpers/store_helper';
const state = {
comment: {
id: 0,
body: '',
deleted: false,
},
comments: [],
};
const actions = {
deletePostComment({ params }) {
// DELETE /api/posts/:post_id/comments/:id
document.client
.delete_post_comment({ params })
.then(ca => {
S_Helper.cmt_data(ca, 'delete_comment', this);
})
.catch(error => {
ClientAlert.std_fail_with_err(error);
});
},
};
delete_comment(context, id) {
context.comment = comment.map(comment => {
if (!!comment.id && comment.id === id) {
comment.deleted = true;
comment.body = '';
}
});
},
};
export default {
state,
actions,
mutations,
getters,
};
I am not quite sure if I wrote my mutation correctly. So far, when I am calling the action via on-click inside the component, nothing is happening.
Guessing you are using vuex the flow should be:
according to this flow, on the component template
#click="buttonAction(someParams)"
vm instance, methods object:
buttonAction(someParams) {
this.$store.dispatch('triggerActionMethod', { 'something_else': someParams })
}
vuex actions - Use actions for the logic, ajax call ecc.
triggerActionMethod: ({commit}, params) => {
commit('SOME_TRANSATION_NAME', params)
}
vuex mutations - Use mutation to make the changes into your state
'SOME_TRANSATION_NAME' (state, data) { state.SOME_ARG = data }
function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
text
}
dispatch(action)
}
http://redux.js.org/docs/basics/Actions.html#action-creators
I saw the code above from redux documentation. What I don't understand is the text in the action. It doesn't look like a valid javascript object or array syntax. Is it an ES6 new syntax? Thanks.
It is just a new ES6 syntax, which simplifies creating properties on the literal syntax
In short, if the name is the same as the property, you only have to write it once
So this would be exactly the same :)
function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
text: text
}
dispatch(action)
}
In the above code
function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
text
}
dispatch(action)
}
here text is an example of object literal shorthand notation. ES6 gives you a shortcut for defining properties on an object whose value is equal to another variable with the same key.
As has been said this is just shorthand for writing
const action = {
type: ADD_TODO,
text: text
}
dispatch(action)
Have a look at this blog
If you look at the next page in document http://redux.js.org/docs/basics/Reducers.html
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
})
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: state.todos.map((todo, index) => {
if(index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
})
default:
return state
}
}
It is expecting property name text. As #Icepickle mentioned it is a valid format but you can also change to below format:
function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
text:text
}
dispatch(action)
}