Hook with abortable fetch - reactjs

note: I am aware of the useAbortableFetch hook. Trying to recreate a simple version of it.
I am trying to create a hook that returns a function that can make an abortable fetch request.
Idea being I want this hook to hold the state and update it when needed.
The update part is controlled by another competent on input change
What I am working on currently is
function useApiData(baseUrl){
const [state, setState] = use state({
data: null,
error: null,
loading: false
})
const controller = useRef(new AbortController)
const fetchData = searchTerm => {
if(state.loading){
controller.current.abort()
controller.current = new AbortController;
}
const signal = controller.signal;
setState(state => ({...state, loading: true})
fetch(url + searchTerm, {signal})
.then(res => res.json())
.then(data => {
setState(state => ({...state, data}))
return data
})
.catch(error => {
setState(state => ({...state, error}))
})
.finally(() => setState({...state, loading: false}))
}
const fetchCallback = useCallback(debounce(fetchData, 500), [])
return {...state, search: fetchCallback}
}
Usage
function App(){
const dataState = useApiData(url);
return ComponentWithInputElement {...dataState} />
}
function ComponentWithInputElement(props){
const [value, setValue] = useState('')
const onInput = ev => {
setValue(ev.target.value)
props.search(ev.tagert.value)
}
return (
<>
<input value={value} on input={onInput}>
{props.data?.length && <render datacomp>}
</>
)
}
This seems to fail to even send the first request.
Any way to make this pattern work?
Doing this in a useEffect would be very simple but I won't have access to the input value to have it as a dep
useEffect(()=>{
const controller = new AbortController();
const signal = controller.signal
fetch(url + value, {signal})
return () => controller.abort()
},[value])

Part of what you are trying to do does not feel "right". One of the things you are trying to do is have the state of the input value (like the form state) stored in the same hook. But those are not the same bits of state, as when the user types, it is (temporarily until its saved back to the server) different to the state fetched from the server. If you reuse the same state item for both, in the process of typing in the field, you lose the state fetched from the server.
You may think, "but I don't need it any more" -- but that often turns out to be a false abstraction later when new requirements come about that require it (like you need to display some static info as well as an editable form). In that sense, in the long term it would likely be less reusable.
It's a classic case of modelling an abstraction around a single use case -- which is a common pitfall.
You could add a new state item to the core hook to manage this form state, but then you have made it so you can only ever have the form state at the same level as the fetched data -- which may work in some cases, but be "overscoping" in others.
This is basically how all state-fetch libs like react query work -- Your fetched data is separate to the form data. And the form data is just initialised from the former as its initial value. But the input is bound to that "copy".
What you want is possible if you just returned setState from an additional state item in the core hook then passed down that setState to the child to be used as a change handler. You would then pass down the actual form string value from this new state from the parent to the child and bind that to the value prop of the input.
However, I'd encourage against it, as its an architectural flaw. You want to keep your local state, and just initialise it from the fetched state. What I suggested might be OK if you intend to use it only in this case, but your answer implies reuse. I guess I would need more info about how common this pattern is in your app.
As for abort -- you just need to return the controller from the hook so the consumer can access it (assuming you want to abort in the consumers?)

Related

Unable to update the `setState` inside `useEffect` conditionally

Component For Either Creating Or Editing The User
A part of the code below where I am setting the isActive state conditionally inside useEffect
I am getting the other fields of user_data and successfully updating the state
but only setIsActive is not updating the state
function CreateUser() {
const [isActive, setIsActive] = useState<boolean | undefined>();
useEffect(() => {
if (params_user_id?.id != null) {
const SINGLE_USER_URL = `users/${params_user_id.id}/edit`;
const getSingleUser = async () => {
const {data} = await axios.get(SINGLE_USER_URL)
console.log(data)
setIsActive(data.isactive)
console.log('isactive', isActive)
}
getSingleUser();
}
}, [params_user_id])
return (
<>
<Form.Check
defaultChecked={isActive}
className='mb-3'
type='checkbox'
id='active'
onChange={e => setIsActive(!(isActive))}
label='Active: Unselect for deleting accounts.'/>
</>
)
}
Form.Check From Bootstrap-React
When I hit the Edit page
I did try many things like ternary operator etc
checkBox is not checked bcoz it's undefined
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, it will likely run before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
In your case you're already using useEffect to update the state, but if you want to act upon that state change, or simply to log it's value, then you can use another separate useEffect for that purpose.
Example:
useEffect(() => {
console.log(isActive)
// Whatever else we want to do after the state has been updated.
}, [isActive])
This console.log will run only after the state has finished changing and a render has occurred.
Note: "isActive" in the example is interchangeable with whatever other state piece you're dealing with.
Check the documentation for more info about this.
Additional comments:
Best to avoid using the loose != to check for equality/inequality and opt for strict inequality check instead, i.e. - !==
Loose inequality is seldom used, if ever, and could be a source for potential bugs.
Perhaps it would better to avoid typing this as boolean | undefined.
Unless justified, this sounds like another potential source for bugs.
I would suggest relying on just boolean instead.
First, you can't get the state's updated value immediately after setting it because State Updates May Be Asynchronous
useEffect(() => {
if (params_user_id?.id != null) {
const SINGLE_USER_URL = `users/${params_user_id.id}/edit`;
const getSingleUser = async () => {
const {data} = await axios.get(SINGLE_USER_URL)
console.log(data)
setIsActive(data.isactive)
console.log('isactive', isActive) // <-- You can't get updated value immediately
}
getSingleUser();
}
}, [params_user_id])
Change the type of useState to boolean only and set the default value to false.
const [isActive, setIsActive] = useState<boolean>(false);
Then, in Form.Check, update onChange to this:
<Form.Check
defaultChecked={isActive}
className='mb-3'
type='checkbox'
id='active'
onChange={() => setIsActive((preVal) => !preVal)}
label='Active: Unselect for deleting accounts.'
/>

State resets when values change in an object, only when I populate from another source

What I'm trying to do is the following:
I want to select a single card view and edit it
After editing it, I would like to save the changes
Please look at video I've upload for reference and could please explain what's going on, why is this happening. Is there a better way to do this without writing t0o much code as the state has a huge variable list and writing each variable will be time consuming.
This is my default useState where all values will be stored temporarily:
const [business, setBusiness] = useState<Partial<BusinessInterface>>({});
And the function to populate the state above is as follows:
const editHandler = async (e: any) => {
let editBusiness = await businessArray.businesses?.find(x =>
x.businessId?.equals(e),
);
console.log(e);
if (editBusiness) {
setBusiness(editBusiness);
setEditBusiness(true);
setModal(true);
}
}
However, the moment I start entering values, my state will clear the old state plus my change functions are all correct. please take a look at registrationNumberHandler
const registrationNumberHandler = (
e: NativeSyntheticEvent<TextInputChangeEventData>,
) => {
let value = e.nativeEvent.text;
setBusiness({...business, registrationNumber: value});
}
But however, when I change editHandler to as follows, it will work and my state will not change or clear the value when I edit a certain text field.
const editHandler = async (e: any) => {
let editBusiness = await businessArray.businesses?.find(x =>
x.businessId?.equals(e),
);
if (editBusiness) {
setBusiness({...business, name: editBusiness.name, registrationNumber: editBusiness.registrationNumber); // a short example, when text fields changes state will retain its values
setEditBusiness(true);
setModal(true);
}
}
I'm not sure in what scope your business variable is, but I think you're probably getting a stale state when you're trying to update it. You usually would want to use the callback version of the setState function.
setBusiness((prevValues) => ({...prevValues, registrationNumber: value}));
let editBusiness = await businessArray.businesses?.find(x =>
x.businessId?.equals(e),
);
console.log(e);
if (editBusiness) {
setBusiness(editBusiness);
Here you are passing editBusiness to setBusiness as a reference. So, editBusiness is also referring to the same object in businessArray. And now when you modify anything, it will modify the mainArray as well as both are pointing to the same memory location.
setBusiness({...business, name: editBusiness.name, registrationNumber: editBusiness.registrationNumber);
Here, instead of passing reference you have created a new object which is not pointing to your businessArray and hence it doesn't modify your previous state on edit.
Your components are probably running with an outdated scope.
A component will only know about changes to business after it re-renders.
await pushes the execution of your code to the back of the queue which may cause the scope of your component to be even more outdated.
Solution: setState (or setBusiness in your case) has a way of giving you the most recent state.
Take a look at the rewritten function below. It should solve your problems 👍🏼
const registrationNumberHandler = (
e: NativeSyntheticEvent<TextInputChangeEventData>
) => {
let value = e.nativeEvent.text
setBusiness(previousState => ({
...previousState,
registrationNumber: value
}))
}

Update React State depending on the previous state

I am a beginner in React and I follow a tutorial in Udemy.I had a confusion about state.
When I am trying to update the state depending on the previous state, why the mentor says that we need to always use the second approach while both approaches seems logical to me.
This is my initialization
const [UserInput, setUserInput] = useState({
enteredTitle:'',
enteredDate:'',
enteredAmount:''
});
So here is the first approach.
const handleTitleChange = (event) =>{
setUserInput({
...UserInput,
enteredTitle:event.target.value
})
}
This is my second approach.
const handleTitleChange = (event) =>{
setUserInput((prevState) => {
return{
...prevState, enteredTitle:event.target.value}
});
Your mentor isn't right. Your first approach can well work in many situations, as you've probably noticed. But there are some in which you do need to use the second approach - namely, when the function, when it gets invoked, might close over an old value of the state. For example, if you have a button that, when clicked, will then change state after 2 seconds - the state may change in those 2 seconds, in which case the UserInput state that the function in the timeout closes over would be out of date.
// Don't do this
const handleClick = () => {
setTimeout(() => {
setState({ ...state, newProp: 'newVal' });
}, 2000);
};
Using the callback form instead - setState(state => or setUserInput((prevState) => - is necessary when you don't have a reliably up-to-date state variable already in scope.
Many prefer to use the simple state setter method - eg
setUserInput({
...UserInput,
enteredTitle:event.target.value
})
and avoid the callback version unless situation calls for the callback version. It's less code to write, read, and parse.

React: Query, delay useReducer

I've been scratching my head around this one for quite some time now, but I'm still not sure what to do.
Basically, I'm pulling data from a database via useQuery, which all works well and dandy, but I'm also trying to use useReducer (which I'm still not 100% familiar with) to save the initial data as the state so as to detect if any changes have been made.
The problem:
While the useQuery is busy fetching the data, the initial data is undefined; and that's what's being saved as the state. This causes all sorts of problems with regards to validation amd saving, etc.
Here's my main form function:
function UserAccountDataForm({userId}) {
const { query: {data: userData, isLoading: isLoadingUserData} } = useUserData(userId);
const rows = React.useMemo(() => {
if (userData) { /* Process userData here into arrays */ }
return [];
}, [isLoadingUserData, userData]); // Watches for changes in these values
const { isDirty, methods } = useUserDataForm(handleSubmit, userData);
const { submit, /* updateFunctions here */ } = methods;
if (isLoadingUserData) { return <AccordionSkeleton /> } // Tried putting this above useUserDataForm, causes issues
return (
<>
Render stuff here
*isDirty* is used to detect if changes have been made, and enables "Update Button"
</>
)
}
Here's useUserData (responsible for pulling data from the DB):
export function useUserData(user_id, column = "data") {
const query = useQuery({
queryKey: ["user_data", user_id],
queryFn: () => getUserData(user_id, column), // calls async function for getting stuff from DB
staleTime: Infinity,
});
}
return { query }
And here's the reducer:
function userDataFormReducer(state, action) {
switch(action.type) {
case "currency":
return {... state, currency: action.currency}
// returns data in the same format as initial data, with updated currency. Of course if state is undefined, formatting all goes to heck
default:
return;
}
}
function useUserDataForm(handleSubmit, userData) {
const [state, dispatch] = React.useReducer(userDataFormReducer, userData);
console.log(state) // Sometimes this returns the data as passed; most of the times, it's undefined.
const isDirty = JSON.stringify(userData) !== JSON.stringify(state); // Which means this is almost always true.
const updateFunction = (type, value) => { // sample only
dispatch({type: type, value: value});
}
}
export { useUserDataForm };
Compounding the issue is that it doesn't always happen. The main form resides in a <Tab>; if the user switches in and out of the tab, sometimes the state will have the proper initial data in it, and everything works as expected.
The quickest "fix" I can think of is to NOT set the initial data (by not calling the reducer) while useQuery is running. Unfortunately, I'm not sure this is possible. Is there anything else I can try to fix this?
Compounding the issue is that it doesn't always happen. The main form resides in a ; if the user switches in and out of the tab, sometimes the state will have the proper initial data in it, and everything works as expected.
This is likely to be expected because useQuery will give you data back from the cache if it has it. So if you come back to your tab, useQuery will already have data and only do a background refetch. Since the useReducer is initiated when the component mounts, it can get the server data in these scenarios.
There are two ways to fix this:
Split the component that does the query and the one that has the local state (useReducer). Then, you can decide to only mount the component that has useReducer once the query has data already. Note that if you do that, you basically opt out of all the cool background-update features of react-query: Any additional fetches that might yield new data will just not be "copied" over. That is why I suggest that IF you do that, you turn off the query to avoid needless fetches. Simple example:
const App = () => {
const { data } = useQuery(key, fn)
if (!data) return 'Loading...'
return <Form data={data} />
}
const Form = ({ data }) => {
const [state, dispatch] = useReducer(userDataFormReducer, data)
}
since the reducer is only mounted when data is available, you won't have that problem.
Do not copy server state anywhere :) I like this approach a lot better because it keeps server and client state separate and also works very well with useReducer. Here is an example from my blog on how to achieve that:
const reducer = (amount) => (state, action) => {
switch (action) {
case 'increment':
return state + amount
case 'decrement':
return state - amount
}
}
const useCounterState = () => {
const { data } = useQuery(['amount'], fetchAmount)
return React.useReducer(reducer(data ?? 1), 0)
}
function App() {
const [count, dispatch] = useCounterState()
return (
<div>
Count: {count}
<button onClick={() => dispatch('increment')}>Increment</button>
<button onClick={() => dispatch('decrement')}>Decrement</button>
</div>
)
}
If that works is totally dependent on what your reducer is trying to achieve, but it could look like this:
const App = () => {
const { data } = useQuery(key, fn)
const [state, dispatch] = useReducer(userDataFormReducer)
const currency = state.currency ?? data.currency
}
By keeping server state and client state separately, you'll only store what the user has chosen. The "default values" like currency stay out of the state, as it would essentially be state duplication. If the currency is undefined, you can still choose to display the server state thanks to the ?? operator.
Another advantage is that the dirty check is relatively easy (is my client state undefined?) and resets to the initial state also just mean to set the client state back to undefined.
So the actual state is essentially a computed state from what you have from the server and what the user has input, giving precedence to the user input of course.

useState Hook Concurrency Issue With Async Functions

This is an issue I faced, investigated, and fixed and would like to share my experience with you.
I found that when you are using useState HOOK to maintain state and then update the state using the style setState({...state, updatedProperty: updatedValue}) in an async function, you may run into some concurrency issues.
This may lead the application in some cases to lose some data due to the fact that async function keeps an isolated version of the state and may overwrite data some other component stored in the state.
The fix in short:
You need to use either a reducer to update state or use the function updater version of set state, if you are going to update the state from an async function because the function updater gets the latest updated version of state as an argument (prevState)
setState(prevState => ({...prevState, updatedProperty: updatedValue});
Long Description:
I was developing data context to manage user's contacts saved on a database which is hosted on a cloud MongoDB cluster and managed by a back end web service.
In the context provider, I used useState hook to maintain the state and was updating it like the following
const [state, setState] = useState({
contacts: [],
selectedContact: null
});
const setSelected = (contact) => setState({...state, selectedContact: contact});
const clearSelected = ()=> setState({...state, selectedContact: null};
const updateContact = async(selectedContact) => {
const res = await [some api call to update the contact in the db];
const updatedContact = res.data;
// To check the value of the state inside this function, I added the following like and found
// selectedContact wasn't null although clearSelected was called directly after this function and
// selectedContact was set to null in Chrome's React dev tools state viewer
console.log('State Value: ' + JSON.stringify(state));
//The following call will set selectedContact back to the old value.
setState({
...state,
contacts: state.contacts.map(ct=> ct._id === updatedContact._id? updatedContact : ct)
});
}
//In the edit contact form submit event, I called previous functions in the following order.
updateContact();
clearSelected();
Problem
It was found that after selecteContact is set to null then set back to the old value of the selected contact after updateContact finishes awaiting for the api call's promise.
This issue was fixed when I updated the state using the function updater
setState(prevState => ({
...prevState,
contacts: prevState.contacts.map(ct=> ct._id === updatedContact._id? updatedContact : ct)
}));
I also tried using reducer to test the behavior given this issue and found reducers are working fine even if you are going to use regular way (without the function updater) to update the sate.

Resources