Welcome to this comprehensive Ionic-React-Native tutorial, where we'll explore key aspects such as file structuring, page navigation using React-Router-Dom, state management with React-Redux and @reduxjs/toolkit, seamless API connections with @reduxjs/toolkit/query/react, and elegant styling using Ionic. Let's dive in!
To get started, follow these steps to create your application using Create React App and install the necessary dependencies for managing user state with React-Redux and React Router. Before proceeding, ensure you have Node.js installed, a code editor (preferably Visual Studio Code), and a basic understanding of React.
Step 1: Create the React Application
Open your terminal or command prompt and run the following command to create a new React application using Create React App:
npm install -g @ionic/cli
Now, create the Ionic React Native app with TypeScript template using the following command:
ionic start my_ionic_react_native_app blank --type=react --capacitor --language=typescript
This will generate a new folder named my-app containing the basic structure of your React application.
This command creates a new Ionic app with a blank template, using React as the framework, TypeScript as the language, and Capacitor for native integration.
Step 2: Navigate to the Application Directory
Change your working directory to the newly created application folder:
cd my_ionic_react_native_app
Step 3: Run the App
To run the app in a development server, use the following command:
ionic serve
This will open the app in your web browser, and you can see the web version of your Ionic React Native app.
Step 4: Set Up Native Platforms (Optional)
If you want to build the app for native platforms (iOS and Android), you'll need to set up the native platforms using Capacitor. First, make sure you have Xcode installed for iOS development and Android Studio for Android development.
To add the native platforms, run the following commands:
npx cap add ios
npx cap add android
Step 5: Build the Native App
After adding the native platforms, you can build the app for each platform:
npx cap copy
npx cap open ios // For iOS
npx cap open android // For Android
These commands will open the native projects in Xcode and Android Studio, where you can build, run, and test your app on actual devices or emulators.
Step 6: Install React-Redux, React Router, @reduxjs/toolkit, and React Hook Form Dependencies
Now that we have our Ionic React Native app set up, let's proceed with installing the necessary dependencies to manage user state, handle API connections, and handle form input with React Hook Form.
In your terminal or command prompt, run the following command to install the required dependencies:
npm install react-redux react-router-dom @reduxjs/toolkit @reduxjs/toolkit/query/react react-hook-form
With these dependencies installed, our app is equipped with powerful tools to efficiently manage state, handle API connections with @reduxjs/toolkit/query/react, and seamlessly handle form input using React Hook Form.
Step 7: Organizing File Structure
A well-organized file structure is crucial for a scalable and maintainable project. Let's reorganize our project to keep it clean and organized. Here's a suggested file structure for our Ionic React Native app:
my_ionic_react_native_app
|- src
|- pages
|- SignUp.tsx
|- Login.tsx
|- Home.tsx
|- ForgotPassword.tsx
|- ResetPassword.tsx
|- store
|- apiSlice.ts
|- rootReducer.ts
|- interfaces.ts
|- Routes.tsx
|- App.tsx
|- index.tsx
|- public
|- index.html
|- ...
In this file structure, we've grouped related components into the components folder, pages into the pages folder, and Redux-related code into the store folder. The interfaces.ts file is used to define types and interfaces for better type safety and code consistency.
Step 8: Setting Up React Router with Ionic
In the Routes.tsx file, we'll set up React Router with Ionic to handle navigation between different pages. Replace the content of Routes.tsx with the following code:
import React from 'react';
import { IonRouterOutlet } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import { Route, Redirect } from 'react-router-dom';
import SignUp from './pages/SignUp';
import Login from './pages/Login';
import Home from './pages/Home';
import ForgotPassword from './pages/ForgotPassword';
import ResetPassword from './pages/ResetPassword';
const Routes: React.FC = () => {
return (
<IonReactRouter>
<IonRouterOutlet>
<Route path="/" component={Home} exact />
<Route path="/sign-up" component={SignUp} exact />
<Route path="/login" component={Login} exact />
<Route path="/forgot-password" component={ForgotPassword} exact />
<Route path="/reset-password" component={ResetPassword} exact />
{/* Add other routes as needed */}
<Route exact path="/">
<Redirect to="/" />
</Route>
</IonRouterOutlet>
</IonReactRouter>
);
};
export default Routes;
Step 9: Setting up the Project and Store
Create a file named interface.ts and add the all necessary interface to it:
//interface.ts
export interface User {
id: number;
username: string;
email: string;
// Add more user properties as needed
}
export interface UserState {
user: User | null;
isLoading: boolean;
error: string | null;
}
export interface RootState {
user: UserState;
}
Inside the store folder, create a file named userSlice.ts with the following content:
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { User, RootState, UserState } from './interface';
const initialState: UserState = {
user: null,
isLoading: false,
error: null,
};
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUser: (state, action: PayloadAction<User>) => {
state.user = action.payload;
state.isLoading = false;
state.error = null;
},
setLoading: (state, action: PayloadAction<boolean>) => {
state.isLoading = action.payload;
},
setError: (state, action: PayloadAction<string>) => {
state.isLoading = false;
state.error = action.payload;
},
},
});
// Export actions
export const { setUser, setLoading, setError } = userSlice.actions;
// Selector to get the user from the state
export const selectUser = (state: RootState) => state.user.user;
// Selector to get the loading state
export const selectLoading = (state: RootState) => state.user.isLoading;
// Selector to get the error
export const selectError = (state: RootState) => state.user.error;
export default userSlice.reducer;
In the src folder, create a new folder named store to house our Redux-related code. In your rootReducer.ts file (in the same store folder), import and add the userSlice reducer to the root reducer:
import { combineReducers } from '@reduxjs/toolkit';
import userReducer from './userSlice'; // Import your userSlice reducer here
const rootReducer = combineReducers({
// Other reducers go here
user: userReducer,
});
export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;
Next, create a file named apiSlice.ts in the store folder. This file will handle API connections using @reduxjs/toolkit/query/react. Here's a basic example of how it can be structured:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
// Define your API endpoints and base query
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com/' }), // Replace with your API base URL
endpoints: (builder) => ({
// Define your API endpoints here
// For example:
// getUserById: builder.query<User, number>({
// query: (id) => `user/${id}`,
// }),
}),
});
// Export the hooks for using the API endpoints
// For example:
// export const { useGetUserByIdQuery } = api;
// Export the API object to use in the store
export default api;
Inside the store folder (same location where userSlice.ts and rootReducer.ts are located), create a file named store.ts with the following content:
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer'; // Import your root reducer here
const store = configureStore({
reducer: rootReducer,
// Add middleware or other store configurations as needed
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof rootReducer>;
export default store;
Now, let's set up the Redux store in App.tsx. Import the necessary dependencies and the rootReducer from the store folder:
import React from 'react';
import { Provider } from 'react-redux';
import Routes from './Routes';
import store from './store/store';
const App: React.FC = () => {
return (
<Provider store={store}>
<Routes />
</Provider>
);
};
export default App;
With these changes, you've now properly set up the project structure and the Redux store using @reduxjs/toolkit/query/react for API connections.
Step 10: Using React Hook Form for Form Handling with Ionic React
Create a file named interface.ts and add the all necessary interface to it:
// interface.ts
export interface SignUpForm {
username: string;
email: string;
password: string;
confirmPassword: string;
}
export interface LoginForm {
email: string;
password: string;
}
export interface ForgotPasswordForm {
email: string;
}
export interface ResetPasswordForm {
newPassword: string;
confirmPassword: string;
}
Component Organization: Create a PasswordField component to handle password input with show/hide password functionality:
SignUp.tsx
// Create PasswordField.tsx
import React, { useState } from 'react';
import { IonInput, IonIcon } from '@ionic/react';
import { eye, eyeOff } from 'ionicons/icons';
interface PasswordFieldProps {
label: string;
field: any;
fieldState: any;
register: (name: string) => void;
}
const PasswordField: React.FC<PasswordFieldProps> = ({ label, field, fieldState, register }) => {
const [visibility, setVisibility] = useState<"text" | "password">("password");
const toggleVisibility = () => {
setVisibility((prevVisibility) => (prevVisibility === "password" ? "text" : "password"));
};
return (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', gap: '20px' }}>
<IonInput
{...field}
type={visibility}
onPaste={(e) => {
e.preventDefault();
return false;
}}
required={true}
onIonChange={(e) => field.onChange(e.detail.value)}
onKeyUp={() => {
register(field.name);
}}
style={{
color: '#345430',
fontSize: '10px',
borderRadius: '10px',
border: '2px solid',
borderColor: fieldState.invalid ? 'red' : '#345430',
height: '50px',
}}
/>
<IonIcon
icon={visibility === 'text' ? eye : eyeOff}
onClick={toggleVisibility}
style={{ color: '#345430' }}
/>
</div>
);
};
export default PasswordField;
**Use the PasswordField component in your SignUp component and apply the other suggestions to your code as well:
import React from 'react';
import { IonContent, IonButton, IonLabel, IonItem, IonPage, IonInput } from '@ionic/react';
import { useForm, Controller } from 'react-hook-form';
import { useDispatch, useSelector } from 'react-redux';
import { setUser, selectLoading } from './store/userSlice';
import { SignUpForm } from './interface';
import PasswordField from '../components/PasswordField'; // Import the PasswordField component
const SignUp: React.FC = () => {
const dispatch = useDispatch();
const isLoading = useSelector(selectLoading);
const { control, handleSubmit, getValues, setError: setErrorFromForm, watch, formState: { errors, isValid }, register, trigger } = useForm<SignUpForm>({
defaultValues: {
email: '',
username: '',
password: '',
confirmPassword: '',
},
});
const onSubmit = (data: SignUpForm) => {
// Handle sign up form submission
console.log(data);
// Example: Dispatch setUser action to update user data in the state
dispatch(setUser(data));
};
const handleConfirmPassword = () => {
const { password, confirmPassword } = getValues();
if (password !== confirmPassword) {
setErrorFromForm('confirmPassword', { type: 'manual', message: 'Passwords do not match' });
}
};
const confirmPassword = watch('confirmPassword');
return (
<IonPage>
<IonContent>
<form onSubmit={handleSubmit(onSubmit)}>
<IonItem>
<IonLabel position="floating">Username</IonLabel>
<Controller
render={({ field, fieldState }) => (
<IonInput
{...field}
{...register("username")}
placeholder="Enter Username"
aria-label="Username"
onIonChange={(e) => field.onChange(e.detail.value)}
required={true}
onKeyUp={() => {
trigger('username');
}}
style={{
color: '#345430',
fontSize: '10px',
borderRadius: '10px',
border: '2px solid',
borderColor: fieldState.invalid ? 'red' : '#345430',
height: '50px',
}}
/>
)}
name="username"
control={control}
rules={{
required: 'Username is required',
pattern: {
value: /^[A-Za-z0-9_!@#$%^&*()-]+$/,
message: "Username must contain only letters, numbers, underscores, and symbols !@#$%^&*()-",
},
minLength: {
value: 4,
message: "Username must be at least 4 characters",
},
maxLength: {
value: 20,
message: "Username must be less than 20 characters",
}
}}
/>
</IonItem>
<span style={{ color: 'red' }}>{errors.username?.message}</span>
<IonItem>
<IonLabel position="floating">Email</IonLabel>
<Controller
render={({ field, fieldState }) =>
<IonInput
{...field}
{...register("email")}
onIonChange={(e) => field.onChange(e.detail.value)}
placeholder="Enter Email"
aria-label="Email"
required={true}
onKeyUp={() => {
trigger("email")
}}
style={{
color: "#345430",
fontSize: "10px",
borderRadius: "10px",
border: "2px solid",
borderColor: fieldState.invalid ? "red" : "#345430",
height: "50px",
}}
/>}
name="email"
control={control}
rules={{
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Invalid email address",
} }}
/>
</IonItem>
<span style={{ color: 'red' }}>{errors.email?.message}</span>
<IonItem>
<IonLabel position="floating">Password</IonLabel>
<Controller
render={({ field, fieldState }) =>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
width: "100%",
gap: "20px",
}}>
<PasswordField
label="Password"
field={field.password}
fieldState={fieldState.password}
register={{...register("password")}}
/>
<span style={{ color: 'red' }}>{errors.password?.message}</span>
</div>
}
name="password"
control={control}
rules={{
required: "Password is required",
pattern: {
value: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[.!@#$%^&*])[A-Za-z\d.!@#$%^&*]{8,20}$/,
message: "Password must have a capital letter, a small letter, a number, and a symbol (e.g., .!@#$%^&*)",
},
minLength: {
value: 8,
message: "Password must be at least 8 characters",
},
maxLength: {
value: 20,
message: "Password must be less than 20 characters",
},
}} />
</IonItem>
<span style={{ color: 'red' }}>{errors.password?.message}</span>
<IonItem>
<IonLabel position="floating">Confirm Password</IonLabel>
<Controller
render={({ field, fieldState }) =>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
width: "100%",
gap: "20px",
}}>
<PasswordField
label="confirmPassword"
field={field.confirmPassword}
fieldState={fieldState.confirmPassword}
register={{...register("confirmPassword")}}
/>
</div>}
name="confirmPassword"
control={control}
rules={{ required: 'Confirm Password is required' }}
/>
</IonItem>
<span style={{ color: 'red' }}>{errors.confirmPassword?.message}</span>
<IonButton type="submit" disabled={!isValid || isLoading}>
{isLoading ? 'Signing Up...' : 'Sign Up'}
</IonButton>
{errors && <span style={{ color: 'red' }}>{errors}</span>}
</form>
</IonContent>
</IonPage>
);
};
export default SignUp;
Login.tsx:
import { IonContent, IonButton, IonLabel, IonItem, IonPage, IonInput } from '@ionic/react';
import { useForm, Controller } from 'react-hook-form';
import { useDispatch, useSelector } from 'react-redux';
import { setUser, selectLoading } from './store/userSlice';
import { LoginForm } from './interface';
import PasswordField from '../components/PasswordField';
const LoginPage = () => {
const dispatch = useDispatch();
const isLoading = useSelector(selectLoading);
const { control, handleSubmit, formState: { errors, isValid }, register, trigger } = useForm<LoginForm>({
defaultValues: {
email: "",
password: "",
},
});
const onSubmit = (data: LoginForm) => {
// Handle login form submission
dispatch(setUser(data));
};
return (
<IonPage>
<IonContent>
<form onSubmit={handleSubmit(onSubmit)}>
<IonItem>
<IonLabel position="floating">Email</IonLabel>
<Controller
name="email"
control={control}
rules={{
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Invalid email address",
}
}}
render={({ field, fieldState }) => (
<IonInput
{...field}
placeholder="Enter Email"
aria-label="Email"
onIonChange={(e) => field.onChange(e.detail.value)}
required={true}
onKeyUp={() => {
trigger('email');
}}
{...register("email")}
style={{
color: '#345430',
fontSize: '10px',
borderRadius: '10px',
border: '2px solid',
borderColor: fieldState.invalid ? 'red' : '#345430',
height: '50px',
}}
/>
)}
/>
<span style={{ color: 'red' }}>{errors.email?.message}</span>
</IonItem>
<IonItem>
<IonLabel position="floating">Password</IonLabel>
<Controller
render={({ field, fieldState }) => (
<PasswordField
label="Password"
field={field.password}
fieldState={fieldState.password}
register={{...register("password")}}
/>
)}
name="password"
control={control}
rules={{
required: "Password is required",
pattern: {
value: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[.!@#$%^&*])[A-Za-z\d.!@#$%^&*]{8,20}$/,
message: "Password must have a capital letter, a small letter, a number, and a symbol (e.g., .!@#$%^&*)",
},
minLength: {
value: 8,
message: "Password must be at least 8 characters",
},
maxLength: {
value: 20,
message: "Password must be less than 20 characters",
},
}}
/>
<span style={{ color: 'red' }}>{errors.password?.message}</span>
</IonItem>
<IonButton type="submit" disabled={!isValid || isLoading}>
{isLoading ? 'Logging In...' : 'Login'}
</IonButton>
{errors && <span style={{ color: 'red' }}>{errors}</span>}
</form>
</IonContent>
</IonPage>
)
}
export default LoginPage
ResetPassword.tsx
import React from 'react';
import { IonContent, IonButton, IonLabel, IonItem, IonPage, IonInput } from '@ionic/react';
import { useForm, Controller } from 'react-hook-form';
import { useDispatch, useSelector } from 'react-redux';
import { setUser, selectLoading } from './store/userSlice';
import { ResetPasswordForm } from './interface';
import PasswordField from '../components/PasswordField';
const ResetPassword: React.FC = () => {
const dispatch = useDispatch();
const isLoading = useSelector(selectLoading);
const { control, handleSubmit, getValues, setError: setErrorFromForm, formState: { errors, isValid }, register, trigger } = useForm<ResetPasswordForm>({
defaultValues: {
newPassword: "",
confirmPassword: "",
},
});
const onSubmit = (data: ResetPasswordForm) => {
// Handle reset password form submission
dispatch(setUser(data));
};
const handleConfirmPassword = () => {
const { newPassword, confirmPassword } = getValues();
if (newPassword !== confirmPassword) {
setErrorFromForm('confirmPassword', { type: 'manual', message: 'Passwords do not match' });
}
};
return (
<IonPage>
<IonContent>
<form onSubmit={handleSubmit(onSubmit)}>
{/* New Password Field */}
<IonItem>
<IonLabel position="floating">New Password</IonLabel>
<Controller
render={({ field, fieldState }) => (
<PasswordField
label="New Password"
field={field.newPassword}
fieldState={fieldState.newPassword}
register={{...register("password")}}
/>
)}
name="newPassword"
control={control}
rules={{
required: "New Password is required",
pattern: {
value: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[.!@#$%^&*])[A-Za-z\d.!@#$%^&*]{8,20}$/,
message: "Password must have a capital letter, a small letter, a number, and a symbol (e.g., .!@#$%^&*)",
},
minLength: {
value: 8,
message: "Password must be at least 8 characters",
},
maxLength: {
value: 20,
message: "Password must be less than 20 characters",
},
}}
/>
</IonItem>
<span style={{ color: 'red' }}>{errors.newPassword?.message}</span>
{/* Confirm Password Field */}
<IonItem>
<IonLabel position="floating">Confirm Password</IonLabel>
<Controller
render={({ field, fieldState }) => (
<PasswordField
label="Confirm Password"
field={field.confirmPassword}
fieldState={fieldState.confirmPassword}
register={{...register("confirmPassword")}}
onKeyUp={handleConfirmPassword}
/>
)}
name="confirmPassword"
control={control}
rules={{ required: 'Confirm Password is required' }}
/>
</IonItem>
<span style={{ color: 'red' }}>{errors.confirmPassword?.message}</span>
<IonButton type="submit" disabled={!isValid || isLoading}>
{isLoading ? 'Submitting...' : 'Submit'}
</IonButton>
{errors && <span style={{ color: 'red' }}>{errors}</span>}
</form>
</IonContent>
</IonPage>
);
};
export default ResetPassword;
ForgotPassword.tsx
import React from 'react';
import { IonContent, IonButton, IonLabel, IonItem, IonPage, IonInput } from '@ionic/react';
import { useForm, Controller } from 'react-hook-form';
import { useDispatch, useSelector } from 'react-redux';
import { setUser, selectLoading } from './store/userSlice';
import { ForgotPasswordForm } from './interface';
import PasswordField from '../components/PasswordField';
const ForgotPassword: React.FC = () => {
const dispatch = useDispatch();
const isLoading = useSelector(selectLoading);
const { control, handleSubmit, formState: { errors, isValid }, register, trigger } = useForm<ForgotPasswordForm>({
defaultValues: {
email: "",
},
});
const onSubmit = (data: ForgotPasswordForm) => {
// Handle forgot password form submission
dispatch(setUser(data));
};
return (
<IonPage>
<IonContent>
<form onSubmit={handleSubmit(onSubmit)}>
{/* Email Field */}
<IonItem>
<IonLabel position="floating">Email</IonLabel>
<Controller
name="email"
control={control}
rules={{
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Invalid email address",
},
}}
render={({ field, fieldState }) => (
<IonInput
{...field}
{...register("password")}
placeholder="Enter Email"
aria-label="Email"
onIonChange={(e) => field.onChange(e.detail.value)}
required={true}
onKeyUp={() => {
trigger('email');
}}
style={{
color: '#345430',
fontSize: '10px',
borderRadius: '10px',
border: '2px solid',
borderColor: fieldState.invalid ? 'red' : '#345430',
height: '50px',
}}
/>
)}
/>
</IonItem>
<span style={{ color: 'red' }}>{errors.email?.message}</span>
<IonButton type="submit" disabled={!isValid || isLoading}>
{isLoading ? 'Submitting...' : 'Submit'}
</IonButton>
{errors && <span style={{ color: 'red' }}>{errors}</span>}
</form>
</IonContent>
</IonPage>
);
};
export default ForgotPassword;
Conclusion
In this tutorial, we covered the essentials of creating forms, debugging, and page navigation in an Ionic-React-Native application. We harnessed tools like TypeScript, React Hook Form, React-Redux, and @reduxjs/toolkit to build a powerful app.
We started by mastering form creation with React Hook Form, ensuring accurate user input through validation. Then, we explored debugging techniques, fine-tuning our app for a smooth user experience.
Our journey continued with React-Router-Dom and Ionic, where we learned to seamlessly navigate between app sections, enhancing user engagement.
We leveraged Redux and @reduxjs/toolkit to manage state efficiently, establishing a solid architecture for communication between components.
By completing this tutorial, you've gained a strong foundation for crafting advanced Ionic-React-Native apps. Good luck applying these skills to your future projects!
Feel free to contact me for jobs and project opportunities
Top comments (2)
This is an absolutely brilliant article (tutorial) - how come this had zero likes? I gave this all of the 5 available likes, lol ...
Sad state of affairs when the umptieth shallow "listicle" gets a 100 likes and this finely crafted masterpiece had zero ...
Seriously, THIS is the kind of content I want to see on dev.to - in-depth tutorials which show how to integrate a number of frameworks/technologies (integrating stuff is always where things get hairy ...)
P.S. still a few points of minor criticism:
a github repo woulda been nice ...
you explain how to use an API slice with "react query" and all that, but you don't show how to use it
the login and signup examples just do a "setUser" on submit, which isn't remotely realistic
how do I do an "async" dispatch
But anyway, for someone who wants to get started with the combo of Ionic, React, Redux and react-hook-form, this is pure gold :)
I appreciate your positive feedback on the article and the constructive criticism you provided. The GitHub repo is private due to sensitivity reasons. Thank you for your suggestions, and I'll consider improving the tutorial based on your points. Feel free to reach out for potential job or collaboration opportunities.