Can anyone help me how to use react strapi auth with email with user persist in session thruough context

can anyone help me how to use react strapi auth with email with user persist in session thruough context
https://github.com/strapi/community-content/issues/770

We are going to look to write an article on the topic, but here is an example you can reference now from one of my personal projects. It is not done yet, but you can see an example of how I am implementing basic login and user registration.

GitHubLink Frontend
GitHubLink Backend

Here is also a video around logging in with Strapi and React I made
Create a login form with React

I will show quick code examples below for reference but you can pull my code, and run it to learn more about my example.

My Home Page

Home.jsx
GitHub Home.jsx

This is where I display the SigninForm or SignupForm based on what the user chooses.

import { useState, useContext } from "react";
import { GlobalContextState } from '../context/globalContext';
import { Navigate } from 'react-router-dom';

import SigninForm from "../components/SigninForm/SigninForm";
import SignupForm from "../components/SignupForm/SignupForm";
import Page from "../styled/base/Page/Page";
import Heading from "../components/Heading/Heading";
import Grid from "../styled/layout/Grid/Grid";
import GridItem from "../styled/layout/Grid/GridItem";
import Benefits from '../components/Benefits/Benefits';

export default function Home() {
  const [selection, setSelection] = useState("signin");
  const { loggedIn } = useContext(GlobalContextState);
  
  if (loggedIn) return <Navigate to="/dashboard/teams" />;

  return (
    <Page className="bg-gray-800">
      <main className="h-full flex justify-center content-center">
        <Grid>
          <GridItem>
            <Heading
              heading="Welcome to Teams/we make it easy"
              subheading="Manage your teams, members and projects all in one place."
            />
          </GridItem>

          <GridItem>
            {selection === "signin" ? (
              <SigninForm setSelection={setSelection} />
            ) : (
              <SignupForm setSelection={setSelection} />
            )}
          </GridItem>
        </Grid>
      </main>
      <Benefits />
    </Page>
  );
}

My Singin Form

Signin.jsx
GitHub Singin Form

import { useState, useContext, useEffect } from "react";
import { GlobalContextDispatch } from "../../context/globalContext";
import { isRegexValid, checkURLRegex } from "../../helpers/isRegexValid";
import useFetchMutation from "../../hooks/useFetchMutation";
import {
  FormWrapper,
  FormContainer,
  FormImage,
  FormBox,
  FormHeading,
  FormError,
} from "../../styled/styles/form";
import Button from "../../styled/base/Button/Button";
import ButtonLink from "../../styled/base/ButtonLink/ButtonLink";
import Input from "../../styled/base/Input/Input";
import ErrorMessage from "../ErrorMessage/ErrorMessage";
import MockUser from '../MockUser/MockUser';
import { baseUrl } from '../../config';
const INITIAL_FORM_DATA = {
  identifier: "",
  password: "",
};

const INITIAL_FORM_ERRORS = {
  identifier: false,
  password: false,
};

const loginUrl = `${baseUrl}/api/auth/local`;

export default function Login({ setSelection }) {
  const dispatch = useContext(GlobalContextDispatch);
  

  const [login, { loading, error, data }] = useFetchMutation(loginUrl);

  const [formData, setFormData] = useState(INITIAL_FORM_DATA);
  const [formError, setFormError] = useState(INITIAL_FORM_ERRORS);

  useEffect(() => {
    if (data) {
      const { jwt, user } = data;
      dispatch({ type: "LOGIN", payload: { jwt, user } });
    }
  }, [data, dispatch]);

  function handleInputChange(event) {
    const { name, value } = event.target;
    setFormData({ ...formData, [name]: value });
  }

  function validateidentifier(identifier) {
    if (!isRegexValid(identifier, checkURLRegex)) {
      setFormError((prevState) => ({ ...prevState, identifier: true }));
      return true;
    } else {
      setFormError((prevState) => ({ ...prevState, identifier: false }));
      return false;
    }
  }

  function validatePassword(password) {
    if (password.length <= 6) {
      setFormError((prevState) => ({ ...prevState, password: true }));
      return true;
    } else {
      setFormError((prevState) => ({ ...prevState, password: false }));
      return false;
    }
  }

  function formValidation(formData) {
    let hasError = false;
    hasError = validateidentifier(formData.identifier) ? true : false;
    hasError = validatePassword(formData.password) ? true : false;
    return hasError;
  }

  async function hadleFormSubmit(event) {
    event.preventDefault();
    const hasErrors = formValidation(formData);

    if (!hasErrors) {
      const loginPayload = {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(formData),
      };
      login(loginPayload);
    }
  }

  return (
    <FormWrapper>
      <FormBox className="mt-8">
        <FormContainer>
          <form onSubmit={hadleFormSubmit}>
            <fieldset className="space-y-6"disabled={loading}>
              <FormBox>
                <FormImage
                  src="https://tailwindui.com/img/logos/workflow-mark-indigo-600.svg"
                  alt="Workflow"
                />
                <FormHeading>Sign in</FormHeading>
              </FormBox>
              <Input
                id="identifier"
                name="identifier"
                type="identifier"
                label="Email Address"
                autoComplete="off"
                placeholder="Enter your email"
                onChange={handleInputChange}
                onBlur={(e) => validateidentifier(e.target.value)}
                value={formData.identifier}
                error={
                  formError.identifier && "Please provide a valid email"
                }
              />

              <Input
                id="password"
                name="password"
                type="password"
                label="Password"
                autoComplete="new-password"
                placeholder="Enter your password"
                onBlur={(e) => validatePassword(e.target.value)}
                value={formData.password}
                error={
                  formError.password && "Password must be at least 6 characters"
                }
                onChange={handleInputChange}
              />

              <MockUser />

              <Button className="mt-6 mb-6" type="submit">
                {loading ? "Loading..." : "Sign in"}
              </Button>

              <div className="flex items-center justify-between">
                <div className="flex items-center">
                  <span className="ml-2 block text-sm text-gray-900">
                    Don't have an account?
                  </span>
                </div>

                <ButtonLink onClick={() => setSelection("signup")}>
                  Sign Up
                </ButtonLink>
              </div>
            </fieldset>
          </form>

          {error && (
            <FormError>
              <ErrorMessage message={error.message} />
            </FormError>
          )}
        </FormContainer>
      </FormBox>
    </FormWrapper>
  );
}

