How to abstract multiple similar function components (providers) in React? - javascript

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

Related

What is the reason getState() not functioning in React-Redux?

export const SMSAdmin_Filter_User = (userName) => (dispatch, getState) => {
var st = getState();
...
}
When this code runs, getState() is defined in the debugger as a function, but st comes up as undefined. I have used getState in multiple other action creator functions with great success, so am uncertain why it is not functioning here.
This function is called as a promise since there are other asynchronous processes running (incremental fetch for large number of users).
Here is where it is being called from:
componentDidMount() {
var el = document.getElementById("userList");
if (el) {
el.focus({ preventScroll: true });
}
// console.log("SMSAdmin")
new Promise((res,rej)=>this.props.SMSAdmin_Get_Users());
// .then(() => {
// this.setState({ users: this.props.SMSAdmin.users });
// });
}
filterUsers = () => {
let target = document.getElementById("userName");
let name = target?.value?.toLowerCase();
new Promise((res, rej)=>SMSAdmin_Filter_User(name?.trim()));
}
filterUsers() is also being called from the render function to ensure updates when SMSAdmin_Get_Users() adds more users to the list.
Here is the mapDispatchToProps():
const mapDispatchToProps = (dispatch) => {
return {
SMSAdmin_Get_Users: () => { return dispatch(SMSAdmin_Get_Users()) },
SMSAdmin_Load_User: (userId, userName, oldData = null, startVal = 0, number = 20) => {
return dispatch(SMSAdmin_Load_User(userId, userName, oldData, startVal, number))
},
SMSAdmin_Update_User: (user, province, credits) => { return dispatch(SMSAdmin_Update_User(user, province, credits)) },
SMSAdmin_setCurrentUpload: (userName) => { return dispatch(SMSAdmin_setCurrentUpload(userName)) },
SMSAdmin_Filter_User: (userName) => { return dispatch(SMSAdmin_Filter_User(userName)) },
}
}
I am not able to provide a sandbox for the code because there are multiple other files associated with this component and the data being used is confidential.
Thanks.
Edit: Showing redux store creation
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import { reducers } from './reducerMain.js';
export const ConfigureStore = () => {
const store = createStore(
reducers,
applyMiddleware(thunk, logger)
);
return store;
}
I guess you are accidently using imported function (not the one mapped in mapDispatchToProps). Did you forgot to use the one from props ? like that:
filterUsers = () => {
// ...
new Promise((res, rej)=>this.props.SMSAdmin_Filter_User(name?.trim()));
}

How to make a refetch wait for a POST in React-Query

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.

createAsyncThunk: abort previous request

I'm using createAsyncThunk to make asynchronous requests to some API. Only one request should be active at any given moment.
I understand that the request can be aborted using a provided AbortSignal if I have the Promise returned from the previous invocation. Question is, can the thunk itself somehow abort the previous request "autonomously"?
I was considering two options:
keeping the last AbortSignal in the state. Seems wrong, because state should be serializable.
keeping the last Promise and its AbortSignal in global variable. Seems also wrong, because, you know, global variables.
Any ideas? Thank you.
I don't know how your specific api works but below is a working example of how you can put the abort logic in the action and reducer, it will abort any previously active fake fetch when a newer fetch is made:
import * as React from 'react';
import ReactDOM from 'react-dom';
import {
createStore,
applyMiddleware,
compose,
} from 'redux';
import {
Provider,
useDispatch,
useSelector,
} from 'react-redux';
import {
createAsyncThunk,
createSlice,
} from '#reduxjs/toolkit';
const initialState = {
entities: [],
};
// constant value to reject with if aborted
const ABORT = 'ABORT';
// fake signal constructor
function Signal() {
this.listener = () => undefined;
this.abort = function () {
this.listener();
};
}
const fakeFetch = (signal, result, time) =>
new Promise((resolve, reject) => {
const timer = setTimeout(() => resolve(result), time);
signal.listener = () => {
clearTimeout(timer);
reject(ABORT);
};
});
// will abort previous active request if there is one
const latest = (fn) => {
let previous = false;
return (signal, result, time) => {
if (previous) {
previous.abort();
}
previous = signal;
return fn(signal, result, time).finally(() => {
//reset previous
previous = false;
});
};
};
// fake fetch that will abort previous active is there is one
const latestFakeFetch = latest(fakeFetch);
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async ({ id, time }) => {
const response = await latestFakeFetch(
new Signal(),
id,
time
);
return response;
}
);
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {},
extraReducers: {
[fetchUserById.fulfilled]: (state, action) => {
state.entities.push(action.payload);
},
[fetchUserById.rejected]: (state, action) => {
if (action?.error?.message === ABORT) {
//do nothing
}
},
},
});
const reducer = usersSlice.reducer;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
({ dispatch, getState }) => (next) => (action) =>
typeof action === 'function'
? action(dispatch, getState)
: next(action)
)
)
);
const App = () => {
const dispatch = useDispatch();
React.useEffect(() => {
//this will be aborted as soon as the next request is made
dispatch(
fetchUserById({ id: 'will abort', time: 200 })
);
dispatch(fetchUserById({ id: 'ok', time: 100 }));
}, [dispatch]);
return 'hello';
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
If you only need to resolve a promise if it was the latest requested promise and no need to abort or cancel ongoing promises (ignore resolve if it wasn't latest) then you can do the following:
const REPLACED_BY_NEWER = 'REPLACED_BY_NEWER';
const resolveLatest = (fn) => {
const shared = {};
return (...args) => {
//set shared.current to a unique object reference
const current = {};
shared.current = current;
fn(...args).then((resolve) => {
//see if object reference has changed
// if so it was replaced by a newer one
if (shared.current !== current) {
return Promise.reject(REPLACED_BY_NEWER);
}
return resolve;
});
};
};
How it's used is demonstrated in this answer
Based on #HMR answer, I was able to put this together, but it's quite complicated.
The following function creates "internal" async thunk that does the real job, and "outer" async thunk that delegates to the internal one and aborts previous dispatches, if any.
The payload creator of the internal thunk is also wrapped to: 1) wait for the previous invocation of payload creator to finish, 2) skip calling the real payload creator (and thus the API call) if the action was aborted while waiting.
import { createAsyncThunk, AsyncThunk, AsyncThunkPayloadCreator, unwrapResult } from '#reduxjs/toolkit';
export function createNonConcurrentAsyncThunk<Returned, ThunkArg>(
typePrefix: string,
payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg>,
options?: Parameters<typeof createAsyncThunk>[2]
): AsyncThunk<Returned, ThunkArg, unknown> {
let pending: {
payloadPromise?: Promise<unknown>;
actionAbort?: () => void;
} = {};
const wrappedPayloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg> = (arg, thunkAPI) => {
const run = () => {
if (thunkAPI.signal.aborted) {
return thunkAPI.rejectWithValue({name: 'AbortError', message: 'Aborted'});
}
const promise = Promise.resolve(payloadCreator(arg, thunkAPI)).finally(() => {
if (pending.payloadPromise === promise) {
pending.payloadPromise = null;
}
});
return pending.payloadPromise = promise;
}
if (pending.payloadPromise) {
return pending.payloadPromise = pending.payloadPromise.then(run, run); // don't use finally(), replace result
} else {
return run();
}
};
const internalThunk = createAsyncThunk(typePrefix + '-protected', wrappedPayloadCreator);
return createAsyncThunk<Returned, ThunkArg>(
typePrefix,
async (arg, thunkAPI) => {
if (pending.actionAbort) {
pending.actionAbort();
}
const internalPromise = thunkAPI.dispatch(internalThunk(arg));
const abort = internalPromise.abort;
pending.actionAbort = abort;
return internalPromise
.then(unwrapResult)
.finally(() => {
if (pending.actionAbort === abort) {
pending.actionAbort = null;
}
});
},
options
);
}

