How to access data from Context Provider using useContext? - reactjs

How to access data from Context Provider using useContext
when whole state is passed inside of value
Suppose my state is like this
const state = {
isAuthenticated: false,
Key: 12345,
data: "Hi"
};
I have passed this state inside of context api by provider
<AuthContext.Provider
value = {{state , dispatch}}
>
</AuthContext.Provider>
Now i am trying to access it in another component by this way but it throws error
const { {state.key: auth},{state.data : data} } = useContext(AuthContext)
Now can i use auth and data anywhere inside of jsx
As i want to access the key and data from context api

Your other component need to be wrapped in Context Provider.
Docs: https://pt-br.reactjs.org/docs/hooks-reference.html#usecontext

In order for you to use React's useContexthook you need to wrap the components with the context's provider. Read more about Context API.
Here is an example with your case:
import React from "react";
const AuthContext = React.createContext();
function AuthProvider(props) {
const [state] = React.useState({
isAuthenticated: false,
key: 12345,
data: "Hi",
});
// Any function has to be wrapped in a React.useCallback
// to avoid re-calculation in any dependency array
const dispatch = React.useCallback(() => {}, []); // Redux or useReducer dispatch
// We have to wrap our values in React.useMemo to avoid any unnecessary re-renders
const values = React.useMemo(
() => ({
state,
dispatch,
}),
[dispatch, state]
);
return <AuthContext.Provider value={values} {...props} />;
}
function useAuth() {
const context = React.useContext(AuthContext);
if (!context) {
throw new Error(
"To utilize `useAuth`, component must be wrapped in `AuthProvider`"
);
}
return context;
}
function Component() {
const {
state: { data, key },
} = useAuth();
// ...
}
function App() {
return (
// Everything wrapped in AuthProvider will have access to its values
<AuthProvider>
<Component />
</AuthProvider>
);
}

Related

React Testing Library - how to correctly test that my provider is being populated with data and the child components are displaying properly?

