useEffect when onClick - reactjs

I'm getting stuck about how to use effects together with app logic.
Suppose this component:
import React, { useEffect, useState } from "react";
export default function App() {
const [query, setQuery] = useState('');
useEffect( () => {
fetch('https://www.google.com?q='+query)
.then(response => console.log(response))
}); // depends on what?
return (
<div>
<input onChange={e => setQuery(e.target.value)} value={query} />
<button>Ask Google about {query}</button>
</div>
);
}
I want that:
when (and only) the user clicks the button the fetch is run with the correct query value of the input
if the fetch is still in progress and the user clicks, the fetch is skipped but the effect is fired (meaning: I intentionally not disable the button, I want that the effect function is run, but I put a check inside that function not to execute the fetch).
Problems:
The effect shouldn't fire on mount (it wouldn't make any sense)
The effect shouldn't fire when the query changes, but if I don't put the query variable inside the useEffect dependency array, React complains (react-hooks/exhaustive-deps)
The effect should fire when the user click on the button; I achieved this for example using a fake state isRun, setting onClick={setIsRun(true)}, making the effect depending on [isRun], setting setIsRun(false) at the end of the effect function, and checking if (!isRun) at the beginning of the effect function to prevent that when is set to false from the effect itself it is run again since the state changes. This works, but I find it very verbose and uncomfortable...
The effect should fire if the button is clicked again (with the same query value or not) and the previous fetch has not yet finished without running the fetch: with the previous solution with isRun it wouldn't fire because isRun is already set to 1 so there is no state change; maybe with another state there is a way, but again very verbose and counterintuitive.
Most importantly: the code should be clean and readable without using "tricks"!
How would you write such a component?

It sounds like you shouldn't be using useEffect for this at all. You want this to happen on a user action, not as an effect:
when (and only) the user clicks the button the fetch is run with the correct query value of the input
Remove useEffect and create a function to handle the click:
const handleClick = (e) => {
fetch('https://www.google.com?q='+e.target.value)
.then(response => console.log(response));
};
And pass that function to the component:
<button onClick={handleClick}>Ask Google about {query}</button>
What seems confusing here are these requirements:
if the fetch is still in progress and the user clicks, the fetch is skipped but the effect is fired
The effect should fire if the button is clicked again (with the same query value or not) and the previous fetch has not yet finished without running the fetch
The only thing the function does is execute a fetch. So should that operation happen or not? Your proposed solution of keeping state in a variable (isRun) to determine if it should happen or not should work in this case. I think the problem before was mixing that up with useEffect when all you really want is a function. Add isRun to state and update it accordingly when performing the operation:
const [isRun, setIsRun] = useState(false);
const handleClick = (e) => {
if (isRun) { return; }
setIsRun(true);
fetch('https://www.google.com?q='+e.target.value)
.then(response => {
console.log(response);
setIsRun(false);
});
};

I'm not sure if you want to insist on using useEffect but it does not seem appropriate for this situation. What I could do is call a function on button click.
import React, { useEffect, useState } from "react";
export default function App() {
const [query, setQuery] = useState('');
const handleQuery = (query) => {
fetch('https://www.google.com?q='+query)
.then(response => console.log(response))
}
return (
<div>
<input onChange={e => setQuery(e.target.value)} value={query} />
<button onClick={() => handleQuery(query)}>Ask Google about {query}</button>
</div>
);
}

Related

How to listen for time change in React?

I have a button which I want to remain disabled unless in a certain time window. Is there any way to do this in react?
I dont want to hard code it like
<button disabled={isCorrectTime()}>...
because if the user is already on the page, and the time changes to the correct time, the button will not get updated right? does anyone know of any solutions?
You call setTimeout inside of the useEffect Hook . useEffect method runs when it first renders then setTimeout block runs after some seconds (these seconds passed into the second parameter of the setTimeout method) then you call the clearTimeout() to cancel a timer .
Example :
import React, {useEffect, useState} from 'react'
function App() {
const [disabled, setdisabled] = useState(true)
useEffect(() => {
const timer = setTimeout(() => {
setdisabled(false);
}, 3000);
return () => clearTimeout(timer);
}, []);
return (
<div>
<h1> hello </h1>
<button disabled= {disabled} > click me </button>
</div>
)
}
export default App
Is the time window a specific amount of time after a certain event? If so, you can use a timeout function to change a state variable that you can then use to control the disabled state of the button:
const [isDisabled, setIsDisabled] = useState(true)
setTimeout(() => {
setIsDisabled(false);
}, 1000)
// set this time to whatever the desired length of time is
...
<button disabled={isDisabled} />
If this doesn't work, you might be able to use setInterval() to periodically check the current time, but I will need more information about what your goals are to know if that's the best way forward.

