Using history.block with asynchronous functions/callback/async/await - reactjs

I have a form inside a route, that if there are any validation errors, it should not allow the user to navigate to another route. If there are no validation errors, then allow navigation to another route.
Below is my current code, which the onBlock function does not work does to its async nature as the functions to submit and then validate the form are asynchronous.
FormComponent.js
import React, { useState, useEffect, useRef } from "react";
import { FieldArray } from "formik";
import { useHistory } from "react-router-dom";
import * as Yup from "yup";
import Form from "./Form";
import TextInput from "./TextInput";
const FormComponent = () => {
const history = useHistory();
const [initialValues, setInitialValues] = useState();
const [isSubmitted, setIsSubmitted] = useState(false);
const block = useRef();
const formRef = useRef(null);
const onFormSubmit = async (values) => {
setIsSubmitted(true);
};
const validationSchema = () => {
const schema = {
test: Yup.string().required("Input is Required")
};
return Yup.object(schema);
};
const onBlock = () => {
const { submitForm, validateForm } = formRef?.current || {};
// submit form first
submitForm()
.then(() => {
// then validate form
validateForm()
.then(() => {
// if form is valid - should navigate to route
// if form is not valid - should remain on current route
return formRef?.current.isValid;
})
.catch(() => false);
})
.catch(() => false);
};
const redirectToPage = () => {
history.push("/other-page");
};
useEffect(() => {
block.current = history.block(onBlock);
return () => {
block.current && block.current();
};
});
useEffect(() => {
if (isSubmitted) redirectToPage();
}, [isSubmitted]);
useEffect(() => {
setInitialValues({
test: ""
});
}, []);
return initialValues ? (
<Form
initialValues={initialValues}
onSubmit={onFormSubmit}
formRef={formRef}
validationSchema={validationSchema}
>
<FieldArray
name="formDetails"
render={(arrayHelpers) =>
arrayHelpers && arrayHelpers.form && arrayHelpers.form.values
? (() => {
const { form } = arrayHelpers;
formRef.current = form;
return (
<>
<TextInput name="test" />
<button type="submit">Submit</button>
</>
);
})()
: null
}
/>
</Form>
) : null;
};
export default FormComponent;
If a user tries to submit the form without any value in the input, I would expect that onBlock would return false to block navigation. But this does not seem to work. Simply returning false in the onBlock function does however. So it seems that the history.block function does not accept any callbacks. I have also tried to convert it to an async function and await the submitForm & validateForm functions, but still no joy. Is there a way around this? Any help would be greatly appreciated.
Here is CodeSandbox with an example.

The history.block function accepts a prompt callback which you can use to prompt the user or do something else in response to the page being blocked. To block the page you just need to call history.block() more info here.
The formik form is validated when you try to submit it and if it successfully validates then it proceeds to submit the form, this is when onSubmit callback will be called. So if you'd like to block the page when there are validation errors you can use the formik context to subscribe to the validation isValid and whenever that is false block.
const useIsValidBlockedPage = () => {
const history = useHistory();
const { isValid } = useFormikContext();
useEffect(() => {
const unblock = history.block(({ pathname }) => {
// if is valid we can allow the navigation
if (isValid) {
// we can now unblock
unblock();
// proceed with the blocked navigation
history.push(pathname);
}
// prevent navigation
return false;
});
// just in case theres an unmount we can unblock if it exists
return unblock;
}, [isValid, history]);
};
Here is a codesandbox for that adapted from yours. I removed some components that weren't needed.
Another solution is validating manually on all page transitions and choosing when to allow the transition yourself and in this case it is if validateForm returns no errors.
// blocks page transitions if the form is not valid
const useFormBlockedPage = () => {
const history = useHistory();
const { validateForm } = useFormikContext();
useEffect(() => {
const unblock = history.block(({ pathname }) => {
// check if the form is valid
validateForm().then((errors) => {
// if there are no errors this form is valid
if (Object.keys(errors).length === 0) {
// Unblock the navigation.
unblock();
// retry the pagination
history.push(pathname);
}
});
// prevent navigation
return false;
});
return unblock;
}, [history, validateForm]);
};
And the codesandbox for that here

