How can I best use rxjs to perform multiple fetch operations in a single action using a forEach approach - reactjs

I am new to React and tasked with modifying an existing application which already performs multiple api calls. The pattern that is in use in the app is demonstrated by the below example. EG if an action mapperActions.selectPolygon of type CaseReducerActions<CaseReducers> is raised, then the below code will execute, calling the fetchPolygon function to retrieve data asynchronously from an AWS lambda and the returned result will be processed by the fetchPolygonCompleted function.
let fetchPolygonStatsEpic = (action$: any, state$: StateObservable<RootState>) => action$.pipe(
ofType(
mapperActions.selectPolygon.type,
mapperActions.alterQueryIndexname.type,
),
filter(() => state$.value.mapper.selectedPolygon !== undefined),
switchMap(() =>
concat(
of(globalActions.startLoading('polygon')),
fetchPolygon(state$.value.mapper).pipe(
map(result => mapperActions.fetchPolygonCompleted(result)),
catchError(e => of(globalActions.errorOccurred(e.message)))),
of(globalActions.stopLoading('polygon')),
)
)
)
The change I need to implement is as follows:
I have a type 'Framework'...
export type Framework = {
....
}
...a collection of these...
export const frameworks: {[framework: string]: Framework} = {
liveng0: {
...
},
liveng1: {
...
},
habmosCairngorms: {
...
},
spaceintCairngorms: {
...
}
}
..and an API method that retrieves data for a specified Framework...
export let fetchHabitats = (requiredFramework: Framework): Observable<FrameworkHabitats> => {
...
}
When the action mapperActions.initialise of type CaseReducerActions<CaseReducers> is raised, I need to execute a block of code which will execute and process 4 separate asynchronous API calls to fetch the data for each of the 4 framework types above in turn.
A naive approach of simply copy-and-pasting the API call for each framework in turn within the app's establised pattern for fetching data actually works correctly and produces the results I require, ie:
let fetchHabitatsEpic = (action$: any, state$: StateObservable<RootState>) => action$.pipe(
ofType(
mapperActions.initialise.type,
),
switchMap(() =>
concat(
of(globalActions.startLoading('habitats')),
fetchHabitats(frameworks.liveng0).pipe(
map(result => mapperActions.fetchHabitatsCompleted(result)),
catchError(e => of(globalActions.errorOccurred(e.message)))),
fetchHabitats(frameworks.liveng1).pipe(
map(result => mapperActions.fetchHabitatsCompleted(result)),
catchError(e => of(globalActions.errorOccurred(e.message)))),
fetchHabitats(frameworks.habmosCairngorms).pipe(
map(result => mapperActions.fetchHabitatsCompleted(result)),
catchError(e => of(globalActions.errorOccurred(e.message)))),
fetchHabitats(frameworks.spaceintCairngorms).pipe(
map(result => mapperActions.fetchHabitatsCompleted(result)),
catchError(e => of(globalActions.errorOccurred(e.message)))),
of(globalActions.stopLoading('habitats')),
)
)
)
Although the above works as required, it clearly needs to be replaced with a forEach approach that operates over each Framework in turn and treats each API call atomically, whilst still being able to trigger all 4 from a single action. Could you please explain the syntax required to achieve this?

You can use Object.values() to iterate over the frameworks and Array.map() to map them to API calls, as follows:
concat(
of(globalActions.startLoading('habitats')),
...Object.values(frameworks).map(framework =>
fetchHabitats(framework).pipe(
map(result => mapperActions.fetchHabitatsCompleted(result)),
catchError(e => of(globalActions.errorOccurred(e.message)))
)
)
)

Related

Zustand - alternative to "slices" pattern

