Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save nurmdrafi/fc57e4c7b2e7573ba6dd105640592441 to your computer and use it in GitHub Desktop.
Save nurmdrafi/fc57e4c7b2e7573ba6dd105640592441 to your computer and use it in GitHub Desktop.
Implementation Context API with (Firebase Authentication, Theme, Main State Management) + useReducer

React Context Offical Doc - reactjs.org

Application State Management with React - kentcdodds.com ❤

How to use React Context effectively - kentcdodds.com ✔

How to optimize your context value - kentcdodds.com

useMemo inside Context API - React - blog.agney.dev

Create a Multi-Language Website with React Context API - 1 - dev.to

Create a Multi-Language Website with React Context API - 2 - medium.com

When to use useState or useReducer - blog.logrocket.com/ ✔

How To Build a CRUD App with React Hooks and the Context API - digitalocean.com ❤

Integrating Firebase Authentication, Hooks, and Context into your ReactJS App - medium.com

Basic

✔ createContext(defaultValue);

  1. creates a context object
  2. every context has Context.Provider, Context.Consumer, Context.displayName
  3. then this created context object will read value from the closest matching provider
  4. the defaultValue argument is only used when we does not have provider
  5. we can access default value without using provider but its not recommanded
  6. defaultValue should be an object(preferable)

✔ <Context.Provider value={{}}></Context.Provider>

  1. provider provides value inside context
  2. creating custom provider component is the best practice
  3. we can access context defaultValue without using provider
  4. passing undefined as a provider value doesn't cause consuming components to use defaultValue
  5. direct passing value causes re-rendering issue, best practice is using useState or useReducer
  6. if provider values changes, children also re-render

Throw Error Example

const CountContext = React.createContext();

function useCount() {
  const context = React.useContext(CountContext);
  if (!context) {
    throw new Error(`useCount must be used within a CountProvider`);
  }
  return context;
}
function countReducer(state, action) {
  switch (action.type) {
    case "INCREMENT": {
      return { count: state.count + 1 };
    }
    default: {
      throw new Error(`Unsupported action type: ${action.type}`);
    }
  }
}

Memorize Context Value

  • To prevent unnecessary rendering
// 01
const [state, dispatch] = React.useReducer(countReducer, { count: 0 });
const value = React.useMemo(() => [state, dispatch], [state]);
return <CountContext.Provider value={value} {...props} />;

// 02
const [count, setCount] = React.useState(0);
const value = React.useMemo(() => [count, setCount], [count]);
return <CountContext.Provider value={value} {...props} />;

// 03
const value = React.useMemo(
  () => ({
    authUser,
    setAuthUser,
  }),
  [authUser]
);
// ✔ basic approach
import React from "react";
// 1. Create context using the createContext() method
export const UserContext = React.createContext();

export default function App() {
  // 2. Wrap with UserContext.Provider
  return (
    // 3. provide value
    <UserContext.Provider value="Reed">
      <User />
    </UserContext.Provider>
  );
}

function User() {
  return (
    // 4. Read that value using UserContext.Consumer
    <UserContext.Consumer>
      {(value) => <h1>{value}</h1>}
      {/* prints: Reed */}
    </UserContext.Consumer>
  );
}
// or 5. Read that value using useContext method
const { value } = userContext(UserContext);
<User>{value}</User>;

✔ When to use useState or useReducer

  • type of state

    • useState - number, string, boolean
    • useReducer - object or array (might be better)
  • number of state transitions

    • useState - one or two
    • useReducer - too many (predictable and maintainable)
  • business logic

    • useState - no business logic
    • useReducer - complex business logic (readable and maintainable)
  • local vs global

    • useState - local
    • useReducer - global

Standard

// ✔ best practice
// 1. create context folder
// 2. create useContext.js file

import { createContext, useContext, useState } from "react";

// initial value for userContext (not mandatory)
const initialValue = {
  user: null,
  logIn: () => {},
  logOut: () => {},
}

// declare userContext
const UserContext = useContext();