Trying to provide createContext with a default value object which contains functions.Is there any correct way to do so?

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*/
}

redux thunk fetch api action and reducer

So decided to use redux-thunk and I have a problem to write a function in my actions and reducer. Actually function looks like this:
async getData() {
if (this.props.amount === isNaN) {
return;
} else {
try {
await fetch(
`https://api.exchangeratesapi.io/latest?base=${this.props.base}`,
)
.then(res => res.json())
.then(data => {
const date = data.date;
const result = (data.rates[this.props.convertTo] * this.props.amount).toFixed(4);
this.setState({
result,
date,
});
}, 3000);
} catch (e) {
console.log('error', e);
}
}
}
Also I already have action types
export const FETCH_DATA_BEGIN = 'FETCH_DATA_BEGIN';
export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS';
export const FETCH_DATA_FAIL = 'FETCH_DATA_FAIL';
and actions like this
export const fetchDataBegin = () => {
return {
type: actionTypes.FETCH_DATA_BEGIN,
};
};
export const fetchDataSuccess = data => {
return {
type: actionTypes.FETCH_DATA_SUCCESS,
data: data,
};
};
export const fetchDataFail = error => {
return {
type: actionTypes.FETCH_DATA_FAIL,
error: error,
};
};
And then comes the hard part for me where I don't know how to get the same result from function async getData(). I already have just this in my action :
export async function fetchData() {
return async dispatch => {
return await fetch(`https://api.exchangeratesapi.io/latest?base=${this.props.base}`)
.then(res => res.json())
.then(data => {
// <------------------- WHAT NEXT?
}
};
export function fetchData() {
return dispatch => {
fetch(`https://api.exchangeratesapi.io/latest?base=${this.props.base}`)
.then(res => res.json())
.then(data => dispatch(fetchDataSuccess(data)), e => dispatch(fetchDataFail(e)))
}
};
Now this code:
const date = data.date;
const result = (data.rates[this.props.convertTo] * this.props.amount).toFixed(4);
this.setState({
result,
date,
});
goes into your reducer
if(action.type === FETCH_DATA_SUCCESS) {
const date = action.data.date;
const rates = action.data.rates;
return { ...state, rates, date };
}
Now you can use the redux state in your component and make the rest of the calculations there (ones that need this.props).
To dispatch the fetchData action now, you do this.props.dispatch(fetchData()) in your react-redux connected component.
EDIT
Here's how you use the state in the component.
I'm assuming you have created the redux store. something like:
const store = createStore(rootReducer,applyMiddleware(thunk));
Now, you can use the react-redux library's connect function to connect the redux state to your component.
function mapStateToProps(state, ownProps) {
return {
date: state.date,
result: (state.rates[ownProps.convertTo] * ownProps.amount).toFixed(4);
}
}
function mapDispatchToProps(dispatch) {
return {
fetchData: () => dispatch(fetchData())
}
}
export default connect(mapStateToProps,mapDispatchToProps)(YourComponent)
You can use this Higher Order Component in your DOM now and pass the appropriate props to it:
import ConnectedComponent from "./pathTo/ConnectedComponent";
...
return <View><ConnectedComponent convertTo={...} amount={...} /></View>;
And, also inside YourComponent you can now read this.props.date and this.props.result and use them wherever you need to.
You might want to look at selectors in the future to memoize the state and reduce the performance cost of redux.

Categories

Resources