Flow Type + Redux: Type for action that won't conflict with actions for other reducers - redux

I have a reducer for storing preferences. It has two action types. One for loading in all preferences from database and another for updating a single preference. I have a working standalone example but it breaks once used inside of my app.
The issue is that my preferences reducer only handles two types of actions, while my app has multiple reducers that fire other actions. A solution to get the code running is to add a third general type for actions not related to this reducer. That however creates Property not found in 'object type'. errors when I try to access properties of the action.
Working flow example
// #flow
const LOAD_PREFS_SUCCESS = 'LOAD_PREFS_SUCCESS';
const UPDATE_PREF = 'UPDATE_PREF';
type aType = {
+type: string
};
export type actionType = {
+type: typeof LOAD_PREFS_SUCCESS,
prefs: Array<{_id: string, value: any}>
} | {
+type: typeof UPDATE_PREF,
id: string,
value: any
};
export default (state: {} = {}, action: actionType) => {
if (action.type === LOAD_PREFS_SUCCESS) {
action.prefs.forEach(p => {
console.log(p);
});
}
switch (action.type) {
case LOAD_PREFS_SUCCESS: {
const newState = {};
action.prefs.forEach(p => {
newState[p._id] = p.value;
});
return newState;
}
case UPDATE_PREF: {
return { ...state, [action.id]: action.value };
}
default:
return state;
}
};
This is valid flow but when the app actually runs, I get an error when an action with type INIT_APP or something runs. The error says action must be one of: and then it lists the two types I have in actionType as the expected and an actual of { type: string }.
I can get the app running by adding a third type to actionType like this:
export type actionType = {
+type: typeof LOAD_PREFS_SUCCESS,
prefs: Array<{_id: string, value: any}>
} | {
+type: typeof UPDATE_PREF,
id: string,
value: any
} | {
+type: string
};
Even though the app now runs without error, it does not pass flow type check. Throwing errors of Property not found in object type. Here is an example on flow.org

Since every reducer ends up seeing every action, you'll want the type of this reducer function to include all the possible actions in your app. I usually define a single variant actionType with everything available in the app and use that in every reducer.
The reason why your last code example doesn't work is because the third, anonymous action type {type: string} is too vague. Before this, Flow could look at the two options in the action, and see that it would know which one was which based on the case statements. But with the third action type, an action like {type: "LOAD_PREFS_SUCCESS"} would match the third case in the type. So testing action.type === LOAD_PREFS_SUCCESS is no longer enough to prove that the action will have a prefs key.
So there are two ways to fix this:
If you change your action type to be more specific and include all the specific action types, your reducer should go back to type-checking.
Otherwise, add a dummy case, like | {type: "NOT-REAL"} so that Flow forces your reducer to have a default case for actions it doesn't understand.

Related

Actions in multiple slices in Redux toolkit

The Redux toolkit docs mention using actions (or rather action types) in multiple reducers
First, Redux action types are not meant to be exclusive to a single slice. Conceptually, each slice reducer "owns" its own piece of the Redux state, but it should be able to listen to any action type and update its state appropriately. For example, many different slices might want to respond to a "user logged out" action by clearing data or resetting back to initial state values. Keep that in mind as you design your state shape and create your slices.
But, “keeping that in mind”, what is the best way to achieve this, given that the toolkit puts the slice name at the start of each action type? And that you export a function from that slice and you call that single function to dispatch the action? What am I missing? Does this have to be done in some way that doesn’t use createSlice?
It looks like this is what extraReducers is for:
One of the key concepts of Redux is that each slice reducer "owns" its slice of state, and that many slice reducers can independently respond to the same action type. extraReducers allows createSlice to respond to other action types besides the types it has generated.
It is a little strange that the action dispatcher should know which reducer the action belongs. I'm not sure the motivation of having reducers and extraReducers, but you can use extraReducers to allow several slices to respond to the same action.
I've found that using the extraReducers functionality when creating a slice with createSlice is the best way to do it.
In my case I've implemented this by creating a 'SliceFactory' class for each related feature. I've used it to do exactly what is in the example and reset relevant slices on user logout by listening for a LOGOUT_USER action.
Reference:
extraReducers: https://redux-toolkit.js.org/api/createSlice#extrareducer
Original article I used for the factory: https://robkendal.co.uk/blog/2020-01-27-react-redux-components-apis-and-handler-utilities-part-two
import { createSlice } from '#reduxjs/toolkit';
import { LOGOUT_USER } from '../redux/actions';
class CrudReducerFactory {
constructor(slice, state = null, initialState = {}) {
state = state || slice;
this.initialState = initialState;
const reducerResult = createSlice({
name: slice,
initialState: initialState[state],
reducers: this._generateReducers(),
extraReducers: (builder) => {
builder.addCase(LOGOUT_USER, (state, action) => {
return { ...this.initialState };
});
},
});
this.reducer = reducerResult.reducer;
this.actions = reducerResult.actions;
}
_generateReducers = () => {
return {
// Create One
requestCreateOne: (state, action) => {
state.isLoading = true;
},
requestCreateOneSuccess: (state, action) => {
state.isLoading = false;
state.one = action.payload;
},
requestCreateOneError: (state, action) => {
state.isLoading = false;
},
// ...snip...
};
};
}
export default CrudReducerFactory;
This is instantiated like so:
const factory = new CrudReducerFactory('users', 'users', { foo: 'bah', one: null, isLoading: false } );
The first argument is the name of the slice, the second is the slice of state and the third is the initial state.
You can then use factory.reducer and factory.actions to use accordingly.