// create custom provider component and return UserContext.Provider with value + children
export const UserContextProvider = ({children}) =>{
  return (
    const [user, setUser] = useState();
    function logIn(){
        setUser({user: ***})
    }

    function logOut(){
        setUser({})
    }
  <UserContext.Provider value={{user, logIn, logOut}}>
    {children}
  </UserContext.Provider >
  )
}

// custom hook
const useAuthUserContext = () => {
    const context = useContext(UserContext);
    if(!context){
        // throw error if call context outside provider
        throw new Error("useAuthUserContext must be used within a UserContextProvider")
    }
    return context;
}
export default useAuthUserContext;
// index.js ✔
import { UserContextProvider } from "./context/UserContext";

<React.StrictMode>
  <UserContextProvider>
    <App />
  </UserContextProvider>
</React.StrictMode>;
// App.js ✔
import useAuthUserContext from "./context/UserContext";

const App = () => {
  const { user, logIn, logOut } = useAuthUserContext();
  return <div></div>;
};

Integrate Firebase with Context API

1. install firebase

# with npm
npm install firebase

# with yarn
yarn add firebase

2. firebase config

  • go to https://console.firebase.google.com/
  • add project
  • register project
  • enable authentication Sign-in methods
  • create firebase.init.js inside src
  • copy paste config
  • import { getAuth } from "firebase/auth";
  • declare and export auth
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";

// Firebase configuration
const firebaseConfig = {
  apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  authDomain: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  projectId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  storageBucket: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  messagingSenderId: "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
  appId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);

export default auth;

3. set environment variable

Adding Custom Environment Variables - https://create-react-app.dev/docs/adding-custom-environment-variables/

  • create .env.local at project root copy firebase config and create variable
REACT_APP_apiKey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;
REACT_APP_authDomain = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;
REACT_APP_projectId = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;
REACT_APP_storageBucket = xxxxxxxxxxxxxxxxxxxxxxxxxxx;
REACT_APP_messagingSenderId = xxxxxxxxxxxxxxxxxxxxxxx;
REACT_APP_appId = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;
  • replace firebase config with env
const firebaseConfig = {
  apiKey: process.env.REACT_APP_apiKey,
  authDomain: process.env.REACT_APP_authDomain,
  projectId: process.env.REACT_APP_projectId,
  storageBucket: process.env.REACT_APP_storageBucket,
  messagingSenderId: process.env.REACT_APP_messagingSenderId,
  appId: process.env.REACT_APP_appId,
};

4. Create Context

// context > UserAuthContext.js
import React, { createContext, useContext, useEffect, useState } from "react";
import {
  createUserWithEmailAndPassword,
  updateProfile,
  signInWithEmailAndPassword,
  signOut,
  onAuthStateChanged,
  GoogleAuthProvider,
  signInWithPopup,
} from "firebase/auth";

import auth from "../firebase.init";
import { useNavigate } from "react-router-dom";

const UserAuthContext = createContext();

// custom provider component
export const UserAuthContextProvider = ({ children }) => {
  const [authUser, setAuthUser] = useState({});
  const [isLoading, setIsLoading] = useState(false);

  function signUp(email, password) {
    return createUserWithEmailAndPassword(auth, email, password);
  }
  function updateDisplayName(name) {
    return updateProfile(authUser?.auth?.currentUser, { displayName: name });
  }

  function logIn(email, password) {
    return signInWithEmailAndPassword(auth, email, password);
  }

  function logOut() {
    return signOut(auth).then(() => setAuthUser({}));
  }

  function googleSignIn() {
    const googleProvider = new GoogleAuthProvider();
    return signInWithPopup(auth, googleProvider);
  }

  const navigate = useNavigate();

  /* onMount get current user from firebase and setAuthUser */
  /* use setTimeout on requireAuth, otherwise if !authUser redirect to "/login" page and jump back to "/home" page */
  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
      if (currentUser) {
        setAuthUser(currentUser);
        navigate("/home");
      } else {
        setAuthUser({});
      }
    });
    return () => {
      unsubscribe();
    };
  }, []);

  return (
    <UserAuthContext.Provider
      value={{
        authUser,
        setAuthUser,
        isLoading,
        setIsLoading,
        signUp,
        updateDisplayName,
        logIn,
        logOut,
        googleSignIn,
      }}
    >
      {children}
    </UserAuthContext.Provider>
  );
};