React/Socket.io not displaying latest message passed down as prop

I am working on a chat application using React and socket.io. Back end is express/node. The relevant components are:
Room.js --> Chat.js --> Messages.js --> Message.js
messageData received from the server is stored in state in Room.js. It is then passed down through Chat.js to Messages.js, where it is mapped onto a series of Message.js components.
When messages are received, they ARE appearing, but only after I start typing in the form again, triggering messageChangeHandler(). Any ideas why the Messages won't re-render when a new message is received and added to state in Room.js? I have confirmed that the state and props are updating everywhere they should be--they just aren't appearing/re-rendering until messageChangeHandler() triggers its own re-render.
Here are the components.
Room.js
export default function Room(props) {
const [messagesData, setMessagesData] = useState([])
useEffect(() => {
console.log('the use effect ')
socket.on('broadcast', data => {
console.log(messagesData)
let previousData = messagesData
previousData.push(data)
// buildMessages(previousData)
setMessagesData(previousData)
})
}, [socket])
console.log('this is messagesData in queue.js', messagesData)
return(
// queue counter will go up here
// <QueueDisplay />
// chat goes here
<Chat
profile={props.profile}
messagesData={messagesData}
/>
)
}
Chat.js
export default function Chat(props) {
// state
const [newPayload, setNewPayload] = useState({
message: '',
sender: props.profile.name
})
// const [messagesData, setMessagesData] = useState([])
const [updateToggle, setUpdateToggle] = useState(true)
const messageChangeHandler = (e) => {
setNewPayload({... newPayload, [e.target.name]: e.target.value})
}
const messageSend = (e) => {
e.preventDefault()
if (newPayload.message) {
socket.emit('chat message', newPayload)
setNewPayload({
message: '',
sender: props.profile.name
})
}
}
return(
<div id='chatbox'>
<div id='messages'>
<Messages messagesData={props.messagesData} />
</div>
<form onSubmit={messageSend}>
<input
type="text"
name="message"
id="message"
placeholder="Start a new message"
onChange={messageChangeHandler}
value={newPayload.message}
autoComplete='off'
/>
<input type="submit" value="Send" />
</form>
</div>
)
}
Messages.js
export default function Messages(props) {
return(
<>
{props.messagesData.map((data, i) => {
return <Message key={i} sender={data.sender} message={data.message} />
})}
</>
)
}
Message.js
export default function Message(props) {
return(
<div key={props.key}>
<p>{props.sender}</p>
<p>{props.message}</p>
</div>
)
}
Thank you in advance for any help!
I don't think that your useEffect() function does what you think it does.
Red flag
Your brain should generate an immediate red flag if you see a useEffect() function that uses variables declared in the enclosing scope (in a closure), but those variables are not listed in useEffect()'s dependencies (the [] at the end of the useEffect())
What's actually happening
In this case, messagesData in being used inside useEffect() but not declared as a dependency. What happens is that after the first broadcast is received and setMessagesData is called, messagesData is no longer valid inside useEffect(). It refers to an array, from the closure when it was last run, which isn't assigned to messageData any longer. When you call setMessagesData, React knows that the value has been updated, and re-renders. It runs the useState() line and gets a new messagesData. useEffect(), which is a memoized function, does NOT get recreated, so it's still using messagesData from a previous run.
How to fix it
Clean up useEffect()
Before we start, let's eliminate some of the noise in the function:
useEffect(() => {
socket.on('broadcast', data => {
setMessagesData([...messagesData, data])
})
}, [socket])
This is functionally equivalent to your code, minus the console.log() messages and the extra variable.
Let's go one step further and turn the handler into a one-liner:
useEffect(() => {
socket.on('broadcast', data => setMessagesData([...messagesData, data]));
}, [socket])
Add missing dependencies
Now, let's add the missing dependencies!
useEffect(() => {
socket.on('broadcast', data => setMessagesData([...messagesData, data]));
}, [socket, messagesData])
Technically, we also depend on setMessagesData(), but React has this to say about setState() functions:
React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.
Too many cooks
The useEffect() function is looking good, but we still depend on messagesData. This is a problem, because every time socket receives a broadcast, messagesData changes, so useEffect() is re-run. Every time it is re-run, it adds a new handler/listener for broadcast messages, which means that when the next message is received, every handler/listener calls setMessagesData(). The code might still accidentally work, at least logic-wise, because listeners are usually called, synchronously, in the order in which they were registered, and I believe that if multiple setState() calls are made during the same render, React only re-renders once using the final setState() call. But it will definitely be a memory leak, since we have no way to unregister all of those listeners.
This tiny problem would normally end up being a huge pain to solve, because to fix this problem, we would need to unregister the old listener every time we registered a new one. And to unregister a listener, we call removeListener() function with the same function we registered - but we don't have that function anymore. Which means we need to save the old function as state or memoize it, but now we also have another dependency for our useEffect() function. Avoiding a continuous loop of infinite re-renders turns out to be non-trivial.
The trick
It turns out that we don't have to jump through all of those hoops. If we look closely at our useEffect() function, we can see that we don't actually use messagesData, except to set the new value. We're taking the old value and appending to it.
The React devs knew that this was a common scenario, so there's actually a built-in helper for this. setState() can accept a function, which will immediately be called with the previous value as an argument. The result of this function will be the new state. It sounds more complicated than it is, but it looks like this:
setState(previous => previous + 1);
or in our specific case:
setMessagesData(oldMessagesData => [...oldMessagesData, data]);
Now we no longer have a dependency on messagesData:
useEffect(() => {
socket.on('broadcast', data => setMessagesData(oldMessagesData => [...oldMessagesData, data]);
}, [socket])
Being polite
Remember earlier when we talked about memory leaks? It turns out this can still happen with our latest code. This Component may get mounted and unmounted multiple times (for example, in a Single-Page App when the user switches pages). Each time this happens, a new listener is registered. The polite thing to do is to have useEffect() return a functions which will clean up. In our case this means unregistering/removing the listener.
First, save the listener before registering it, then return a function to remove it
useEffect(() => {
const listener = data => setMessagesData(oldMessagesData => [...oldMessagesData, data];
socket.on('broadcast', listener);
return () => socket.removeListener('broadcast', listener);
}, [socket])
Note that our listener will still be dangling if socket changes, and since it's not clear in the code where socket comes from, whatever changes that will also have to remove all old listeners, e.g. socket.removeAllListeners() or socket.removeAllListeners('broadcast').
Changing the useEffect in room to contain the following fixed the issue:
useEffect(() => {
console.log('the use effect ')
socket.on('broadcast', data => {
console.log(messagesData)
// let previousData = messagesData
// previousData.push(data)
// setMessagesData(previousData)
setMessagesData(prev => prev.concat([data]))
})
}, [socket])```

Use custom hook in callback function

I have a customHook, and I need to call it in two places. One is in the top level of the component. The other place is in a onclick function of a button, this button is a refresh button which calls the customHook to fetch new data like below. I am thinking of two approaches:
create a state for the data, call hook and set the data state in the component and in the onclick function, call hook and set the data state. However, the hook cannot be called inside another function i.e onclick in this case.
create a boolean state called trigger, everytime onclick of the button, toggle the trigger state and pass the trigger state into the myCallback in the dependent list so that myCallback function gets recreated, and the hook gets called. However, I don't really need to use this trigger state inside the callback function, and the hook gives me error of removing unnecessary dependency. I really like this idea, but is there a way to overcome this issue?
Or is there any other approaches to achieve the goal?
const MyComponent = () => {
const myCallback = React.useCallback(() => { /*some post processing of the data*/ }, []);
const data = customHook(myCallback);
return <SomeComponent data={data}>
<button onclick={/*???*/}></button>
</SomeComponent>;
};
It is possible to make your second example work with some tweaking. Instead of passing in a dependency to update the effect function, just make the effect function a stand-alone function that you pass into useEffect, but can also call in other places (e.g. you can return the effect function from your hook so your hook users can use it too)
For example:
const App = () => {
const { resource, refreshResource } = useResource()
return (
<div>
<button onClick={refreshResource}>Refresh</button>
{resource || 'Loading...'}
</div>
)
}
const useResource = () => {
const [resource, setResource] = useState(null)
const refreshResource = async () => {
setResource(null)
setResource(await fetchResource())
}
useEffect(refreshResource, [])
return { resource, refreshResource }
}
const fetchResource = async () => {
await new Promise(resolve => setTimeout(resolve, 500))
return Math.random()
}
Edit
I hadn't realized that the hook couldn't be edited. I honestly can't think of any good solutions to your problem - maybe one doesn't exist. Ideally, the API providing this custom hook would also provide some lower-level bindings that you could use to get around this issue.
If worst comes to worst and you have to proceed with some hackish solution, your solution #2 of updating the callback should work (assuming the custom hook refetches the resource whenever the parameter changes). You just have to get around the linting rule, which, I'm pretty sure you can do with an /* eslint-disable-line */ comment on the line causing the issue, if eslint is being used. Worst comes to worst, you can make a noop function () => {} that you call with your trigger parameter - that should put the linter at bay.

How to Unmount React Functional Component?

I've built several modals as React functional components. They were shown/hidden via an isModalOpen boolean property in the modal's associated Context. This has worked great.
Now, for various reasons, a colleague needs me to refactor this code and instead control the visibility of the modal at one level higher. Here's some sample code:
import React, { useState } from 'react';
import Button from 'react-bootstrap/Button';
import { UsersProvider } from '../../../contexts/UsersContext';
import AddUsers from './AddUsers';
const AddUsersLauncher = () => {
const [showModal, setShowModal] = useState(false);
return (
<div>
<UsersProvider>
<Button onClick={() => setShowModal(true)}>Add Users</Button>
{showModal && <AddUsers />}
</UsersProvider>
</div>
);
};
export default AddUsersLauncher;
This all works great initially. A button is rendered and when that button is pressed then the modal is shown.
The problem lies with how to hide it. Before I was just setting isModalOpen to false in the reducer.
When I had a quick conversation with my colleague earlier today, he said that the code above would work and I wouldn't have to pass anything into AddUsers. I'm thinking though that I need to pass the setShowModal function into the component as it could then be called to hide the modal.
But I'm open to the possibility that I'm not seeing a much simpler way to do this. Might there be?
To call something on unmount you can use useEffect. Whatever you return in the useEffect, that will be called on unmount. For example, in your case
const AddUsersLauncher = () => {
const [showModal, setShowModal] = useState(false);
useEffect(() => {
return () => {
// Your code you want to run on unmount.
};
}, []);
return (
<div>
<UsersProvider>
<Button onClick={() => setShowModal(true)}>Add Users</Button>
{showModal && <AddUsers />}
</UsersProvider>
</div>
);
};
Second argument of the useEffect accepts an array, which diff the value of elements to check whether to call useEffect again. Here, I passed empty array [], so, it will call useEffect only once.
If you have passed something else, lets say, showModal in the array, then whenever showModal value will change, useEffect will call, and will call the returned function if specified.
If you want to leave showModal as state variable in AddUsersLauncher and change it from within AddUsers, then yes, you have to pass the reference of setShowModal to AddUsers. State management in React can become messy in two-way data flows, so I would advise you to have a look at Redux for storing and changing state shared by multiple components

React Hook - Use effect on component unmount only, not when dependency updates

I'm trying to build an input component using React Hooks that hits a remote server to save an updated value on component unmount only.
The remote server call is expensive, so I do not want to hit the server every time the input updates.
When I use the cleanup hook in useEffect, I am required to include the input value in the effect dependency array, which makes the remote API call execute on each update of the input value. If I don't include the input value in the effect dependency array, the updated input value is never saved.
Here is a code sandbox that shows the problem and explains the expected outcome: https://codesandbox.io/s/competent-meadow-nzkyv
Is it possible to accomplish this using React hooks? I know it defies parts of the paradigm of hooks, but surely this is a common-enough use case that it should be possible.
You can use a ref to capture the changing value of your text, then you can reference it in another useEffect hook to save the text:
const [text, setText] = useState("");
const textRef = React.useRef(text);
React.useEffect( () => {
textRef.current = text;
}, [text])
React.useEffect( () => {
return () => doSomething(textRef.current)
}, [])
thedude's approach is right. Tweaked it a bit, for this particular usecase as input ref is always same :
function SavedInput() {
const inputEl = useRef(null);
React.useEffect(() => {
return () => {
save(inputEl.current.value);
};
}, []);
return (
<div>
<input ref={inputEl} />
</div>
);
}
By this way you'll avoid re-render as you are not setting any state.

Resources