Use Context API and functional component as service in React - reactjs

I have a context API:
import React, { createContext, useState } from "react";
const UserContext = createContext();
const UserContextProvider = (props) => {
const [userInfo, setUserInfo] = useState({});
return (
<UserContext.Provider value={{ userInfo, setUserInfo }}>
{props.children}
</UserContext.Provider>
)
};
export { UserContextProvider, UserContext };
and use it in App.js:
<UserContextProvider>
// Router,...
</UserContextProvider>
Well, I gonna use context API in component like a service:
import { UserContext } from "...";
function UserService() {
const { userInfo, setUserInfo } = useContext(UserContext);
const updateUserInfo = (newUserInfo) => {
setUserInfo(newUserInfo); // for example: {name:'x'}
}
return null;
}
Now I wanna use UserService inside a component without add <UserService /> ? How can I call UserService.updateUserInfo()?

Your don't need to userService.You can use UserContext directly inside App.js and access to its function but you must wrap App.js inside UserContextProvider like:
<UserContextProvider>
<App />
</UserContextProvider>
Or you can use HOC(higher order component) like:
const withUsersContext = (Comp) => {
return (props) => (
<UserContextProvider>
<Comp {...props} />
</UserContextProvider>
);
};
// then inside App.js:
...
export default withUsersContext(App)
Now inside App.js:
const { userInfo, setUserInfo } = useContext(UserContext);
Note: if you wanna use UserContext inside something like UserService you must write a custom hook for UserService(rules of react-hooks).
Custom hooks for UserService:
function useUserService() {
const { userInfo, setUserInfo } = useContext(UserContext);
const updateUserInfo = (newUserInfo) => {
setUserInfo(newUserInfo);
}
return { updateUserInfo };
}
How to use inside a component:
...
const { updateUserInfo }= useUserService();
...

Related

React Context for Array passing