Related

How can I deal with the state updating asynchronously?

I have a login page where if a user enters the wrong credentials or submits blank fields, an error will be displayed on the page. Currently, when a user fails to signin with the right credentials, the error will only be displayed on the second click. I'm aware that my current problem is due to the state updating asynchronously, but I'm not sure how to resolve the problem in my code:
onst Login: React.FC<Props> = () => {
const user = useAppSelector(selectUser);
const auth = useAppSelector(selectAuth);
const dispatch = useAppDispatch();
...
const [signInError, setSignInError] = useState<boolean>(false);
const handleSignInError = () => {
if (auth.error?.status === 401 && auth.error.message === Constants.Errors.WRONG_CREDENTIALS) {
setSignInError(true);
}
}
const renderSigninError = () => {
if (signInError) {
return (
<Box paddingTop={2.5}>
<Alert variant="outlined" severity="error">
{Constants.Auth.FAILED_SIGN_IN}
</Alert>
</Box>
);
} else {
return (
<div/>
);
}
}
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData: any = new FormData(event.currentTarget);
const loginData: LoginData = {
email: formData.get("email"),
password: formData.get("password"),
}
try {
const res: any = await dispatch(login(loginData));
const resType: string = res.type;
if (resType === "auth/login/fulfilled") {
const userPayload: UserLogin = res.payload;
const loginUser: UserInterface = {
...
}
setSignInError(false);
dispatch(setUser(loginUser))
navigate("/");
}
else {
console.log("Failed to login");
handleSignInError();
}
}
catch (error: any) {
console.log(error.message);
}
}
return (
<Box
...code omitted...
{renderSigninError()}
...
)
}
What can I do to make sure that when the app loads and the user fails to login on the first click, the state for signInError should be true and the error displays?
You have at least 2 options.
add a conditional component.
Add a useEffect listenning for signInError and handle there as you want. It will trigger everytime signInError state changes
import React from 'react';
const [signInError, setError] = useState(false)
import React from 'react';
const [signInError, setError] = useState(false)
useEffect(() => {
console.log('new signInError value >', signInError)
// do whatever you want
}, [signInError])
export function App(props) {
return (
<div className='App'>
{
signInError ? (<p>something happened</p>) : null
}
</div>
);
}
There might be better approaches. Hope this can help you
I found a work around by changing handleSignInError() to update the state directly through setSignInError(true) as in:
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData: any = new FormData(event.currentTarget);
const loginData: LoginData = {
email: formData.get("email"),
password: formData.get("password"),
}
try {
const res: any = await dispatch(login(loginData));
const resType: string = res.type;
if (resType === "auth/login/fulfilled") {
const userPayload: UserLogin = res.payload;
const loginUser: UserInterface = {
...
}
setSignInError(false);
dispatch(setUser(loginUser))
navigate("/");
}
else {
console.log("Failed to login");
setSignInError(true); //changed here
}
}
catch (error: any) {
console.log(error.message);
}
}
Could someone help me understand why using another function initially didnt work?

How to create a customhook in reactJS which when clicked browser back button pops a confirm box.and on cancel stay on same page?

