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.
Related
I am attempting to clean up my app and reduce the amount of re-renders that occur by using the React.memo feature, however I am struggling to understand fully why my page/component is re-rendering the amount of times it is. I believe it is down to my Redux usage and the different reducer steps that hit (fire, success, error).
App.jsx (pseudocode)
console.log("App");
return <div>
<Switch>
...
<Route render={Home} />
...
</Switch>
</div>
export default App;
Home.jsx
const Home = () => {
console.log("Home");
return (
<div>
<DashboardBill/>
</div>
)
}
export default Home;
Dashboard.jsx
const Dashboard = () => {
console.log("Dashboard");
return <div>
<MyBill/>
</div>
}
export default memo(Dashboard);
MyBill.jsx
const mapStateToProps = ({billData}) => ({
billData
});
const mapDispatchToProps = (dispatch) => ({
loadData: () => dispatch(loadBillData)
});
const MyBill = ({billData, loadData}) => {
console.log("MyBill");
const [manipulatedData, setManipulatedData] = useState({});
useEffect(() => {
!billData && loadData();
}, []);
useEffect(() => {
billData && setManipulatedData(someUtil.manipulate(billData));
}, [billData]);
return <div>{manipulatedData}</div>;
}
export default connect(mapStateToProps, mapDispatchToProps)(memo(MyBill));
The dispatch event ends up in a reducer that has the usual trigger, success and error cases to set the data within the Redux store etc.
When I load the page I am getting the following logging:
There are a few more dispatch events within the main App.jsx going on, but only a couple so that could account for the extra App mentions in the logs.
Can somebody attempt to explain what is going on here and why my memo() usages aren't doing what I am expecting?
Note: I am importing memo like import React, {memo, useEffect} from 'react';
Also, how would things be affected if MyBill were to have children that themselves made Redux store changes?
As I understand react.memo, render will be optimized if you use the component many times with the same props.
However, in your example, mapDispatchToProps provide a new function reference for loadData on every render. So your component is never rendering with the same props.
I guess you can optimize one rendering using useMemo hook
const MyBill = ({billData, loadData}) => {
const manipulatedData = useMemo(() => billData ? someUtil.manipulate(billData) : {}, [billData]);
useEffect(() => {
!billData && loadData();
}, []);
return <div>{manipulatedData}</div>;
}
I'm using a React Context const Context = React.createContext() with a useEffect hook to set a variable in my outer-most component that wraps my entire app. On a child component within my app I am using history.push('/') to route back to the root. This does not appear to trigger an update on my Context variable. Is this expected? If so, is there a better way to route that would update my context variable?
I'm using react 16.14.0 & react-router-dom 5.2.0.
For example, in the code below. Shouldnt var increment on history.push('/')
Context.js
const Context= React.createContext();
const ContextProvider= (props) => {
const [var, setVar] = useState(null);
useEffect(() => {
setVar(var++)
}, []);
return (
<Context.Provider value={user}>{props.children}</Context.Provider>
);
};
ChildComponent.js
...
import { useHistory } from "react-router-dom";
...
const ChildComponent = () => {
const history = useHistory();
function doSomething(){
history.push('/')
}
}
return(
<Button onClick={() => doSomething()} />
)
This does not appear to trigger an update on my Context variable. Is this expected?
Changing the history does not cause context providers to rerender. You mention you have a useEffect, and in principle you could write some code in that useEffect which would listen to the history, and when it gets a change it sets state to cause a rerender. If you have code in there that you think is supposed to listen for history changes, feel free to share that and i'll comment on it.
However, instead of writing your own code to listen for changes, i'd recommend using the hooks provided by react-router. The useHistory and useLocation hooks will both listen for changes and rerender the component.
const Example = () => {
// When the location changes, Example will rerender
const location = useLocation();
const [someState, setSomeState] = useState('foo');
useEffect(() => {
if (/* check something you care about in the location */) {
setSomeState('bar');
}
}, [location]);
return (
<Context.Provider value={someState}>
{children}
</Context.Provider>
)
}
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.
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;
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>
);
};