// custom hook
const useUserAuth = () => {
  const context = useContext(UserAuthContext);

  if (!context) {
    throw Error("useUserAuth must be used within a UserAuthContextProvider");
  }
  return context;
};

export default useUserAuth;

5. App.js

import { ThemeContextProvider } from "./context/ThemeContext";
import { ArticleContextProvider } from "./context/ArticleContext";
import { UserAuthContextProvider } from "./context/UserAuthContext";

function App() {
  return (
    <ArticleContextProvider>
      <UserAuthContextProvider>
        <ThemeContextProvider>
          <Navigation />
        </ThemeContextProvider>
        <Routes>
          <Route path="/" element={<LandingPage />} />
          <Route
            path="/home"
            element={
              <RequireAuth>
                <Home />
              </RequireAuth>
            }
          />
          <Route path="/login" element={<Login />} />
          <Route path="/registration" element={<Registration />} />
          <Route path="*" element={<Notfound />} />
        </Routes>
      </UserAuthContextProvider>
    </ArticleContextProvider>
  );
}

export default App;

6. Registration Page

const Registration = () => {
  const { signUp, updateDisplayName, isLoading, setIsLoading } = useUserAuth();
  const {
    register,
    handleSubmit,
    setError,
    formState: { errors },
    reset,
  } = useForm();

  // Handle Registration
  const handleRegistration = async (data) => {
    if (data.password !== data.confirmPassword) {
      setError("confirmPassword", {
        type: "match",
        message: "Please confirm your password",
      });
    } else {
      try {
        setIsLoading(true);
        await signUp(data.email, data.password);
      } catch (err) {
        setIsLoading(false);
        toast.error(err.message, {
          id: "signUp error",
        });
      }
      try {
        await updateDisplayName(data.name);
        reset();
        setIsLoading(false);
      } catch (err) {
        toast.error(err.message, {
          id: "updateProfile error",
        });
      }
    }
  };
  if (isLoading) {
    return <Loading />;
  }

  return(<></>)
  export default Registration;

7. Login Page

const Login = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
    reset,
  } = useForm();

  const { logIn, isLoading, setIsLoading, googleSignIn } = useUserAuth();

  // handle login
  const handleLogin = async (data) => {
    try {
      setIsLoading(true);
      await logIn(data.email, data.password);
      reset();
      setIsLoading(false);
    } catch (err) {
      setIsLoading(false);
      toast.error(err.message, {
        id: "logIn error",
      });
    }
  };

  // handleGoogleSignIn
  const handleGoogleSignIn = async () => {
    try {
      setIsLoading(true);
      await googleSignIn();

      setIsLoading(false);

    } catch (err) {
      setIsLoading(false);
      toast.error(err.message, {
        id: "googleLogIn error",
      });
    }
  };

  if (isLoading) {
    return <Loading />;
  }

  return(<></>)
  export default Login;

8. Navigation

const Navigation = () => {
  const { authUser, logOut } = useUserAuth();

  const navigate = useNavigate();

  const handleLogOut = () => {
    try {
      logOut();
    } catch (err) {
      toast.error(err.message, {
        id: "logOut error",
      });
    }
    navigate("/login");
  };

  return(<></>)

  export default Navigation;

ThemeContext

import { createContext, useContext, useEffect, useState } from "react";

const ThemeContext = createContext();

export const ThemeContextProvider = ({ children }) => {
  const [theme, setTheme] = useState(localStorage.getItem("theme") || "light");
  const [isDarkMode, setDarkMode] = useState();

  function changeCurrentTheme(newTheme) {
    setTheme(newTheme);
    localStorage.setItem("theme", newTheme);
  }

  useEffect(() => {
    if (theme === "light") {
      document.body.classList.remove("dark");
      setDarkMode(false);
    } else {
      document.body.classList.add("dark");
      setDarkMode(true);
    }
  }, [theme]);

  return (
    <ThemeContext.Provider
      value={{ currentTheme: theme, isDarkMode, changeCurrentTheme }}
    >
      {children}
    </ThemeContext.Provider>
  );
};

const useThemeContext = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw Error("useThemeContext must be used within a ThemeContextProvider");
  }
  return context;
};