I have a React Native app that consists of modals (popups) showing up at different places. This can be a user login modal, a modal that allows a user to be added to a list of groups/orgs, a modal that allows editing a user profile etc. For all these types of modals, I decided to create a central state store using the Zustand state management library.
Following the Zustand "slices" pattern mentioned here, I was able to get the following structure for each type of Modal -
Slices
useEditUserProfileModalSlice.ts
export const createEditUserProfileModalSlice = (set) => ({
isEditUserProfileModalOpen: false,
openEditUserProfileModal: () => set(() => ({ isEditUserProfileModalOpen: true })),
closeEditUserProfileModal: () => set(() => ({ isEditUserProfileModalOpen: false }))
});
useAddUserToOrgModalSlice.ts
export const createAddUserToOrgModalSlice = (set) => ({
isAddUserToOrgModalOpen: false,
openAddUserToOrgModal: () => set(() => ({ isAddUserToOrgModalOpen: true })),
closeAddUserToOrgModal: () => set(() => ({ isAddUserToOrgModalOpen: false }))
});
Store
useModalStore.ts
import create from 'zustand'
export const useModalStore = create((...a) => ({
...createEditUserProfileModalSlice(...a),
...createAddUserToOrgModalSlice(...a),
}))
Usage
const isEditUserProfileModalOpen = useModalStore((state) => state.isEditUserProfileModalOpen);
const openEditUserProfileModal = useModalStore((state) => state.openEditUserProfileModal);
const closeEditUserProfileModal = useModalStore((state) => state.closeEditUserProfileModal);
Is there an alternative to this pattern that you would recommend?
I ask because any time I need to add a new modal type, I'll need to create a new slice file, with basically the same 3 properties (isOpen, open function and close function), which kinda seems boilerplate-y.

Add if statement conditional to React epic function

