❄ 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
✔ createContext(defaultValue);
- creates a context object
- every context has Context.Provider, Context.Consumer, Context.displayName
- then this created context object will read value from the closest matching provider
- the defaultValue argument is only used when we does not have provider
- we can access default value without using provider but its not recommanded
- defaultValue should be an object(preferable)
✔ <Context.Provider value={{}}></Context.Provider>
- provider provides value inside context
- creating custom provider component is the best practice
- we can access context defaultValue without using provider
- passing undefined as a provider value doesn't cause consuming components to use defaultValue
- direct passing value causes re-rendering issue, best practice is using useState or useReducer
- if provider values changes, children also re-render
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}`);
}
}
}
- 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>;
-
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
// ✔ 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>;
};
# with npm
npm install firebase
# with yarn
yarn add firebase
- 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;
❄ 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,
};
// 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;
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;
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;
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;
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;
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;
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;
❄ Example
- 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;