Does a React Context update on history.push('/') - reactjs

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>
)
}

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.

React preserve state in custom hook

Is there a way to preserve the state when calling a custom react hook from separate components? I made a simple example here but I was thinking of using the same logic in my app to store a fetch call to an api and use the data in different places in my app without calling the api more than once.
import React, { useState, useEffect } from 'react';
function useCounter(intialCount = 0){
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return [count, setCount];
}
const AnotherComponent = () => {
const [count] = useCounter();
return <div>{count}</div>
}
export default function App() {
// Call custom hook `useCounter` to reuse Counter logic
const [count, setCount] = useCounter(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={() => setCount(count - 1)}>
Decrement
</button>
<AnotherComponent />
</div>
);
}
In this example, is it possible for AnotherComponent to have the same count as App. I dont want to use context either in my app component for performance reasons because the data I would get from an api is a large list.
If you don't want to use context then it is possible to achieve what you want using some shared state outside of the hook:
let sharedCount = 0;
function useCounter(initialCount) {
const [count, _setCount] = useState(sharedCount);
// On first run, set initial count
useEffect(() => {
if (initialCount !== undefined) {
sharedCount = initialCount;
}
}, []);
// If shared count is changed by other hook instances, update internal count
useEffect(() => {
_setCount(sharedCount);
}, [sharedCount]);
const setCount = (value) => {
sharedCount = value; // Update shared count for use by other hook instances
_setCount(value); // Update internal count
};
return [count, setCount];
}
Yeap, you can easily achieve your goal by setting a Context to provide a centralized state and a custom hook to access that state in a modular way.
Let's assume that a want to share foo with my entire application.
Create a context provider
FooContext.js
import { createContext, useState } from 'react'
export const FooContext = createContext()
const { Provider } = FooContext
export const FooProvider = ({ children }) =>{
const [foo, setFoo] = useState('bar')
return(
<Provider value={{foo, setFoo}}>
{ children }
</Provider>
)
}
Now wrap you application (or the section you want to be aware of foo with FooProvider
<FooProvider>
<RestOfApp/>
</FooPrivider>
Now just create a custom hook to make the value and setter of foo easily accessible
useFoo.js
import { useContext } from 'react'
import { FooContext } from './FooContext.js'
export const useFoo = () =>{
const { foo, setFoo } = useContext(FooContext)
return [foo, setFoo]
}
To use it in a component (that's under FooProvider)
import { useFoo } from './useFoo'
const ComponentWithFoo = () =>{
const [foo, setFoo] = useFoo()
const changeFoo = value => setFoo(value)
return <p> { foo } </p>
}
Notice that besides hooks you could also use HOCs or render props.
Use SWR or React Query, they will automatically cache your data from the server and stop duplicate callings.

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;

React.js: Possible to assign useState() outside function component?

Is there any syntax that would allow assigning useState outside of the function component? I have tried this so far but it has not worked:
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
function App() {
useEffect(() => setStateData("from useEffect"), []);
return <div className="App">{stateData}</div>;
}
const [stateData, setStateData] = App.useState("default value"); // fails
// const [stateData, setStateData] = App.prototype.useState("default value"); // also fails
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
CodeSandbox:
Disclaimer: This is an anti-pattern. If you wanna share state there's better ways, but just for info…
You can use the useState's update function outside of a component by assigning it to a variable. Just be sure to clear it when the component unmounts.
/* Reference to hold the update function */
let updateOutside
function App() {
const [state, update] = useState()
useEffect(() => {
/* Assign update to outside variable */
updateOutside = update
/* Unassign when component unmounts */
return () => updateOutside = null
})
return `App State: ${state}`
}
if (updateOutside) updateOutside(/* will re-render App */)
Is there any syntax that would allow assigning useState outside of the function component?
If you take a look at the docs and Rules of Hooks
Only Call Hooks from React Functions
Don’t call Hooks from regular JavaScript functions. Instead, you can:
✅ Call Hooks from React function components.
✅ Call Hooks from custom Hooks (we’ll learn about them on the next page).
So... no, you can't use useState outside of a functional component.
If you want to use useState outside of components think about using https://jotai.org/
It's useState replacement with some additional bonuses
import { atom, useAtom } from 'jotai'
// Create your atoms and derivatives
const textAtom = atom('hello')
const uppercaseAtom = atom(
(get) => get(textAtom).toUpperCase()
)
// Use them anywhere in your app
const Input = () => {
const [text, setText] = useAtom(textAtom)
const handleChange = (e) => setText(e.target.value)
return (
<input value={text} onChange={handleChange} />
)
}
const Uppercase = () => {
const [uppercase] = useAtom(uppercaseAtom)
return (
<div>Uppercase: {uppercase}</div>
)
}
// Now you have the components
const App = () => {
return (
<>
<Input />
<Uppercase />
</>
)
}

React get state from Redux store within useEffect

What is the correct way to get state from the Redux store within the useEffect hook?
useEffect(() => {
const user = useSelector(state => state.user);
});
I am attempting to get the current state within useEffect but I cannot use the useSelector call because this results in an error stating:
Invariant Violation: Hooks can only be called inside the body of a function component.
I think I understand why as it breaks one of the primary rules of hooks.
From reviewing the example on the Redux docs they seem to use a selectors.js file to gather the current state but this reference the mapStateToProps which I understood was no longer necessary.
Do I need to create some kind of "getter" function which should be called within the useEffect hook?
Don't forget to add user as a dependency to useEffect otherwise your effect won't get updated value.
const user = useSelector(state => state.user);
useEffect(() => {
// do stuff
}, [user]);
You can place useSelector at the top of your component along with the other hooks:
const MyComponent = () => {
...
const user = useSelector(state => state.user);
...
}
Then you can access user inside your useEffects.
I found using two useEffects to works for me, and have useState to update the user (or in this case, currUser).
const user = useSelector(state=>state.user);
const [currUser, setCurrUser] = useState(user);
useEffect(()=>{
dispatch(loadUser());
}, [dispatch]);
useEffect(()=>{
setCurrUser(user);
}, [user]);
You have to use currUser to display and manipulate that object.
You have two choices.
1 - If you only need the value from store once or 'n' time your useEffect is called and don't want to listen for any changes that may occur to user state from redux then use this approach
//import the main store file from where you've used createStore()
import {store} from './store' // this will give you access to redux store
export default function MyComponent(){
useEffect(() =>{
const user = store.getState().user;
//...
},[])
}
2 - If you want to listen to the changes that may occur to user state then the recommended answer is the way to go about
const MyComponent = () => {
//...
const user = useSelector(state => state.user);
useEffect(() => {
//...
},[])
//...
}
const tournamentinfofromstore=useSelector(state=>state.tournamentinfo)
useEffect(() => {
console.log(tournamentinfofromstore)
}, [tournamentinfofromstore])
So the problem is that if you change the state inside the useEffect that causes a rerender and then again the useEffect gets called "&&" if that component is passing data to another component will result in infinite loops.and because you are also storing that data in the child component's state will result in rerendering and the result will be infinite loop.!!
Although it is not recommended, you can use store directly in your component, even in the useEffect.
First, you have to export store from where it is created.
import invoiceReducer from './slices/invoiceSlice';
import authReducer from './slices/authSlice';
export const store = configureStore({
reducer: {
invoices: invoicesReducer,
auth: authReducer,
},
});
Then you can import it to a React Component, or even to a function, and use it.
import React, { useEffect } from 'react';
import { store } from './store';
const MyComponent = () => {
useEffect(()=> {
const invoiceList = store.getState().invoices
console.log(invoiceList)
}, [])
return (
<div>
<h1>Hello World!</h1>
</div>
)
}
export default MyComponent
You can study the API for Store in here.
You can also see why this approach is not recommended in
here.
Or, if you are interested in using redux store outside a react component, take a look at this blog post.
To add on top of #Motomoto's reply. Sometimes you depend on store to be loaded before useEffect. In this case you can simply return in if the state is undefined. useEffect will rerender once the store is loaded
const user = useSelector(state => state.user);
useEffect(() => {
if(user === undefined){
return}else{
// do stuff
}}, [user]);
I'm having the same issue, The problem to the useSelector is that we cant call it into the hook, so I can't be able to update with the action properly. so I used the useSelector variable as a dependency to the useEffect and it solved my problem.
const finalImgData_to_be_assigned = useSelector((state) => state.userSelectedImg);
useEffect(()=>{
console.log('final data to be ready to assign tags : ', finalImgData_to_be_assigned.data);
}, [finalImgData_to_be_assigned ])

Resources