Skip to content

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;

Código