We are saving our data to context via

import { GlobalContextDispatch } from "../../context/globalContext";
useEffect(() => {
    if (data) {
      const { jwt, user } = data;
      dispatch({ type: "LOGIN", payload: { jwt, user } });
    }
  }, [data, dispatch]);

Here is how my context looks like
context/globalContext.js

import { createContext, useEffect } from "react";
import { useImmerReducer } from "use-immer";
import { useLocalStorage } from "../hooks/useLocalStorage";
import { useFetchTeams } from "./contextHooks/useFetchTeams";
import { useFetchTeamUsers } from "./contextHooks/useFetchTeamUsers";

export const GlobalContextState = createContext();
export const GlobalContextDispatch = createContext();
export const GlobalContextMethods = createContext();

const initialState = {
  loggedIn: false,
  user: null,
  token: null,
  teams: [],
  teamUsers: {},
};

const reducer = (draft, action) => {
  switch (action.type) {
    case "LOGIN":

      draft.loggedIn = true
      draft.user = action.payload.user;
      draft.token = action.payload.jwt;
      
      return;

    case "LOGOUT":
      draft.loggedIn = false;
      return;

    case "GET_TEAMS": {
      draft.teams = action.payload;
      return;
    }

    case "GET_TEAM_USERS": {
      draft.teamUsers = {
        ...draft.teamUsers,
        [action.payload.teamID]: action.payload.users,
      };
      return;
    }

    default:
      return draft;
  }
};

const GlobalContextProvider = ({ children }) => {
  const [data, setData] = useLocalStorage("teams-app-data", initialState);
  const [state, dispatch] = useImmerReducer(reducer, data);

  useEffect(() => {
    if (data.loggedIn === false) localStorage.removeItem("teams-app-data");
    setData(state);
  }, [state, setData, data]);

  const fetchTeams = useFetchTeams(state, dispatch);
  const fetchTeamUsers = useFetchTeamUsers(state, dispatch);

  return (
    <GlobalContextState.Provider value={state}>
      <GlobalContextDispatch.Provider value={dispatch}>
        <GlobalContextMethods.Provider value={{ fetchTeams, fetchTeamUsers }}>
          {children}
        </GlobalContextMethods.Provider>
      </GlobalContextDispatch.Provider>
    </GlobalContextState.Provider>
  );
};

export default GlobalContextProvider;

My SignIUp Form

SignupFotrm.jsx
GitHub SignupForm

import { useState, useContext, useEffect } from "react";
import { baseUrl } from '../../config';
import {
  GlobalContextDispatch,
} from "../../context/globalContext";
import useFetchMutation from '../../hooks/useFetchMutation';
import { isRegexValid, checkURLRegex } from "../../helpers/isRegexValid";
import {
  FormWrapper,
  FormContainer,
  FormImage,
  FormBox,
  FormHeading,
  FormError,
} from "../../styled/styles/form";
import Button from "../../styled/base/Button/Button";
import ButtonLink from "../../styled/base/ButtonLink/ButtonLink";
import Input from "../../styled/base/Input/Input";

const registerUrl = `${baseUrl}/api/auth/local/registerUser`;

