React context provider not setting value on page load/refresh - reactjs

I have the following hook for Pusher and I use it to share one instance across the application.
import React, { useContext, useEffect, useRef } from "react";
import Pusher from "pusher-js";
const PusherContext = React.createContext<Pusher | undefined>(undefined);
export const usePusher = () => useContext(PusherContext)
export const PusherProvider: React.FC = (props) => {
const pusherRef = useRef<Pusher>();
useEffect(() => {
pusherRef.current = new Pusher(PUSHER_APP_KEY, {
cluster: 'eu'
})
return () => pusherRef.current?.disconnect()
}, [pusherRef]);
return (
<PusherContext.Provider value={pusherRef.current}>
{props.children}
</PusherContext.Provider>
)
}
The problem is that the provider always has an undefined value on page refresh/load. But when I trigger a re-render the value is correctly set. I would like to have the instance without the need of re-rendering.
Why is this happening?

I believe you can use the next construction:
export const PusherProvider = (props) => {
const pusher = useMemo(() => new Pusher(APP_PUSHER_KEY, { cluster: 'eu' }), [])
useEffect(() => () => pusher.disconnect(), [pusher])
return <PusherContext.Provider value={pusher}>{props.children}</PusherContext.Provider>
}

I have solved this issue by following way.
If you set State/const data inside "useEffect" will not work. as that will not run when page refresh but the state declaration those are outside the "useEffect" will run. Hence it will reset default values.
So I resolved by setting the state/const value outside of "useEffect" and done.

Related

Why is my useState variable initialized every time my React components is rendered?

While working on some custom hooks in React I have observed the following behavior: Every time my component renders, my useState variables are initialized. More specifically, I have observed this by using an auxiliary function to dynamically create data for testing.
In order to demonstrate this behavior of React, I have created a super simple "TestComponent" (please refer to the code below). Every time my component renders, it logs "testData called!" in the console.
What did I do wrong? How can I prevent React from calling my "test-data-generating-function" testData every time my component renders?
PS: I tried to wrap my function testData into a useCallback hook. But that does not work since useCallback cannot be used at "top level". Also, instead of finding a "workaround", I would really like to understand the root cause of my problem.
import React, { useEffect, useState } from 'react';
const testData = (): number[] => {
console.log('testData called!');
const returnValues: number[] = [];
for (let i = 1; i <= 10; i++) {
returnValues.push(i);
}
return returnValues;
};
const TestComponent = () => {
// useState hooks
const [data, setData] = useState<number[]>(testData());
const [counter, setCounter] = useState<number>(1);
// useEffect hook
useEffect(() => {
const increaseCounter = () => {
setCounter((counter) => counter + 1);
setTimeout(() => increaseCounter(), 1000);
};
increaseCounter();
}, []);
return (
<div>
{data.map((item) => (
<p key={item}>{item}</p>
))}
<p>{counter}</p>
</div>
);
};
export default TestComponent;
I believe what happens here is that your function to retrieve the data (testData()) is being called for each render, but not necessarily updates the state.
A react component is essentialy a function, and for each render the whole function is executed (including the call to testData()).
If you use the callback signature of useState, it will be called only on the first render:
const [data, setData] = useState(() => testData());

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.

useContext returns undefined when my redux store updates

