Syväsukellus Reactin `useFormState`-hookiin tehokkaaseen ja vankkaan lomaketilan hallintaan, sopii globaaleille kehittäjille.
React-lomakkeiden tilanhallinnan hallinta `useFormState`-hookilla
Verkkokehityksen dynaamisessa maailmassa lomakkeen tilanhallinnasta voi usein tulla monimutkainen hanke. Sovellusten kasvaessa laajuudeltaan ja toiminnallisuudeltaan, käyttäjäsyötteiden, validointivirheiden, lähetystilojen ja palvelin vastausten seuranta vaatii vankan ja tehokkaan lähestymistavan. React-kehittäjille useFormState
-hookin, joka usein yhdistetään palvelintoimintoihin (Server Actions), käyttöönotto tarjoaa tehokkaan ja virtaviivaisen ratkaisun näihin haasteisiin. Tämä kattava opas johdattaa sinut useFormState
n monimutkaisuuteen, sen etuihin ja käytännön toteutusstrategioihin, palvellen globaalia kehittäjäyleisöä.
Omistetun lomaketilan hallinnan tarpeen ymmärtäminen
Ennen kuin syvennytään useFormState
en, on olennaista ymmärtää, miksi yleiset tilanhallintaratkaisut, kuten useState
tai jopa kontekstirajapinnat, voivat olla riittämättömiä monimutkaisille lomakkeille. Perinteiset lähestymistavat sisältävät usein:
- Yksittäisten syötteiden tilojen manuaalinen hallinta (esim.
useState('')
kullekin kentälle). - Monimutkaisen logiikan toteuttaminen validointiin, virheenkäsittelyyn ja lataustiloihin.
- Propsien välittäminen useiden komponenttitasojen läpi, mikä johtaa prop drillingiin.
- Asynkronisten operaatioiden ja niiden sivuvaikutusten, kuten API-kutsujen ja vastausten käsittelyyn.
Vaikka nämä menetelmät ovat toimivia yksinkertaisille lomakkeille, ne voivat nopeasti johtaa:
- Boilerplate-koodiin: Merkittäviin määriin toistuvaa koodia kullekin lomakekentälle ja sen liittyvälle logiikalle.
- Ylläpidettävyysongelmiin: Vaikeuksiin päivittää tai laajentaa lomaketoiminnallisuutta sovelluksen kehittyessä.
- Suorituskyvyn pullonkauloihin: Tarpeettomiin uudelleenrenderöinteihin, jos tilapäivityksiä ei hallita tehokkaasti.
- Lisääntyneeseen monimutkaisuuteen: Korkeampaan kognitiiviseen kuormitukseen kehittäjille, jotka yrittävät ymmärtää lomakkeen kokonaistilan.
Tässä kohtaa erikoistuneet lomaketilan hallintaratkaisut, kuten useFormState
, tulevat kuvaan, tarjoten deklaratiivisemman ja integroidumman tavan käsitellä lomakkeen elinkaaria.
Esittelyssä `useFormState`
useFormState
on React-hook, joka on suunniteltu yksinkertaistamaan lomaketilan hallintaa, erityisesti integroitaessa palvelintoimintoihin (Server Actions) React 19:ssä ja uudemmissa versioissa. Se erottaa lomakelähetysten ja niistä johtuvan tilan käsittelylogiikan käyttöliittymäkomponenteistasi, edistäen puhtaampaa koodia ja parempaa huolten eriyttämistä.
Pohjimmiltaan useFormState
ottaa kaksi pääargumenttia:
- Palvelintoiminto (Server Action): Tämä on erityinen asynkroninen funktio, joka suoritetaan palvelimella. Se vastaa lomakedatan käsittelystä, liiketoimintalogiikan suorittamisesta ja uuden tilan palauttamisesta lomakkeelle.
- Alkutila (Initial State): Tämä on lomakkeen tilan alkuarvo, tyypillisesti objekti, joka sisältää kenttiä, kuten
data
(lomakkeen arvoille),errors
(validointiviesteille) jamessage
(yleiselle palautteelle).
Hook palauttaa kaksi olennaista arvoa:
- Lomakkeen tila (Form State): Lomakkeen nykyinen tila, joka päivitetään palvelintoiminnon suorituksen perusteella.
- Lähetystoiminto (Dispatch Function): Funktio, jonka voit kutsua käynnistääksesi palvelintoiminnon lomakkeen tiedoilla. Tämä liitetään tyypillisesti lomakkeen
onSubmit
-tapahtumaan tai lähetyspainikkeeseen.
`useFormState`n keskeiset edut
useFormState
n käyttöönoton edut ovat lukuisia, erityisesti kehittäjille, jotka työskentelevät kansainvälisissä projekteissa, joissa on monimutkaisia tiedonkäsittelyvaatimuksia:
- Palvelinkeskeinen logiikka: Delegoimalla lomakkeen käsittelyn palvelintoimintoihin (Server Actions), arkaluontoinen logiikka ja suorat tietokantavuorovaikutukset pysyvät palvelimella, mikä parantaa turvallisuutta ja suorituskykyä.
- Yksinkertaistetut tilapäivitykset:
useFormState
päivittää lomakkeen tilan automaattisesti palvelintoiminnon palautusarvon perusteella, mikä poistaa manuaaliset tilapäivitykset. - Sisäänrakennettu virheenkäsittely: Hook on suunniteltu toimimaan saumattomasti palvelintoimintojen virheraportoinnin kanssa, jolloin voit näyttää validointiviestit tai palvelinpuolen virheet tehokkaasti.
- Parempi luettavuus ja ylläpidettävyys: Lomakelogikan irrottaminen tekee komponenteista siistimpiä ja helpommin ymmärrettäviä, testattavia ja ylläpidettäviä, mikä on ratkaisevan tärkeää globaaleille yhteistyötiimeille.
- Optimoitu React 19:lle: Se on moderni ratkaisu, joka hyödyntää Reactin uusimpia edistysaskeleita tehokkaampaan ja tehokkaampaan lomakekäsittelyyn.
- Johdonmukainen tiedonkulku: Se luo selkeän ja ennustettavan kaavan sille, miten lomaketietoja lähetetään, käsitellään ja miten käyttöliittymä heijastaa tulosta.
Käytännön toteutus: Vaiheittainen opas
Kuvataan useFormState
n käyttöä käytännön esimerkillä. Luomme yksinkertaisen käyttäjän rekisteröintilomakkeen.
Vaihe 1: Palvelintoiminnon määrittely
Ensin tarvitsemme palvelintoiminnon (Server Action), joka käsittelee lomakkeen lähetyksen. Tämä funktio vastaanottaa lomakedatan, suorittaa validoinnin ja palauttaa uuden tilan.
// actions.server.js (or a similar server-side file)
'use server';
import { z } from 'zod'; // A popular validation library
// Define a schema for validation
const registrationSchema = z.object({
username: z.string().min(3, 'Username must be at least 3 characters long.'),
email: z.string().email('Invalid email address.'),
password: z.string().min(6, 'Password must be at least 6 characters long.')
});
// Define the structure of the state returned by the action
export type FormState = {
data?: Record<string, string>;
errors?: {
username?: string;
email?: string;
password?: string;
};
message?: string | null;
};
export async function registerUser(prevState: FormState, formData: FormData) {
const validatedFields = registrationSchema.safeParse({
username: formData.get('username'),
email: formData.get('email'),
password: formData.get('password')
});
if (!validatedFields.success) {
return {
...validatedFields.error.flatten().fieldErrors,
message: 'Registration failed due to validation errors.'
};
}
const { username, email, password } = validatedFields.data;
// Simulate saving user to a database (replace with actual DB logic)
try {
console.log('Registering user:', { username, email });
// await createUserInDatabase({ username, email, password });
return {
data: { username: '', email: '', password: '' }, // Clear form on success
errors: undefined,
message: 'User registered successfully!'
};
} catch (error) {
console.error('Error registering user:', error);
return {
data: { username, email, password }, // Keep form data on error
errors: undefined,
message: 'An unexpected error occurred during registration.'
};
}
}
Selitys:
- Määritämme
registrationSchema
n käyttäen Zodia vankan datavalidoinnin varmistamiseksi. Tämä on olennaista kansainvälisille sovelluksille, joissa syöttömuodot voivat vaihdella. registerUser
-funktio on merkitty'use server'
-direktiivillä, mikä osoittaa sen olevan palvelintoiminto (Server Action).- Se hyväksyy
prevState
n (edellinen lomaketila) jaformData
n (lomakkeen lähettämä data). - Se käyttää Zodia saapuvan datan validoimiseen.
- Jos validointi epäonnistuu, se palauttaa objektin, jossa on kentän nimellä avainistettyjä virheilmoituksia.
- Jos validointi onnistuu, se simuloi käyttäjän rekisteröintiprosessia ja palauttaa onnistumisviestin tai virheviestin, jos simuloitu prosessi epäonnistuu. Se myös tyhjentää lomakekentät onnistuneen rekisteröinnin jälkeen.
Vaihe 2: Käytä `useFormState`-hookia React-komponentissasi
Käytetään nyt useFormState
-hookia asiakaspuolen React-komponentissamme.
// RegistrationForm.jsx
'use client';
import { useEffect, useRef } from 'react';
import { useFormState } from 'react-dom';
import { registerUser, type FormState } from './actions.server';
const initialState: FormState = {
data: { username: '', email: '', password: '' },
errors: {},
message: null
};
export default function RegistrationForm() {
const [state, formAction] = useFormState(registerUser, initialState);
const formRef = useRef<HTMLFormElement>(null);
// Reset form on successful submission or when state changes significantly
useEffect(() => {
if (state.message === 'User registered successfully!') {
formRef.current?.reset();
}
}, [state.message]);
return (
<form action={formAction} ref={formRef} className="registration-form">
User Registration
<div className="form-group">
<label htmlFor="username">Username:</label>
<input
type="text"
id="username"
name="username"
defaultValue={state.data?.username || ''}
aria-describedby="username-error"
/>
{state.errors?.username && (
<div id="username-error" className="error-message">
{state.errors.username}
</div>
)}
</div>
<div className="form-group">
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
defaultValue={state.data?.email || ''}
aria-describedby="email-error"
/>
{state.errors?.email && (
<div id="email-error" className="error-message">
{state.errors.email}
</div>
)}
</div>
<div className="form-group">
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
defaultValue={state.data?.password || ''}
aria-describedby="password-error"
/>
{state.errors?.password && (
<div id="password-error" className="error-message">
{state.errors.password}
</div>
)}
</div>
<button type="submit">Register</button>
{state.message && (
<div className="submission-message">
<strong>{state.message}</strong>
</div>
)}
</form>
);
}
Selitys:
- Komponentti tuo sisään
useFormState
n jaregisterUser
-palvelintoiminnon. - Määritämme
initialState
n, joka vastaa palvelintoimintomme odotettua palautustyyppiä. useFormState(registerUser, initialState)
a kutsutaan, palauttaen nykyisenstate
n jaformAction
-funktion.formAction
välitetään HTML-<form>
-elementinaction
-propille. Näin React tietää kutsua palvelintoiminnon lomakkeen lähetyksen yhteydessä.- Jokaisella syötteellä on
name
-attribuutti, joka vastaa palvelintoiminnon odotettuja kenttiä, jadefaultValue
state.data
sta. - Ehdollista renderöintiä käytetään näyttämään virheilmoitukset (
state.errors.fieldName
) kunkin syötteen alla. - Yleinen lähetysviesti (
state.message
) näytetään lomakkeen jälkeen. useEffect
-hookia käytetään lomakkeen nollaamiseenformRef.current.reset()
-kutsulla, kun rekisteröinti on onnistunut, mikä tarjoaa puhtaan käyttökokemuksen.
Vaihe 3: Tyylittely (valinnainen mutta suositeltava)
Vaikka hyvä tyylittely ei olekaan osa useFormState
n ydintoimintoa, se on ratkaisevan tärkeää käyttökokemuksen kannalta, erityisesti globaaleissa sovelluksissa, joissa käyttöliittymän odotukset voivat vaihdella. Tässä perusesimerkki CSS:stä:
.registration-form {
max-width: 400px;
margin: 20px auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
font-family: sans-serif;
}
.registration-form h2 {
text-align: center;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box; /* Ensures padding doesn't affect width */
}
.error-message {
color: #e53e3e; /* Red color for errors */
font-size: 0.875rem;
margin-top: 5px;
}
.submission-message {
margin-top: 15px;
padding: 10px;
background-color: #d4edda; /* Green background for success */
color: #155724;
border: 1px solid #c3e6cb;
border-radius: 4px;
text-align: center;
}
.registration-form button {
width: 100%;
padding: 12px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s ease;
}
.registration-form button:hover {
background-color: #0056b3;
}
Monimutkaisempien skenaarioiden ja näkökohtien käsittely
useFormState
on tehokas, mutta ymmärtämällä, miten käsitellä monimutkaisempia skenaarioita, lomakkeistasi tulee todella vankkoja.
1. Tiedostojen lähettäminen
Tiedostojen lähettämiseen sinun on käsiteltävä FormData
oikein palvelintoiminnossasi (Server Action). formData.get('fieldName')
palauttaa File
-objektin tai null
in.
// In actions.server.js for file upload
export async function uploadDocument(prevState: FormState, formData: FormData) {
const file = formData.get('document') as File | null;
if (!file) {
return { message: 'Please select a document to upload.' };
}
// Process the file (e.g., save to cloud storage)
console.log('Uploading file:', file.name, file.type, file.size);
// await saveFileToStorage(file);
return { message: 'Document uploaded successfully!' };
}
// In your React component
// ...
// const [state, formAction] = useFormState(uploadDocument, initialState);
// ...
// <form action={formAction}>
// <input type="file" name="document" />
// <button type="submit">Upload</button>
// </form>
// ...
2. Useita toimintoja tai dynaamisia toimintoja
Jos lomakkeesi tarvitsee käynnistää erilaisia palvelintoimintoja (Server Actions) käyttäjän vuorovaikutuksen perusteella (esim. eri painikkeet), voit hallita tätä:
- Käyttämällä piilotettua syötettä: Aseta piilotetun syötteen arvo ilmaisemaan, mikä toiminto suoritetaan, ja lue se palvelintoiminnossasi.
- Tunnisteen välittäminen: Anna erityinen tunniste osana lomakedataa.
Esimerkiksi käyttämällä piilotettua syötettä:
// In your form component
function handleAction(actionType: string) {
// You might need to update a state or ref that the form action can read
// Or, more directly, use form.submit() with a pre-filled hidden input
}
// ... within the form ...
// <input type="hidden" name="actionToRun" value="register" />
// <button type="submit">Register</button>
// <button type="submit" formAction="/api/user/update">Update Profile</button> // Example of a different action
Huomaa: Reactin formAction
-propiota elementeissä, kuten <button>
tai <form>
, voidaan käyttää myös määrittämään eri toimintoja eri lähetysten yhteydessä, mikä tarjoaa enemmän joustavuutta.
3. Asiakaspuolen validointi
Vaikka palvelintoiminnot (Server Actions) tarjoavat vankan palvelinpuolen validoinnin, on hyvä käytäntö sisällyttää myös asiakaspuolen validointi välittömän palautteen antamiseksi käyttäjälle. Tämä voidaan tehdä käyttämällä kirjastoja, kuten Zod, Yup, tai mukautettua validointilogikkaa ennen lähetystä.
Voit integroida asiakaspuolen validoinnin seuraavasti:
- Suorittamalla validointia syötteen muutoksissa (
onChange
) tai fokuksen menetyksessä (onBlur
). - Tallentamalla validointivirheet komponenttisi tilaan.
- Näyttämällä nämä asiakaspuolen virheet palvelinpuolen virheiden rinnalla tai niiden sijaan.
- Mahdollisesti estämällä lähetyksen, jos asiakaspuolen virheitä on olemassa.
Muista kuitenkin, että asiakaspuolen validointi on tarkoitettu käyttökokemuksen parantamiseen; palvelinpuolen validointi on ratkaisevan tärkeää turvallisuuden ja tiedon eheyden kannalta.
4. Integrointi kirjastoihin
Jos käytät jo lomakkeenhallintakirjastoa, kuten React Hook Form tai Formik, saatat miettiä, miten useFormState
sopii kokonaisuuteen. Nämä kirjastot tarjoavat erinomaisia asiakaspuolen hallintaominaisuuksia. Voit integroida ne seuraavasti:
- Käyttämällä kirjastoa asiakaspuolen tilan ja validoinnin hallintaan.
- Lähetyksen yhteydessä rakenna manuaalisesti
FormData
-objekti ja välitä se palvelintoiminnollesi, mahdollisesti käyttämälläformAction
-propiota painikkeessa tai lomakkeessa.
Esimerkiksi React Hook Formin kanssa:
// RegistrationForm.jsx with React Hook Form
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { registerUser, type FormState } from './actions.server';
import { z } from 'zod';
const registrationSchema = z.object({
username: z.string().min(3, 'Username must be at least 3 characters long.'),
email: z.string().email('Invalid email address.'),
password: z.string().min(6, 'Password must be at least 6 characters long.')
});
type FormData = z.infer<typeof registrationSchema>;
const initialState: FormState = {
data: { username: '', email: '', password: '' },
errors: {},
message: null
};
export default function RegistrationForm() {
const [state, formAction] = useFormState(registerUser, initialState);
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(registrationSchema),
defaultValues: state.data || { username: '', email: '', password: '' } // Initialize with state data
});
// Handle submission with React Hook Form's handleSubmit
const onSubmit = handleSubmit((data) => {
// Construct FormData and dispatch the action
const formData = new FormData();
formData.append('username', data.username);
formData.append('email', data.email);
formData.append('password', data.password);
// The formAction will be attached to the form element itself
});
// Note: The actual submission needs to be tied to the form action.
// A common pattern is to use a single form and let the formAction handle it.
// If using RHF's handleSubmit, you'd typically prevent default and call your server action manually
// OR, use the form's action attribute and RHF will manage the input values.
// For simplicity with useFormState, it's often cleaner to let the form's 'action' prop manage.
// React Hook Form's internal submission can be bypassed if the form 'action' is used.
return (
<form action={formAction} className="registration-form">
User Registration
<div className="form-group">
<label htmlFor="username">Username:</label>
<input
{...register('username')}
id="username"
name="username"
aria-describedby="username-error"
// Use RHF's error, but also consider server errors
/>
{(errors.username || state.errors?.username) && (
<div id="username-error" className="error-message">
{errors.username?.message || state.errors?.username}
</div>
)}
</div>
<div className="form-group">
<label htmlFor="email">Email:</label>
<input
{...register('email')}
id="email"
name="email"
aria-describedby="email-error"
/>
{(errors.email || state.errors?.email) && (
<div id="email-error" className="error-message">
{errors.email?.message || state.errors?.email}
</div>
)}
</div>
<div className="form-group">
<label htmlFor="password">Password:</label>
<input
{...register('password')}
type="password"
id="password"
name="password"
aria-describedby="password-error"
/>
{(errors.password || state.errors?.password) && (
<div id="password-error" className="error-message">
{errors.password?.message || state.errors?.password}
</div>
)}
</div>
<button type="submit">Register</button>
{state.message && (
<div className="submission-message">
<strong>{state.message}</strong>
</div>
)}
</form>
);
}
Tässä hybridilähestymistavassa React Hook Form hoitaa syötteiden sidonnan ja asiakaspuolen validoinnin, kun taas lomakkeen action
-attribuutti, jota useFormState
hyödyntää, hallinnoi palvelintoiminnon (Server Action) suoritusta ja tilapäivityksiä.
5. Kansainvälistäminen (i18n)
Globaaleissa sovelluksissa virheilmoitukset ja käyttäjäpalaute on kansainvälistettävä. Tämä voidaan saavuttaa seuraavasti:
- Viestien tallentaminen käännöstiedostoon: Käytä kirjastoa, kuten react-i18next tai Next.js:n sisäänrakennettuja i18n-ominaisuuksia.
- Paikallistietojen välittäminen: Jos mahdollista, välitä käyttäjän alueasetukset palvelintoiminnolle, jotta se voi palauttaa lokalisoidut virheilmoitukset.
- Virheiden kartoitus: Kartoita palautetut virhekoodit tai avaimet vastaaviin lokalisoituihin viesteihin asiakaspuolella.
Esimerkki lokalisoiduista virheilmoituksista:
// actions.server.js (simplified localization)
import i18n from './i18n'; // Assume i18n setup
// ... inside registerUser ...
if (!validatedFields.success) {
const errors = validatedFields.error.flatten().fieldErrors;
return {
username: errors.username ? i18n.t('validation:username_min', { count: 3 }) : undefined,
email: errors.email ? i18n.t('validation:email_invalid') : undefined,
password: errors.password ? i18n.t('validation:password_min', { count: 6 }) : undefined,
message: i18n.t('validation:registration_failed')
};
}
Varmista, että palvelintoimintosi (Server Actions) ja asiakaskomponenttisi on suunniteltu toimimaan valitsemasi kansainvälistysstrategian kanssa.
Parhaat käytännöt `useFormState`n käyttöön
Maksimoidaksesi useFormState
n tehokkuuden, harkitse näitä parhaita käytäntöjä:
- Pidä palvelintoiminnot keskittyneinä: Jokaisen palvelintoiminnon (Server Action) tulisi ihanteellisesti suorittaa yksi, tarkasti määritelty tehtävä (esim. rekisteröinti, sisäänkirjautuminen, profiilin päivitys).
- Palauta johdonmukainen tila: Varmista, että palvelintoimintosi palauttavat aina tilaobjektin ennustettavalla rakenteella, mukaan lukien kentät tiedoille, virheille ja viesteille.
- Käytä `FormData`a oikein: Ymmärrä, miten eri tietotyyppejä liitetään ja haetaan
FormData
sta, erityisesti tiedostojen lähettämiseen. - Hyödynnä Zodia (tai vastaavaa): Käytä vahvoja validointikirjastoja sekä asiakas- että palvelinpuolella varmistaaksesi tiedon eheyden ja tarjotaksesi selkeitä virheilmoituksia.
- Tyhjennä lomakkeen tila onnistuessa: Toteuta logiikka lomakekenttien tyhjentämiseksi onnistuneen lähetyksen jälkeen hyvän käyttökokemuksen tarjoamiseksi.
- Käsittele lataustilat: Vaikka
useFormState
ei suoraan tarjoa lataustilaa, voit päätellä sen tarkistamalla, onko lomake lähetyksessä tai onko tila muuttunut edellisen lähetyksen jälkeen. Voit lisätä erillisenuseState
lla hallitun lataustilan, jos tarpeen. - Esteettömät lomakkeet: Varmista aina, että lomakkeesi ovat esteettömiä. Käytä semanttista HTML:ää, anna selkeät otsikot ja käytä ARIA-attribuutteja tarvittaessa (esim.
aria-describedby
virheille). - Testaus: Kirjoita testejä palvelintoiminnoillesi varmistaaksesi, että ne toimivat odotetusti erilaisissa olosuhteissa.
Johtopäätös
useFormState
edustaa merkittävää edistysaskelta siinä, miten React-kehittäjät voivat lähestyä lomakkeiden tilanhallintaa, erityisesti yhdistettynä palvelintoimintojen (Server Actions) tehoon. Keskittämällä lomakkeen lähetyslogiikan palvelimelle ja tarjoamalla deklaratiivisen tavan päivittää käyttöliittymää, se johtaa puhtaampiin, ylläpidettävämpiin ja turvallisempiin sovelluksiin. Riippumatta siitä, rakennatko yksinkertaisen yhteydenottolomakkeen vai monimutkaisen kansainvälisen verkkokaupan kassasovelluksen, useFormState
n ymmärtäminen ja toteuttaminen epäilemättä parantaa React-kehitystyötäsi ja sovellustesi kestävyyttä.
Verkkosovellusten kehittyessä jatkuvasti, näiden modernien React-ominaisuuksien omaksuminen antaa sinulle valmiudet rakentaa hienostuneempia ja käyttäjäystävällisempiä kokemuksia globaalille yleisölle. Mukavaa koodausta!