I have a pretty simple use case - I have a global app context where I'm trying to store data fetched from an endpoint. My goal is to load this data into the context on app load and I'm going about it using the useReducer hook. I settled on the solution of calling an action getIssuerDetails() that dispatches various states throughout the method and invokes the issuerApi service to actually call the API (it's a simple Axios GET wrapper). This action is called from a useEffect within the Provider and is called on mount as shown below.
I'm having some trouble wrapping my head around how to properly test that 1) my AppProvider actually gets populated with the data fetched within the useEffect and 2) my child components within my AppProvider are being populated correctly with the data passed down from the provider. Am I approaching this data fetching portion correctly? I can either make the actual API call within my App component on mount and then dispatch actions to update the global state OR I keep my solution of fetching my data from within the useEffect of the provider.
I know I'm not supposed to be testing implementation details but I'm having a hard time separating out what data/methods I should mock and which ones I should allow to execute on their own. Any help would be greatly appreciated.
AppContext.tsx
import { createContext, FC, useEffect, useContext, useReducer, useRef } from 'react';
import { getIssuerDetails } from './issuer/actions';
import { appStateReducer } from './global/reducer';
import { combineReducers } from '#utils/utils';
import { GlobalAppStateType } from './global/types';
/**
* Our initial global app state. It just stores a bunch
* of defaults before the data is populated.
*/
export const defaultInitialState = {
issuerDetails: {
loading: false,
error: null,
data: {
issuerId: -1,
issuerName: '',
ipoDate: '',
ticker: '',
},
},
};
export type AppStateContextProps = {
state: GlobalAppStateType;
};
export type AppDispatchContextProps = {
dispatch: React.Dispatch<any>;
};
export const AppStateContext = createContext<AppStateContextProps>({
state: defaultInitialState,
});
export const AppDispatchContext = createContext<AppDispatchContextProps>({
dispatch: () => null,
});
/**
*
* #param
* #returns
*/
export const mainReducer = combineReducers({
appState: appStateReducer,
});
export type AppProviderProps = {
mockInitialState?: GlobalAppStateType;
mockDispatch?: React.Dispatch<any>;
};
/**
* Our main application provider that wraps our whole app
* #param mockInitialState - mainly used when testing if we want to alter the data stored in our
* context initially
* #param children - The child components that will gain access to the app state and dispatch values
*/
export const AppProvider: FC<AppProviderProps> = ({ mockInitialState, mockDispatch, children }) => {
const [state, dispatch] = useReducer(mainReducer, mockInitialState ? mockInitialState : defaultInitialState);
const nState = mockInitialState ? mockInitialState : state;
const nDispatch = mockDispatch ? mockDispatch : dispatch;
// Ref that acts as a flag to aid in cleaning up our async data calls
const isCanceled = useRef(false);
useEffect(() => {
async function fetchData() {
// Await the API request to get issuer details
if (!isCanceled.current) {
await getIssuerDetails(nDispatch);
}
}
fetchData();
return () => {
isCanceled.current = true;
};
}, [nDispatch]);
return (
<AppStateContext.Provider value={{ state: nState }}>
<AppDispatchContext.Provider value={{ dispatch: nDispatch }}>{children}</AppDispatchContext.Provider>
</AppStateContext.Provider>
);
};
/**
* Custom hook that gives us access to the global
* app state
*/
export const useAppState = () => {
const appStateContext = useContext(AppStateContext);
if (appStateContext === undefined) {
throw new Error('useAppState must be used within a AppProvider');
}
return appStateContext;
};
/**
* Custom hook that gives us access to the global
* app dispatch method to be able to update our global state
*/
export const useAppDispatch = () => {
const appDispatchContext = useContext(AppDispatchContext);
if (appDispatchContext === undefined) {
throw new Error('useAppDispatch must be used within a AppProvider');
}
return appDispatchContext;
};
AppReducer.ts
Note: Code still needs to be cleaned up here but it's functioning at the moment.
import * as T from '#context/global/types';
export const appStateReducer = (state: T.GlobalAppStateType, action: T.GLOBAL_ACTION_TYPES) => {
let stateCopy;
switch (action.type) {
case T.REQUEST_ISSUER_DETAILS:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = true;
return stateCopy;
case T.GET_ISSUER_DETAILS_SUCCESS:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = false;
stateCopy.issuerDetails.data = action.payload;
return stateCopy;
case T.GET_ISSUER_DETAILS_FAILURE:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = false;
stateCopy.issuerDetails.error = action.payload;
return stateCopy;
default:
return state;
}
};
getIssuerDetails()
export const getIssuerDetails = async (dispatch: React.Dispatch<any>) => {
dispatch({ type: GlobalState.REQUEST_ISSUER_DETAILS, payload: null });
try {
// Fetch the issuer details
const response = await issuerApi.getIssuerDetails(TEST_ISSUER_ID);
if (response) {
/***************************************************************
* React Testing Library gives me an error on the line below:
* An update to AppProvider inside a test was not wrapped in act(...)
***************************************************************/
dispatch({ type: GlobalState.GET_ISSUER_DETAILS_SUCCESS, payload: response });
return response;
}
// No response
dispatch({
type: GlobalState.GET_ISSUER_DETAILS_FAILURE,
error: { message: 'Could not fetch issuer details.' },
});
} catch (error) {
dispatch({ type: GlobalState.GET_ISSUER_DETAILS_FAILURE, error });
}
};
dashboard.test.tsx
import { render, screen, cleanup, act } from '#testing-library/react';
import { AppProvider, AppStateContext } from '#context/appContext';
import { GlobalAppStateType } from '#context/global/types';
afterEach(() => {
cleanup();
jest.clearAllMocks();
});
describe('Dashboard page', () => {
it('should render the page correctly', async () => {
act(() => {
render(
<AppProvider>
<Dashboard />
</AppProvider>
);
});
expect(await screen.findByRole('heading', { level: 1 })).toHaveTextContent('Stock Transfer');
});
});
I won't dive into the code specifically since there is too much you want to test all at once.
From what I could gather, you are trying to do an Integration Test and not a Unitary Test anymore. No problem there, you just need to define where you want to draw the line. For me, it's pretty clear that the line lies in the issuerApi.getIssuerDetails call, from which you could easily mock to manipulate the data how you want.
1) my AppProvider actually gets populated with the data fetched within the useEffect and 2) my child components within my AppProvider are being populated correctly with the data passed down from the provider.
Well, I would advise you to make a simple mock component that uses the hook and displays the data after fetching. You could make a simple assertion for that, no need for an actual component (<Dashboard />).
Am I approaching this data fetching portion correctly?
It all depends on how you want to structure it but ideally the AppProvider should be thin and lay those data fetching and treatments inside a service just for that. This would provide a better way to unit test the components and smoother code maintenance.