This is my current progress. But i am not able to make it work.Even if the history is changing internally within the buttons of my application.still it is getting trigerred.can anybody help me??
import { useEffect,useState } from 'react';
import { useHistory,useLocation } from 'react-router-dom';
export const useGoBack = ()=>{
const history = useHistory();
const location = useLocation();
useEffect(()=>{
const unlisten=(event)=>{
history.listen((location, action) => {
if(action === 'POP');
{
let result = window.confirm("Are you sure u want to leave?");
if(!result)
{
history.goForward();
}
}
});
}
window.addEventListener('popstate',unlisten);
return () => {window.removeEventListener('popstate',unlisten)};
},[location.pathname]);
}
You don't need to define any dependency in useEffect, an empty array will be enough, also I recommend you to use window.addEventListener("popstate", unlisten) or history.listen(), not both:
useEffect(() => {
const unlisten = (event) => {
console.log(event);
//...
};
window.addEventListener("popstate", unlisten);
return () => {
window.removeEventListener("popstate", unlisten);
};
}, []);
or:
useEffect(() => {
return history.listen((location) => {//...}
}, []);

React - Error: Rendered more hooks than during the previous render with Custom Hook

I have a component in which I'm calling my custom hook.
The custom hook looks like this:
import { useQuery } from 'react-apollo';
export function useSubscription() {
const { loading, error, data } = useQuery(GET_SUBSCRIPTION_BY_ID)
if (loading) return false
if (error) return null
return data
}
And then the component I'm using it in that causes the error is:
export default function Form(props) {
const router = useRouter();
let theSub = useSubscription();
if (theSub === false) {
return (
<Spinner />
)
} // else I'll have the data after this point so can use it.
useEffect(() => {
if (!isDeleted && Object.keys(router.query).length !== 0 && router.query.constructor === Object) {
setNewForm(false);
const fetchData = async () => {
// Axios to fetch data
};
fetchData();
}
}, [router.query]);
// Form Base States
const [newForm, setNewForm] = useState(true);
const [activeToast, setActiveToast] = useState(false);
// Form Change Methods
const handleUrlChange = useCallback((value) => setUrl(value), []);
const handleSubmit = useCallback(async (_event) => {
// Submit Form Code
}, [url, selectedDuration, included, excluded]);
return (
<Frame>
My FORM
</Frame>
)
}
Any ideas?
You can use useEffect for calling hook at the first time of rendering component.
export default function Form(props) {
const router = useRouter();
const [theSub, setTheSub] = useState(null);
useEffect(() => { setTheSub(useSubscription()); }, []);
if (theSub === false) {
return (
<Spinner />
)
} // else I'll have the data after this point so can use it.
// I have some other states being set and used related to the form e.g:
// const [whole, setWhole] = useState(true);
return (... The form ...)

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

Clear the form field after submitting the form using ReduxForm in ReactJS

After submitting the form successfully i have called the destroy() to clear the fields as given in the redux form document
result = (values) => {
const { history } = this.props;
this.props.dispatch(addVisitors(values)).then(
(success) => {
toast.success(success);
history.push('/visitors');
this.props.destroy();
},
(error) => {
toast.error(error);
}
);
};
You can call this.props.resetForm() from inside your form after your submission succeeds.
submitMyForm(data) {
const {createRecord, resetForm} = this.props;
return createRecord(data).then(() => {
resetForm();
// do other success stuff
});
}
render() {
const {handleSubmit, submitMyForm} = this.props;
return (
<form onSubmit={handleSubmit(submitMyForm.bind(this))}>
// inputs
</form>
);
}
You can dispatch reset() from any connected component
Extremely flexible, but you have to know your form name and have dispatch available.
import {reset} from 'redux-form';
...
dispatch(reset('myForm')); // requires form name
With your code I think you can use
import {reset} from 'redux-form';
...
result = (values) => {
const { history } = this.props;
this.props.dispatch(addVisitors(values)).then(
(success) => {
toast.success(success);
history.push('/visitors');
this.props.dispatch(reset('myForm')) // Change to your form name
},
(error) => {
toast.error(error);
}
);
};
I think you get this error because you're inside an anonymous function and this doesn't refer to your form anymore. For reference, take a look at: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this. To resolve this issue I would try to define a variable outside of the your promise and anonoymous function and refer to that newly declared variable instead of this.props.destroy(); in your promise. For example,
result = (values) => {
var formFields = this.props;
const { history } = this.props;
this.props.dispatch(addVisitors(values)).then(
(success) => {
toast.success(success);
history.push('/visitors');
formFields.destroy();
},
(error) => {
toast.error(error);
}
);
};

Resources