React Redux Search Reducer

Currently I have the below reducer switch statement. All it does is toggles the state of Sidebar, so first it shows then hides then shows. It's easy.
switch(action.type) {
case 'SIDEBAR_DISPLAY_TOGGLE':
return {
...state,
Sidebar : {
...state.Sidebar,
Display : !state.Sidebar.Display
}
}
default:
return state;
}
Now I have a input field like here
that people can type to search account. I am trying to set up Redux so when user types, it gets saved to the Redux global state and I can pull it from another component. I have this reducer code set up for it but I don't know how can I pull what user types into this reducer from that component?
function reducer(state = initialState, action) {
switch(action.type) {
case 'ACCOUNT_SEARCH':
return {
...state,
AccountNumberSearch : {
...state.AccountNumberSearch,
AccountNumber : ''
}
}
default:
return state;
}
}
}
An action is just an object with a string value named type. Any other properties on this object will also be passed, so you use this to pass the typed text.
If you're using a function to create your actions, something along the lines of:
export function accountNumberSearch(accountNumber) {
return { type: 'ACCOUNT_SEARCH', accountNumber };
}
Then in your reducer, you'll be able to assign the value in the state to action.accountNumber.
AccountNumberSearch : {
...state.AccountNumberSearch,
AccountNumber : action.accountNumber,
}
Then you can map your state to props as you normally would (as you did for the sidebar toggle).
Also, as an aside, you should look into modularising your reducers with combineReducers - Docs
This would be much easier than the way you're doing it.
EDIT: Handling the changes
First of all, you'd want to wire up your input field for the search box to an onChange listener. If you do this like onChange={this.onSearchChange} you can get the value from event in the function:
onSearchChange = event => {
this.props.AccountNumberSearch(event.target.value);
}
Then in mapDispatchToProps you'd send your action + the passed value to dispatch:
const mapDispatchToProps = dispatch => {
return {
AccountNumberSearch: AccountNumber => dispatch(importedActions.AccountNumberSearch(AccountNumber)),
}
}
Then, in the component you want to RECEIVE this value, you'd map the redux state to props, like:
const mapStateToProps = state => {
return {
AccountNumber: state.AccountNumberSearch.AccountNumber,
}
}
Then you can access that value in your render function by calling this.props.AccountNumber.
If you need to do something when this value changes, you can always listen on componentDidUpdate, and compare the value with the old one - if it changed, call whatever function that you need to do.

StoreModule.forRoot() - how to return object without additional key

