Appearance
Firebase 9 + Router 6.4 + Context API
Vite
bash
npm create vite@latest .
Dependencies
sh
npm i firebase@9 react-router-dom@6.4
json
"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
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
jsx
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
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
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
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
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
jsx
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
bash
npm i formik
npm i yup
jsx
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
jsx
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
jsx
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
jsx
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
sh
npm install @mui/material @emotion/react @emotion/styled
npm install @fontsource/roboto
npm install @mui/icons-material
npm install @mui/lab #LoadingButton etc
Button
jsx
import { Button } from "@mui/material";
jsx
<Button variant="contained">Hello World</Button>
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
jsx
import { CssBaseline } from "@mui/material";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<CssBaseline />
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
Container
jsx
import { Container } from "@mui/material";
jsx
<Container maxWidth="md">
<Routes>
<Route
path="/"
element={<Home />}
/>
<Route
path="/contact"
element={<Contact />}
/>
<Route
path="*"
element={<NotFound />}
/>
</Routes>
</Container>
Typography
jsx
import { Typography } from "@mui/material";
const Home = () => {
return (
<>
<Typography
variant="h1"
component="h2"
>
Home
</Typography>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Tenetur culpa
porro cupiditate, nostrum a explicabo delectus pariatur at facere ipsam
perspiciatis rerum aspernatur molestias ut reprehenderit quidem
asperiores, voluptatum ad.
</p>
<Typography variant="body1">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aperiam
tempora quae accusamus voluptates quia odit, ipsa laborum soluta itaque.
Expedita, at. Harum quisquam vitae delectus non animi minus in deserunt.
</Typography>
</>
);
};
export default Home;
Box
jsx
import { Box } from "@mui/material";
const Contact = () => {
return (
<>
<h1>Contact</h1>
<Box
component="div"
sx={{ bgcolor: "primary.main", color: "primary.contrastText", p: 2 }}
>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptate
maxime molestias accusamus autem fugit, perferendis, impedit, laborum
eius voluptatibus natus cumque? Impedit delectus pariatur perspiciatis
incidunt quas illum. Ea, quod.
</p>
</Box>
</>
);
};
export default Contact;
AppBar
jsx
import { AppBar, Button, Container, Toolbar, Typography } from "@mui/material";
import AddToDriveIcon from "@mui/icons-material/AddToDrive";
import { NavLink } from "react-router-dom";
const Navbar = () => {
return (
<AppBar position="static">
<Container maxWidth="md">
<Toolbar>
<AddToDriveIcon />
<Typography
variant="h6"
sx={{ ml: 1, flexGrow: 1 }}
>
MyCompany
</Typography>
<Button
color="inherit"
component={NavLink}
to="/"
sx={{ pt: 1 }}
>
Home
</Button>
<Button
color="inherit"
component={NavLink}
to="/contact"
sx={{ pt: 1 }}
>
Contact
</Button>
</Toolbar>
</Container>
</AppBar>
);
};
export default Navbar;
jsx
<Button
component={NavLink}
to="/"
sx={{ pt: 1 }}
style={({ isActive }) => (isActive ? { color: "black" } : { color: "white" })}
>
Home
</Button>
jsx
<NavLink
to="/"
style={{ textDecoration: "none" }}
>
{({ isActive }) => (
<Button
color="inherit"
sx={{
pt: 1,
color: isActive ? "salmon" : "white",
}}
>
Home
</Button>
)}
</NavLink>
MUI & Login
jsx
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,
Grid,
TextField,
Typography,
} from "@mui/material";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import { LoadingButton } from "@mui/lab";
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={{
marginTop: 8,
maxWidth: 400,
mx: "auto",
textAlign: "center",
}}
>
<Avatar sx={{ mx: "auto", bgcolor: "secondary.main" }}>
<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
sx={{ mb: 3 }}
fullWidth
label="Email Address"
id="email"
type="text"
placeholder="Ingrese email"
value={values.email}
onChange={handleChange}
name="email"
onBlur={handleBlur}
error={errors.email && touched.email}
helperText={errors.email && touched.email && errors.email}
/>
<TextField
fullWidth
label="Password"
id="password"
type="password"
placeholder="Ingrese contraseña"
value={values.password}
onChange={handleChange}
name="password"
onBlur={handleBlur}
error={errors.password && touched.password}
helperText={
errors.password && touched.password && errors.password
}
/>
<LoadingButton
variant="contained"
sx={{ mt: 3, mb: 2 }}
fullWidth
type="submit"
disabled={isSubmitting}
loading={isSubmitting}
>
Login
</LoadingButton>
<Grid container>
<Grid
item
xs
>
<Button
component={Link}
to="/register"
>
¿No tienes cuenta? Registrate
</Button>
</Grid>
</Grid>
</Box>
)}
</Formik>
</Box>
</>
);
};
export default Login;
Register
jsx
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 { Link } from "react-router-dom";
import {
Avatar,
Box,
Button,
Grid,
TextField,
Typography,
} from "@mui/material";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import { LoadingButton } from "@mui/lab";
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("Email no válido").required("Email obligatorio"),
password: Yup.string()
.trim()
.min(6, "Mínimo 6 carácteres")
.required("Password obligatorio"),
});
return (
<>
<Box
sx={{
marginTop: 8,
maxWidth: 400,
mx: "auto",
textAlign: "center",
}}
>
<Avatar sx={{ mx: "auto", bgcolor: "secondary.main" }}>
<LockOutlinedIcon />
</Avatar>
<Typography
component="h1"
variant="h5"
>
Registro
</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
sx={{ mb: 3 }}
fullWidth
label="Email Address"
id="email"
type="text"
placeholder="Ingrese email"
value={values.email}
onChange={handleChange}
name="email"
onBlur={handleBlur}
error={errors.email && touched.email}
helperText={errors.email && touched.email && errors.email}
/>
<TextField
fullWidth
label="Password"
id="password"
type="password"
placeholder="Ingrese contraseña"
value={values.password}
onChange={handleChange}
name="password"
onBlur={handleBlur}
error={errors.password && touched.password}
helperText={
errors.password && touched.password && errors.password
}
/>
<LoadingButton
variant="contained"
color="secondary"
sx={{ mt: 3, mb: 2 }}
fullWidth
type="submit"
disabled={isSubmitting}
loading={isSubmitting}
>
Register
</LoadingButton>
<Grid container>
<Grid
item
xs
>
<Button
component={Link}
to="/"
color="secondary"
>
¿Ya tienes cuenta? Accede aquí
</Button>
</Grid>
</Grid>
</Box>
)}
</Formik>
</Box>
</>
);
};
export default Register;