Can't stop React from updating the main component after state change

I'm having the following issue.
I have a component called "BackgroundService" who has a setInterval for requesting data from an API every 5 seconds. The received data from API is stored in "backgroundServiceResult" hook with useState, located in App and shared by a context provider.
_app.js:
const App = ({ Component, pageProps }) => {
const [backgroundServiceResult, setBackgroundServiceResult] = useState([false]);
console.log("App reloaded")
return (
<AppContext.Provider value={{ backgroundServiceResult, setBackgroundServiceResult }}>
<BackgroundService/>
<Component {...pageProps} />
</AppContext.Provider>
)
}
BackgroundService.js:
import { useState, useEffect, useContext } from "react"
import AppContext from '#/hooks/AppContext'
export const BackgroundService = () => {
const { getLatestSyncInfo } = api()
const { isDBSet, getJson } = OfflineStorage()
const appContext = useContext(AppContext);
const [alreadyNotified, setalreadyNotified] = useState(false)
useEffect(async () => {
const intervalId = setInterval(async () => {
// REQUIRE DATA FROM API STUFF, AND CAll:
appContext.setBackgroundServiceResult(data or stuff);
}, 5000)
return () => clearInterval(intervalId);
}, [])
return (
<></>
)
}
The problem is, every time the appContext.setBackgroundServiceResult is called from BackgroundService.js, the entire App component is re-rendered! so the "console log" in App is called, and all the components mounted again.
How can I store the received data from API through all my application without rendering again all from App?
Any way for solving this?
Thanks you
Your application is following expected behaviour, when state or props update the component will re-render.
There are many options you could use to prevent this from negatively affecting parts of your application.
useEffect could be used to only run code in child components when the component is initially mounted or when specific props or state change.
useMemo could be used to only recalculate values upon specific props or state change.
useCallback could be used to only recreate a function when specific props or state change.
In your specific case here it doesn't make sense to create the BackgroundService if it isn't going to render anything. Instead you should be creating a hook like this:
import { useState, useEffect, useContext } from "react"
import AppContext from '#/hooks/AppContext'
export const useBackgroundService = () => {
const appContext = useContext(AppContext);
// Also bear in mind that the `useEffect` callback cannot be `async`
useEffect(() => {
// the `async` over here is fine though
const intervalId = setInterval(async () => {
appContext.setBackgroundServiceResult(data or stuff);
}, 5000)
return () => clearInterval(intervalId);
}, [])
}
And then call it in your app as follows:
const App = ({ Component, pageProps }) => {
const [backgroundServiceResult, setBackgroundServiceResult] = useState([false]);
useBackgroundService();
return (
<AppContext.Provider value={{ backgroundServiceResult, setBackgroundServiceResult }}>
<Component {...pageProps} />
</AppContext.Provider>
)
}
Don't worry about the console.log going off, it won't negatively affect your application. If you had to do something like sort a massive list at the top level of your app component you could do something like this:
const App = ({ Component, pageProps }) => {
const [backgroundServiceResult, setBackgroundServiceResult] = useState([false]);
useBackgroundService();
const sortedList = useMemo(() => pageProps.myList.sort(), [pageProps.myList]);
return (
<AppContext.Provider value={{ backgroundServiceResult, setBackgroundServiceResult }}>
<Component {...pageProps} />
</AppContext.Provider>
)
}
Then the sortedList value would only update when it needs to and your updated backgroundServiceResult wouldn't cause that value to be recalculated.
In the same way you could make use of useEffect in the children components to make sure code only runs on initial mount and not on the components being re-rendered.
If you update your question to be more specific about what problems your App being rendered are causing we could come up with a better solution to tackle that specific issue.

