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

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.

Related

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.

After useEffect API call, state set by useState for json data being passed to a component as props returns empty array

I'm still a beginner in React and I'm trying to use useEffect to fetch data from an API and then useState to set the state and then pass that state as props to a child component.
But in my child component, it appears as an empty array each time when I do console.log. I understand that on the first render the state of my initial state is an empty array []. But I've been trying to combat this and send the right JSON data but can't seem to do so.
I am trying to do this as I have multiple child components that I wanna send data to.
Below is a workaround I coded up with some digging around but doesn't work either:
const api = 'url string'
const [races, setRaces] = useState([]);
const [races2, setRaces2] = useState([races]);
useEffect(() => {
fetch(api)
.then((resp) => resp.json())
.then((response) => setRaces(response));
}, []);
useEffect(() => {
if (races.length) setRaces2(races);
}, [races]);
<Child data={races2}
But this does not seem work to work either when I do console.log(props.data) in the child component.
This is how normally one would fetch data and try and send the data but in both cases, it's been the same.
const api = 'url string'
const [races, setRaces] = useState([]);
useEffect(() => {
fetch(api)
.then((resp) => resp.json())
.then((response) => setRaces(response));
}, []);
<Child data={races}
Following is a rough flow diagram explaining what I wanna do:
Thank you for your help in advance.
I made this quick example.
Here is what the code does:
Fetching the Data using UseEffect
Storing into State
Passing the State into Component as Props
Fetching the Props and Displaying the data.
Code for App.js
import "./styles.css";
import ChildComponent from "./ChildComponent";
import { useEffect, useState } from "react";
export default function App() {
const [title, setTitle] = useState(null);
// * Init on Page Load
useEffect(() => {
fetchTitle();
}, []);
const fetchTitle = async () => {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts/1"
);
const data = await response.json();
setTitle(data.title); //Setting the response into state
};
return (
<div className="App">
<ChildComponent data={title} />
</div>
);
}
Code for ChildComponent.js
export default function ChildComponent({ data }) {
return <div>{data}</div>;
}
I created this Codesandbox. This might help.
https://codesandbox.io/s/elegant-lumiere-cg66nt
Array and object are referential data types, passing as array dependency will not re-run side effect. useEffect dependencies should be primitive data type (string, number, boolean,undefined or null).
useEffect(() => {
if (races.length) setRaces2(races);
}, [races.length])// Dependencies must be primitive data type not referencial.

How to get data from the route url to put inside the callback function in React router?

I'm working on my search feature. I want to trigger a callback function in the route to fetch all data before it goes into the search component.
Like this:
<Route path="/search/:query" component={QuestionSearchContainer} onChange={()=>store.dispatch(fetchData(query?)) }/>
here is the QuestionSearchContainer:
const mapStateToProps = (state,ownProps) => {
return {
questions: Object.values(state.entities.questions),
currentUser: state.entities.users[state.session.id],
query: ownProps.match.params.query,
url: ownProps.match.url
}}
But how could I get the query keyword in the search url to put inside my fetchData as a parameter? I want to fetch the data and save it to the redux store before going to the QuestionSearchContainer so that I can get all data for questions in the container.
If you don't want to do the data fetching withing your QuestionSearchContainer component, you can make a higher-order component to wrap it with that does your data fetching for you.
You can easily modify this HOC to only return the Wrapped component when the data finishes loading as well. The loading part of this is assuming that fetchData is a redux thunk action creator . useParams is a hook exported from react-router-dom that gives you access to the match params. useDispatch is a hook exported from react-redux that gives you access to your store's dispatch function.
import { useParams } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { useEffect, useState } from 'react';
const withFetchData = (Component) => ({ children, ...props }) => {
const { query } = useParams();
const [loading, setLoading] = useState(true);
const dispatch = useDispatch();
useEffect(() => {
// Assuming fetchData is a a redux thunk action creator
setLoading(true);
dispatch(fetchData(query)).then(() => {
setLoading(false);
});
}, [query]);
if(loading){
return 'loading...'
}
return <Component {...props} />;
};
const QuestionSearchContainerWithFetchData = withFetchData(
QuestionSearchContainer
);
const Parent = () => {
return (
<Route
path="/search/:query"
component={QuestionSearchContainerWithFetchData}
/>
);
};
Another option is to create a special route that does what you desire. For instance, this OnChangeRoute function would call the callback onChangeParams with the current params every time the params change. In this one, there's a loading prop that you have to pass in as the component itself doesn't care about what you are doing with the params.
import { useEffect, useRef } from "react";
function InnerOnChangeRoute({ loading, onParamsChange, Component, ...rest }) {
const onChangeRef = useRef(onParamsChange);
useEffect(()=>{
onChangeRef.current=onParamsChange;
},[onParamsChange])
useEffect(() => {
onChangeRef.current(rest.match.params);
}, [rest.match.params]);
if(loading){
return 'loading....'
}
return <Component {...rest} />;
}
// A wrapper for <Route> that redirects to the login
// screen if you're not yet authenticated.
function OnChangeRoute({ Component, onParamsChange, loading, ...rest }) {
return (
<Route
{...rest}
render={(data) => (
<InnerOnChangeRoute
Component={Component}
onParamsChange={onParamsChange}
loading={loading}
{...data}
/>
)}
/>
);
}
In general for redux, you have to use dispatch (or mapDispatchToProps in the connector HOC) to run an action that updates the store with your data.
Here are some links that will hopefully help you get redux more under control.
https://redux.js.org/advanced/async-actions
https://redux-toolkit.js.org/usage/usage-guide#asynchronous-logic-and-data-fetching
https://github.com/reduxjs/redux-thunk
Firstly, Route doesn't have an onChange handler. (onEnter was available in previous versions(3 & earlier) of react-router-dom
Since your requirement seems to be specific to a single component(QuestionSearchContainer), using custom hooks or hocs may not be an ideal solution.
You can simply use a useEffect and listen to url changes(query). You can get the query using props.match.params and pass it as an argument to your dispatch callback.
Just make sure to maintain a loading state in redux and render a fallback while data is being fetched.
code snippet
const QuestionSearchContainer = props => {
...
useEffect(() => {
const {query} = props.match.params
console.log(query);
store.dispatch(fetchData(query))
}, [query]);
...
return <div>
{!props.isLoading && <div>My actual question search component with data !</div>}
</div>;
};
export default QuestionSearchContainer;