I have the following app entry component:
React.useEffect(() => {
const fetchData = async () => {
try {
const libraries: unknown[] = await sendRequest('/libraries');
const softwareComponents: unknown[] = await sendRequest('/softwareComponents');
localStorage.setItem('libraries', JSON.stringify(arraySetup(libraries, 'libraries')));
localStorage.setItem('softwareComponents', JSON.stringify(arraySetup(softwareComponents, 'software-components')));
} catch (err) {
console.error(err);
}
};
isAuthenticated() && fetchData();
}, []);
I am fetching Arrays from two endpoints and then set the result in the Local Storage, so I can read from it in other components.
A child component is using the data like this:
const [data, setData] = React.useState<Array<any>>([]);
React.useEffect(() => {
const libraries = getLocalStorageItem('libraries');
const softwareComponents = getLocalStorageItem('softwareComponents');
const condition = libraries && softwareComponents;
if (condition) {
setData([...libraries, ...softwareComponents]);
}
}, []);
const getDataLength = (category: string) => {
return (data || []).filter((item: any) => item.category === category).length;
};
return (
<React.Fragment>
<OwcGrid item xs={12} s={4}>
<LibrariesCard numberOfElements={getDataLength('libraries')} /> // rendering here the length of the localStorage item.
</OwcGrid>
Goal/Challenge:
I want to use React.Context to remove local storage implementation but I am not sure how to keep it as simple as possible.
I only saw guides which implemented dispatch actions and so on but this seems already too complex because I only fetch the data and don't change it as I only render it.
Are there any tipps or guides how to start with this?
Possible implementation with context:
//context.tsx
import {
createContext,
ReactNode,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
export interface LibsAndComponentsInterface {
data: unknown[];
}
const LibsAndComponentsContext = createContext<
LibsAndComponentsInterface | undefined
>(undefined);
// Wrap your App component with this
export function LibsAndComponentsProvider({
children,
}: {
children: ReactNode;
}) {
const [libs, setLibs] = useState<unknown[]>([]);
const [components, setComponents] = useState<unknown[]>([]);
useEffect(() => {
const fetchData = async () => {
try {
const libraries: unknown[] = await sendRequest('/libraries');
const softwareComponents: unknown[] = await sendRequest(
'/softwareComponents'
);
setLibs(libraries);
setComponents(softwareComponents);
} catch (err) {
console.error(err);
}
};
isAuthenticated() && fetchData();
}, []);
const ctxValue = useMemo(
() => ({
data: [...libs, ...components],
}),
[libs, components]
);
return (
<LibsAndComponentsContext.Provider value={ctxValue}>
{children}
</LibsAndComponentsContext.Provider>
);
}
export function useLibsAndComponents() {
const ctx = useContext(LibsAndComponentsContext);
if (ctx == null) {
throw new Error(
'useLibsAndComponents must be inside LibsAndComponentsProvider'
);
}
return ctx;
}
// later in your components
const { data } = useLibsAndComponents()
Here is the complete setup for React Context. Please use typescript if needed.
MyContextProvider.js
const { createContext, useState } = require("react");
//Create a context
export const Mycontext = createContext();
//Created a component that helps to provide the context.
const MyContextProvider = ({ children }) => {
//Declare all the states that you need
const [libraries, setLibraries] = useState([]);
const [softwareComponents, setSoftwareComponents] = useState([]);
return (
<Mycontext.Provider
//provide all the state, function as value that you need in any child component
value={{
libraries,
setLibraries,
softwareComponents,
setSoftwareComponents
}}
>
{children}
</Mycontext.Provider>
);
};
export default MyContextProvider;
index.js
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import MyContextProvider from "./MyContextProvider";
import App from "./App";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
//Wrap App component By the MycontextProvider component
root.render(
<StrictMode>
<MyContextProvider>
<App />
</MyContextProvider>
</StrictMode>
);
App.js
import { useContext } from "react";
import ChildComponent from "./Child";
import { Mycontext } from "./MyContextProvider";
import "./styles.css";
export default function App() {
//This is the way of getting value from context here useContext is the builtin hook and Mycontext is the context name
const { setLibraries, setSoftwareComponents } = useContext(Mycontext);
//After getting data from API use setLibraries and setSoftwareComponents to store data in the state(context) instead of local storage.
return (
<div>
<ChildComponent />
</div>
);
}
Child.js
import { useContext } from "react";
import { Mycontext } from "./MyContextProvider";
const ChildComponent = () => {
const { libraries, softwareComponents } = useContext(Mycontext);
//Here is your state you can use it as your need.
console.log(libraries, softwareComponents);
return <h1>Child Component</h1>;
};
export default ChildComponent;

Context Api is returning undefined

Here I have declared a basic prototype for my context store and when I try to use this store it is returning undefined even though I double checked the syntax so someone can please help why its returning undefined:
Store:
import { createContext } from "react";
const AuthContext = createContext("Default");
export const AuthContextProvider = (props) => {
const loginHandler = () => {
console.log("Login Handler");
};
const logoutHandler = () => {
console.log("Logout Handler");
};
const contextValue = {
token: "false",
loging: loginHandler,
logout: logoutHandler,
};
return (
<AuthContext.Provider value={contextValue}>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContext;
App:
import { useContext } from "react";
import Main from "./components/Main";
import "./App.css";
import { Provider } from "react-redux";
import store from "./store/DataStore";
import AuthContext from "./store/AuthStore";
const App = () => {
const ctx = useContext(AuthContext);
return (
<Provider store={store}>
{console.log(ctx.token)}
<Main />
</Provider>
);
};
export default App;
1.) Your default value for the context is the string "Default" (https://reactjs.org/docs/context.html#reactcreatecontext)
2.) You are reading the context before using the provider you defined, so you are never setting the context value to your object. (see second paragraph of https://reactjs.org/docs/context.html#reactcreatecontext)
The result is you are reading the token property off the string "Default"...which would be undefined. Linked is an example using your provider and how it changes context in components further down the tree. https://playcode.io/980421
Wrap your App component by the AuthContextProvider component.
index.js
root.render(
<StrictMode>
<AuthContextProvider>
<App />
</AuthContextProvider>
</StrictMode>
);
App.js
import { useContext } from "react";
import AuthContext from "./store/authStore";
const App = () => {
const ctx = useContext(AuthContext);
return (
<div>
{console.log(ctx.token)}
<p>Hello</p>
{console.log(ctx)}
</div>
);
};
export default App;
authStore.js
import { createContext } from "react";
const AuthContext = createContext("Default");
export const AuthContextProvider = (props) => {
const loginHandler = () => {
console.log("Login Handler");
};
const logoutHandler = () => {
console.log("Logout Handler");
};
const contextValue = {
token: "false",
loging: loginHandler,
logout: logoutHandler,
};
return (
<AuthContext.Provider value={contextValue}>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContext;

Test component with context and react hook

I have a simple Dashboard component that relies on React context to manage auth. It contains a custom hook useAuth to extract the current user as well as the auth related functions: login, logout, etc.
This is the Context file: AuthContext.js:
import React, { createContext, useContext, useState, useEffect } from "react";
import { auth } from "../config/firebase";
const AuthContext = createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true);
function signup(email, password) {
return auth.createUserWithEmailAndPassword(email, password);
}
function login(email, password) {
return auth.signInWithEmailAndPassword(email, password);
}
function logout() {
return auth.signOut();
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
const value = {
currentUser,
signup,
login,
logout,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
This is the Dashboard.js component:
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
export default function Dashboard() {
const { currentUser, logout } = useAuth();
const [error, setError] = useState("");
const history = useHistory();
const handleLogout = async () => {
setError("");
try {
await logout();
history.push("/login");
} catch (e) {
setError(e.message);
}
};
return (
<div>
{error && <p>{error}</p>}
<h1>This is the Dashboard</h1>
<h5>Email: {currentUser.email}</h5>
<button onClick={handleLogout} type="button">
Logout
</button>
</div>
);
}
As recommened by React Testing Library I have created a test-utils.js file:
import React, { createContext } from "react";
import { render } from "#testing-library/react";
import { BrowserRouter as Router } from "react-router-dom";
const AuthContext = createContext();
const currentUser = {
email: "abc#abc.com",
};
const signup = jest.fn();
const login = jest.fn();
const logout = jest.fn();
const AllTheProviders = ({ children }) => {
return (
<Router>
<AuthContext.Provider value={{ currentUser, signup, login, logout }}>
{children}
</AuthContext.Provider>
</Router>
);
};
const customRender = (ui, options) => {
render(ui, { wrapper: AllTheProviders, ...options });
};
export * from "#testing-library/react";
export { customRender as render };
However, when running Dashboard.test.js I get error
TypeError: Cannot destructure property 'currentUser' of '((cov_5mwatn2cf(...).s[0]++) , (0 , _AuthContext.useAuth)(...))' as it is undefined.
4 |
5 | export default function Dashboard() {
> 6 | const { currentUser, logout } = useAuth();
| ^
7 | const [error, setError] = useState("");
8 | const history = useHistory();
import React from "react";
import Dashboard from "./Dashboard";
import { act, render, screen } from "../config/test-utils-dva";
beforeEach(async () => {
await act(async () => {
render(<Dashboard />);
});
});
test("displays dashboard", () => {
expect(screen.getByText(/dashboard/i)).toBeInTheDocument();
});
I think it is because Dashboard component is trying to use useAuth from AuthContext.js, how can I force the rendered Dashboard component to use the mocked data that I am sending in the test-utils.jsfile?
Instead of creating a new context, use the AuthContext from context/AuthContext for <AuthContext.Provider>, as that's the context that the hook uses.
So, in AuthContext.js, export the context instance:
export const AuthContext = createContext();
Then, in your test-util.js file, instead of again calling createContext (which will create a completely separate context instance - the contexts are not the same even if they are stored in a variable with the same name!), just import the previously exported instance:
import { AuthContext } from "../context/AuthContext";
const AllTheProviders = ({ children }) => {
return (
<Router>
<AuthContext.Provider value={{ currentUser, signup, login, logout }}>
{children}
</AuthContext.Provider>
</Router>
);
};

How to use context with hooks for authentication?

I'm trying to use context for handling pieces of authentication in my app. I was running into issues because I was trying to call useContext outside of my Context.Provider, so I moved the logic to a child component of the provider.
Now I'm getting an error message TypeError: Object is not iterable (cannot read property Symbol(Symbol.iterator)) where I'm calling useContext in the child component. Is the issue really with getting the values from the context or something else?
In app.js
import AuthContextProvider from "./components/context/authContext";
import RegisterRoutes from "./components/routing/registerRoutes";
function App() {
return (
<AuthContextProvider>
<Route
exact
path="/register"
render={(props) => (
<RegisterRoutes {...props} />
)}
/>
</AuthContextProvider>
)
}
In my authContext.js
import React, { useState, useEffect, createContext } from "react";
export const AuthContext = createContext();
const AuthContextProvider = (props) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const setAuth = (boolean) => {
setIsAuthenticated(boolean);
};
//Auth API logic here//
const apiOptions = {
url: "users/is-verified",
method: "GET",
headers: {
token: localStorage.token,
},
};
async function isAuth() {
axios(apiOptions)
.then((response) => {
const resData = response.data;
resData === true ? setIsAuthenticated(true) : setIsAuthenticated(false);
})
.catch((error) => {
console.log(error.response);
});
}
useEffect(() => {
isAuth();
}, []);
return (
<AuthContext.Provider
value={[isAuthenticated, setIsAuthenticated, setAuth]}
>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContextProvider;
In my registerRoutes.js
import React, { useContext } from "react";
import { Redirect } from "react-router-dom";
import Register from "../pages/register";
import AuthContext from "../context/authContext";
function RegisterRoutes(props) {
const [isAuthenticated, setAuth] = useContext(AuthContext);
return !isAuthenticated ? (
<Register {...props} setAuth={setAuth} />
) : (
<Redirect to="/login" />
);
}
export default RegisterRoutes;
As the error says, the Context.Provider in authContext.js value is not iterable:
<AuthContext.Provider value={[isAuthenticated, setIsAuthenticated, setAuth]}>
The value passed to the provider needs to be an iterable value, in this case, a valid JSON object, instead of the array that you have provided. so, we change it to:
<AuthContext.Provider value={{isAuthenticated, setIsAuthenticated, setAuth}}>
Then you change the reference in registerRoutes.js to correctly consume the new structure:
const [isAuthenticated, setAuth] = useContext(AuthContext);
becomes
const { isAuthenticated, setAuth } = useContext(AuthContext);
Voila! Your Context.Provider value is iterable and you can consume it in your application.
I think this will help you. My solution for accessing data in the context is creating a custom hook.
//localState.js
import { createContext, useState, useContext } from 'react'
const LocalStateContext = createContext()
const LocalStateProvider = LocalStateContext.Provider
function LocalState({children}) {
const [someState, setSomeState] = useState('')
const defaultValues = {
someState, setSomeState
}
return <LocalStateProvider value={defaultValues}>
{children}
</LocalStateProvider>
}
function useLocalState() {
const all = useContext(LocalStateContext)
return all
}
export {LocalState, LocalStateContext, useLocalState}
With this code you can wrap your whole app in the LocalState component and access context values by using the new useLocalState hook. For example
import { useLocalState} from './localstate'
const SomeComponent = () => {
const { someState } = useLocalState()
return (
///...whatever you want
)
}
export default SomeComponent
I think your issue may be that you have put your default values in an array inside of the value object.

Consuming a context within another context in React

I currently have 2 contexts within my React app and I was trying to call a method from my top-level context within my 2nd context.
Here is how the context are nested:
App.js
function App(props) {
return (
<SessionContextProvider>
<APIContextProvider>
// I have some components here
</APIContextProvider>
</SessionContextProviders>
)
}
is there a way to consume the SessionContext within my APIContextProvider?
import { SessionContext } from 'contexts/session'
export const APIContext = createContext();
export default class APIContextProvider extends Component {
static contextType = SessionContext
randomMethod() {
this.context.logoutUser()
}
render() {
return (
<APIContext.Provider value={{randomMethod: this.randomMethod}}>
{this.props.children}
</APIContext.Provider>
)
}
}
The issue is that when running randomMethod within my APIContext doesn't work because this.context is undefined.
Is this feasible or am I missing something?
I created an example for you, where ApiProvider uses logoutUser from SessionContext and providing randomMethod, which calls the function logoutUser.
import React, { createContext } from "react";
const SessionContext = createContext();
const SessionProvider = props => {
const logoutUser = () => {
alert("Logout user, but fast!");
};
return (
<SessionContext.Provider value={logoutUser}>
{props.children}
</SessionContext.Provider>
);
};
export { SessionContext as default, SessionProvider };
Inner context
import React, { createContext, useContext } from "react";
import SessionContext from "./SessionContext";
const ApiContext = createContext();
const ApiProvider = props => {
const logoutUser = useContext(SessionContext);
const randomMethod = () => {
logoutUser();
};
return (
<ApiContext.Provider value={{ randomMethod: randomMethod }}>
{props.children}
</ApiContext.Provider>
);
};
export { ApiContext as default, ApiProvider };
App.js
export default function App() {
return (
<SessionProvider>
<ApiProvider>
<TestComponent />
</ApiProvider>
</SessionProvider>
);
}
https://codesandbox.io/s/late-bush-959st

Resources