I have a page that connects to a SockJS socket over stomp. But when pass a function that is supposed to change the state to the .subscribe callback it doesn't change the page's state (the rest of the function completes normally)
Here is the code:
export default function HomePage() {
...some other states...
const [isIncomingCall, setIsIncomingCall] = useState(false)
function initSocket() {
const socket = new SockJS(`${process.env.API_URL}/ws`);
const stompClient = over(socket);
stompClient.connect(
{},
() => onSocketConnected(stompClient),
onSocketError
)
appContext.setSocket(stompClient)
}
function handleIncomingCall(data) {
const state = JSON.parse(data.body.toLowerCase());
setIsIncomingCall(state)
}
function onSocketConnected(stompClient) {
const options = {
accessToken: cookies['logged-in'],
};
stompClient.send("/app/connect", {}, JSON.stringify(options));
stompClient.subscribe(`/user/search/complete`, handleSearchComplete)
stompClient.subscribe(`/user/search/failed`, handleSearchError)
stompClient.subscribe(`/user/call/incoming`, handleIncomingCall)
}
useEffect(() => {
if (!appContext.socket && cookies['logged-in']) {
initSocket()
}
}, [])
return (
<>
<AnimatePresence>
{
isIncomingCall && <CallModal
onAccept={acceptIncomingCall}
onReject={rejectIncomingCall}
/>
}
</AnimatePresence>
...other page code...
</>
)
}
The initSocket function is called on page render inside the useEffect.
I have tried wrapping the callback with a useCallback and binding it to the page and calling setIsIncomingCall inside an arrow function, but it didn't seem to help.
Ok, so the problem was:
Callbacks were initialized on first visiting the HomePage, but for user to set that they can receive calls, they needed to go to the ProfilePage and flip the switcher. In the process of going back and forth between the pages React loses the pointer to the original isIncomingCall state and sets it on the previous version of the HomePage it seems.
Related
I'm fetching WordPress posts asynchronously via getStaticProps()...
export async function getStaticProps({ params, preview = false, previewData }) {
const data = await getPostsByCategory(params.slug, preview, previewData)
return {
props: {
preview,
posts: data?.posts
},
}
}
... and passing them to useState:
const [filteredArticles, setFilteredArticles] = useState(posts?.edges)
Then, I pass the state to a component:
router.isFallback ? (
// If we're still fetching data...
<div>Loading…</div>
) : (
<ArticleGrid myArticles={filteredArticles} />
This is necessary because another component will setFilteredArticles with a filter function.
But when we are passing the state to ArticlesGrid, the data is not ready when the component loads. This is confusing to me since we passing the state within a router.isFallback condition.
Even if we set state within useEffect...
const [filteredArticles, setFilteredArticles] = useState()
useEffect(() => {
setFilteredArticles(posts)
}, [posts?.edges])
... the data arrives too late for the component.
I'm new to Next.js. I can probably hack my way through this, but I assume there's an elegant solution.
Let's look at some useEffect examples:
useEffect(() => {
console.log("Hello there");
});
This useEffect is executed after the first render and on each subsequent rerender.
useEffect(() => {
console.log("Hello there once");
}, []);
This useEffect is executed only once, after the first render.
useEffect(() => {
console.log("Hello there when id changes");
}, [props.id]);
This useEffect is executed after the first render, every time props.id changes.
Some possible solutions to your problem:
No articles case
You probably want to treat this case in your ArticleGrid component anyway, in order to prevent any potential errors.
In ArticleGrid.js:
const {myArticles} = props;
if(!myArticles) {
return (<div>Your custom no data component... </div>);
}
// normal logic
return ...
Alternatively, you could also treat this case in the parent component:
{
router.isFallback ? (
// If we're still fetching data...
<div>Loading…</div>
) : (
<>
{
filteredArticles
? <ArticleGrid myArticles={filteredArticles} />
: <div>Your custom no data component... </div>
}
</>
)
}
Use initial props
Send the initial props in case the filteres haven't been set:
const myArticles = filteredArticles || posts?.edges;
and then:
<ArticleGrid myArticles={myArticles} />
Here is the CodeSandbox: https://codesandbox.io/s/competent-dream-8lcrt
I have implemented a modal with React context which exposes 1) the open state 2) the modal config 3) an open and close method
// ...
const onOpenDialog = (mode, config) => {
setDialogMode(mode)
if (config) {
setDialogConfig(config);
}
}
const onCloseDialog = () => {
setDialogMode("");
if (Object.keys(dialogConfig).length > 0) {
setDialogConfig({})
}
}
// ...
return (
<Provider value={{ dialogMode, dialogConfig, onOpenDialog, onCloseDialog }}>
{children}
</Provider>
)
From the main component, I have an onClick handler that call the onOpenDialog method and pass an onSubmit and onClose callback in its config object (this onCloseDialog callback is the issue)
const { onOpenDialog, onCloseDialog } = useDialog()
// ...
const onClick = () => {
onOpenDialog("add", {
data: null,
onSubmit: (data) => {
console.log("Form Data: ", data)
},
onCancel: onCloseDialog
})
}
And finally, I have a FormInModal component that call the two callbacks passed in the dialogConfig object when hitting submit and close.
const onSubmit = (data) => {
dialogConfig.onSubmit({
username: data.username,
password: data.password
})
}
const onCancel = () => {
dialogConfig.onCancel()
}
Steps to reproduce:
Click on Open
You should see in the console that 1) The dialog opens 2) The config input that was passed to the Dialog config has data, onSubmit and onCancel, the config input has been set in the dialogConfig state
Click on Cancel, you should see that 1) The dialog closes 2) The dialogConfig state is empty 3) the dialogConfig state is not reseted
To be fair 3. works half of the time. This is weird because the dialogConfig state is always updated when the dialog opens. You can see on the React Dev tool that the state update only half the time
This is a known problem in React when trying to access the state from a function closures. The old state is captured at the closure creation and returned by the function even though the state has updated.
One way to solve the issue is to access the updated state with:
setState(previousState => newState) instead of setState(newState)
The answer from the react community: Function that read an outdated sate
I' trying to build a toast message API for React. My goal is to provide a fireNotification() api that can be called anywhere in the app and have React render the toast component.
I built this simple notification manager with sub/pub pattern and hope to be able to subscribe to new notifications in a useEffect hook
const notifications = [];
const listeners = new Set();
function subscribe(callback) {
listeners.add(callback);
}
function publish() {
listeners.forEach((cb) => {
cb(notifications);
});
}
export function fireNotification(content) {
notifications.push(content);
publish();
}
export default function App() {
const [state, setState] = React.useState();
React.useEffect(() => {
subscribe((updated) => {
setState(updated);
});
}, []);
// state will be logged correctly 2 times
// won't be updated after that
console.log("state", state);
return (
<div className="App">
<button onClick={() => {fireNotification('test')}}>fire</button>
</div>
);
}
codesandbox
However, fireNotification() will only trigger setState twice
From the 3rd time onward, the state is not updated at all.
I'm able to make state update work by changing setState(updated) to setState([...updated]), but not sure why it works.
Can someone explain why setState(updated); only triggers twice? Thanks!
You need to provide data to watch for changes, to the useEffect
React.useEffect(() => {
subscribe((updated) => {
setState(updated);
});
}, [updated]);
Other wise the useEffect will run only twice
so i have a bit of a weird problem i dont know how to solve.
In my code i have a custom hook with a bunch of functionality for a fetching a list
of train journeys. I have some useEffects to that keeps loading in new journeys untill the last journey of the day.
When i change route, while it is still loading in new journeys. I get the "changes to unmounted component" React error.
I understand that i get this error because the component is doing an async fetch that finishes after i've gone to a new page.
The problem i can't figure out is HOW do i prevent it from doing that? the "unmounted" error always occur on one of the 4 lines listed in the code snippet.
Mock of the code:
const [loading, setLoading] = useState(true);
const [journeys, setJourneys] = useState([]);
const [hasLaterDepartures, setHasLaterDepartures] = useState(true);
const getJourneys = async (date, journeys) => {
setLoading(true);
setHasLaterDepartures(true);
const selectedDateJourneys = await fetchJourney(date); // Fetch that returns 0-3 journeys
if (condition1) setHasLaterDepartures(false); // trying to update unmounted component
if (condition2) {
if (condition3) {
setJourneys(something1); // trying to update unmounted component
} else {
setJourneys(something2) // trying to update unmounted component
}
} else {
setJourneys(something3); // trying to update unmounted component
}
};
// useEffects for continous loading of journeys.
useEffect(() => {
if (!hasLaterDepartures) setLoading(false);
}, [hasLaterDepartures]);
useEffect(() => {
if (hasLaterDepartures && journeys.length > 0) {
const latestStart = ... // just a date
if (latestStart.addMinutes(5).isSameDay(latestStart)) {
getJourneys(latestStart.addMinutes(5), journeys);
} else {
setLoading(false);
}
}
}, [journeys]);
I can't use a variable like isMounted = true in the useEffect beacuse it would reach inside the if statement and reach a "setState" by the time i'm on another page.
Moving the entire call into a useEffect doesn't seem to work either. I am at a loss.
Create a variable called mounted with useRef, initialised as true. Then add an effect to set mounted.current to false when the component unmounts.
You can use mounted.current anywhere inside the component to see if it's mounted, and check that before setting any state.
useRef gives you a variable you can mutate but which doesn't cause a rerender.
When you use useEffect hook with action which can be done after component change you should also take care about clean effect when needed. Maybe example help you, also check this page.
useEffect(() => {
let isClosed = false
const fetchData = async () => {
const data = await response.json()
if ( !isClosed ) {
setState( data )
}
};
fetchData()
return () => {
isClosed = true
};
}, []);
In your use case, you probably want to create a Store that doesn't reload everytime you change route (client side).
Example of a store using useContext();
const MyStoreContext = createContext()
export function useMyStore() {
const context = useContext(MyStoreContext)
if (!context && typeof window !== 'undefined') {
throw new Error(`useMyStore must be used within a MyStoreContext`)
}
return context
}
export function MyStoreProvider(props) {
const [ myState, setMyState ] = useState()
//....whatever codes u doing with ur hook.
const exampleCustomFunction = () => {
return myState
}
const getAllRoutes = async (mydestination) => {
return await getAllMyRoutesFromApi(mydestination)
}
// you return all your "getter" and "setter" in value props so you can use them outside the store.
return <MyStoreContext.Provider value={{ myState, setMyState, exampleCustomFunction, getAllRoutes }}>{props.children}</MyStoreContext.Provider>
}
You will wrap the store around your entire App, e.g.
<MyStoreProvider>
<App />
</MyStoreProvider>
In your page where you want to use your hook, you can do
const { myState, setMyState, exampleCustomFunction, getAllRoutes } = useMyStore()
const onClick = async () => getAllRouters(mydestination)
Considering if you have client side routing (not server side), this doesn't get reloaded every time you change your route.
I make an request in react js and i get data using useSelector. So in useEffect i show the message from backend after the request.
useEffect(() => {
if (selector.response ? .message) {
console.log(selector);
message.success(selector.response?.message, 2);
setLoading(false);
}
console.log('render');
}, [selector.response]);
The code works fine, but appears an issue when i change the page(route). Clicking on another menu item i go to another page and when i come back, the useEffect is triggered again and user again sees the message from message.success(selector.response?.message, 2);. Question: How to stop showing the message each time after i come back to my route and to show the message just one time?
You have to use User Effects with Cleanup
You need to clean up the effects from the previous render or actions. Otherwise it will hold the previous state
You will find more details in official tutorial
userEffect Cleanup Process
Update Answer
import React, {useState, useEffect} from "react";
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
function handleChange(val) {
setCount(val)
}
window.addEventListener('load', handleChange(1))
return () =>{
handleChange(0);
console.log(count)
};
})
return (
<div className="App">
{count}
</div>
);
}
Here I take a count variable and setCount methods to set the count variable
Inside useEffect I create a function which will be responsible for setting up count value
Then I create an addEventListner when the page will load it will set the count value to 1
Then I call an anonymous function for clean up things which will remove the previously set value to 0
After that I set a console to just check if its sets the value
So when user come back to your page again initially it will find the default value then set the dynamic value.
You can make it more efficient way. I just give you an example
Below I share a link which will help you to handle api response
How to call api and cleanup response using react usereffect hooks
It seems like in your case your component unmounts upon page change and remounts when the user comes back. When the componet remounts, the useEffect always fires. That's simply how it works.
Answering your question, this is a simple thing you can do for your case.
// this will create a function that will return true only once
const createShouldShowSuccessMessage = () => {
let hasShownMessage = false;
()=> {
if(hasShownMessage) {
return false;
} else {
hasShownMessage = true;
return true;
}
}
};
const shouldShowSuccessMessage = createShouldShowSuccessMessage();
const SomeComponent = () => {
useEffect(() => {
if(shouldShowSuccessMessage()) {
message.success(selector.response?.message, 2);
}
setLoading(false);
}, [selector.response]);
}
I would advise you got with a better setup that performing your side effects inside your components but I hope my answer helps you till you get there