export default useThemeContext;

Main State Management

import { createContext, useContext, useState } from "react";

const initialState = {
  articles: [
    {
      id: "8adf57ac-66cc-45e2-8402-85b616b42e7f",
      title: "hello1",
      time: "Sun, 30 May 2021 14:59:15 GMT",
      body: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Soluta eius laborum voluptate hic aut doloremque officiis quasi quos explicabo molestiae!",
      comments: [
        {
          id: "542813e9-e882-401b-967d-d6fbc1460278",
          user: "admin",
          email: "[email protected]",
          text: "hello",
          loveCount: 0,
          loveVoters: [],
          time: "Mon, 12 Sep 2022 04:11:51 GMT",
        },
        {
          id: "25f818c4-9cba-4924-b67c-617d18884ec6",
          user: "admin",
          email: "[email protected]",
          text: "world",
          loveCount: 0,
          loveVoters: [],
          time: "Mon, 12 Sep 2022 04:12:09 GMT",
        },
      ],
      upVote: 0,
      upVoteUsers: [],
      downVote: 0,
      downVoteUsers: [],
      email: "[email protected]",
      userName: "admin",
    },
    {
      id: "72eac71f-f84c-4c17-a5db-77709dd84946",
      title: "hello2",
      time: "Mon, 12 Sep 2022 04:12:32 GMT",
      body: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Soluta eius laborum voluptate hic aut doloremque officiis quasi quos explicabo molestiae!",
      comments: [],
      upVote: 0,
      upVoteUsers: [],
      downVote: 0,
      downVoteUsers: [],
      email: "[email protected]",
      userName: "admin",
    },
  ],
};

const ArticleContext = createContext();