React Hook useEffect dependency issue

I'm getting a warning message on my app and I've tried lots of things to remove it, without success. Error Message:
React Hook useEffect has a missing dependency: 'updateUserData'.
Either include it or remove the dependency array
react-hooks/exhaustive-deps
I don't want to exclude that with a comment to avoid the issue, but I want to fix it in a "best practices" way.
I want to call that updater function and get my component updated, so I can share that context in other components.
So... what i'm doing wrong? (any code review about the rest is very welcomed!)
Thanks a million!
If I add [] as the 2nd parameter of useEffect I get the warning, and removing it I get an inifinite loop.
Also adding [updateuserData] gets an infinite loop.
import React, { useState } from "react";
import UserContext from "./UserContext";
interface iProps {
children: React.ReactNode
}
const UserProvider: React.FC<iProps> = (props) => {
// practice details
const [userState, setUserState] = useState({
id'',
name: ''
});
// practice skills
const [userProfileState, setuserProfileState] = useState([]);
// user selection
const [userQuestionsState, setuserQuestionsState] = useState({});
return (
<UserContext.Provider value={{
data: {
user: userState,
userProfile: userProfileState,
questions: userQuestionsState
},
updateuserData: (id : string) => {
// call 3 services with axios in parallel
// update state of the 3 hooks
}
}}
>
{props.children}
</UserContext.Provider>
);
};
export default UserProvider;
const UserPage: React.FC<ComponentProps> = (props) => {
const {data : {user, profile, questions}, updateUserData}: any = useContext(UserContext);
useEffect(() => {
// update information
updateUserData("abcId")
}, []);
return <div>...</div>
}
The idea is the following:
I have a context
I created provider for that content
that context exposes the data and an updater function
I use that provider in a component with the useEffect hook and I get the warning
I want to keep all the logic about fetching and updating the context inside the provider, so I don't replicate it all over the other components that are needing it.
Firstly, the infinite loop is caused by the fact that your context is updating, which is causing your component to be re-rendered, which is updating your context, which is causing your component to be re-rendered. Adding the dependency should prevent this loop, but in your case it isn't because when your context updates, a brand new updateuserData is being provided, so the ref equality check detects a change and triggers an update when you don't want it to.
One solution would be to change how you create updateUserState in your UserProvider, using e.g. useCallback to pass the same function unless one of the dependencies changes:
const UserProvider: React.FC<iProps> = (props) => {
// practice details
const [userState, setUserState] = useState({
id'',
name: ''
});
// practice skills
const [userProfileState, setuserProfileState] = useState([]);
// user selection
const [userQuestionsState, setuserQuestionsState] = useState({});
const updateuserData = useCallback(id=>{
// call your services
}, [userState, userProfileState, userQuestionsState])
return (
<UserContext.Provider value={{
data: {
user: userState,
userProfile: userProfileState,
questions: userQuestionsState
},
updateuserData
}}
>
{props.children}
</UserContext.Provider>
);
};

Update React Context using a REST Api call in a functional component