I am having trouble understanding why my useContext value updates from the expected values to undefined when the redux store is updated. I am using create context to house my socket connection for my discussions. The following is my context class
import React, { createContext, useRef } from 'react'
export const WebSocketContext = createContext()
export default ({ children }) => {
const webSocket = useRef(null);
let ws;
const openedDiscussionID = useSelector(state => state.presence.discussionId || '')
const sendMessages = (messageObject) => {
webSocket.current.send(JSON.stringify(messageObject))
}
if (!webSocket.current){
webSocket.current = new WebSocket(``)
webSocket.current.onmessage = (message) => {
const discussionMessage = JSON.parse(message.data)
};
ws = {
webSocket: webSocket.current,
sendMessages
}
}
return(
<WebSocketContext.Provider value={ws}>
{children}
</WebSocketContext.Provider>
)
}
I am currently using it in my parent component called projectDetailContainer,
import WebSocketProvider from '../../redux-state/middleware/socketService'
function ProjectDetailContainer() {
return (
<WebSocketProvider>
<div>parent component</div>
</WebSocketProvider>
)
}
export default ProjectDetailContainer
somewhere down the chain, I have the following discussion component:
import { WebSocketContext } from '../../redux-state/middleware/socketService'
function DiscussionOptionContainer() {
const ws = useContext(WebSocketContext)
...
useEffect(() => {
if(openedDiscussion.title?.length){
dispatch(setOpenDiscussion(openedDiscussion))
}
},[openedDiscussion])
const sendMessage = () => {
const mes = {
action:'discussion',
message:{
customerId:'7240304',
projectId:projectId,
message:message,
itemId:openedDiscussion.discussionId,
sender: me
}
}
debugger
ws.sendMessages(mes)
}
...}
The thing I noticed was that as soon as the app loads, I see the sendMessages function being returned but as soon as the dispatch occurs, it changes the ws (useContext) to undefined. If I comment out the part that updates the redux store, it works as expected. My theory is that the redux store provider is higher in the chain than my WebSocketProvider but i cant confirm.
useSelector triggers a rerender when selected value changes. ws is undefined by default and gets a value if webSocket.current is falsy. And it seems to happen only on the initial render. Every consecutive render will set ws to undefined and pass it to WebSocketContext.Provider.
Try moving ws = {.....} outside condition.

Export function from inside a React function

I have a function that handles React Native location. For demonstration:
const useLocation = () => {
const [fetchingLocation, setFetchingLocation] = useState(true);
...
const changeSystemPermissions = useCallback(() => {...});
useEffect(() => {
//does many things
}, [...])
}
I need to have the function changeSystemPermissions inside useLocation as it uses the state.
I realize that I can export the changeSystemPermissions function as a const with a return [changeSystemPermissions, ...] and then import it in another component with:
const [
changeSystemPermissions,
...
] = useLocation();
However, it will ALSO run the useEffect function. I do want it to run once, but I need to access changeSystemPermissions in several other components and I don't want the useEffect to run multiple times.
I was thinking I will just take out the changeSystemPermissions function outside of useLocation, but it needs to use the state. I suppose I COULD pass the state vars into the changeSystemPermissions when it is outside useLocation, but that would be verbose and ugly.
How can I export changeSystemPermissions and just that function without having to import the whole useLocation function?
Can you move the useEffect to the component one ?
const useLocation = () => {
const [fetchingLocation, setFetchingLocation] = useState(true);
const changeSystemPermissions = useCallback(() => {
...
});
const funcToExecute = useCallback(() => {
....
}, []);
return { changeSystemPermissions, funcToExecute }
}
And put it in the component :
const {
changeSystemPermissions,
funcToExecute,
} = useLocation();
useEffect(() => {
funcToExecute()
}, [...])
Also, if you really need the useEffect to be in the custom hook,
maybe you can add a param to this hook.
const useLocation = (shouldTriggerEffect) => {
const [fetchingLocation, setFetchingLocation] = useState(true);
const changeSystemPermissions = useCallback(() => {
...
});
useEffect(() => {
if (shouldTriggerEffect) {
...
}
}, [shouldTriggerEffect])
return { changeSystemPermissions, funcToExecute }
}
And then in the component,
const {
changeSystemPermissions,
} = useLocation(false);
Tell me if I misunderstood something or if it helps :)
When ever you call a hook inside a React functional component, it will create a new state for that hook and not sharing among components. But there is a library which could help you achieve that:
https://github.com/betula/use-between
You could follow example to use this library or maybe just read the code and utilize the approach for your case to share the hook state between components.

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.

Resources