export const ArticleContextProvider = ({ children }) => {
  const [articles, setArticles] = useState(initialState);
  // console.log(articles)

  // addArticle
  function addArticle(newArticle) {
    const updatedArticles = [...articles.articles, newArticle];
    return setArticles({ articles: updatedArticles });
  }

  // delete article
  function deleteArticle(article_id) {
    const updatedArticleList = articles.articles.filter(
      (article) => article.id !== article_id
    );
    return setArticles({ articles: updatedArticleList });
  }

  // upVote count
  function upVote(article_id, updateArticleWithUpVotes) {
    const articleList = articles.articles.filter(
      (article) => article.id !== article_id
    );
    const updatedArticles = [...articleList, updateArticleWithUpVotes];
    return setArticles({ articles: updatedArticles });
  }
  // downVote count
  function downVote(article_id, updateArticleWithDownVotes) {
    const articleList = articles.articles.filter(
      (article) => article.id !== article_id
    );
    const updatedArticles = [...articleList, updateArticleWithDownVotes];
    return setArticles({ articles: updatedArticles });
  }

  // updateToggleVote count
  function updateToggleVote(article_id, updateArticleWithVotes) {
    const articleList = articles.articles.filter(
      (article) => article.id !== article_id
    );
    const updatedArticles = [...articleList, updateArticleWithVotes];
    return setArticles({ articles: updatedArticles });
  }

  // addComment
  function addComment(article_id, updatedArticleWithComments) {
    const articleList = articles.articles.filter(
      (article) => article.id !== article_id
    );
    const updatedArticles = [...articleList, updatedArticleWithComments];
    return setArticles({ articles: updatedArticles });

  // deleteComment
  function deleteComment(article_id, updatedArticleWithComments) {
    const articleList = articles.articles.filter(
      (article) => article.id !== article_id
    );
    const updatedArticles = [...articleList, updatedArticleWithComments];
    return setArticles({ articles: updatedArticles });
  }

  // loveComment
  function loveVote(article_id, updatedArticleWithLoveVote) {
    const articleList = articles.articles.filter(
      (article) => article.id !== article_id
    );
    const updatedArticles = [...articleList, updatedArticleWithLoveVote];
    return setArticles({ articles: updatedArticles });
  }

  return (
    <ArticleContext.Provider
      value={{
        articles,
        addArticle,
        deleteArticle,
        addComment,
        deleteComment,
        upVote,
        downVote,
        updateToggleVote,
        loveVote,
      }}
    >
      {children}
    </ArticleContext.Provider>
  );
};

const useArticleContext = () => {
  const context = useContext(ArticleContext);
  if (!context) {
    throw Error(
      "useArticleContext must be used within a ArticleContextProvider"
    );
  }
  return context;
};

export default useArticleContext;

Context API with useReducer

Example

✔ Basic of useReducer

  • https://www.youtube.com/watch?v=R5G2acJ6_vQ
  • useReducer is an alternative of useState
  • useReducer is more preferable then useState when we have to manage complex state and nested updates
  • useReducer is familiar with redux
  • reducer is a pure function that take state and action and return new state
const initialState = 0;
> initial value is 0.

const reducer = (state, action) => {
	switch(action.type){
	case "increment":
		return state + 1;
	case "decrement":
		return state - 1;
	default:
		throw new Error();
	}
}

const [state, dispatch] = useReducer(reducer(reducer function), initialState)
/*
> state: display the data
> dispatch: trigger action method using type
> payloads: the content of an action

when ever we click button for take action, we need to trigger action using dispatch
*/


<button onClick={() => dispatch{type:"increment"}}>Increment</button>
// GlobalContext.js
import React, { createContext, useContext, useReducer } from "react";
import appReducer from "./AppReducer";

const initialState = {
  articles: [
    {
      id: 1,
      title: "hello1",
      category: "test1",
      time: "05/09/2022, 21:13:19",
      body: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Soluta eius laborum voluptate hic aut doloremque officiis quasi quos explicabo molestiae!",
      comments: [
        { id: 1, user: "admin", text: "hello" },
        { id: 2, user: "admin", text: "world" },
      ],
      upVote: 0,
      upVoteUsers: [],
      downVote: 0,
      downVoteUsers: [],
      email: "[email protected]",
      userName: "admin",
    },
    {
      id: 2,
      title: "hello2",
      category: "test2",
      time: "05/09/2022, 21:13:30",
      body: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Soluta eius laborum voluptate hic aut doloremque officiis quasi quos explicabo molestiae!",
      comments: [],
      upVote: 0,
      upVoteUsers: [],
      downVote: 0,
      downVoteUsers: [],
      email: "[email protected]",
      userName: "admin",
    },
  ],
};

export const GlobalContext = createContext(initialState);

export const GlobalProvider = ({ children }) => {
  const [state, dispatch] = useReducer(appReducer, initialState);

  function addArticle(article) {
    dispatch({
      type: "ADD_ARTICLE",
      payload: article,
    });
  }

  function updateArticle(article) {
    dispatch({
      type: "UPDATE_ARTICLE",
      payload: article,
    });
  }

  return (
    <GlobalContext.Provider
      value={{
        articles: state.articles,
        addArticle,
        updateArticle,
      }}
    >
      {children}
    </GlobalContext.Provider>
  );
};

const useArticle = () => {
  const context = useContext(GlobalContext);
  if (!context) {
    throw Error("useArticle must be used within a GlobalContextProvider");
  }
  return context;
};

export default useArticle;
// AppReducer.js
const appReducer = (state, action) => {
  switch (action.type) {
    case "ADD_ARTICLE":
      return {
        ...state,
        articles: [...state.articles, action.payload],
      };

    case "UPDATE_ARTICLE":
      const updatedArticle = action.payload;

      const updatedArticles = state.articles.map((article) => {
        if (article.id === updatedArticle.id) {
          return updatedArticle;
        }
        return article;
      });

      return {
        ...state,
        articles: updatedArticles,
      };

    default: {
      throw new Error(`Unsupported action type: ${action.type}`);
    }
  }
};
export default appReducer;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment