Updating React Component state Dynamically based on server state - javascript

I have a component that shows no of players logged in as soon as the component loads.
function PlayerList({gamePin}){
const [playerList, setPlayerList] = useState("");
useEffect( ()=>{
axios.get("http://localhost:8080/game/lobby/"+gamePin)
.then( response =>{
setPlayerList(response.data)
})
})
return(
<div className='container'>
<div className='lobbycontainer'>
<h1>Lobby</h1>
<Grid container spacing={3}>
{playerList.map(player=>{
<Player {PlayerName,PlayerId} />
})}
</Grid>
</div>
</div>
)}
export default PlayerList;
This will display the name of the player who is logged in and any other players already logged into the lobby.
But my question is how do the players who are already logged in will get to know about the new players who joined.
Possible Approach
Send a request with a time interval of every 2 seconds.
setInterval(httpRequest,2000);
Is this the right way to do this? are there any alternate approaches?
How does a component dynamically update its state based on the changes in the backend? and respond to the changes by rerendering the component to reflect the changes.

That is pretty close. Use a "componentDidMount" useEffect hook patter, i.e. provide an empty dependency array ([]). Refactor the GET request into a callback function invoked on an interval and don't forget to return an effect cleanup function to clear the interval when this component unmounts.
useEffect(() => {
const timerId = setInterval(() => {
axios.get("http://localhost:8080/game/lobby/" + gamePin)
.then(response => setPlayerList(response.data))
}, 2000);
return () => clearInterval(timerId);
}, []);

Related

Have a function return JSX, then setState

I'm new to React (and programming in general) and have come across an issue. I have created this component and using react-copy-to-clipboard package, onCopy I would like to copy a string, set copied to true and then after a couple of seconds set copied to false. When copied is true I want a div with some text to be displayed and when copied is set to false, I want it to disappear.
I have tried using setTimeout without success, as you can see in the code below and I suppose it's not working as I wish as the JSX doesn't re-render when the copied state is false again. I thought of using promises but couldn't figure out how to do that when I want to return JSX.
import React, { useState, useCallback } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import styled from 'styled-components';
const ClipBoard = () => {
const [copied, setCopied] = useState(false);
const onCopy = useCallback(() => {
setTimeout(setCopied(false), 2000)
setCopied(true);
console.log(copied)
}, [copied]);
return (
<div className="app">
<section className="section">
{copied ? <div><p>Copied</p></div> : null}
<CopyToClipboard onCopy={onCopy} text="Text I want to copy">
<button type="button">
<span className="sr-only">
E-mail
</span>
Text I want to copy
</button>
</CopyToClipboard>
</section>
</div>
);
}
export default ClipBoard;
Issue
The code is immediately invoking the state update to enqueue the copied state value to false.
setTimeout(
setCopied(false), // <-- immediately called!
2000
);
setTimeout expects a callback function that will be invoked when the timeout expires.
Solution
Pass an anonymous function to setTimeout to be called when the timeout expires. Since React state updates are enqueued and asynchronously processed, the console.log(copied) will only log the unupdated copied state value closed over in callback scope from the current render cycle. If you want to log state values use the useEffect hook. Also, you should consider the edge case where the component unmounts prior to the timeout expiring and clear any running timers.
Full Example:
const ClipBoard = () => {
const [copied, setCopied] = useState(false);
const timerRef = useRef(); // React ref for timer reference
useEffect(() => {
// Return cleanup function to clear timer when unmounting
return () => {
clearTimeout(timerRef.current);
};
}, []);
useEffect(() => {
// Effect to log any copied state updates
console.log(copied);
}, [copied]);
const onCopy = useCallback(() => {
timerRef.current = setTimeout( // save timer reference
() => setCopied(false), // function callback to update state
2000,
);
setCopied(true);
}, []); // empty dependency array
return (
<div className="app">
<section className="section">
{copied && <div><p>Copied</p></div>}
<CopyToClipboard onCopy={onCopy} text="Text I want to copy">
<button type="button">
<span className="sr-only">
E-mail
</span>
Text I want to copy
</button>
</CopyToClipboard>
</section>
</div>
);
}

React hook stale closure: setState only triggers twice in callback function of a subscription

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

React hooks: why does useEffect need an exhaustive array of dependencies?

