How to make function with a few setStates in it synchronous? - reactjs

I need to make a button click handler which have a few other function calls in it. One of them is a onAccept function which has a few setStates in it and want to wait until them all is done. Is there a way to make onAccept synchronous?
button click handler
const onUpdateBoundaries = async (recommendation) => {
await getSnippetIndex(
//some props
).then(response => {
onAccept({...recommendation, index: response});
});
fetchRecommendations() //<- this function shouldn't be called until onAccept's setStates are done
};
onAccept
const onAccept = (recommendation) => {
setAccepted((accepted) => [
...new Set([...accepted, ...recommendation.cluster_indices.map(recommendation => recommendation.index)]),
]);
setRejected((rejected) => [
...new Set(removeFromArray(rejected, recommendation.cluster_indices.map(recommendation => recommendation.index)))
]);
};
fetchRecommendations
const fetchRecommendations = async () => {
try {
const {//some props
propagated_accepted,
propagated_rejected,
} = await getRecommendations(
//some props
);
setAccepted((accepted) => [...accepted, ...propagated_accepted]);
setRejected((rejected) => [...rejected, ...propagated_rejected]);
} catch (err) {
//handling
}
setIsWaitingForRecommendations(false);
};

You can try with useEffect and useRef to achieve it
//track all previous values before state updates
const previousValues = useRef({ rejected, accepted });
useEffect(() => {
//only call `fetchRecommendations` once both `rejected` and `accepted` get updated
if(previousValues.current.rejected !== rejected && previousValues.current.accepted !== accepted) {
fetchRecommendations()
}
}, [rejected, accepted])
Another easier way that you can try setState, which is the old-school function with callback (the problem with this solution is you need to use class component - NOT function component)
const onAccept = (recommendation) => {
setState((prevState) => ({
accepted: [
...new Set([...prevState.accepted, ...recommendation.cluster_indices.map(recommendation => recommendation.index)]),
],
rejected: [
...new Set(removeFromArray(prevState.rejected, recommendation.cluster_indices.map(recommendation => recommendation.index)))
]
}), () => {
//callback here
fetchRecommendations()
})
}

React is declarative, which means it will control the setState function calls incl. batching them if necessary to optimise performance.
What you can do is make use of a useEffect to listen for changes in state and run code you need to run after state change there.
For eg: ( I'm assuming your two states are accepted and rejected)
useEffect(() => {
fetchRecommendations() //<- gets called everytime accepted or rejected changes
}, [accepted, rejected])
// onAccept remains the same
//button click handler
const onUpdateBoundaries = async (recommendation) => {
const response = await getSnippetIndex( //some props )
onAccept({...recommendation, index: response});
};
If you want to run it only if current values of accepted or rejected has changed, you can make use of use Ref to store the previous values of accepted and rejected.
You can create a custom hook like
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Then
// import usePrevious hook
const prevAccepted = usePrevious(accepted)
const prevRejected = usePrevious(rejected)
useEffect(() => {
if(prevAccepted!=accepted && prevRejected!=rejected)
fetchRecommendations() //<- gets called everytime accepted or rejected changes
}, [accepted, rejected])
const onUpdateBoundaries = async (recommendation) => {
const response = await getSnippetIndex( //some props )
onAccept({...recommendation, index: response});
};
Think something like this would do the trick. Let me know if this works :)

you can make a async method like this
const SampleOfPromise = () => {
onClick=async()=>{
await myPromise();
}
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('sample');
}, 300);
});
return(
<Button onClick={onClick}>
</Button>
)
}

Related

React prevent re render