How can I re-fetch an API using react hooks

devs,
I have decided to finally learn react hooks with what I thought would be a simple project. I can't quite figure out how I re-fetch an API using react hooks. Here is the code I have so far.
import React, { useState, useEffect } from "react"
import useFetch from "./utils/getKanya"
const kanye = "https://api.kanye.rest"
const Index = () => {
let [kanyaQuote, setKanyeQuote] = useState(null)
let data = useFetch(kanye)
const getMore = () => {
setKanyeQuote(useFetch(kanye))
}
return (
<>
<h1>Welcome to Next.js!</h1>
<p>Here is a random Kanye West quote:</p>
{!data ? <div>Loading...</div> : <p>{!kanyaQuote ? data : kanyaQuote}</p>}
<button onClick={getMore}>Get new quote</button>
</>
)
}
export default Index
I get the kanyeQuote state value to null
I fetch the initial data
I either show "Loading..." or the initial quote
I am trying to set up a button to re-fetch the API and store the data in kanyeQuote via getKanyeQuote (setState)
This is the error I get Error: Invalid hook call...
I would greatly appreciate any guidance you can provide on this.
The issue here is, that you can only use hooks directly inside the root of your component.
It's the number 1 'rule of hooks'. You can read more about that here
const getMore = () => {
setKanyeQuote(useFetch(kanye) /* This cannot work! */)
}
There are a few ways you could work around that. Without knowing the internal logic in your useFetch-hook I can only assume you are able to change it.
Change hook to handle its state internally
One way to work around that would be to change the logic of your custom useFetch hook to provide some form of function that fetches the data and updates the state internally. It could then look something like this:
const { data, doFetch } = useFetch(kanye);
useEffect(() => {
doFetch(); // initialFetch
}, []);
const getMore = () => {
doFetch();
};
// ...
You would then need to change the internal logic of your useFetch-hook to use useState internally and expose the getter of it. It would look something like this:
export const useFetch = (url) => {
const [data, setData] = useState(null);
const doFetch = () => {
// Do your fetch-Logic
setData(result);
};
return { data, doFetch };
};
Change hook not to handle any state at all.
If you only want to manage the state of the loaded data in the parent component, you could just provide the wrapped fetch function through the hook; Something like that:
const doFetch = useFetch(kanye);
const [data, setData] = useState(null);
useEffect(() => {
setData(doFetch()); // initialFetch
}, []);
const getMore = () => {
setData(doFetch())
};
// ...
You would then need to change the internal logic of your useFetch-hook to not have any internal state and just expose the wrapped fetch. It would look something like this:
export const useFetch = (url) => {
const doFetch = () => {
// Do your fetch-Logic
return result;
};
return doFetch;
};

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