I've got the following use case in a React component.
It is a search user input that uses React Autosuggest. Its value is always an ID, so I only have the user ID as a prop. Therefore at first load to show the username value, I need to fetch it at first mount.
EDIT: I don't want to fetch the value again when it changes later, because I already have the value from my suggestions request.
type InputUserProps = {
userID?: string;
onChange: (userID: string) => void;
};
// Input User is a controlled input
const InputUser: React.FC<InputUserProps> = (props) => {
const [username, setUsername] = useState<string | null>(null);
useEffect(() => {
if (props.userID && !username) {
fetchUsername(props.userID).then((username) => setUsername(username));
}
}, []);
async function loadSuggestions(inputValue: string): Suggestion[] {
return fetchSuggestions(inputValue);
}
function selectSuggestion(suggestion: Suggestion): void {
props.onChange(suggestion.userID); // I don't want to rerun the effect from that change...
setUsername(suggestion.username); // ...because I set the username here
}
return (
<InputSuggest
value={props.userID}
label={username}
onSuggestionsRequested={loadSuggestions}
onSuggestionSelected={selectSuggestion}
/>
);
};
export default InputUser;
(I'm adding a simplified way this component is called)
const App: React.FC<AppProps> = (props) => {
// simplified, I use react-hook-form in the real code
const [userID, setUserID] = useState<string?>(any_initial_value_or_null);
return <InputUser userID={userID} onChange={(newValue)=>setUserID(newValue)} />
};
export default App;
It works, but I have the following warning on my useEffect hook
React Hook useEffect has missing dependencies: 'props.userID' and 'username'. Either include them or remove the dependency array.eslint(react-hooks/exhaustive-deps)
But if I do so, I will run it more than once as the username value is changed by the hook itself! As it works without all the dependencies, I'm wondering:
How can I solve my case cleanly?
What are those dependencies for and why is it advised to be exhaustive with them?
Looks like the userId should indeed be a dependency, since if it changes you want to run your query again.
I think you can drop the check for username and always fetch when, and only when the userId changes:
useEffect(() => {
if(props.userID) {
fetchUsername(props.userID)
.then((username) => setUsername(username))
}
}, [props.userID])
Generally speaking, you want to list all closure variables in your effect to avoid stale references when the effect is executed.
-- EDIT to address OP questions:
Since in your use case you know you only want to perform the action on the initial mount passing an empty dependency array is a valid approach.
Another option is to keep track of the fetched userID, e.g.
const fetchedIds = useRef(new Set())
whenever you fetch username for a new ID you can update the ref:
fetchedIds.current.add(newId)
and in your effect you can test:
if (props.userID && !fetchedIds.current.has(props.userID)) {
// do the fetch
}
What are those depdendencies?
useEffect takes an optional second argument which is an array of dependencies. Dependencies of the useEffect hook tell it to run the effect whenever one if its dependency changes.
If you don't pass an optional second argument to useEffect hook, it will execute every time the component re-renders. Empty dependency array specifies that you want to run the effect only once, after the initial render of the component. In this case, useEffect hook will almost behave like componentDidMount in class components.
why is it advised to be exhaustive on them ?
Effects see props and state from the render they were defined in. So when you use something from the scope of functional component that participates in react's data flow like props or state, inside the callback function of useEffect hook, that callback function function will close over that data and unless new effect is defined with the new values of props and state, your effect will see the stale props and state values.
Following code snippet demonstrates what could go wrong if you lie about the dependencies of useEffect hook.
In the following code snippet, there are two components, App and User. App component has three buttons and maintains the id of the user which is displayed by the User component. User id is passed as a prop from App to User component and User component fetches the user with the id, passed as prop, from the jsonplaceholder API.
Now the problem in the following code snippet is that it doesn't works correctly. Reason is that it lies about the dependency of the useEffect hook. useEffect hook depends on userID prop to fetch the user from the API but as i skipped adding userID as a dependency, useEffect hook doesn't executes every time userID prop changes.
function User({userID}) {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
if (userID > 0) {
fetch(`https://jsonplaceholder.typicode.com/users/${userID}`)
.then(response => response.json())
.then(user => setUser(user))
.catch(error => console.log(error.message));
}
}, []);
return (
<div>
{user ? <h1>{ user.name }</h1> : <p>No user to show</p>}
</div>
);
}
function App() {
const [userID, setUserID] = React.useState(0);
const handleClick = (id) => {
setUserID(id);
};
return(
<div>
<button onClick={() => handleClick(1)}>User with ID: 1</button>
<button onClick={() => handleClick(2)}>User with ID: 2</button>
<button onClick={() => handleClick(3)}>User with ID: 3</button>
<p>Current User ID: {userID}</p>
<hr/>
<User userID={userID} />
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Above code snippet shows one of the few problems that could arise if you lie about the dependencies and this is why you must not skip or lie about the dependencies of the useEffect hook or any other hook that has a dependency array, for example useCallback hook.'
To fix the previous code snippet, you just have to add userID as a dependency to the dependency array of useEffect hook so that if executes whenever userID prop changes and new user is fetched with the id equal to userID prop.
function User({userID}) {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
if (userID > 0) {
fetch(`https://jsonplaceholder.typicode.com/users/${userID}`)
.then(response => response.json())
.then(user => setUser(user))
.catch(error => console.log(error.message));
}
}, [userID]);
return (
<div>
{user ? <h1>{ user.name }</h1> : <p>No user to show</p>}
</div>
);
}
function App() {
const [userID, setUserID] = React.useState(0);
const handleClick = (id) => {
setUserID(id);
};
return(
<div>
<button onClick={() => handleClick(1)}>User with ID: 1</button>
<button onClick={() => handleClick(2)}>User with ID: 2</button>
<button onClick={() => handleClick(3)}>User with ID: 3</button>
<p>Current User ID: {userID}</p>
<hr/>
<User userID={userID} />
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
In your case, if you skip adding props.userID in the dependency array of useEffect hook, your effect will not fetch the new data when the prop userID changes.
To learn more about the negative impacts of omitting the dependencies of useEffect hook, see:
Don’t Lie to React About Dependencies
How can I solve my case cleanly ?
Since your effect depends on the prop value userID, you should include it in the dependency array to always fetch the new data whenever the userID changes.
Adding props.userID as a dependency to useEffect hook will trigger the effect every time props.userID changes but the problem is that you are unnecessarily using username inside the useEffect. You should remove it because that is not needed since username value doesn't and shouldn't decide when new user data should be fetched. You just want the effect to run whenever props.userID changes.
You could also decouple the action from the state update by using the useReducer hook to manage and update the state.
Edit
Since you only want to run the effect even when userID is used by useEffect hook, in your case it is ok to have an empty array as the second argument and ignore the eslint warning. You could also not omit any dependencies of the useEffect hook and use some condition in useEffect hook that evaluates to false after the effect runs for the first time and updates the state.
Personally i would suggest you to try to change how your components are structured so that you don't have to deal with this kind of problem in the first place.
If you are sure that before mounting the InputUser component, all dependent props have been filled and they have the right value, then add eslint-disable-next-line react-hooks/exhaustive-deps right before }, []) line, but If depended props have no value at first component mounting, So you have to add them in useEffect dependencies.