I am trying to update the context of a React App using data resulted from an API call to a REST API in the back end. The problem is that I can't synchronize the function.
I've tried this solution suggested in this blog post https://medium.com/#__davidflanagan/react-hooks-context-state-and-effects-aa899d8c8014 but it doesn't work for my case.
Here is the code for the textContext.js
import React, {useEffect, useState} from "react";
import axios from "axios";
var text = "Test";
fetch(process.env.REACT_APP_TEXT_API)
.then(res => res.json())
.then(json => {
text = json;
})
const TextContext = React.createContext(text);
export const TextProvider = TextContext.Provider;
export const TextConsumer = TextContext.Consumer;
export default TextContext
And this is the functional component where I try to access the data from the context
import TextProvider, {callTextApi} from "../../../../services/textService/textContext";
function Profile()
{
const text = useContext(TextProvider);
console.log(text);
const useStyles = makeStyles(theme => ({
margin: {
margin: theme.spacing(1)
}
}));
I can see the fetch request getting the data in the network section of the browser console but the context is not getting updated.
I've tried doing this in the textContext.js.
export async function callTextApi () {
await fetch(process.env.REACT_APP_TEXT_API)
.then(res => res.json())
.then(json => {
return json;
})
}
And I was trying to get the data in the Profile.js using the useEffect function as so
const [text, setText] = useState(null);
useEffect(()=> {
setText (callTextApi())
},[])
It's my first time using React.context and it is pretty confusing. What am I doing wrong or missing?
You have a lot of problems here. fetching and changing should happen inside Provider by modifying the value property. useContext receives an entire Context object not only the Provider. Check the following
//Context.js
export const context = React.createContext()
Now inside your Provider
import { context } from './Context'
const MyProvider = ({children}) =>{
const [data, setData] = useState(null)
useEffect(() =>{
fetchData().then(res => setData(res.data))
},[])
const { Provider } = context
return(
<Provider value={data}>
{children}
</Provider>
)
}
Now you have a Provider that fetches some data and pass it down inside value prop. To consume it from inside a functional component use useContext like this
import { context } from './Context'
const Component = () =>{
const data = useContext(context)
return <SomeJSX />
}
Remember that Component must be under MyProvider
UPDATE
What is { children }?
Everything that goes inside a Component declaration is mapped to props.children.
const App = () =>{
return(
<Button>
Title
</Button>
)
}
const Button = props =>{
const { children } = props
return(
<button className='fancy-button'>
{ children /* Title */}
</button>
)
}
Declaring it like ({ children }) it's just a shortcut to const { children } = props. I'm using children so that you can use your Provider like this
<MyProvider>
<RestOfMyApp />
</MyProvider>
Here children is RestOfMyApp
How do I access the value of the Provider inside the Profile.js?
Using createContext. Let's assume the value property of your Provider is {foo: 'bar'}
const Component = () =>{
const content = useContext(context)
console.log(content) //{ foo : 'bar' }
}
How can you double declare a constant as you've done in the Provider?
That was a typo, I've changed to MyProvider
To access it from inside a class based component
class Component extends React.Component{
render(){
const { Consumer } = context
return(
<Consumer>
{
context => console.log(contxt) // { foo: 'bar' }
}
</Consumer>
)
}
}
First thing that I am seeing is that you are not returning the promise within your function which will lead to setting the state to undefined.
I added the return statement below:
export async function callTextApi () {
return await fetch(process.env.REACT_APP_TEXT_API)
.then(res => res.json())
.then(json => {
return json;
})
}
Also your last then-chain could be cleaned up a bit and I am quite sure you can remove the await statement in an async function when returning a promise. It will automatically be awaited:
export async function callTextApi () {
return fetch(process.env.REACT_APP_TEXT_API)
.then(res => res.json())
.then(json => json)
}
Second step would be to have a look at your useEffect hook. You want to setText after the promise from the api call has been resolved. So you have to make the callback function of useEffect asynchronous as well.
useEffect(async ()=> {
const newText = await callTextApi();
setText (newText);
},[])
Third step, would be to look at how to properly use the context api and the useContext hook. The useContext hook takes a context as a parameter but you passed the ContextProvider as the argument.
const text = useContext(TextContext);
The context and the context-provider are two different entities in the React world. Think of the context as state and functionality that you want to share across your application (like a global state), and think about the provider as a react component that manages one context and offers this context state to it's child components.
return(
<TextContext.Provider value={/* some value */}>
{children}
</TextContext.Provider>);
This is how a return statement of a provider component would look like and I think this code is currently missing in your application.

Best way to fetch data from a REST api using react hooks and context for state management?

I am trying out state management with react hooks and the context API. I have implemented a reducer pattern following some code from a todo app, but now I want to starting fetching data regularly from an API (e.g. implementing an infinite scroll), and I'm not sure now where the best place in the code is to make these async-REST-api calls.
I'm used to using a redux middleware library like redux-observable, redux-thunk, etc. for asynchronous tasks. But now that I'm not using redux, it's not clear to me what the best way is to do async updates. I suppose I could use await-promise reducers, but that doesn't feel right.
Any suggestions? (Having implemented a reducer pattern, I'm tempted to just fall back to a full redux-with-redux-obnservable implementation, though I was hoping context would slim down all that boilerplate.)
This is probably how I would implement it. I have a standard reducer. I will also create a helper functional component to help me set up the value for my context provider.
I also made some comments in the source code. I hope the following code snippet is simple enough to follow.
import React, { useReducer, useEffect, createContext } from 'react';
import FetchService from './util/FetchService'; // some helper functions
const OrderInfoContext = createContext();
const reducer = (state, action) => {
switch (action.type) {
case 'init':
return {};
case 'changeData':
return action.payload;
default:
return state;
}
};
const changeData = data => ({
type: 'changeData',
payload: data
});
/**
* This is a helper component that generate the Provider wrapper
*/
function OrderInfoProvider(props) {
// We will persist API payload in the state so we can assign it to the Context
const [orders, dispatch] = useReducer(reducer, {});
// We use useEffect to make API calls.
useEffect(() => {
async function fetchData() {
/**
* This is just a helper to fetch data from endpoints. It can be done using
* axios or similar libraries
*/
const orders = await FetchService
.get('/api/orders');
dispatch(changeData(orders))
}
fetchData();
}, []);
/**
* we create a global object that is available to every child components
*/
return <OrderInfoContext.Provider value={[orders, dispatch]} {...props} />;
}
// Helper function to get Context
function useOrderInfo() {
const context = useContext(OrderInfoContext);
if (!context) {
throw new Error('useOrderInfo must be used within a OrderInfoProvider');
}
return context;
}
export { OrderInfoProvider, useOrderInfo , changeData };
Here is an example that uses context and useReducer hook to set an app state and a context provider for state and dispatch.
The container uses useContext to get the state and the dispatch function, useEffect to do side effects like you'd use thunk, saga or middleware if you were using redux, useMemo to map state to props and useCallback to map each auto dispatched action to props (I assume you are familiar with react redux connect.
import React, {
useEffect,
useContext,
useReducer,
useCallback,
useMemo,
} from 'react';
//store provider
const Store = React.createContext();
const initStoreProvider = (rootReducer, initialState) => ({
children,
}) => {
const [state, dispatch] = useReducer(
rootReducer,
initialState
);
return (
<Store.Provider value={{ state, dispatch }}>
{children}
</Store.Provider>
);
};
//container for component
const ComponentContainer = ({ id }) => {
const { state, dispatch } = useContext(Store);
const num = state.find((n, index) => index === id);
//side effects, asynchonously add another one if num%5===0
//this is your redux thunk
const addAsync = num % 5 === 0;
useEffect(() => {
if (addAsync)
Promise.resolve().then(dispatch({ type: 'add', id }));
}, [addAsync, dispatch, id]);
//use callback so function does not needlessly change and would
//trigger render in Component. This is mapDispatch but only for
//one function, if you have more than one then use
//useCallback for each one
const add = useCallback(
() => dispatch({ type: 'add', id }),
[dispatch, id]
);
//This is your memoized mapStateToProps
const props = useMemo(() => ({ counter: num, id }), [
num,
id,
]);
return (
<Component add={add} doNothing={dispatch} {...props} />
);
};
//use React.memo(Component) to avoid unnecessary renders
const Component = React.memo(
({ id, add, doNothing, counter }) =>
console.log('render in component', id) || (
<div>
<button onClick={add}>{counter}</button>
<button onClick={doNothing}>do nothing</button>
</div>
)
);
//initialize the store provider with root reducer and initial state
const StoreProvider = initStoreProvider(
(state, action) =>
action.type === 'add'
? state.map((n, index) =>
index === action.id ? n + 1 : n
)
: state,
[1, 8]
);
//using the store provider
export default () => (
<StoreProvider>
<ComponentContainer id={0} />
<ComponentContainer id={1} />
</StoreProvider>
);
Example is here
https://resthooks.io/ uses the flux pattern just like you want, which allows things like middlwares, debuggability, etc. However, instead of having to write thousands of lines of state management, you just need a simple declarative data definition.
const getTodo = new RestEndpoint({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
});
function TodoDetail({ id }: { id: number }) {
const todo = useSuspense(getTodo, { id });
return <div>{todo.title}</div>;
}

Resources