Firebase 9 + Router 6.4 + Context API
⭐ Videos Premium ⭐
Esta sección es parte del curso en Udemy. Si quieres acceder a ella, puedes comprar el curso en Udemy: React + Firebase by bluuweb.
Vite
npm create vite@latest .
Dependencies
npm i firebase@9 react-router-dom@6.4
"dependencies": {
"firebase": "^9.14.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.4"
},
"devDependencies": {
"@types/react": "^18.0.24",
"@types/react-dom": "^18.0.8",
"@vitejs/plugin-react": "^2.2.0",
"vite": "^3.2.3"
}
Descargar
Firebase & env variables
config/firebase.js
import { initializeApp } from "firebase/app";
import {
createUserWithEmailAndPassword,
getAuth,
signInWithEmailAndPassword,
signOut,
} from "firebase/auth";
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID,
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const login = ({ email, password }) =>
signInWithEmailAndPassword(auth, email, password);
export const register = ({ email, password }) =>
createUserWithEmailAndPassword(auth, email, password);
export const logOut = () => signOut(auth);
TIP
Para evitar la filtración accidental de variables al cliente, solo las variables con el prefijo VITE_
están expuestas a su código procesado por Vite.
VITE_SOME_KEY=123
.env.local
VITE_FIREBASE_API_KEY=
VITE_FIREBASE_AUTH_DOMAIN=
VITE_FIREBASE_PROJECT_ID=
VITE_FIREBASE_STORAGE_BUCKET=
VITE_FIREBASE_MESSAGING_SENDER_ID=
VITE_FIREBASE_APP_ID=
Login & Register
pages/Login.js
import { useEffect } from "react";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { login } from "../config/firebase";
import { useUserContext } from "../context/UserContext";
const Login = () => {
const [email, setEmail] = useState("test@test.com");
const [password, setPassword] = useState("123123");
const { user } = useUserContext();
const navigate = useNavigate();
useEffect(() => {
if (user) navigate("/dashboard");
}, [user]);
const handleSubmit = async (e) => {
e.preventDefault();
try {
await login({ email, password });
console.log("user logged in");
} catch (error) {
console.log(error.code);
console.log(error.message);
}
};
return (
<>
<h1>Login</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
placeholder="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
</>
);
};
export default Login;
pages/Register.jsx
import { useState } from "react";
import { register } from "../config/firebase";
import { useUserContext } from "../context/UserContext";
import { useRedirectActiveUser } from "../hooks/useRedirectActiveUser";
const Register = () => {
const [email, setEmail] = useState("test@test.com");
const [password, setPassword] = useState("123123");
const { user } = useUserContext();
// alternativa con hook
useRedirectActiveUser(user, "/dashboard");
const handleSubmit = async (e) => {
e.preventDefault();
try {
await register({ email, password });
console.log("user registered");
} catch (error) {
console.log(error.code);
console.log(error.message);
}
};
return (
<>
<h1>Register</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
placeholder="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Register</button>
</form>
</>
);
};
export default Register;
hooks/useRedirectActiveUser.js
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
export const useRedirectActiveUser = (user, path) => {
const navigate = useNavigate();
useEffect(() => {
if (user) {
navigate(path);
}
}, [user]);
};
UserContext.jsx
import { onAuthStateChanged } from "firebase/auth";
import { useState } from "react";
import { useEffect } from "react";
import { useContext } from "react";
import { createContext } from "react";
import { auth } from "../config/firebase";
const UserContext = createContext();
export default function UserContextProvider({ children }) {
const [user, setUser] = useState(false);
console.log("UserContext");
// Check si user está activo
useEffect(() => {
// observable por firebase 👇
const unsubscribe = onAuthStateChanged(auth, (user) => {
console.log(user);
setUser(user);
});
return unsubscribe;
}, []);
// Cuando inicia la aplicación siempre el user estará false
// Pero al terminar el useEffect, el user podrá ser null o un objeto
if (user === false) return <p>Loading app...</p>;
return (
<UserContext.Provider value={{ user }}>{children}</UserContext.Provider>
);
}
export const useUserContext = () => useContext(UserContext);
PrivateLayout.jsx
import { Navigate, Outlet } from "react-router-dom";
import { useUserContext } from "../context/UserContext";
const Private = () => {
const { user } = useUserContext();
console.log("PrivateLayout");
return user ? <Outlet /> : <Navigate to="/" />;
};
export default Private;
Dashboard & Logout
import { logOut } from "../config/firebase";
const Dashboard = () => {
const handleLogout = async () => {
await logOut();
};
return (
<>
<h1>Dashboard</h1>
<button onClick={handleLogout}>LogOut</button>
</>
);
};
export default Dashboard;
Formik && Yup
npm i formik
npm i yup
import { Formik } from "formik";
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { login } from "../config/firebase";
import { useUserContext } from "../context/UserContext";
const Login = () => {
const { user } = useUserContext();
const navigate = useNavigate();
useEffect(() => {
if (user) navigate("/dashboard");
}, [user]);
const onSubmit = async (values, { setSubmitting }) => {
try {
await login({ email: values.email, password: values.password });
console.log("user logged in");
} catch (error) {
console.log(error.code);
console.log(error.message);
} finally {
setSubmitting(false);
}
};
return (
<>
<h1>Login</h1>
<Formik
initialValues={{ email: "", password: "" }}
onSubmit={onSubmit}
>
{({ handleSubmit, handleChange, values, isSubmitting }) => (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="email"
value={values.email}
onChange={handleChange}
name="email"
/>
<input
type="password"
placeholder="password"
value={values.password}
onChange={handleChange}
name="password"
/>
<button
type="submit"
disabled={isSubmitting}
>
Login
</button>
</form>
)}
</Formik>
</>
);
};
export default Login;
Validation
import { Formik } from "formik";
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { login } from "../config/firebase";
import { useUserContext } from "../context/UserContext";
import * as Yup from "yup";
const Login = () => {
const { user } = useUserContext();
const navigate = useNavigate();
useEffect(() => {
if (user) navigate("/dashboard");
}, [user]);
const onSubmit = async (values, { setSubmitting }) => {
try {
await login({ email: values.email, password: values.password });
console.log("user logged in");
} catch (error) {
console.log(error.code);
console.log(error.message);
} finally {
setSubmitting(false);
}
};
const validationSchema = Yup.object().shape({
email: Yup.string().email().required(),
password: Yup.string().trim().min(6).required(),
});
return (
<>
<h1>Login</h1>
<Formik
initialValues={{ email: "", password: "" }}
onSubmit={onSubmit}
validationSchema={validationSchema}
>
{({
handleSubmit,
handleChange,
values,
isSubmitting,
errors,
touched,
handleBlur,
}) => (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="email"
value={values.email}
onChange={handleChange}
name="email"
onBlur={handleBlur}
/>
{errors.email && touched.email && errors.email}
<input
type="password"
placeholder="password"
value={values.password}
onChange={handleChange}
name="password"
onBlur={handleBlur}
/>
{errors.password && touched.password && errors.password}
<button type="submit" disabled={isSubmitting}>
Login
</button>
</form>
)}
</Formik>
</>
);
};
export default Login;
setErrors & resetForm
const onSubmit = async (
values,
{ setSubmitting, setErrors, resetForm }
) => {
try {
await login({ email: values.email, password: values.password });
console.log("user logged in");
resetForm();
} catch (error) {
console.log(error.code);
console.log(error.message);
if (error.code === "auth/user-not-found") {
setErrors({ email: "Email already in use" });
}
if (error.code === "auth/wrong-password") {
setErrors({ password: "Wrong password" });
}
} finally {
setSubmitting(false);
}
};
Register
import { Formik } from "formik";
import * as Yup from "yup";
import { register } from "../config/firebase";
import { useUserContext } from "../context/UserContext";
import { useRedirectActiveUser } from "../hooks/useRedirectActiveUser";
const Register = () => {
const { user } = useUserContext();
// alternativa con hook
useRedirectActiveUser(user, "/dashboard");
const onSubmit = async (
{ email, password },
{ setSubmitting, setErrors, resetForm }
) => {
try {
await register({ email, password });
console.log("user registered");
resetForm();
} catch (error) {
console.log(error.code);
console.log(error.message);
if (error.code === "auth/email-already-in-use") {
setErrors({ email: "Email already in use" });
}
} finally {
setSubmitting(false);
}
};
const validationSchema = Yup.object().shape({
email: Yup.string().email().required(),
password: Yup.string().trim().min(6).required(),
});
return (
<>
<h1>Register</h1>
<Formik
initialValues={{ email: "", password: "" }}
onSubmit={onSubmit}
validationSchema={validationSchema}
>
{({
handleSubmit,
handleChange,
values,
isSubmitting,
errors,
touched,
handleBlur,
}) => (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="email"
value={values.email}
onChange={handleChange}
name="email"
onBlur={handleBlur}
/>
{errors.email && touched.email && errors.email}
<input
type="password"
placeholder="password"
value={values.password}
onChange={handleChange}
name="password"
onBlur={handleBlur}
/>
{errors.password && touched.password && errors.password}
<button
type="submit"
disabled={isSubmitting}
>
Register
</button>
</form>
)}
</Formik>
</>
);
};
export default Register;
Material UI
Material UI es una biblioteca de componentes de interfaz de usuario para React que sigue los diseños de Material Design de Google. Proporciona componentes estilizados y listos para usar para acelerar el desarrollo de aplicaciones web y móviles. Material UI permite a los desarrolladores crear interfaces de usuario atractivas y funcionales con facilidad, mientras mantiene una consistencia en la apariencia y la experiencia de usuario.
Instalación
npm install @mui/material @emotion/react @emotion/styled
npm install @fontsource/roboto
npm install @mui/icons-material
npm install @mui/lab #LoadingButton etc
CssBaseline
- CssBaseline: Material UI proporciona un componente CssBaseline opcional. Corrige algunas incoherencias entre navegadores y dispositivos al tiempo que proporciona restablecimientos que se adaptan mejor a la interfaz de usuario de Material que las hojas de estilo globales alternativas como normalize.css.
main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import "@fontsource/roboto/300.css";
import "@fontsource/roboto/400.css";
import "@fontsource/roboto/500.css";
import "@fontsource/roboto/700.css";
import "./index.css";
import { RouterProvider } from "react-router-dom";
import { router } from "./config/router";
import { CssBaseline } from "@mui/material";
ReactDOM.createRoot(document.getElementById("root")).render(
<>
<CssBaseline />
<RouterProvider router={router} />
</>
);
Ejemplo componente Button
import { Button } from "@mui/material";
<Button variant="contained">Hello World</Button>
MUI & Login
import { Formik } from "formik";
import * as Yup from "yup";
import { useEffect } from "react";
import { login } from "../config/firebase";
import { Link, useNavigate } from "react-router-dom";
import { useUserContext } from "../context/UserContext";
import { Avatar, Box, Button, TextField, Typography } from "@mui/material";
import { LoadingButton } from "@mui/lab";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
const Login = () => {
const navigate = useNavigate();
const { user } = useUserContext();
useEffect(() => {
if (user) {
navigate("/dashboard");
}
}, [user]);
const onSubmit = async (
{ email, password },
{ setSubmitting, setErrors, resetForm }
) => {
try {
const credentialUser = await login({ email, password });
console.log(credentialUser);
resetForm();
} catch (error) {
console.log(error);
if (error.code === "auth/user-not-found") {
setErrors({ email: "Email no registrado" });
}
if (error.code === "auth/wrong-password") {
setErrors({ password: "Contraseña incorrecta" });
}
} finally {
setSubmitting(false);
}
};
const validationSchema = Yup.object().shape({
email: Yup.string().email("Email no válido").required("Email requerido"),
password: Yup.string()
.trim()
.min(6, "Mínimo 6 caracteres")
.required("Contraseña requerida"),
});
return (
<Box sx={{ mt: 8, maxWidth: 400, mx: "auto", textAlign: "center" }}>
<Avatar sx={{ mx: "auto", bgcolor: "#444" }}>
<LockOutlinedIcon />
</Avatar>
<Typography
component="h1"
variant="h5"
>
Sign in
</Typography>
<Formik
initialValues={{ email: "", password: "" }}
onSubmit={onSubmit}
validationSchema={validationSchema}
>
{({
handleChange,
handleSubmit,
values,
isSubmitting,
errors,
touched,
handleBlur,
}) => (
<Box
onSubmit={handleSubmit}
component="form"
sx={{ mt: 1 }}
>
<TextField
type="text"
placeholder="test@example.com"
value={values.email}
onChange={handleChange}
name="email"
onBlur={handleBlur}
id="email"
label="Ingrese email"
fullWidth
sx={{ mb: 3 }}
error={errors.email && touched.email}
helperText={errors.email && touched.email && errors.email}
/>
<TextField
type="password"
placeholder="Ingrese contraseña"
value={values.password}
onChange={handleChange}
name="password"
onBlur={handleBlur}
id="password"
label="Ingrese contraseña"
fullWidth
sx={{ mb: 3 }}
error={errors.password && touched.password}
helperText={
errors.password && touched.password && errors.password
}
/>
<LoadingButton
type="submit"
disabled={isSubmitting}
loading={isSubmitting}
variant="contained"
fullWidth
sx={{ mb: 3 }}
>
Login
</LoadingButton>
<Button
component={Link}
to="/register"
fullWidth
>
¿No tienes cuenta? Regístrate
</Button>
</Box>
)}
</Formik>
</Box>
);
};
export default Login;
Register
import { Formik } from "formik";
import * as Yup from "yup";
import { register } from "../config/firebase";
import { useUserContext } from "../context/UserContext";
import { useRedirectActiveUser } from "../hooks/useRedirectActiveUser";
import { Avatar, Box, Button, TextField, Typography } from "@mui/material";
import { LoadingButton } from "@mui/lab";
import { Link } from "react-router-dom";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
const Register = () => {
const { user } = useUserContext();
// alternativa con hook
useRedirectActiveUser(user, "/dashboard");
const onSubmit = async (
{ email, password },
{ setSubmitting, setErrors, resetForm }
) => {
try {
await register({ email, password });
console.log("user registered");
resetForm();
} catch (error) {
console.log(error.code);
console.log(error.message);
if (error.code === "auth/email-already-in-use") {
setErrors({ email: "Email already in use" });
}
} finally {
setSubmitting(false);
}
};
const validationSchema = Yup.object().shape({
email: Yup.string().email().required(),
password: Yup.string().trim().min(6).required(),
});
return (
<Box sx={{ mt: 8, maxWidth: 400, mx: "auto", textAlign: "center" }}>
<Avatar sx={{ mx: "auto", bgcolor: "#444" }}>
<LockOutlinedIcon />
</Avatar>
<Typography
component="h1"
variant="h5"
>
Sign up
</Typography>
<Formik
initialValues={{ email: "", password: "" }}
onSubmit={onSubmit}
validationSchema={validationSchema}
>
{({
handleSubmit,
handleChange,
values,
isSubmitting,
errors,
touched,
handleBlur,
}) => (
<Box
onSubmit={handleSubmit}
component="form"
sx={{ mt: 1 }}
>
<TextField
type="text"
label="Ingrese email"
value={values.email}
onChange={handleChange}
name="email"
fullWidth
sx={{ mb: 3 }}
id="email"
placeholder="test@example.com"
onBlur={handleBlur}
error={errors.email && touched.email}
helperText={errors.email && touched.email && errors.email}
/>
<TextField
type="password"
placeholder="Ingrese contraseña"
value={values.password}
onChange={handleChange}
name="password"
onBlur={handleBlur}
id="password"
label="Ingrese contraseña"
fullWidth
sx={{ mb: 3 }}
error={errors.password && touched.password}
helperText={
errors.password && touched.password && errors.password
}
/>
<LoadingButton
type="submit"
disabled={isSubmitting}
loading={isSubmitting}
variant="contained"
fullWidth
sx={{ mb: 3 }}
>
Register
</LoadingButton>
<Button
component={Link}
to="/"
fullWidth
>
¿Ya tienes cuenta? Ingresa
</Button>
</Box>
)}
</Formik>
</Box>
);
};
export default Register;