const INITIAL_FORM_DATA = {
  firstName: "",
  lastName: "",
  email: "",
  password: "",
};

const INITIAL_FORM_ERRORS = {
  firstName: false,
  lastName: false,
  email: false,
  password: false,
};

export default function SignupForm({ setSelection }) {

  const dispatch = useContext(GlobalContextDispatch);

  const [registerUser, { data, loading, error} ] = useFetchMutation(registerUrl)

  const [formData, setFormData] = useState(INITIAL_FORM_DATA);
  const [formError, setFormError] = useState(INITIAL_FORM_ERRORS);

  useEffect(() => {
    if (data) {
      const { jwt, user } = data;
      dispatch({ type: "LOGIN", payload: { jwt, user } });
    }
  }, [data, dispatch]);

  function handleInputChange(event) {
    const { name, value } = event.target;
    setFormData({ ...formData, [name]: value });
  }

  function validateGeneric(text, name) {
    if (text.length === 0) {
      setFormError((prevState) => ({ ...prevState, [name]: true }));
      return true;
    } else {
      setFormError((prevState) => ({ ...prevState, [name]: false }));
      return false;
    }
  }

  function validateEmail(email) {
    if (!isRegexValid(email, checkURLRegex)) {
      setFormError((prevState) => ({ ...prevState, email: true }));
      return true;
    } else {
      setFormError((prevState) => ({ ...prevState, email: false }));
      return false;
    }
  }

  function validatePassword(password) {
    if (password.length <= 6) {
      setFormError((prevState) => ({ ...prevState, password: true }));
      return true;
    } else {
      setFormError((prevState) => ({ ...prevState, password: false }));
      return false;
    }
  }

  function formValidation(formData) {
    let hasError = false;
    hasError = validateGeneric(formData.firstName, "firstName") ? true : false;
    hasError = validateGeneric(formData.lastName, "lastName") ? true : false;
    hasError = validateEmail(formData.email) ? true : false;
    hasError = validatePassword(formData.password) ? true : false;
    return hasError;
  }

  function hadleFormSubmit(event) {
    event.preventDefault();
    const hasErrors = formValidation(formData);

    if (!hasErrors) {

      const registerPayload = {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(formData),
      };

      registerUser(registerPayload)
    }
  }

  if (error) return <h1>OOPPS!</h1>

  return (
    <FormWrapper>
      <FormBox className="mt-8">
        <FormContainer>
          <form onSubmit={hadleFormSubmit}>
            <fieldset className="space-y-6" disabled={loading}>
              <FormBox>
                <FormImage
                  src="https://tailwindui.com/img/logos/workflow-mark-indigo-600.svg"
                  alt="Workflow"
                />
                <FormHeading>Sign Up</FormHeading>
              </FormBox>

              <Input
                id="firstName"
                name="firstName"
                type="text"
                label="First Name"
                placeholder="Enter your first name"
                onChange={handleInputChange}
                onBlur={(e) => validateGeneric(e.target.value, "firstName")}
                value={formData.firstName}
                error={formError.firstName && "Please provide a first name"}
              />

              <Input
                id="lastName"
                name="lastName"
                type="text"
                label="Last Name"
                placeholder="Enter your last name"
                onChange={handleInputChange}
                onBlur={(e) => validateGeneric(e.target.value, "lastName")}
                value={formData.lastName}
                error={formError.lastName && "Please provide a last name"}
              />

              <Input
                id="email"
                name="email"
                type="email"
                autoComplete="off"
                label="Email Address"
                placeholder="Enter your email"
                onChange={handleInputChange}
                onBlur={(e) => validateEmail(e.target.value)}
                value={formData.email}
                error={formError.email && "Please provide a valid email"}
              />

              <Input
                id="password"
                name="password"
                type="password"
                label="Password"
                autoComplete="new-password"
                placeholder="Enter your password"
                onBlur={(e) => validatePassword(e.target.value)}
                value={formData.password}
                error={
                  formError.password && "Password must be at least 6 characters"
                }
                onChange={handleInputChange}
              />

              <Button className="mt-6 mb-6" type="submit">
                Register
              </Button>

              <div className="flex items-center justify-between">
                <div className="flex items-center">
                  <span className="ml-2 block text-sm text-gray-900">
                    Have an account?
                  </span>
                </div>

                <ButtonLink onClick={() => setSelection("signin")}>
                  Sign In
                </ButtonLink>
              </div>
            </fieldset>
          </form>

          <FormError></FormError>
        </FormContainer>
      </FormBox>
    </FormWrapper>
  );
}

You can use Strapi docs to see how user sign in and registration is handled here

Here is also a video around logging in with Strapi and React I made
Create a login form with React

We are looking for an author for WFC to make a tutorial, but in the mean time I hope this example helps you.

1 Like