I'm using React + Redux + Rxjs + typesafe-actions + TS and I want to call action with params. My code for now:
Actions:
import { createAsyncAction } from 'typesafe-actions';
import {ICats} from '/api/cats';
export const FETCH_CATS_REQUEST = 'cats/FETCH_CATS_REQUEST';
export const FETCH_CATS_SUCCESS = 'cats/FETCH_CATS_SUCCESS';
export const FETCH_CATS_ERROR = 'cats/FETCH_CATS_ERROR';
export const fetchCats = createAsyncAction(
FETCH_CATS_REQUEST,
FETCH_CATS_SUCCESS,
FETCH_CATS_ERROR
) <void, ICats, Error> ();
Call dispatch:
store.dispatch(fetchCats.request());
My epics:
const fetchCatsFlow: Epic<Types.RootAction, Types.RootAction, Types.RootState> = (action$) =>
action$.pipe(
filter(isActionOf(fetchCats.request)),
switchMap(() =>
fromPromise(Cats.getDataFromAPI()).pipe(
map(fetchCats.success),
catchError(pipe(fetchCats.failure, of))
)
)
);
API:
export const Cats = {
getDataFromAPI: () => $http.get('/cats').then(res => {
return res.data as any;
}),
};
And it's working - making a call to API but without params. I tried many times and still I don't know how to pass a params when dispatch is called.
I found answer:
export const fetchCats = createAsyncAction(
FETCH_CATS_REQUEST,
FETCH_CATS_SUCCESS,
FETCH_CATS_ERROR
) <void, ICats, Error> ();
changed to:
type ICatsRequest = {
catType: string;
};
export const fetchCats = createAsyncAction(
FETCH_CATS_REQUEST,
FETCH_CATS_SUCCESS,
FETCH_CATS_ERROR
) <ICatsRequest, ICats, Error> ();
and then it allows me to pass specified type to dispatch:
store.dispatch(fetchCats.request({catType: 'XXX'}));
also I needed to modify this:
export const Cats = {
getDataFromAPI: (params) => $http.get('/cats', {
params: {
type: params.payload.catType
}
}).then(res => {
return res.data as any;
}),
};
and
switchMap((params) =>
fromPromise(Cats.getDataFromAPI(params)).pipe(
map(fetchCats.success),
catchError(pipe(fetchCats.failure, of))
)
)
Related
I have several providers / contexts in a React app that do the same, that is, CRUD operations calling a Nestjs app:
export const CompaniesProvider = ({children}: {children: any}) => {
const [companies, setCompanies] = useState([])
const fetchCompany = async () => {
// etc.
try {
// etc.
setCompanies(responseData)
} catch (error) {}
}
const updateCompany = async () => {
// etc.
try {
// etc.
} catch (error) {}
}
// same for delete, findOne etc..
return (
<CompaniesContext.Provider value={{
companies,
saveSCompany,
}}>
{children}
</CompaniesContext.Provider>
)
}
export const useCompanies = () => useContext(CompaniesContext)
Another provider, for instance, the Technology model would look exactly the same, it just changes the api url:
export const TechnologiesProvider = ({children}: {children: any}) => {
const [technologies, setTechnologies] = useState([])
const fetchTechnology = async () => {
// etc.
try {
// etc.
setTechnologies(responseData)
} catch (error) {}
}
const updateTechnology = async () => {
// etc.
try {
// etc.
} catch (error) {}
}
// same for delete, findOne etc..
return (
<TechnologiesContext.Provider value={{
technologies,
savesTechnology,
}}>
{children}
</TechnologiesContext.Provider>
)
}
export const useTechnologies = () => useContext(TechnologiesContext)
What is the best way to refactor? I would like to have an abstract class that implements all the methods and the different model providers inherit the methods, and the abstract class just needs the api url in the constructor..
But React prefers function components so that we can use hooks like useState.
Should I change function components to class components to be able to refactor? But then I lose the hooks capabilities and it's not the react way nowadays.
Another idea would be to inject the abstract class into the function components, and the providers only call for the methods.
Any ideas?
One way to achieve it is to create a factory function that gets a url (and other parameters if needed) and returns a provider & consumer
This is an example for such function:
export const contextFactory = (url: string) => {
const Context = React.createContext([]); // you can also get the default value from the fn parameters
const Provider = ({ children }: { children: any }) => {
const [data, setData] = useState([]);
const fetch = async () => {
// here you can use the url to fetch the data
try {
// etc.
setData(responseData);
} catch (error) {}
};
const update = async () => {
// etc.
try {
// etc.
} catch (error) {}
};
// same for delete, findOne etc..
return (
<Context.Provider
value={{
data,
save
}}
>
{children}
</Context.Provider>
);
};
const hook = () => useContext(Context)
return [Provider, hook]
};
And this is how you can create new providers & consumers
const [CompaniesProvider, useCompanies] = contextFactory('http://...')
const [TechnologiesProvider, useTechnologies] = contextFactory('http://...')
I ended up creating a class representing the CRUD operations:
export class CrudModel {
private api = externalUrls.api;
constructor(private modelUrl: string) {}
async fetchRecords() {
const url = `${this.api}/${this.modelUrl}`
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-type': 'application/json'
},
})
return await response.json()
} catch (error) {}
}
// removeRecord
// updateRecord
// saveRecord
}
Then for every provider I reduced the code, since I just call an instance of the CrudModel, which has the method implementations.
type Technology = {
id: number;
name: string;
}
type Context = {
technologies: Technology[];
saveTechnology: any;
removeTechnology: any;
updateTechnology: any;
}
const TechnologiesContext = createContext<Context>({
technologies: [],
saveTechnology: null,
removeTechnology: null,
updateTechnology: null,
})
export const TechnologiesProvider = ({children}: {children: any}) => {
const [technologies, setTechnologies] = useState([])
const router = useRouter()
const crudModel = useMemo(() => {
return new CrudModel('technologies')
}, [])
const saveTechnology = async (createForm: any): Promise<void> => {
await crudModel.saveRecord(createForm)
router.reload()
}
// fetchTechnologies
// removeTechnology
// updateTechnology
useEffect(() => {
async function fetchData() {
const fetchedTechnologies = await crudModel.fetchRecords()
setTechnologies(fetchedTechnologies)
}
fetchData()
}, [crudModel])
return (
<TechnologiesContext.Provider value={{
technologies,
saveTechnology,
removeTechnology ,
updateTechnology,
}}>
{children}
</TechnologiesContext.Provider>
)
}
This way I can have types for every file, and it's easy to debug / maintain. Having just one factory function like previous answer it feels cumbersome to follow data flow. The downside is that there is some repetition of code among the provider files. Not sure if I can refactor further my answer
There are two requests, a POST and a GET. The POST request should create data and after it has created that data, the GET request should fetch the newly created data and show it somewhere.
This are the hooks imported into the component:
const { mutate: postTrigger } = usePostTrigger();
const { refetch } = useGetTriggers();
And they are used inside an onSubmit method:
const onAddSubmit = async (data) => {
await postTrigger(data);
toggle(); // this one and the one bellow aren't important for this issue
reset(emptyInput); //
refetch();
};
Tried to add async / await in order to make it wait until the POST is finished but it doesn't.
Any suggestions?
I added here the code of those 2 hooks if it's useful:
POST hook:
import { useMutation } from 'react-query';
import { ICalculationEngine } from '../constants/types';
import calculationEngineAPI from '../services/calculation-engine-api';
export const usePostTrigger = () => {
const apiService = calculationEngineAPI<ICalculationEngine['TriggerDailyOpt1']>();
const mutation = useMutation((formData: ICalculationEngine['TriggerDailyOpt1']) =>
apiService.post('/trigger/DailyOpt1', formData)
);
return {
...mutation
};
};
export default usePostTrigger;
GET hook:
import { useMemo } from 'react';
import { useInfiniteQuery } from 'react-query';
import { ICalculationEngine } from '../constants/types';
import { calculationEngineAPI } from '../services/calculation-engine-api';
export const TAG_PAGE_SIZE = 20;
export interface PaginatedData<D> {
totalPages: number;
totalElements: number;
content: D[];
}
export const useGetTriggers = () => {
const query = 'getTriggers';
const apiService = calculationEngineAPI<PaginatedData<ICalculationEngine['Trigger']>>();
const fetchTriggers = (pageNumber: number) => {
const search = {
pageNumber: pageNumber.toString(),
pageSize: TAG_PAGE_SIZE.toString()
};
return apiService.get(`/trigger/paged/0/${search.pageSize}`);
};
const {
data: response,
isError,
isLoading,
isSuccess,
isFetching,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
refetch,
...rest
} = useInfiniteQuery(query, ({ pageParam = 1 }) => fetchTriggers(pageParam), {
getNextPageParam: (lastPage, pages) => {
const totalPages = lastPage.data.totalPages || 1;
return totalPages === pages.length ? undefined : pages.length + 1;
}
});
const data = useMemo(
() => response?.pages.map((page) => page.data.content).flat() || [],
[response?.pages]
);
return {
data,
isError,
isLoading,
isSuccess,
isFetching,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
refetch,
...rest
};
};
export default useGetTriggers;
You can use the onSuccess method of react-query (https://react-query.tanstack.com/reference/useMutation)
onSuccess: (data: TData, variables: TVariables, context?: TContext) => Promise | void
Optional
This function will fire when the mutation is successful and will be passed the mutation's result.
If a promise is returned, it will be awaited and resolved before proceeding
const { mutate, isLoading, error, isSuccess } = useMutation(
(formData: ICalculationEngine['TriggerDailyOpt1']) =>
apiService.post('/trigger/DailyOpt1', formData),
{
mutationKey: 'DailyOpt1',
onSuccess: (_, { variables }) => {
// Execute your query as you see fit.
apiService.get(...);
},
}
);
As a best practice thought I would suggest the POST request to return the updated data if possible to avoid this exact need.
Here is my context creator in which i made an initial state for the context but i also have to pass the functions that i want to use by useContext in the project.I passed the functions this way.can anybody tell me a better method to do so. P.S this method works fine but i think itll create problems for big projects
import { createContext } from "react"
import { initialState } from "./Initstate"
import { IInitState } from "./alert/IInitState"
export interface IGithubContext {
State: IInitState,
searchusers: (login: string) => void,
clearUsers: () => void,
getuser: (login: string) => void,
getuserrepos: (login: string) => void
}
const istate: IGithubContext = {
State: initialState,
clearUsers: () => null,
getuser: () => null,
getuserrepos: () => null,
searchusers: () => null
}
const GithubContext = createContext<IGithubContext>(istate)
export default GithubContext
Here is my provider which contains functions that i am trying to pass in the value={{}} as you can see
import GithubReducer, { Action } from "./GithubReducer"
import GithubContext from "./GithubContext"
import { SET_LOADING, CLEAR_USERS, USER_LOADING, GET_USER, SEARCH_USERS, GET_REPOS } from "./Types"
import { IInitState } from "./alert/IInitState"
import { initialState } from "./Initstate"
const GithubState = (props: any) => {
const [state, dispatch] = useReducer<React.Reducer<IInitState, Action>>(GithubReducer, initialState)
const searchusers = async (search: string) => {
setloading()
const data = await fetch(`https://api.github.com/search/users?q=${search}`)
const items = await data.json()
dispatch({ type: SEARCH_USERS, payload: items.items })
}
const getuser = async (login: string) => {
userloading()
const data = await fetch(`https://api.github.com/users/${login}`)
const items = await data.json()
dispatch({ type: GET_USER, payload: items })
}
const getuserrepos = async (login: string) => {
const data = await fetch(`https://api.github.com/users/${login}/repos?per_page=5&sort=created:asc`)
const items = await data.json()
dispatch({ type: GET_REPOS, payload: items })
}
const clearUsers = () => dispatch({ type: CLEAR_USERS })
const setloading = () => dispatch({ type: SET_LOADING })
const userloading = () => dispatch({ type: USER_LOADING })
return (
<GithubContext.Provider value={{
State: {
users: state.users,
user: state.user,
loading: state.loading,
userloading: state.userloading,
repos: state.repos,
alert: state.alert,
},
searchusers,
clearUsers,
getuser,
getuserrepos
}}>
{props.children}
</GithubContext.Provider>
)
}
export default GithubState
The values for the function in the context will default to the value provided when either the Provider doesn't enclose the components that need the context or when a component tries to access the values from the context without having access to it(not children of the Provider).
The current implementation of the default values for the functions for example, getuser: () => null just fail silently when some component calls the function getuser and has no access to the context provider. So yes this will cause some issues.
An alternative approach, throw an error inside the default values for the functions so that when ComponentA which is not a child of the Provider invokes the function getuser or searchusers, instead of failing silently the function will throw an error. With this approach at least you will know that some component which doesn't have access to the context tried to access some value from it.
const istate: IGithubContext = {
State: initialState,
clearUsers: () => { throw new Error('GithubContext not avaliable') },
getuser: () => { throw new Error('GithubContext not avaliable') },
/*other values*/
}
Consider the following code:
authPopup.ts
import { msal, consentScopes } from 'src/services/auth/authIndex'
export const getAccount = () => msal.getAccount()
export const loginPopup = () => msal.loginPopup(consentScopes)
export const logout = () => {
msal.logout()
}
export const getTokenPopup = (request) => {
return msal
.acquireTokenSilent(request)
.catch(() => msal.acquireTokenPopup(request))
}
authRedirect
import { msal, consentScopes } from 'src/services/auth/authIndex'
export const getAccount = () => msal.getAccount()
export const loginRedirect = () => {
msal.loginRedirect(consentScopes)
}
export const logout = () => {
msal.logout()
}
export const getTokenRedirect = (request, endpoint?: string) => {
return msal
.acquireTokenSilent(request, endpoint)
.catch(() => msal.acquireTokenRedirect(request)) // page reload
}
authService.ts
import { Screen } from 'quasar'
import { isInternetExplorer } from 'src/services/utils/utilsService'
import * as authPopup from 'src/services/auth/authPopup'
import * as authRedirect from 'src/services/auth/authRedirect'
const loginMethod = Screen.lt.sm || isInternetExplorer ? 'redirect' : 'popup'
export const auth = (loginMethod === 'popup')
? { loginMethod, ...authPopup }
: { loginMethod, ...authRedirect }
useAccounts.ts
import { auth } from 'src/services/auth/authService'
auth.getAccount // works fine, no TS error
auth.loginPopup // works fine, with TS error that it can't be found
Why is the method loginPopup not recognised by TypeScript in the file useAccount.ts? The thrown error is:
TS2339: Property 'loginPopup' does not exist on type '{ getAccount: ()
=> Account; loginPopup: () => Promise; }'.
Visible in useAccounts.ts:
Visible in authService.ts:
When looking in the docs I assume this is because it has another function signature because the wrapping function doesn't need an argument. How would this be resolved? Do we need to declare a new interface and how?
Thank you for your help.
Because when you spread all the functions in your authService in your auth constant a new object of type any is exported. So you need to specify what auth from authService is
If you have the same function names. why do you have two different files.
const auth: () => {
getAccount: () => boolean;
loginPopup: () => boolean;
logout: () => boolean;
getTokenPopup: () => boolean;
loginMethod: string;
} | {
getAccount: () => boolean;
loginRedirect: () => boolean;
logout: () => boolean;
getTokenRedirect: () => boolean;
loginMethod: string;
}
Ignore the => boolean I had to see the problem with my eyes you can have one single file.
function loginMethod () {
// validation to see if it is IE
return true || true ? 'redirect' : 'popup'
}
export const getAccount = () => {
if (loginMethod()) {
/// do things
}
// else things
}
export const login = () => {
if (loginMethod()) {
/// do things
}
// else things
}
export const logout = () => {
if (loginMethod()) {
/// do things
}
// else things
}
export const getTokenRedirect = () => {
if (loginMethod()) {
/// do things
}
// else things
}
I have several routes that use the same controller:
<Route component={Search} path='/accommodation(/:state)(/:region)(/:area)' />
and when the route is changed I call the api function from within the component:
componentWillReceiveProps = (nextProps) => {
if (this.props.params != nextProps.params) {
loadSearch(nextProps.params);
}
}
which is an action as follows:
export function loadSearch (params) {
return (dispatch) => {
return dispatch(
loadDestination(params)
).then(() => {
return dispatch(
loadProperties(params)
);
});
};
}
which loads:
export const DESTINATION_REQUEST = 'DESTINATION_REQUEST';
export const DESTINATION_SUCCESS = 'DESTINATION_SUCCESS';
export const DESTINATION_FAILURE = 'DESTINATION_FAILURE';
export function loadDestination (params) {
const state = params.state ? `/${params.state}` : '';
const region = params.region ? `/${params.region}` : '';
const area = params.area ? `/${params.area}` : '';
return (dispatch) => {
return api('location', {url: `/accommodation${state}${region}${area}`}).then((response) => {
const destination = formatDestinationData(response);
dispatch({
type: DESTINATION_SUCCESS,
destination
});
});
};
}
export const PROPERTIES_REQUEST = 'PROPERTIES_REQUEST';
export const PROPERTIES_SUCCESS = 'PROPERTIES_SUCCESS';
export const PROPERTIES_FAILURE = 'PROPERTIES_FAILURE';
export function loadProperties (params, query, rows = 24) {
return (dispatch, getState) => {
const locationId = getState().destination.id || 99996;
return api('search', {locationId, rows}).then((response) => {
const properties = response.results.map(formatPropertiesData);
dispatch({
type: PROPERTIES_SUCCESS,
properties
});
});
};
}
On initial page load this works and returns data from an api and renders the content. However on changing the route, the loadSearch function is fired but the dispatch (which returns the actual data) doesn't.
Please change your code to this. You missed a dispatch.
Assumption : You are using redux-thunk, and the component has access to dispatch via props (connected). Since you mentioned that you are dispatching on page load, I think this is the case.
componentWillReceiveProps = (nextProps) => {
const {dispatch} = this.props;
if (this.props.params != nextProps.params) {
nextProps.dispatch(loadSearch(nextProps.params));
}
}