How should I prevent a re render when I click on onClose to close the modal.
It looks like the dispatch function: dispatch(setActiveStep(0) cause some troubles.
export default function ImportOrderModal(props: ImportOrderModalProps) {
const { open, onClose } = props
const {
orderImport: { activeStep }
} = useAppSelector((state: RootState) => state)
const steps = useImportOrderConfig()
const dispatch = useDispatch()
React.useEffect(() => {
getOrdersList()
}, [])
const onCloseModal = () => {
onClose()
// Force a re render because of activeStep value
dispatch(setActiveStep(0))
}
const getOrdersList = async () => {
const orders = //API call
dispatch(setOrdersList(orders))
}
return (
<Modal open={open} onClose={onCloseModal}>
<Stepper steps={steps} currentStepNumber={activeStep} />
<FormSteps />
</Modal>
)
}
This block is outside of your useEffect()
const getOrdersList = async () => {
const orders = //API call
dispatch(setOrdersList(orders))
}
This will cause rendering troubles.
if you're using an older version of React (<17) that doesn't enforce <React.StrictMode> you can get away with rewriting that as:
useEffect(() => {
getOrderList
.then((orders) => dispatch(setOrdersList(orders))
.catch((error) => console.error(error));
}, [dispatch]);
if you're using a newer version of React (>18) you will have to cleanup your asynchronous call in the cleanup function of your useEffect().
useEffect(() => {
// This has to be passed down to your fetch/axios call
const controller = new AbortController();
getOrderList(controller)
.then((orders) => dispatch(setOrdersList(orders))
.catch((error) => console.error(error));
return () => {
// This will abort any ongoing async call
controller.abort();
}
}, [dispatch]);
For this to make sense I will probably have to write an example of the api call for you as well, if you don't mind I'll use axios for the example but it essentially works the same-ish with .fetch.
const getOrderList = async (controller) => {
try {
const { data } = await axios.get("url", { signal: controller.signal });
return data.orders;
} catch (e) {
throw e;
}
}

How to call a Firestore unsubscribe function from another function in a functional component?

I have the following function that runs upon a click. It basically starts a Firestore listener to grab messages. It also has an unsubscribe function declared which I am trying to call from another function:
const getMessages = (uid) => {
const ref = firebase.firestore().collection('Chats').doc(uid).collection('Messages');
const query = ref.where("uid", "==", uid).orderBy('timestamp', 'desc').limit(25);
const unsubFromMessages = query.onSnapshot((snapshot) => {
if (snapshot.empty) {
console.log('No matching documents.');
}
snapshot.docChanges().forEach((change) => {
if (change.type === 'removed') {
console.log(change.doc.data().content)
} else if (change.type === 'added') {
setMessages(prevFiles => ([...prevFiles, {
id: change.doc.id, body: change.doc.data()
}]))
// setTimeout( this.scrollToBottom(), 2000)
}
});
}, (error) => {console.log(error)});
}
As you can see inside of it, I declare a function to unsubscribe from the Firestore listener (const unsubFromMessages = query.onSnapshot). I want to be able to call this "unsubFromMessages" function upon another button click from another function which basically closes a chat.
Here's that closeChat function:
const closeChat = () => {
setMessages([]);
unsubFromMessages();
}
Unfortunately, the closeChat function can not access the unsubFromMessages function to unsubscribe from the Firestore listener. I get the following error:
Line 177:5: 'unsubFromMessages' is not defined no-undef
I know how to do it in a class component where I would simply declare the function as this.unsubFromMessages = ... and then call it from any other function but I can not figure out how to do it in a functional component. Please advise.
You could store the unsubFromMessages callback in a React ref and access it in the other click hander.
const unsubFromMessagesRef = React.useRef();
...
const getMessages = (uid) => {
...
const unsubFromMessages = query.onSnapshot((snapshot) => { ..... };
unsubFromMessagesRef.current = unsubFromMessages;
...
}
...
const closeChat = () => {
setMessages([]);
unsubFromMessagesRef.current && unsubFromMessagesRef.current();
}
Don't forget to unsubscribe when the component unmounts:
useEffect(() => {
return () => {
unsubFromMessagesRef.current && unsubFromMessagesRef.current()
};
}, []);

Can't update react state, even component is not unmounted

Description
I have component which shows data that get from server and display it on the table using the state, tableData and it must be set when Redux action is dispatched.
I've use action listener library which uses Redux middleware which consisting of 63 lines of code. redux-listeners-qkreltms.
For example when I register a function on analysisListIsReady({}).type which is ANALYSISLIST_IS_READY then when the action is dispatched, the function is called.
Issue
The issue is that react throws sometimes the error: Can't update react state... for setTableData so response data is ignored to be set. I want to figure it out when it happens.
I've assumed that it's because of unmounting of component, so I printed some logs, but none of logs are printed and also ComponentA is not disappeared.
It's not throing any error when I delete getAnalysisJsonPathApi and getResource, so I tried to reporuduce it, but failed... link
It's not throing any error when I delete listenMiddleware.addListener see: #2
#1
// ComponentA
const [tableData, setTableData] = useState([])
useEffect(() => {
return () => {
console.log("unmounted1")
}}, [])
useEffect(() => {
listenMiddleware.addListener(analysisListIsReady({}).type, (_) => {
try {
getAnalysisJsonPathApi().then((res) => {
//...
getResource(volumeUrl)
.then((data: any) => {
// ...
setTableData(data)
})
})
} catch (error) {
warn(error.message)
}
})
return () => {
console.log("unmounted2")
}
}, [])
export const getAnalysisJsonPathApi = () => {
return api
.post('/segment/volume')
.then(({ data }) => data)
export const getResource = async (src: string, isImage?: boolean): Promise<ArrayBuffer> =>
api
.get(src)
.then(({ data }) => data)
#2
// ComponentA
const [tableData, setTableData] = useState([])
useEffect(() => {
return () => {
console.log("unmounted1")
}}, [])
useEffect(() => {
if (steps.step2a) {
try {
getAnalysisJsonPathApi().then((res) => {
//...
getResource(volumeUrl)
.then((data: any) => {
// ...
setTableData(data)
})
})
} catch (error) {
warn(error.message)
}
}
return () => {
console.log("unmounted2")
}
}, [steps.step2a])
Well, its as you said:
because of unmounting of component
In your UseEffect() function, you need to check if the componenet is mounted or not, in other words, you need to do the componentDidMount & componentDidUpdate (if needed) logics:
const mounted = useRef();
useEffect(() => {
if (!mounted.current) {
// do componentDidMount logic
console.log('componentDidMount');
mounted.current = true;
} else {
// do componentDidUpdate logic
console.log('componentDidUpdate');
}
});
i didn't go to your question code detail, but my hint might help you, usually this error happens in fetchData function,
suppose you have a fetchData function like below:
fetchData(){
...
let call = await service.getData();
...
--->setState(newItems)//Here
}
so when api call end and state want to be updated, if component been unmounted, there is no state to be set,
you can use a bool variable and set it false when component will unmount:
let stillActive= true;
fetchData(){
active = true;
...
let call = await service.getData();
...
if(stillActive)
setState(newItems)//Here
}
}
componentWillUnmount(){
active = false;
}
I've found out it's because of redux-listeners-qkreltms, Redux middleware.
It keeps function when component is mounted into listener, but never changes its functions even component is unmounted.
middleware.addListener = (type, listener) => {
for (let i = 0; i < listeners.length; i += 1) {
if (listeners[i].type === type) {
return;
}
}
listeners.push(createListener(type, listener));
};

Request to server each N seconds after flag

I have a specific case. The first thing I do is request the Index.DB. After I got the taskId from it, I need to start asking the server every 5 seconds. And stop doing this on a specific flag. How can i do that properly with hooks?
I'tried to use useInterval hook like this:
https://github.com/donavon/use-interval;
But when i set it in useEffect causes consistent error:
Invalid hook call. Hooks can only be called inside of the body of a function component.
const Page = () => {
const [task, setTask] = useState({})
const isLoaded = (task.status === 'fatal');
const getTask = (uuid: string) => {
fetch(`${TASK_REQUEST_URL}${uuid}`)
.then(res => {
return res.json();
})
.then(json => {
setTask(json.status)
})
.catch(error => console.error(error));
};
useEffect(() => {
Storage.get('taskId')
.then(taskId => {
if (!taskId) {
Router.push('/');
}
useInterval(() => getTask(taskId), 5000, isTaskStatusEqualsSomthing)
})
}, []);
return (
<p>view</p>
);
};
I also tried to play around native setInterval like this
useEffect(() => {
Storage.get('taskId')
.then(taskId => {
if (!taskId) {
Router.push('/');
}
setInterval(() => getTask(taskId), 5000)
})
}, []);
But in this case i don't know how to clearInterval and also code looks dirty.
The solution is simple. You just need to configure your setInterval within .then callback like
useEffect(() => {
let timer;
Storage.get('taskId')
.then(taskId => {
if (!taskId) {
Router.push('/');
else {
timer = setInterval(() => getTask(taskId), 5000)
}
}
})
return () => {clearInterval(timer)}
}, []);
The reason, first approach doesn't work for you is because you cannot call a hook conditionally or in useEffect as you did for useInterval

How to send request on click React Hooks way?

How to send http request on button click with react hooks? Or, for that matter, how to do any side effect on button click?
What i see so far is to have something "indirect" like:
export default = () => {
const [sendRequest, setSendRequest] = useState(false);
useEffect(() => {
if(sendRequest){
//send the request
setSendRequest(false);
}
},
[sendRequest]);
return (
<input type="button" disabled={sendRequest} onClick={() => setSendRequest(true)}
);
}
Is that the proper way or is there some other pattern?
export default () => {
const [isSending, setIsSending] = useState(false)
const sendRequest = useCallback(async () => {
// don't send again while we are sending
if (isSending) return
// update state
setIsSending(true)
// send the actual request
await API.sendRequest()
// once the request is sent, update state again
setIsSending(false)
}, [isSending]) // update the callback if the state changes
return (
<input type="button" disabled={isSending} onClick={sendRequest} />
)
}
this is what it would boil down to when you want to send a request on click and disabling the button while it is sending
update:
#tkd_aj pointed out that this might give a warning: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function."
Effectively, what happens is that the request is still processing, while in the meantime your component unmounts. It then tries to setIsSending (a setState) on an unmounted component.
export default () => {
const [isSending, setIsSending] = useState(false)
const isMounted = useRef(true)
// set isMounted to false when we unmount the component
useEffect(() => {
return () => {
isMounted.current = false
}
}, [])
const sendRequest = useCallback(async () => {
// don't send again while we are sending
if (isSending) return
// update state
setIsSending(true)
// send the actual request
await API.sendRequest()
// once the request is sent, update state again
if (isMounted.current) // only update if we are still mounted
setIsSending(false)
}, [isSending]) // update the callback if the state changes
return (
<input type="button" disabled={isSending} onClick={sendRequest} />
)
}
You don't need an effect to send a request on button click, instead what you need is just a handler method which you can optimise using useCallback method
const App = (props) => {
//define you app state here
const fetchRequest = useCallback(() => {
// Api request here
}, [add dependent variables here]);
return (
<input type="button" disabled={sendRequest} onClick={fetchRequest}
);
}
Tracking request using variable with useEffect is not a correct pattern because you may set state to call api using useEffect, but an additional render due to some other change will cause the request to go in a loop
In functional programming, any async function should be considered as a side effect.
When dealing with side effects you need to separate the logic of starting the side effect and the logic of the result of that side effect (similar to redux saga).
Basically, the button responsibility is only triggering the side effect, and the side effect responsibility is to update the dom.
Also since react is dealing with components you need to make sure your component still mounted before any setState or after every await this depends on your own preferences.
to solve this issue we can create a custom hook useIsMounted this hook will make it easy for us to check if the component is still mounted
/**
* check if the component still mounted
*/
export const useIsMounted = () => {
const mountedRef = useRef(false);
const isMounted = useCallback(() => mountedRef.current, []);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
});
return isMounted;
};
Then your code should look like this
export const MyComponent = ()=> {
const isMounted = useIsMounted();
const [isDoMyAsyncThing, setIsDoMyAsyncThing] = useState(false);
// do my async thing
const doMyAsyncThing = useCallback(async () => {
// do my stuff
},[])
/**
* do my async thing effect
*/
useEffect(() => {
if (isDoMyAsyncThing) {
const effect = async () => {
await doMyAsyncThing();
if (!isMounted()) return;
setIsDoMyAsyncThing(false);
};
effect();
}
}, [isDoMyAsyncThing, isMounted, doMyAsyncThing]);
return (
<div>
<button disabled={isDoMyAsyncThing} onClick={()=> setIsDoMyAsyncThing(true)}>
Do My Thing {isDoMyAsyncThing && "Loading..."}
</button>;
</div>
)
}
Note: It's always better to separate the logic of your side effect from the logic that triggers the effect (the useEffect)
UPDATE:
Instead of all the above complexity just use useAsync and useAsyncFn from the react-use library, It's much cleaner and straightforward.
Example:
import {useAsyncFn} from 'react-use';
const Demo = ({url}) => {
const [state, doFetch] = useAsyncFn(async () => {
const response = await fetch(url);
const result = await response.text();
return result
}, [url]);
return (
<div>
{state.loading
? <div>Loading...</div>
: state.error
? <div>Error: {state.error.message}</div>
: <div>Value: {state.value}</div>
}
<button onClick={() => doFetch()}>Start loading</button>
</div>
);
};
You can fetch data as an effect of some state changing like you have done in your question, but you can also get the data directly in the click handler like you are used to in a class component.
Example
const { useState } = React;
function getData() {
return new Promise(resolve => setTimeout(() => resolve(Math.random()), 1000))
}
function App() {
const [data, setData] = useState(0)
function onClick() {
getData().then(setData)
}
return (
<div>
<button onClick={onClick}>Get data</button>
<div>{data}</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
You can define the boolean in the state as you did and once you trigger the request set it to true and when you receive the response set it back to false:
const [requestSent, setRequestSent] = useState(false);
const sendRequest = () => {
setRequestSent(true);
fetch().then(() => setRequestSent(false));
};
Working example
You can create a custom hook useApi and return a function execute which when called will invoke the api (typically through some onClick).
useApi hook:
export type ApiMethod = "GET" | "POST";
export type ApiState = "idle" | "loading" | "done";
const fetcher = async (
url: string,
method: ApiMethod,
payload?: string
): Promise<any> => {
const requestHeaders = new Headers();
requestHeaders.set("Content-Type", "application/json");
console.log("fetching data...");
const res = await fetch(url, {
body: payload ? JSON.stringify(payload) : undefined,
headers: requestHeaders,
method,
});
const resobj = await res.json();
return resobj;
};
export function useApi(
url: string,
method: ApiMethod,
payload?: any
): {
apiState: ApiState;
data: unknown;
execute: () => void;
} {
const [apiState, setApiState] = useState<ApiState>("idle");
const [data, setData] = useState<unknown>(null);
const [toCallApi, setApiExecution] = useState(false);
const execute = () => {
console.log("executing now");
setApiExecution(true);
};
const fetchApi = useCallback(() => {
console.log("fetchApi called");
fetcher(url, method, payload)
.then((res) => {
const data = res.data;
setData({ ...data });
return;
})
.catch((e: Error) => {
setData(null);
console.log(e.message);
})
.finally(() => {
setApiState("done");
});
}, [method, payload, url]);
// call api
useEffect(() => {
if (toCallApi && apiState === "idle") {
console.log("calling api");
setApiState("loading");
fetchApi();
}
}, [apiState, fetchApi, toCallApi]);
return {
apiState,
data,
execute,
};
}
using useApi in some component:
const SomeComponent = () =>{
const { apiState, data, execute } = useApi(
"api/url",
"POST",
{
foo: "bar",
}
);
}
if (apiState == "done") {
console.log("execution complete",data);
}
return (
<button
onClick={() => {
execute();
}}>
Click me
</button>
);
For this you can use callback hook in ReactJS and it is the best option for this purpose as useEffect is not a correct pattern because may be you set state to make an api call using useEffect, but an additional render due to some other change will cause the request to go in a loop.
<const Component= (props) => {
//define you app state here
const getRequest = useCallback(() => {
// Api request here
}, [dependency]);
return (
<input type="button" disabled={sendRequest} onClick={getRequest}
);
}
My answer is simple, while using the useState hook the javascript doesn't enable you to pass the value if you set the state as false. It accepts the value when it is set to true. So you have to define a function with if condition if you use false in the usestate

Resources