I am fairly new to React and Redux, and am attempting to add a conditional statement inside an epic function. Based on the results of the conditional, it will either filter on a single privilege or on multiple.
Is this possible?
Here is what I have so far:
export const handleSetPrivileges: Epic<RootAction, RootAction, RootState> = (action$, store$) =>
action$.pipe(
filter(isActionOf(UserActions.SetPrivileges)),
if (specific component) {
filter((action) => ![304, 1112].some(priv => action.payload.includes(priv))),
}
else {
filter((action) => !action.payload?.includes(304)),
map(() => routerActions.push('/unauthorized'))
);

How Can I Setup `react-select` to work correctly with server-side data by using AsyncSelect?

I would like to setup a component react-select to work server-side data and do server-side filtering, but it doesn't work for a plethora of reasons.
Can you explain it and also show working code?
react-select has several examples in the documentation including an entire section dedicated to AsyncSelect which include inline code examples with codesandbox links.
It's worth noting that there are three unique props specific to the AsyncSelect
loadOptions
defaultOptions
cacheOptions
The primary difference between AsyncSelect and Select is that a Select is reliant on an options prop (an array of options) whereas the AsyncSelect is instead reliant on a loadOptions prop (an async function which provides a callback to set the options from an api).
Often api autocomplete lookups filter results on the server so the callback on the loadOptions does not make assumptions on filtering the results returned which is why they may need to be filtered client-side prior to passing them to the AsyncSelect state.
Here is a simple code example.
import React from 'react';
import AsyncSelect from 'react-select/async';
const filterOptions = (options, inputValue) => {
const candidate = inputValue.toLowerCase();
return options.filter(({ label }) => label.toLowerCase().includes(candidate);
};
const loadOptions = (inputValue, callback) => {
const url = `www.your-api.com/?inputValue=${inputValue}`;
fetch(url).then(resp => {
const toSelectOption = ({ id, name }) => ({ label: name, value: id });
// map server data to options
const asyncOptions = resp.results.map(toSelectOption);
// Filter options if needed
const filtered = filterOptions(asyncOptions, inputValue);
// Call callback with mapped and filtered options
callback(filtered);
})
};
const AsyncLookup = props => (
<AsyncSelect
cacheOptions
loadOptions={loadOptions}
defaultOptions
{...props}
/>
);
export default AsyncLookup
Let's start by me expressing the opinion that react-select seems great, but not very clearly documented. Personally I didn't fall in love with the documentation for the following reasons:
No search
All the props and put on a single page. If I do CTRL+F on something everything lights up. Pretty useless
Most descriptions are minimal and not describing the important edge cases, some are even missing
There are some examples, but not nearly enough to show the different varieties, so you have to do guesswork
And so I will try to help a bit with this article, by giving steps by steps, code and problems + solutions.
Step 1: Simplest form react-select:
const [options, setOptions] = useState([
{ id: 'b72a1060-a472-4355-87d4-4c82a257b8b8', name: 'illy' },
{ id: 'c166c9c8-a245-48f8-abf0-0fa8e8b934d2', name: 'Whiskas' },
{ id: 'cb612d76-a59e-4fba-8085-c9682ba2818c', name: 'KitKat' },
]);
<Select
defaultValue={options[0]}
isClearable
options={options}
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
It generally works, but you will notice that if I type the letter d which doesn't match any of the choices anywhere, choices stay, instead of showing "no options" as it should.
I will ignore this issue, since it is minor and seems unfixable.
So far so good, we can live with that small issue.
Step 2: Convert static data to server data
Our goal is now to simply swap the static data with server loaded data. Meh, how difficult could it be?
We will first need to swap <Select/> for <AsyncSelect/>. Now how do we load data?
So looking at the documentation there are multiple ways of loading data:
defaultOptions: The default set of options to show before the user starts searching. When set to true, the results for loadOptions('') will be autoloaded.
and
loadOptions: Function that returns a promise, which is the set of options to be used once the promise resolves.
Reading it carefully you understand defaultOptions needs to be a boolean value true and loadOptions should have a function returning the choices:
<AsyncSelect
defaultValue={options[0]}
isClearable
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
defaultOptions
loadOptions={loadData}
/>
Looks great, we have remote data loaded. But we want to preset our default value now. We have to match it by Id, rather than choosing the first one. Here comes our first problem:
PROBLEM: You can't set the defaultValue in the very beginning, because you have no data to match it against. And if you try to set the defaultValue after component has loaded, then it doesn't work.
To solve that, we need to load data in advance, match the initial value we have, and once we have both of those, we can initialize the component. A bit ugly but that's the only way I could figure it out given the limitations:
const [data, setData] = useState(null);
const [initialObject, setInitialObject] = useState(null);
const getInitial = async () => {
// make your request, once you receive data:
// Set initial object
const init= res.data.find((item)=>item.id=ourInitialId);
setInitialObject(init);
// Set data so component initializes
setData(res.data);
};
useEffect(() => {
getInitial();
}, []);
return (
<>
{data!== null && initialObject !== null ? (
<AsyncSelect
isClearable
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
defaultValue={initialObject}
defaultOptions={options}
// loadOptions={loadData} // we don't need this anymore
/>
) : null}
</>
)
Since we are loading the data ourselves, we don't need loadOptions so we will take it out. So far so good.
Step 3: Make filter with server-side filtering call
So now we need a callback that we can use for getting data. Let's look back at the documentation:
onChange: (no description, from section "StateManager Props")
onInputChange: Same behaviour as for Select
So we listen to documentation and go back to "Select Props" section to find:
onInputChange: Handle change events on the input`
Insightful...NOT.
We see a function types definition that seems to have some clues:
I figured, that string must by my text/query. And apparently it drops in the type of change. Off we go --
const [data, setData] = useState(null);
const [initialObject, setInitialObject] = useState(null);
const getInitial = async () => {
// make your request, once you receive data:
// Set initial object
const init= res.data.find((item)=>item.id=ourInitialId);
setInitialObject(init);
// Set data so component initializes
setData(res.data);
};
useEffect(() => {
getInitial();
}, []);
const loadData = async (query) => {
// fetch your data, using `query`
return res.data;
};
return (
<>
{data!== null && initialObject !== null ? (
<AsyncSelect
isClearable
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
defaultValue={initialObject}
defaultOptions={options}
onInputChange={loadData} // +
/>
) : null}
</>
)
Data gets fetched with the right query, but options don't update as per our server data results. We can't update the defaultOptions since it is only used during initialization, so the only way to go would be to bring back loadOptions. But once we do, we have 2 calls on every keystroke. Blak. By countless hours and miracle of painstaking experimentation, we now figure out that:
USEFUL REVELATION: loadOptions actually fires on inputChange, so we don't actually need onInputChange.
<AsyncSelect
isClearable
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
defaultValue={initialObject}
defaultOptions={options}
// onInputChange={loadData} // remove that
loadOptions={loadData} // add back that
/>
Things look good. Even our d search has automagically been fixed somehow:
Step 4: Update formik or whatever form value you have
To do that we need something that fires on select:
onChange: (no explanation or description)
Insightful...NOT. We have a pretty and colorful definition again to our rescue and we pick up some clues:
So we see the first param (which we don't know what it is can be object, array of array, null, or undefined. And then we have the types of actions. So with some guessing we figure out, it must be passing the selected object:
We will pass setFieldValue function as a prop to the component:
onChange={(selectedItem) => {
setFieldValue(fieldName, selectedItem?.id); // fieldName is also passed as a prop
}}
NOTE: careful, if you clear the select it will pass null for selectedItem and your JS will explode for looking for .id of undefined. Either use optional chaining or as in my case set it conditionally to '' (empty string so formik works).
Step 5: Final code:
And so we are all set with a fully functional reusable Autocomplete dropdown select server-fetching async filtering, clearable thingy.
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import AsyncSelect from 'react-select/async';
export default function AutocompleteComponent({
fieldName,
initialValue,
setFieldValue,
getOptionLabel,
queryField,
}) {
const [options, setOptions] = useState(null);
const [initialObject, setInitialObject] = useState(null);
// this function only finds the item from all the data that has the same id
// that comes from the parent component (my case - formik initial)
const findByValue = (fullData, specificValue) => {
return fullData.find((e) => e.id === specificValue);
};
const loadData = async (query) => {
// load your data using query HERE
return res.data;
};
const getInitial = async () => {
// load your data using query HERE
const fetchedData = res.data;
// match by id your initial value
const initialItem = findByValue(fetchedData, initialValue);
// Set both initialItem and data options so component is initialized
setInitialObject(initialItem);
setOptions(fetchedData);
}
};
// Hit this once in the beginning
useEffect(() => {
getInitial();
}, []);
return (
<>
{options !== null && initialObject !== null ? (
<AsyncSelect
isClearable
getOptionLabel={getOptionLabel}
getOptionValue={(option) => option.id}
defaultValue={initialObject}
defaultOptions={options}
loadOptions={loadData}
onChange={(selectedItem) => {
const val = (selectedItem === null?'':selectedItem?.id);
setFieldValue(fieldName, val)
}}
/>
) : null}
</>
);
}
AutocompleteComponent.propTypes = {
fieldName: PropTypes.string.isRequired,
initialValue: PropTypes.string,
setFieldValue: PropTypes.func.isRequired,
getOptionLabel: PropTypes.func.isRequired,
queryField: PropTypes.string.isRequired,
};
AutocompleteComponent.defaultProps = {
initialValue: '',
};
I hope this saves you some time.

Return Observable from Function that contains subscription and another observable

I am trying to get status of printer connected to my app. Here is the function, I have in one of the page:
checkPrinterStatus() { //I want to this this function into an observable
this.afdb.object('food/settings/printer').snapshotChanges().subscribe((res: any) => {
const printerSettings = res.payload.val();
if(printerSettings.isConnected) {
timer(5000, 3000).pipe(
switchMap(_ => this.getConnPrinterStatus()),
distinctUntilChanged()
).subscribe(async res => {
let printerStatus = res;
return printerStatus; // I want this to be an observable
});
}
});
}
So basically, I am subscribing to my firebase database which provided me a value of wheather the printer is connected/set up or not. If it is connect, I am creating a polling where I call my getConnPrinterStatus() which returns me the status every 3 second if it has changed.
Now, currently, I have this function inside one of my page but I want to turn this function into observable so that I can put this in one of the services and then subscribe to printerStatus value in every page. How can I achieve that? Any help or direction is greatly appreciated.
checkPrinterStatus() { //I want to this this function into an observable
return this.afdb.object('food/settings/printer').snapshotChanges().pipe(
switchMap(res => iif(() => res.payload.val().isConnected, timer(5000, 3000).pipe(
switchMap(_ => this.getConnPrinterStatus()),
distinctUntilChanged()
)
))
);
}
This function returns an observable
You can return an observable from the function checkPrinterStatus like this -
checkPrinterStatus() {
return this.afdb.object('food/settings/printer').snapshotChanges()
.pipe(
map(res => {
return res.payload.val().isConnected;
}),
filter(isConnected => !!isConnected),
take(1),//use this if you want only 1 value; otherwise remove it
switchMap(() => {
return timer(5000, 3000).pipe(
switchMap(_ => this.getConnPrinterStatus()),
distinctUntilChanged()
)
})
)
}
Since the function returns an observable, the caller can subscribe the observable return by the method.

How to properly memoize mapDispatchToProps?

First, here's my HOC:
export default function connectField({
nameProp = 'name',
valueProp = 'value',
dispatchProp = 'dispatch'
}: ConnectOptions) {
return compose(
getContext(contextTypes),
connect((state, ownProps) => {
const path = [namespace,...getPath(ownProps),...toPath(ownProps[nameProp])];
const value = getOr('', path, state);
return {
[valueProp]: value
};
}, (dispatch,ownProps) => { // <----------- mapDispatchToProps
const path = [...getPath(ownProps),...toPath(ownProps[nameProp])];
return {
[dispatchProp]: value => dispatch({type: ActionTypes.Change, payload: {path, value}})
};
}, (stateProps, dispatchProps, {[FIELD_PATH]: _, ...ownProps}) => {
return {...stateProps, ...dispatchProps, ...ownProps};
}, {
areMergedPropsEqual: (a,b) => {
let eq = shallowEqual(a,b);
console.log('areMergedPropsEqual',a,b,eq);
return eq;
},
}),
withContext(contextTypes, props => {
return {[FIELD_PATH]: [...getPath(props), props[nameProp]]};
}),
);
}
In the middle there is my mapDispatchToProps. That's causing areMergedPropsEqual to return false every time because it's creating a new action creator every time.
I can't figure out how to memoize this bit:
value => dispatch({type: ActionTypes.Change, payload: {path, value}})
Such that I get back the same function instance every time.
There's some notes in the docs about "per-instance memoization" which is what I want, but I can't quite make heads or tails of what I'm supposed to do here.
To be clear, I know how to memoize a function. However, I don't want a use a big cache with infinite history. It's unnecessary memory consumption. I just need a cache size of 1 like how reselect does it. The problem is that I can't create the "selector" directly inside connectField because that still creates a single shared instance -- i.e., all "connected fields" will share the same cache and they'll overwrite each other, negating the benefit. It has to be per component instance. This is specific to React-Redux's connect method. There's a syntax for it so that you can create your selector at the right spot, and it will only get ran once per instance. I'm just having trouble deciphering the API -- do they expect a function that returns a function that returns an object? Or an object with propnames as keys and functions as values? What does that function return? i.e., the docs aren't clear about all the different variations that are accepted for the mapDispatchToProps option.
If you already have lodash, you have a memoize function that allow to transform any function into a memoized function. This memoized function will calculate the return value for a given parameter and will then always return this same return value each you supply the same parameter.
You can use it like this for example :
import { memoize } from 'lodash'
const makeChangeDispatcher = memoize( (dispatch, path) =>
value => dispatch({type: ActionTypes.Change, payload: {path, value}})
)
...
(dispatch,ownProps) => { // <----------- mapDispatchToProps
const path = [...getPath(ownProps),...toPath(ownProps[nameProp])];
return {
[dispatchProp]: makeChangeDispatcher(dispatch, path)
};
}
...
You can see more infos on the lodash documentation of memoize

Resources