I am wondering how can I return object of the same type as reducer function:
function storeReducer(
state = INITIAL_APPLICATION_STATE,
action: Actions
): ApplicationState {
switch (action.type) {
case LOAD_USER_THREADS_ACTION:
return handleLoadUserThreadsAction(state, action);
default:
return state;
}
}
I expect object of type ApplicationState, but with that approach:
StoreModule.forRoot({storeReducer})
I am getting object with key:
storeReducer:{ // object of type Application State}
I am expecting to get object (without additional storeReducer key):
{//object of type Application State}
Tried also StoreModule.forRoot(storeReducer) but then I am getting empty objects and it is not working.
The forRoot method on StoreModule expects and ActionReducerMap, not the result of your reducer.
I typically set mine up in a seperate file like this:
export interface IAppState {
aPieceOfState: IAPieceOfState;
}
export const reducers: ActionReducerMap<IAppState> = {
aPieceOfState: aPieceOfStateReducer
};
Then import this to app.module.ts and use it like:
StoreModule.forRoot(reducers)
Or you can put an assertion StoreModule.forRoot({storeReducer} as ActionReducerMap<IAppState>)

How to add a set of parameters to all actions before executing the reducer?

I'm looking for a way to add some parameters to every action when entering a (root) reducer.
Quick example, let's presume I have a userState that, among other things holds the currently signed in user, maybe in some child state even.
rootState
|
+-------------------------------------+
| |
userState +----+-----+
| | |
+---+-----------+ ... salesState
| |
... currentlySignedInUserState
In order to execute an action in another reducer (say, salesReducer) I need to have the currently signed in user, which is not readily available (in a sibling state or worse) so I have to intercept that action in the root state, read currentlySignedInUserState from userState, add the user to the action payload and reissue the action with the changed payload.
case SALES_ACTIONS.ADD_SALE:
return Object.assign({}, state, {
let payload = Object.assign({}, action.payload, {
userName: state.user.signedIn.name
});
sales: salesReducer(state.sales, {
type: SALES_ACTION.ADD_SALE,
payload
});
});
This gets messy in a jiffy and prevents me from using combineReducers because I have to forward each and every single action in a root reducer of my own making.
So I came up with the idea of just adding the user to every action when it enters the root reducer and then just combineReducers all the way.
Is that an accepted approach and if so, is there a way to extend combineReducers so that it takes additional payload parameters (I was thinking redux-reuse, but I'm unsure if that will do what I have in mind).
If not, what are my alternatives?
You can use a simple middleware that gets the userName from the state, and adds it as meta data to all actions unless not signedIn. You can add other conditions, such as specific actions that should be skipped.
I prefer adding additional data as meta, and not payload.
I've included two ways to augment the action.
The (untested) code:
const middleware = ({ getState }) => next => action => {
const state = getState();
if(!state.user.signedIn) { // if not signed in skip
return next(action);
}
const userName = state.user.signedIn.name;
/** add userName using assign **/
const meta = Object.assign({}, action.meta, { userName });
const newAction = Object.assign({}, action, { meta });
/**********************/
/** or add userName using object spread **/
const newAction = {
...action,
meta: { ...action.meta, username }
};
/**********************/
next(newAction);
};

React-redux project - chained dependent async calls not working with redux-promise middleware?

I'm new to using redux, and I'm trying to set up redux-promise as middleware. I have this case I can't seem to get to work (things work for me when I'm just trying to do one async call without chaining)
Say I have two API calls:
1) getItem(someId) -> {attr1: something, attr2: something, tagIds: [...]}
2) getTags() -> [{someTagObject1}, {someTagObject2}]
I need to call the first one, and get an item, then get all the tags, and then return an object that contains both the item and the tags relating to that item.
Right now, my action creator is like this:
export function fetchTagsForItem(id = null, params = new Map()) {
return {
type: FETCH_ITEM_INFO,
payload: getItem(...) // some axios call
.then(item => getTags() // gets all tags
.then(tags => toItemDetails(tags.data, item.data)))
}
}
I have a console.log in toItemDetails, and I can see that when the calls are completed, we eventually get into toItemDetails and result in the right information. However, it looks like we're getting to the reducer before the calls are completed, and I'm just getting an undefined payload from the reducer (and it doesn't try again). The reducer is just trying to return action.payload for this case.
I know the chained calls aren't great, but I'd at least like to see it working. Is this something that can be done with just redux-promise? If not, any examples of how to get this functioning would be greatly appreciated!
I filled in your missing code with placeholder functions and it worked for me - my payload ended up containing a promise which resolved to the return value of toItemDetails. So maybe it's something in the code you haven't included here.
function getItem(id) {
return Promise.resolve({
attr1: 'hello',
data: 'data inside item',
tagIds: [1, 3, 5]
});
}
function getTags(tagIds) {
return Promise.resolve({ data: 'abc' });
}
function toItemDetails(tagData, itemData) {
return { itemDetails: { tagData, itemData } };
}
function fetchTagsForItem(id = null) {
let itemFromAxios;
return {
type: 'FETCH_ITEM_INFO',
payload: getItem(id)
.then(item => {
itemFromAxios = item;
return getTags(item.tagIds);
})
.then(tags => toItemDetails(tags.data, itemFromAxios.data))
};
}
const action = fetchTagsForItem(1);
action.payload.then(result => {
console.log(`result: ${JSON.stringify(result)}`);
});
Output:
result: {"itemDetails":{"tagData":"abc","itemData":"data inside item"}}
In order to access item in the second step, you'll need to store it in a variable that is declared in the function scope of fetchTagsForItem, because the two .thens are essentially siblings: both can access the enclosing scope, but the second call to .then won't have access to vars declared in the first one.
Separation of concerns
The code that creates the action you send to Redux is also making multiple Axios calls and massaging the returned data. This makes it more complicated to read and understand, and will make it harder to do things like handle errors in your Axios calls. I suggest splitting things up. One option:
Put any code that calls Axios in its own function
Set payload to the return value of that function.
Move that function, and all other funcs that call Axios, into a separate file (or set of files). That file becomes your API client.
This would look something like:
// apiclient.js
const BASE_URL = 'https://yourapiserver.com/';
const makeUrl = (relativeUrl) => BASE_URL + relativeUrl;
function getItemById(id) {
return axios.get(makeUrl(GET_ITEM_URL) + id);
}
function fetchTagsForItemWithId(id) {
...
}
// Other client calls and helper funcs here
export default {
fetchTagsForItemWithId
};
Your actions file:
// items-actions.js
import ApiClient from './api-client';
function fetchItemTags(id) {
const itemInfoPromise = ApiClient.fetchTagsForItemWithId(id);
return {
type: 'FETCH_ITEM_INFO',
payload: itemInfoPromise
};
}

Resources