reactjs - Är inte denna validering synkron?
Genom att titta på denna commit i Formik.tsx-filen flyttades valideringen från (i fallet med touched som ett exempel):
React.useEffect(() => {
if (
prevState.touched !== state.touched &&
!!validateOnBlur &&
!state.isSubmitting &&
isMounted.current != null
) {
const [validate, cancel] = makeCancelable(validateForm());
validate.then(x => x).catch(x => x); // catch the rejection silently
return cancel;
}
return;
}, [
prevState.touched,
state.isSubmitting,
state.touched,
validateForm,
validateOnBlur,
isMounted,
]);
till här:
const setFieldTouched = React.useCallback(
(
field: string,
touched: boolean = true,
shouldValidate: boolean = true
) => {
dispatch({
type: 'SET_FIELD_TOUCHED',
payload: {
field,
value: touched,
},
});
return validateOnBlur && shouldValidate
? validateFormWithLowPriority(state.values)
: Promise.resolve();
},
[validateFormWithLowPriority, state.values, validateOnBlur]
);
Hoppar till den nuvarande implementationen ser det ut så här:
const setFieldTouched = useEventCallback(
(field: string, touched: boolean = true, shouldValidate?: boolean) => {
dispatch({
type: 'SET_FIELD_TOUCHED',
payload: {
field,
value: touched,
},
});
const willValidate =
shouldValidate === undefined ? validateOnBlur : shouldValidate;
return willValidate
? validateFormWithHighPriority(state.values)
: Promise.resolve();
}
);
Men varför och hur säkerställer de två sistnämnda att valideringen kommer att köras med de "nyaste" värdena? Med effekten kan du till exempel skicka en förändring och sedan en blur (båda kommer att ändra tillståndet). Tillståndet kommer då att ändras vid något tillfälle vilket gör att effekten sedan körs med de aktuella värdena.
I det senare fallet bör valideringen - när man följer anropskedjan - fortfarande köras synkront (executor-funktionen i ett promise körs synkront) - endast inställningen av felen kommer att vänta tills promisens är klar (har explicit then). Detta borde betyda att när jag anropar setFieldValue och sedan setFieldTouched omedelbart efter, kommer tillståndet fortfarande att vara det gamla (eftersom tillståndsförändring är asynkron). Ett exempel skulle vara att jag kunde skicka en förändring till ett tillstånd och försöka komma åt samma tillstånd direkt efter, vilket fortfarande kommer att vara det gamla tillståndet (code sandbox).
Men när man tittar på denna kodsandbox, är värdena som skickas till validate (från valideringsflödet i setFieldTouched) de faktiska nya värdena. Jag förstår inte varför detta är fallet...det kan mycket väl vara en felaktig tolkning av flödet från min sida, men det är därför jag ställer denna fråga.
Svar #1
Redigera
Det visar sig att hela frågan härstammade från att codesandbox använde en gammal version av [email , som har ett annat beteende än den nuvarande versionen av Formik. Detta ledde till förvirring om det förväntade beteendet från att läsa koden i [email men se ett beteende som inte matchade koden.
Det är en klassisk fallgrop, att läsa eller redigera en källfil men köra en annan.
Originalt svar nedan:
Du ställer en massa mycket intressanta frågor. Jag ska försöka svara på alla, men först vill jag prata lite om hur React-rendering fungerar i allmänhet, eftersom det kommer att informera svaren nedan.
Först, låt oss titta på detta kodexempel:
export default function App() {
const [count, setCount] = useState(0);
const doubleCount = useMemo(() => count * 2, [count]);
const buttonRef = useRef();
return (
<div className="App">
<button
ref={buttonRef}
onClick={() => {
setCount(count + 1);
console.log(count, doubleCount, buttonRef.current.textContent);
}}
>
{doubleCount}
</button>
</div>
);
}
(baserat på: https://twitter.com/RyanCarniato/status/1353801009844240389/photo/1)
Kod Sandbox
Att klicka på knappen 3 gånger skriver ut:
0 0 "0"
1 2 "2"
2 4 "4"
Detta illustrerar en viktig punkt - vid det tillfälle då klickhändelsen utförs och du anropar setState(count + 1), har själva setState()-operationen ännu inte utförts - den är istället schemalagd som en förändring som kommer att lösas vid nästa React-render. Värdet av variabeln count har inte omedelbart ökat. Detta beror på att onClick()-funktionen skapades när den senaste renderingen skedde, och den har fångat vilket värde count hade vid det tillfället.
Du måste behandla variabler som kommer från React-hookar som useState(), useReducer() och useMemo() som oföränderliga - de ändras inte direkt, de kommer endast att uppdateras vid nästa rendering.
Detta betyder att att kalla på setState() ett par gånger kanske inte heller gör exakt vad du förväntar dig. Låt oss säga att vi börjar med count=0.
setCount(count + 1); // same as setCount(0 + 1)
setCount(count + 1); // same as setCount(0 + 1)
setCount(count + 1); // same as setCount(0 + 1)
console.log(count) // 0, it will be 1 in the next render
Om du ville att ett värde skulle bero på ett tidigare inställt värde, bör du göra:
setCount(count => count + 1); // same as setCount(0 => 0 + 1)
setCount(count => count + 1); // same as setCount(1 => 1 + 1)
setCount(count => count + 1); // same as setCount(2 => 2 + 1)
console.log(count) // still 0 here, but it will be 3 in the next render
Med ovanstående i åtanke kan vi besvara följande fråga:
Men när man tittar på denna kodsandbox är de värden som skickas till validate (från valideringsflödet i setFieldTouched) de faktiska färska värdena. Jag förstår inte varför detta är fallet... det kan mycket väl vara en missuppfattning av flödet från min sida, men det är därför jag ställer denna fråga.
Anledningen till att du ser olika värden är att du ser att variabeln values var som den var vid tidpunkten då du klickade på knappen, och innan setFieldValue() har körts, eftersom den beter sig som setCount() i exemplet ovan. Dessutom kör kodsandboxen Formik 1.3.2, jag har förklarat varför den versionen är speciell nedan. Du skulle inte se nya värden med den senaste versionen.
Hur får validate() rätt data om values-datan i React ännu inte har uppdaterats?
Värdet som skickas till validate-callbacken i Formik beräknas av Formik själv i realtid, som en del av händelsehanteraren! Detta görs så att valideringsfelen också kan beräknas utan att vänta på att värdet ska renderas om. Det är i princip en prestandaoptimering och inte strikt nödvändigt. Låt oss ändå dyka in:
Vad är useEventCallback()?
Du kommer att se detta utspritt i Formik-koden. Allt det är är en mild prestandaoptimering - variablerna inuti den funktionen kommer alltid att vara variablerna från den aktuella renderingen, men funktionen kommer att ha ett stabilt referensvärde. Det betyder helt enkelt att setFieldValue från en rendering är strikt lika (===) med setFieldValue från en annan rendering. Detta används för att optimera vissa memoiseringsmetoder.
I praktiken, varje gång du ser useEventCallback() bör du mentalt föreställa dig att det bara är en vanlig funktion;
Hur säkerställer setFieldTouched() att valideringen kommer att köras med de "fräschaste" värdena?
Det gör det inte i Formic 2.x! setFieldTouched() körs mot de senaste värden som renderades.
Varning: Nästa avsnitt utforskar beteendet hos React Class Components. De rekommenderas inte för nya projekt, istället bör React Hooks användas
I Formik 1.x däremot, lade setFieldTouched() till valideringar som skulle köras som en callback med den andra parametern till React.Component.setState() - denna callback körs EFTER nästa render.
Så vad som hände där var att setFieldTouched() köade denna callback, senare uppdaterade setFieldValue() värdet, och sedan, efter att React renderat om, körde den callback som köats av setFieldTouched().
Hur fungerar setFieldValue() och varför har validate() färska värden?
(Detta är baserat på Formik 2.1.6)
Jag kommer nu att inlinea och kommentera all relevant kod som körs i setFieldValue():
const setFieldValue = (field: string, value: any, shouldValidate?: boolean) => {
// use dispatch() from React.useReducer() to update the value of the field and trigger a next render
dispatch({
type: 'SET_FIELD_VALUE',
payload: {
field,
value,
},
});
// from here on out I've inlined the code of validateFormWithHighPriority()
// set IS_VALIDATING state on the form.
// This change executes as part of the above render, alongside the value change!
dispatch({ type: 'SET_ISVALIDATING', payload: true });
// create a fresh copy of `values` with the updated field
const updatedValues = setIn(state.values, field, value);
Promise.all(
// validation functions go here; They will run with the `updatedValues`
// check out the functions `runAllValidations()` and ``
validate(updatedValues)
).then(combinedErrors => {
// this code is guaranteed to execute AFTER React has rendered the `SET_ISVALIDATING` state above,
// and will trigger a NEW React render
dispatch({ type: 'SET_ISVALIDATING', payload: false });
dispatch({ type: 'SET_ERRORS', payload: combinedErrors });
});
}
I praktiken är det som händer att setFieldValue() både triggar en ny React-rendering och också beräknar vad values skulle vara i den nya rendering med setIn(state.values, field, value). Det skickar sedan det till validate(), även om det görs på ett något omständligt sätt.
Jag hoppas att detta var till hjälp och att jag kunde besvara dina frågor. Om det är något oklart, fråga gärna i kommentarerna
