Can I use condition in my action reducer? - javascript

Basically, in our case, we need to either get an alerts list that shows the first few items (mounting it first time in the DOM) or show the initial list + the next list (clicking a load more button).
Hence we needed to do this condition in our GET_ALERTS action:
case "GET_ALERTS":
if (action.initialList) {
newState.list = [...newState.list, action.res.data.list];
} else {
newState.list = newState.list.concat(
action.res.data.list
);
}
And when we call the action reducer in our Alerts component, we need to indicate whether initialList is true or false.
E.g.
componentDidMount() {
this.props.getAlerts(pageNum, true);
}
markAllAsRead() {
// other code calling api to mark all as read
this.props.getAlerts(pageNum, false);
}
readMore() {
// other code that increases pageNum state counter
this.props.getAlerts(pageNum, true);
}
Anyway in such a case, is it fine to use conditional statement in the reducer?

I am against this idea. The reducer has a single responsibility: update Redux state according to the action.
Here are three ways to slove this:
easy way - initialize your list in Redux state to empty list
if you set the list in state to empty list ([]) then it's much simpler.
You can basically just change your reducer to this:
case "GET_ALERTS":
return {...state, list: [...state.list, action.res.data.list]
This will make sure that even if you have get initial list or more items to add to the list, they will be appended. No need to add any logic - which is awesome IMHO.
redux-thunk and separating type into two different types
create two actions: GET_INIT_ALERTS and GET_MORE_ALERTS.
switch(action.type) {
case "GET_INIT_ALERTS":
return {...state, list: action.res.data.list }
case "GET_MORE_ALERTS":
return {...state, list: [...state.list, ...action.res.data.list]}
case "CHECK_READ_ALERTS":
return {...state, read: [...state.read, ...action.res.data.list]}
}
In the component I will have:
componentDidMount() {
this.props.getInitAlerts();
}
markAllAsRead() {
// other code calling api to mark all as read
this.props.getAlerts(pageNum, false);
}
readMore() {
// other code that increases pageNum state counter
this.props.getAlerts(pageNum);
}
In alerts action with the help of redux-thunk:
export const getAlerts = (pageNum : number) => (dispatch) => {
return apiAction(`/alerts/${pageNum}`, 'GET').then(res => dispatch({type: "GET_MORE_ALERTS", res});
}
export const getInitAlerts = () => (dispatch) => {
return apiAction('/alerts/1', 'GET').then(res => dispatch({type: "GET_INIT_ALERTS", res});
}
I guess you update pageNum after readMore or componentDidMount. Of course you can save that state in Redux and map it back to props and just increment it when calling the getAlerts action.
write your own middleware
Another way to do this is to write an ad-hoc/feature middleware to concat new data to a list.
const concatLists = store => next => action => {
let newAction = action
if (action.type.includes("GET") && action.initialList) {
newAction = {...action, concatList: action.res.data.list}
} else if (action.type.includes("GET") {
newAction = {...action, concatList: [...state[action.key].list, action.res.data.list]}
}
return next(newAction);
}
And change your reducer to simply push concatList to the state:
case "GET_ALERTS":
return {...state, list: action.concatList}
In addition, you will have to change your action to include key (in this case the key will be set to alert (or the name of the key where you store the alert state in redux) and initialList to determine whether to concat or not.
BTW, it's a good practice to put these two under the meta key.
{
type: "GET_ALERT",
meta: {
initialList: true,
key: "alert",
},
res: {...}
}
I hope this helps.

I would suggest you to have following set of actions:
ALERTS/INIT - loads initial list
ALERTS/LOAD_MORE - loads next page and then increments pageNo, so next call will know how many pages are loaded
ALERTS/MARK_ALL_AS_READ - does server call and reinitializes list
The store structure
{
list: [],
currentPage: 0
}
And component code should not track pageNum
componentDidMount() {
this.props.initAlerts();
}
markAllAsRead() {
this.props.markAllAsRead();
}
readMore() {
this.props.loadMore();
}

Related

Adding data to state in Ngrx reducer function

I am learning front-end and trying making an Angular app using NgRx for state management.
I have a table of Messages. I want to remove and add messages to store.
I am able to remove rows using a reducer function as shown below.
function RemoveHandler(state: StoreMessages, action) {
return {
...state,
selections: state.selections.filter(messageId => messageId !== action.messageId),
messages: state.messages.filter(item => item.messageId !== action.messageId),
all: state.all.filter(item => item.messageId !== action.messageId)
};
}
This works fine, but my logic for add message functionality is not working.
function AddHandler(state: StoreMessages, action) {
return {
...state,
messages: state.messages.push(action.newMessage),
all: state.all.push(action.newMessage)
};
}
The problem is that pop method return length of array and hence length is assigned to 'messages' and 'all' properties of my state. How can I add new messages to my state. Any help is appreciated.
you don't need the array.push method in the reducer. the first code work, because you are filtering an array that is in the state. to add data to the state simply assign it like
function AddHandler(state: StoreMessages, action) {
return {
...state,
messages: action.newMessage,
all: action.newMessage
};
}
or if you are insert an array
function AddHandler(state: StoreMessages, action) {
return {
...state,
messages: [...action.newMessage],
all: [...action.newMessage]
};
}

I'm not sure how to access and compare an object when keys are made using Date.now()

I'm quite new to coding and I'm currently practicing the useReducer() hook in React to manage some state in a simple todo app.
I'm having trouble when trying to implement the TOGGLE_TODO action. I've done it before using arrays, but as I'll likely be working with a lot of objects, I'm trying to figure out why I can't get this right. I'd say I'm learning by failing, but all I'm learning is how to switch the computer off and walk away!
Each time I toggle, I'm passing the state with the spread operator, I've tried it throughout all of the item, I've logged out the key and action.payload to make sure I'm getting a match (it works when I do a simple alert with matching).
I'm aware that the toggle isn't a toggle yet, I was just trying to simply get complete to be true.
I've tried a multitude of things to return state, I've added return to the beginning of the statement, and I"ve encountered some weird bugs along the way. As mentioned, this is quite simple state for now, but it will be more complex in another project I'm working on, so useState get's quite messy.
Any help on what I'm doing wrong here would be highly appreciated.
const initialAppState = {
isOpen: true,
todos: {}
};
export const ACTIONS = {
TOGGLE_MODAL: "toggle-modal",
ADD_TODO: "add-todo",
TOGGLE_TODO: "toggle-todo"
};
const reducer = (state, action) => {
// switch statement for actions
switch (action.type) {
case ACTIONS.TOGGLE_MODAL:
return { ...state, isOpen: !state.isOpen };
case ACTIONS.ADD_TODO:
return {
...state,
todos: {
...state.todos,
// Object is created with Unix code as the key
[Date.now()]: {
todo: action.payload.todo,
complete: false
}
}
};
case ACTIONS.TOGGLE_TODO:
// Comparing the key and the action payload. If they match, it should set complete to 'true'. This will be updated to a toggle when working.
Object.keys(state.todos).map((key) => {
if (key === action.payload) {
return {
...state,
todos: { ...state.todos, [key]: { complete: true } }
};
}
return state;
});
default:
throw new Error("Nope. not working");
}
};
In the render, I pass the key as an id so it can get returned with the payload.
Here is the dispatch function from the component...
const Todo = ({ id, value, dispatch }) => {
return (
<div className="todo">
<h1>{`Todo: ${value.todo}`}</h1>
<p>Done? {`${value.complete}`}</p>
<button
onClick={() =>
dispatch({
type: ACTIONS.TOGGLE_TODO,
payload: id
})
}
>
Mark as Done
</button>
</div>
);
};
and the render is using Object.entries which all works just fine. There were times when I'd get an error, or the initial todo would disappear, so I knew that state wasn't being updated correctly.
Here is the code on CodeSandbox too. I'll update here if I get it working, but I've been stuck here a couple of days. :-(
You were almost there, good idea to index your items with Date.now()!
Only a few issues in the TOGGLE_TODO case:
your reducer should always return a state, your return statement should be at the end of the case, but you put it with the map's function
your reducer should compute a new state, not mutate the current state. So you have to create a new todo object with the complete property.
Here is how it goes:
case ACTIONS.TOGGLE_TODO:
const newTodos = Object.keys(state.todos).map((key) => {
if (key === action.payload) {
return { ...state.todos[key], complete: true } // create a new todo item
}
else {
return state.todos[key]; // keep the existing item
}
});
return {...state, todos: newTodos};

NgRx Select Errors When Attempting Access on Nested Properties

I'm getting TypeErrors when using NgRx select functions when accessing nested properties.
I have my root store configured in app.module.ts like this:
StoreModule.forRoot({ app: appReducer }),
where app reducer is just a standard reducer. It sets the state correctly; I can see that in the redux dev tools. The selectors for some nested properties that are erroring are:
const getAppFeatureState = createFeatureSelector<IAppState>('app');
export const getAppConfig = createSelector(getAppFeatureState, state => {
return state.appConfig.data;
});
export const getConfigControls = createSelector(getAppConfig, state => {
console.log({ state }) // logs values from initial state
return state.controls;
});
export const getConfigDropdowns = createSelector(
getConfigControls,
state => state.dropdowns,
);
When I subscribe to these selectors in app.compontent.ts like this
ngOnInit() {
this.store.dispatch(new appActions.LoadAppConfig());
this.store
.pipe(select(appSelectors.getConfigDropdowns))
.subscribe(data => {
console.log('OnInit Dropdowns Data: ', data);
});
}
app.component.ts:31 ERROR TypeError: Cannot read property 'dropdowns' of null
at app.selectors.ts:18
When I add logging to the selectors higher up the chain, I can see that the only elements logged are the initialState values, which are set to null. I don't think this selector function should fire until the value changes from its initial value. But since it doesn't, its unsurprising that I'm getting this error, since it is trying to access a property on null. Is it a necessity that initialState contain the full tree of all potential future nested properties in order not to break my selectors?
How can I prevent this selector firing when its value is unchanged?
Also, Is the StoreModule.forRoot configured correctly? It is somewhat puzzling to me that creating a "root" store, creates the app key in my redux store parallel to my modules' stores, ie, the module stores are not underneath app.
Edit:
Adding general structure of app.reducer.ts. I use immer to shorten boilerplate necessary for updating nested properties, however I have tried this reducer also as the more traditional kind with spread operator all over the place and it works identically.
import produce from 'immer';
export const appReducer = produce(
(
draftState: rootStateModels.IAppState = initialState,
action: AppActions,
) => {
switch (action.type) {
case AppActionTypes.LoadAppConfig: {
draftState.appConfig.meta.isLoading = true;
break;
}
/* more cases updating the properties accessed in problematic selectors */
default: {
return draftState; // I think this default block is unnecessary based on immer documentation
}
}
}
Edit: Add initialState:
const initialState: rootStateModels.IAppState = {
user: null,
appConfig: {
meta: {isError: false, isLoading: false, isSuccess: false},
data: {
controls: {
dropdowns: null,
}
},
},
};
Because you updated your question the answer is https://www.learnrxjs.io/learn-rxjs/operators/filtering/distinctuntilchanged
it allows to emit values only when they have been changed.
store.pipe(
map(state => state.feature.something),
distinctUntilChanged(),
)
requires state.feautre.something to have been changed.
The right way would be to use createSelector function that returns memorized selectors that works in the same way as distinctUntilChanged.
You can use filter operator to make sure it emits values only for valid values, and after that you can use pluck operator to emit value of respective nested property.
store.pipe(
filter(value => state.feature.something),
pluck('feature', 'something'),
)
The dispatch method is async.
So:
ngOnInit() {
this.store.dispatch(new appActions.LoadAppConfig());
this.store
.pipe(select(appSelectors.getConfigDropdowns))
.subscribe(data => {
console.log('OnInit Dropdowns Data: ', data);
});
}
Here the subscription runs faster than the dispatch so the select returns with null value from your initial state. Simply check this in the selector or add initial state. EX:
const getAppFeatureState = createFeatureSelector<IAppState>('app');
export const getAppConfig = createSelector(getAppFeatureState, state => {
return state.appConfig.data;
});
export const getConfigControls = createSelector(getAppConfig, state => {
console.log({ state }) // logs values from initial state
return state.controls;
});
export const getConfigDropdowns = createSelector(
getConfigControls,
state => state ? state.dropdown : null,
);
Ok, I took a look again in code and updated my answer.
Can you try below given sample.
this.store
.pipe(
// Here `isStarted` will be boolean value which will enable and disable selector.
//This can be derived from initial state, if null it wont go to next selector
switchMap(data => {
if (isStarted) {
return never();
} else {
return of(data);
}
}),
switchMap(data => select(appSelectors.getConfigDropdowns))
)
.subscribe(data => {
console.log("OnInit Dropdowns Data: ", data);
});

Queuing Actions in Redux

I've currently got a situation whereby I need Redux Actions to be run consecutively. I've taken a look at various middlewares, such a redux-promise, which seem to be fine if you know what the successive actions are at the point of the root (for lack of a better term) action being triggered.
Essentially, I'd like to maintain a queue of actions that can be added to at any point. Each object has an instance of this queue in its state and dependent actions can be enqueued, processed and dequeued accordingly. I have an implementation, but in doing so I'm accessing state in my action creators, which feels like an anti-pattern.
I'll try and give some context on use case and implementation.
Use Case
Suppose you want to create some lists and persist them on a server. On list creation, the server responds with an id for that list, which is used in subsequent API end points pertaining to the list:
http://my.api.com/v1.0/lists/ // POST returns some id
http://my.api.com/v1.0/lists/<id>/items // API end points include id
Imagine that the client wants to perform optimistic updates on these API points, to enhance UX - nobody likes looking at spinners. So when you create a list, your new list instantly appears, with an option at add items:
+-------------+----------+
| List Name | Actions |
+-------------+----------+
| My New List | Add Item |
+-------------+----------+
Suppose that someone attempts to add an item before the response from the initial create call has made it back. The items API is dependent on the id, so we know we can't call it until we have that data. However, we might want to optimistically show the new item and enqueue a call to the items API so that it triggers once the create call is done.
A Potential Solution
The method I'm using to get around this currently is by giving each list an action queue - that is, a list of Redux actions that will be triggered in succession.
The reducer functionality for a list creation might look something like this:
case ADD_LIST:
return {
id: undefined, // To be filled on server response
name: action.payload.name,
actionQueue: []
}
Then, in an action creator, we'd enqueue an action instead of directly triggering it:
export const createListItem = (name) => {
return (dispatch) => {
dispatch(addList(name)); // Optimistic action
dispatch(enqueueListAction(name, backendCreateListAction(name));
}
}
For brevity, assume the backendCreateListAction function calls a fetch API, which dispatches messages to dequeue from the list on success/failure.
The Problem
What worries me here is the implementation of the enqueueListAction method. This is where I'm accessing state to govern the advancement of the queue. It looks something like this (ignore this matching on name - this actually uses a clientId in reality, but I'm trying to keep the example simple):
const enqueueListAction = (name, asyncAction) => {
return (dispatch, getState) => {
const state = getState();
dispatch(enqueue(name, asyncAction));{
const thisList = state.lists.find((l) => {
return l.name == name;
});
// If there's nothing in the queue then process immediately
if (thisList.actionQueue.length === 0) {
asyncAction(dispatch);
}
}
}
Here, assume that the enqueue method returns a plain action that inserts an async action into the lists actionQueue.
The whole thing feels a bit against the grain, but I'm not sure if there's another way to go with it. Additionally, since I need to dispatch in my asyncActions, I need to pass the dispatch method down to them.
There is similar code in the method to dequeue from the list, which triggers the next action should one exist:
const dequeueListAction = (name) => {
return (dispatch, getState) => {
dispatch(dequeue(name));
const state = getState();
const thisList = state.lists.find((l) => {
return l.name === name;
});
// Process next action if exists.
if (thisList.actionQueue.length > 0) {
thisList.actionQueue[0].asyncAction(dispatch);
}
}
Generally speaking, I can live with this, but I'm concerned that it's an anti-pattern and there might be a more concise, idiomatic way of doing this in Redux.
Any help is appreciated.
I have the perfect tool for what you are looking for. When you need a lot of control over redux, (especially anything asynchronous) and you need redux actions to happen sequentially there is no better tool than Redux Sagas. It is built on top of es6 generators giving you a lot of control since you can, in a sense, pause your code at certain points.
The action queue you describe is what is called a saga. Now since it is created to work with redux these sagas can be triggered to run by dispatching in your components.
Since Sagas use generators you can also ensure with certainty that your dispatches occur in a specific order and only happen under certain conditions. Here is an example from their documentation and I will walk you through it to illustrate what I mean:
function* loginFlow() {
while (true) {
const {user, password} = yield take('LOGIN_REQUEST')
const token = yield call(authorize, user, password)
if (token) {
yield call(Api.storeItem, {token})
yield take('LOGOUT')
yield call(Api.clearItem, 'token')
}
}
}
Alright, it looks a little confusing at first but this saga defines the exact order a login sequence needs to happen. The infinite loop is allowed because of the nature of generators. When your code gets to a yield it will stop at that line and wait. It will not continue to the next line until you tell it to. So look where it says yield take('LOGIN_REQUEST'). The saga will yield or wait at this point until you dispatch 'LOGIN_REQUEST' after which the saga will call the authorize method, and go until the next yield. The next method is an asynchronous yield call(Api.storeItem, {token}) so it will not go to the next line until that code resolves.
Now, this is where the magic happens. The saga will stop again at yield take('LOGOUT') until you dispatch LOGOUT in your application. This is crucial since if you were to dispatch LOGIN_REQUEST again before LOGOUT, the login process would not be invoked. Now, if you dispatch LOGOUT it will loop back to the first yield and wait for the application to dispatch LOGIN_REQUEST again.
Redux Sagas are, by far, one of my favorite tools to use with Redux. It gives you so much control over your application and anyone reading your code will thank you since everything now reads one line at a time.
Have a look at this: https://github.com/gaearon/redux-thunk
The id alone shouldn't go through the reducer. In your action creator (thunk), fetch the list id first, and then() perform a second call to add the item to the list. After this, you can dispatch different actions based on whether or not the addition was successful.
You can dispatch multiple actions while doing this, to report when the server interaction has started and finished. This will allow you to show a message or a spinner, in case the operation is heavy and might take a while.
A more in-depth analysis can be found here: http://redux.js.org/docs/advanced/AsyncActions.html
All credit to Dan Abramov
I was facing a similar problem to yours. I needed a queue to guarantee that optimistic actions were committed or eventually committed (in case of network problems) to remote server in same sequential order they were created, or rollback if not possible. I found that with Redux only, fells short for this, basically because I believe it was not designed for this and doing it with promises alone can be really a hard problem to reason with, besides the fact you need to manage your queue state somehow... IMHO.
I think #Pcriulan's suggestion on using redux-saga was a good one. At first sight, redux-saga doesn't provide anything to help you with until you get to channels. This opens you a door to deal with concurrency in other ways other languages do, CSP specifically (see Go or Clojure's async for example), thanks to JS generators. There are even questions on why is named after the Saga pattern and not CSP haha... anyway.
So here is how a saga could help you with your queue:
export default function* watchRequests() {
while (true) {
// 1- Create a channel for request actions
const requestChan = yield actionChannel('ASYNC_ACTION');
let resetChannel = false;
while (!resetChannel) {
// 2- take from the channel
const action = yield take(requestChan);
// 3- Note that we're using a blocking call
resetChannel = yield call(handleRequest, action);
}
}
}
function* handleRequest({ asyncAction, payload }) {
while (true) {
try {
// Perform action
yield call(asyncAction, payload);
return false;
} catch(e) {
if(e instanceof ConflictError) {
// Could be a rollback or syncing again with server?
yield put({ type: 'ROLLBACK', payload });
// Store is out of consistency so
// don't let waiting actions come through
return true;
} else if(e instanceof ConnectionError) {
// try again
yield call(delay, 2000);
}
}
}
}
So the interesting part here is how the channel acts as a buffer (a queue) which keeps "listening" for incoming actions but won't proceed with future actions until it finish with the current one. You might need to go over their documentation in order to grasp the code better, but I think it's worth it. The resetting channel part might or not work for your needs :thinking:
Hope it helps!
This is how I would tackle this problem:
Make sure each local list have an unique identifier. I'm not talking about the backend id here. Name is probably not enough to identify a list? An "optimistic" list not yet persisted should be uniquely identifiable, and user may try to create 2 lists with the same name, even if it's an edge case.
On list creation, add a promise of backend id to a cache
CreatedListIdPromiseCache[localListId] = createBackendList({...}).then(list => list.id);
On item add, try to get the backend id from Redux store. If it does not exist, then try to get it from CreatedListIdCache. The returned id must be async because CreatedListIdCache returns a promise.
const getListIdPromise = (localListId,state) => {
// Get id from already created list
if ( state.lists[localListId] ) {
return Promise.resolve(state.lists[localListId].id)
}
// Get id from pending list creations
else if ( CreatedListIdPromiseCache[localListId] ) {
return CreatedListIdPromiseCache[localListId];
}
// Unexpected error
else {
return Promise.reject(new Error("Unable to find backend list id for list with local id = " + localListId));
}
}
Use this method in your addItem, so that your addItem will be delayed automatically until the backend id is available
// Create item, but do not attempt creation until we are sure to get a backend id
const backendListItemPromise = getListIdPromise(localListId,reduxState).then(backendListId => {
return createBackendListItem(backendListId, itemData);
})
// Provide user optimistic feedback even if the item is not yet added to the list
dispatch(addListItemOptimistic());
backendListItemPromise.then(
backendListItem => dispatch(addListItemCommit()),
error => dispatch(addListItemRollback())
);
You may want to clean the CreatedListIdPromiseCache, but it's probably not very important for most apps unless you have very strict memory usage requirements.
Another option would be that the backend id is computed on frontend, with something like UUID. Your backend just need to verify the unicity of this id. Thus you would always have a valid backend id for all optimistically created lists, even if backend didn't reply yet.
You don't have to deal with queuing actions. It will hide the data flow and it will make your app more tedious to debug.
I suggest you to use some temporary ids when creating a list or an item and then update those ids when you actually receive the real ones from the store.
Something like this maybe ? (don't tested but you get the id) :
EDIT : I didn't understand at first that the items need to be automatically saved when the list is saved. I edited the createList action creator.
/* REDUCERS & ACTIONS */
// this "thunk" action creator is responsible for :
// - creating the temporary list item in the store with some
// generated unique id
// - dispatching the action to tell the store that a temporary list
// has been created (optimistic update)
// - triggering a POST request to save the list in the database
// - dispatching an action to tell the store the list is correctly
// saved
// - triggering a POST request for saving items related to the old
// list id and triggering the correspondant receiveCreatedItem
// action
const createList = (name) => {
const tempList = {
id: uniqueId(),
name
}
return (dispatch, getState) => {
dispatch(tempListCreated(tempList))
FakeListAPI
.post(tempList)
.then(list => {
dispatch(receiveCreatedList(tempList.id, list))
// when the list is saved we can now safely
// save the related items since the API
// certainly need a real list ID to correctly
// save an item
const itemsToSave = getState().items.filter(item => item.listId === tempList.id)
for (let tempItem of itemsToSave) {
FakeListItemAPI
.post(tempItem)
.then(item => dispatch(receiveCreatedItem(tempItem.id, item)))
}
)
}
}
const tempListCreated = (list) => ({
type: 'TEMP_LIST_CREATED',
payload: {
list
}
})
const receiveCreatedList = (oldId, list) => ({
type: 'RECEIVE_CREATED_LIST',
payload: {
list
},
meta: {
oldId
}
})
const createItem = (name, listId) => {
const tempItem = {
id: uniqueId(),
name,
listId
}
return (dispatch) => {
dispatch(tempItemCreated(tempItem))
}
}
const tempItemCreated = (item) => ({
type: 'TEMP_ITEM_CREATED',
payload: {
item
}
})
const receiveCreatedItem = (oldId, item) => ({
type: 'RECEIVE_CREATED_ITEM',
payload: {
item
},
meta: {
oldId
}
})
/* given this state shape :
state = {
lists: {
ids: [ 'list1ID', 'list2ID' ],
byId: {
'list1ID': {
id: 'list1ID',
name: 'list1'
},
'list2ID': {
id: 'list2ID',
name: 'list2'
},
}
...
},
items: {
ids: [ 'item1ID','item2ID' ],
byId: {
'item1ID': {
id: 'item1ID',
name: 'item1',
listID: 'list1ID'
},
'item2ID': {
id: 'item2ID',
name: 'item2',
listID: 'list2ID'
}
}
}
}
*/
// Here i'm using a immediately invoked function just
// to isolate ids and byId variable to avoid duplicate
// declaration issue since we need them for both
// lists and items reducers
const lists = (() => {
const ids = (ids = [], action = {}) => ({
switch (action.type) {
// when receiving the temporary list
// we need to add the temporary id
// in the ids list
case 'TEMP_LIST_CREATED':
return [...ids, action.payload.list.id]
// when receiving the real list
// we need to remove the old temporary id
// and add the real id instead
case 'RECEIVE_CREATED_LIST':
return ids
.filter(id => id !== action.meta.oldId)
.concat([action.payload.list.id])
default:
return ids
}
})
const byId = (byId = {}, action = {}) => ({
switch (action.type) {
// same as above, when the the temp list
// gets created we store it indexed by
// its temp id
case 'TEMP_LIST_CREATED':
return {
...byId,
[action.payload.list.id]: action.payload.list
}
// when we receive the real list we first
// need to remove the old one before
// adding the real list
case 'RECEIVE_CREATED_LIST': {
const {
[action.meta.oldId]: oldList,
...otherLists
} = byId
return {
...otherLists,
[action.payload.list.id]: action.payload.list
}
}
}
})
return combineReducers({
ids,
byId
})
})()
const items = (() => {
const ids = (ids = [], action = {}) => ({
switch (action.type) {
case 'TEMP_ITEM_CREATED':
return [...ids, action.payload.item.id]
case 'RECEIVE_CREATED_ITEM':
return ids
.filter(id => id !== action.meta.oldId)
.concat([action.payload.item.id])
default:
return ids
}
})
const byId = (byId = {}, action = {}) => ({
switch (action.type) {
case 'TEMP_ITEM_CREATED':
return {
...byId,
[action.payload.item.id]: action.payload.item
}
case 'RECEIVE_CREATED_ITEM': {
const {
[action.meta.oldId]: oldList,
...otherItems
} = byId
return {
...otherItems,
[action.payload.item.id]: action.payload.item
}
}
// when we receive a real list
// we need to reappropriate all
// the items that are referring to
// the old listId to the new one
case 'RECEIVE_CREATED_LIST': {
const oldListId = action.meta.oldId
const newListId = action.payload.list.id
const _byId = {}
for (let id of Object.keys(byId)) {
let item = byId[id]
_byId[id] = {
...item,
listId: item.listId === oldListId ? newListId : item.listId
}
}
return _byId
}
}
})
return combineReducers({
ids,
byId
})
})()
const reducer = combineReducers({
lists,
items
})
/* REDUCERS & ACTIONS */

Managing state in angular2 application - side effects?

This is more of a general question, but based on Victor Savkin post Managing state in angular2
Let's consider approach described there that uses RxJs:
interface Todo { id: number; text: string; completed: boolean; }
interface AppState { todos: Todo[]; visibilityFilter: string; }
function todos(initState: Todo[], actions: Observable<Action>): Observable<Todo[]> {
return actions.scan((state, action) => {
if (action instanceof AddTodoAction) {
const newTodo = {id: action.todoId, text: action.text, completed: false};
return [...state, newTodo];
} else {
return state;
}
}, initState);
}
All is fine, but let's add few more requirements:
Upon adding new Todo item, its text should be sent to the backend and analysed to extract possible due date and location.
If Todo item has due date, it should be added to my Google calendar
So if i add Todo "Get my hair done at Sally's Saloon on Thursday", with first call i would get from backend Sally's Saloon and date which is set to this weeks (or next weeks) Thursday and second call would add this todo to my Google calendar and mark item as in calendar.
So my new Todo item structure might look something like this:
interface Todo {
id: number;
text: string;
completed: boolean;
location?: Coordinates;
date?: Date;
inCalendar?: boolean;
parsed?: boolean;
}
And now i have two side effects :
After todo has been added i need to parse the text
After date has been added to Todo, i need to add it to calendar.
How do i deal with these side effects in this approach? Redux says that reducers should be kept clean, and they also have a notion of Sagas.
Option 1 - fire new event(s) for side effects when todo is added
function todos(initState: Todo[], actions: Observable<Action>): Observable<Todo[]> {
return actions.scan((state, action) => {
if (action instanceof AddTodoAction) {
const newTodo = {id: action.todoId, text: action.text, completed: false};
actions.onNext(new ParseTodoAction(action.todoId));
return [...state, newTodo];
} else if (action instanceOf ParseTodoAction){
const todo = state.find(t => t.todoId === action.todoId)
parserService
.parse(todo.todoId, todo.text)
.subscribe(r => actions.onNext(new TodoParsedAction(todo.todoId, r.coordinates, r.date)))
} else {
return state;
}
}, initState);
}
But this will fail, because new todo is not yet available on the state.
I could of course use only TodoParsedAction and instead of ParseTodoAction just invoke backend call inline, but this would also assume that backend call will take longer to process, and by the time it finishes state will already have that new Todo item which is trouble waiting to happen.
Option 2 - subscribe to actions and check each todo for missing properties
actions
.flatMap(todos => Observable.from(todos))
.subscribe(todo => {
if (!todo.coordinates && !todo.parsed) {
parserService
.parse(todo.todoId, todo.text)
.subscribe(r => actions.onNext(new TodoParsedAction(todo.todoId, r.coordinates, r.date)))
}
if (todo.date && todo.inCalendar === undefined) {
calendarService
.add(todo.text, todo.date)
.subscribe(_ => actions.onNext(new TodoInCalendarAction(todo.todoId, true)))
}
})
But this somehow does not feel right - shouldn't be everything managed by actions, and should i always loop through all of todo items?
Your option 1 can't work as stated: actions is an Observable<Action> observables are read-only and onNext isn't part of that API. You need an Observer<Action> to support option 1. This highlights the real flaw of option 1: your state function (same thing as a Redux reducer) needs to be pure and side-effect free. That means they cannot and should not dispatch more actions.
Now in the blog article you reference, indeed the code is really passing in a Subject, which is both Observer and Observable. So you probably do have an onNext. But I can tell you that recursively publishing data to a Subject while you are handling data being published by that Subject will get you into no end of trouble and is rarely worth the headaches to make work correctly.
In Redux, the typical solution to invoking backend processing to enrich your state would be to dispatch multiple actions at the beginning when you have already decided to dispatch AddTodo. This can often be done by using redux-thunk and dispatching functions as "smart actions":
Instead of:
export function addToDo(args) {
return new AddToDoAction(args);
}
you'd do:
export function addToDo(args) {
return (dispatch) => {
dispatch(new AddToDoAction(args)); // if you want to dispatch the Todo before parsing
dispatch(parseToDo(args)); // handle parsing
};
}
export function parseToDo(args) {
return (dispatch) => {
if (thisToDoNeedsParsing(args)) {
callServerAndParse(args).then(result => {
// dispatch an action to update the Todo
dispatch(new EnrichToDoWithParsedData(result));
});
}
};
}
// UI code would do:
dispatch(addToDo(args));
The UI dispatches a smart action (thunk) which will dispatch the AddToDoAction to get the unparsed todo in your state (your UI can choose to not show it until the parse completes if you want). It then dispatches another smart action (thunk) which will actually call the server to get more data then dispatch an EnrichToDoWithParsedData action with the results so that your Todo can be updated.
As for updating of the calendar...you can probably use the pattern above (inserting calls to possiblyUpdateCalendar() in both addToDo and parseToDo so that if the todo has all the stuff you need, it can update the calendar and when that finishes dispatch an action to mark the todo as added.
Now this example I've shown is Redux-specific and I don't think the RxJs-based example you are working from has anything like a thunk. One way to add support for this in your scheme is to add a flatMap operator to the subject that goes something like this:
let actionStream = actionsSubject.flatMap(action => {
if (typeof action !== "function") {
// not a thunk. just return it as a simple observable
return Rx.Observable.of(action);
}
// call the function and give it a dispatch method to collect any actions it dispatches
var actions = [];
var dispatch = a => actions.push(a);
action(dispatch);
// now return the actions that the action thunk dispatched
return Rx.Observable.of(actions);
});
// pass actionStream to your stateFns instead of passing the raw subject
var state$ = stateFn(initState, actionStream);
// Now your UI code *can* pass in "smart" actions:
actionSubject.onNext(addTodo(args));
// or "dumb" actions:
actionSubject.onNext(new SomeSimpleAction(args));
Notice all of that code above is in the code that dispatches an action. I didn't show any of your state function. Your state function would be pure and something like:
function todos(initState: Todo[], actions: Observable<Action>): Observable<Todo[]> {
return actions.scan((state, action) => {
if (action instanceof AddTodoAction) {
const newTodo = {id: action.todoId, text: action.text, completed: false};
return [...state, newTodo];
} else if (action instanceof EnrichTodoWithParsedData) {
// (replace the todo inside the state array with a new updated one)
} else if (action instanceof AddedToCalendar) {
// (replace the todo inside the state array with a new updated one)
}
} else {
return state;
}
}, initState);
}

Categories

Resources