The component does not work asynchronously

I have a component that makes a request and displays a list of jobs.
import React, { useState, useEffect, Fragment } from 'react';
import { Jobs } from '../components/Jobs.component';
export const Fixed = () => {
const [jobs, setJobs] = useState([]);
useEffect(() => {
getItems();
}, []);
async function getItems() {
const url = 'http://localhost:8081/api/fixed/list';
const res = await fetch(url, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
});
const data = await res.json();
console.log(data);
setJobs(data.jobsList);
console.log(jobs);
}
return (
<Fragment>
{jobs.map(job => (
<div>
<Jobs job={job} />
</div>
))}
</Fragment>
);
};
My problem is that the first console outputs an array of jobs, but the second console displays an empty array. And an empty value is passed to the job component, which causes an error.
He does not have time to write the work in a state? Where am I wrong?
Method setJobs needs some time to change state so console.log runs faster than value changes.
You should render list if the array length is bigger than 0.
{jobs.length && jobs.map(job => <Jobs job={job} />)}
State updates are run asynchroniously
The reason your console.log shows an empty array is because setJobs runs asynchroniously and will update jobs value on next render. Looking at react setState documentation (same as useState react hooks) :
setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state.
And so
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall.
const ... jobs ... is a constant - it will be a different constant in 2 different renders, but it will not change value during a single render
The jobs inside getItems is a closure and will reference to the value from the first render, while setJobs will only change the value in second render.
It's similar to the following:
const rememberJobs = (jobs) => () => console.log(jobs)
const first = rememberJobs([])
const second = rememberJobs([1, 2, 3])
first()
second()

how update the value of useEffect() hook on button click

I have a functional component and I have created a button inside it. I am also using a "Use_effect()" hook. My main is to re-render the functional component, update the use_effect() hook when the button is clicked.
const Emp_list = (props) => {
useEffect(() => {
props.getList(props.state.emp);
}, []);
return (
<div>
{props.state.emp.map((val ) =>
{val.feature_code}
{val.group_code}
<button onClick = {() => props.removeEmpFromList(val.feature_code)} > Remove </button>
<EmpForm empList={props.state.emp}
onChangeText = {props.onChangeText}
/>
</div>
<button onClick= {() => props.getdata (props.state)}>Get Names</button>
<p>
</div>
);
};
export default Emp_list;
removeEmpFromList = (i) => {
const remaining = this.state.emp( c => c.feature_code !== i)
this.setState({
emp: [...remaining]
})
}
When I click the Remove button , it will basically remove the employee from the list. The function removeEmpFromList will update the state.
The functional component EmpForm basically shows the list of all employees.
So I want to re-render the page so that, it updates the state value in useEffect() hook. So when EmpForm is called again on re-rending it shows the updated list.
You didn't provide the code for removeEmpFromList() ... but probably it updates the state by mutation therefor component gets the same object ref - compared shallowly - no difference, no reason to rerender.
Modify removeEmpFromList() method to create a new object for emp - f.e. using .filter.
If not above then passing entire state is the source of problem (the same reason as above).
Simply pass only emp as prop or use functions in setState() (to return a new object for the entire state) this way.
I figured it out! Thanks for the help guys.
So, it was not re-rendering because initally, useEffect() second parameter was [] , if you change it to props.state then it will update the changes made to the state and re-render the component automatically.
useEffect(() => {
props.getList(props.state.emp);
}, [props.state.emp]);

Categories

Resources