How to have callbacks in useState - reactjs

My component looks like this:
constMyComponent = props => {
const [events, setEvents] = useState();
useEffect(() => {
getData(id).then(function(myEvents){
setEvents(myEvents);
});
}, [id]);
When I render the variable "events" it looks good thanks to the question mark operator:
<ul>
{ events && Object.keys(events.events).map( (data, i) => (
<li key = { i }>
<span>
{ events.events[data].mykey}
</span>
</li>
))}
</ul>
However, in "useEffect" I need to apply some logic to the retrieved data. After the row with
setEvents(myEvents);
I cannot access the variable "events". I guess it is not ready yet. I read a little bit about callback. Is that the way to go?
Just adding a ".then" won't work. How do you usually access data when it is accessible in this case?

If i understand ur question correctly u wanna make the new events made logically depending on old events And wana access it,
U can access it like this,
setEvents(prevEvents => {
// some logical computations
return theNewEvents
});
As of this example,
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}
From React Docs
But, if u wanna make the logical changes to the new events,
U can easily do it in the function before setting the state (as #jonrsharpe pointed out)
constMyComponent = props => {
const [events, setEvents] = useState();
useEffect(() => {
getData(id).then(function(myEvents){
// make ur changes to `myEvents`
...
// then, set it to state
setEvents(myEvents);
});
}, [id]);

Related

Props is updating before useEffect side effect

I have the following component which shows a question, there's a button within it that allows you to reveal the answer, this is handled through the revealedResults property/state.
const Question = ({
item
}: {
item: QuestionType;
}) => {
const [revealedResults, setRevealedResults] = useState(false);
const { question, answers } = item;
useEffect(() => {
setRevealedResults(false);
}, [item]);
const handleResultReveal = () => {
setRevealedResults(true);
};
return (
<section>
<h1>Question: {question}</h1>
<button onClick={() => handleResultReveal()}>Reveal Answer</button>
<div>
{revealedResults && answers.map((answer) => <p>{answer}</p>)}
</div>
</section>
);
};
export default Question;
const Questionaire = () => {
const [question, setQuestion] = useState(questions[0]);
const [correctAnswers, setCorrectAnswers] = useState(0);
const [incorrectAnswers, setIncorrectAnswers] = useState(0);
const handleQuestionAnswer = (isCorrect: boolean): void => {
if (isCorrect) {
setCorrectAnswers(correctAnswers + 1);
} else {
setIncorrectAnswers(incorrectAnswers + 1);
}
setQuestion(questions[1]);
};
return (
<>
<Question item={question} />
<section>
<div>
<p> Did you get the answer correct?</p>
<button onClick={() => handleQuestionAnswer(true)}>Yes</button>
<button onClick={() => handleQuestionAnswer(false)}>No</button>
</div>
</section>
</>
);
};
export default Questionaire;
The question updates through the item prop. The idea is that when the item prop updates setRevealedResults is ran again to hide the revealed result of the next question.
The problem I'm having is that the prop of the new question is being flashed right before the useEffect side effect is being ran. You can see this here:
What is the correct way to deal with this?
useEffect runs after the render is done. That's why you see the page change for a moment there.
Try to use useMemo instead. It should update during the render.

Why is my state not updating in another state?

Wy state is not updating in another state, I don't know how I can fix this problem.
I wanna do a multiplayer card game with Socket.io, but I run into this problem. Whenever the selectColor state is changing, it's not going to update in the other state. I tried to print the state whenever I click on the component, but the state is just equal as the initial state. Does anybody know how to solve this?
Thanks
const [cards, setCards] = useState([]);
const [id, setId] = useState();
const [color, setColor] = useState("?");
const [selectColor, setSelectColor] = useState(false);
useEffect(() => {
socket.on("id", (id) => setId(id));
socket.on("forceDisconnect", () => setId("Full"));
socket.on("cards", (data) => {
setKarten([
data.map((element) => {
return (
<div
onClick={() =>
console.log(selectColor)
}
key={element}
>
<Card card={element} />
</div>
);
}),
]);
});
socket.on("selectColor", () => {
setSelectColor(true);
console.log("selectColor");
});
}, []);
You have created a closure and placed the value of selectColor in it when your socket.on("cards", etc. ) callback was executed (which is therefore 'frozen in time').
It is no good to create your react elements when your data arrives and store them away in your state. You are supposed to create them when your render function is called. Something like so:
socket.on("cards", (data) => setCards(data));
return (
<>
{ cards.map(
card => (
<div onClick={() => console.log(selectColor)} key={card}>
<Card card={card} />
</div>
)
)}
</>
);

How to user useState hooks on different instances?

I am pretty new on JavaScript, React, and hooks and I have created a React useState's hook to increase the counter. Based on the following code, the number changes and it goes up on one record, but when I have more than one record all counters increase no matter which button I click. I would like any suggestion as to why it behaves like this will be greatly appreciated:
import React, {useState, useEffect } from 'react';
import { Link } from 'react-router-dom'
import vendors from './vendors.css'
const VendorsShow = (props) => {
const handleDelete = (vendor) => {
props.deleteVendor(vendor.id);
}
const [ count, setCount ] = useState(0)
// similar to componentDidMount:
useEffect(() => {
document.title = `You clicked ${count} times`
})
return (
<div className="vendor-show-div" style={vendors}>
{props.vendors.map((vendor) =>
<ul key={vendor.id}>
<Link to={`/vendors/${vendor.id}`}>{vendor.name}
- ${vendor.total_cost}
</Link>
- <button onClick={() => handleDelete(vendor)}>X</button>
- <button onClick={() => setCount(count + 1)}>{count}</button>
</ul>)}
</div>
);
}
export default VendorsShow;
Each element you are mapping would need its own counter state. Either an array/object of counters, or you abstract a component that maintains its own count state.
I suggest using an object to store count values using the vendor's id property.
const VendorsShow = (props) => {
const handleDelete = (vendor) => {
props.deleteVendor(vendor.id);
};
const [counts, setCounts] = useState({}); // <-- initial empty object for counts
// similar to componentDidMount:
useEffect(() => {
document.title = `You clicked ${Object.values(counts).reduce(
(total, { count }) => total + count,
0
)} times`;
});
// initialize/update state when vendors array changes
useEffect(() => {
setCounts(
props.vendors.reduce(
(counts, { id }) => ({
...counts,
[id]: 0 // <-- store counts by vendor id
}),
{}
)
);
}, [props.vendors]);
const handleCount = (id) => () =>
setCounts((counts) => ({
...counts,
[id]: counts[id] + 1 // <-- update specific vendor's count
}));
return (
<div className="vendor-show-div" style={vendors}>
{props.vendors.map((vendor) => (
<ul key={vendor.id}>
<Link to={`/vendors/${vendor.id}`}>
{vendor.name}- ${vendor.total_cost}
</Link>
- <button onClick={() => handleDelete(vendor)}>X</button>-{" "}
<button onClick={handleCount(vendor.id)}>{counts[vendor.id]}</button>
</ul>
))}
</div>
);
};
In your code, count is an independent state value not depends on your venders.
You need to include count variable for each vender and update them accordingly.
Something like this :
const [venders, setVenders] = setState(venders);
<button onClick={() => {
var temp = [...venders];
temp [i].count = temp [i].count + 1;
setVenders(temp);
}>{venders[i].count}</button>

How To Access Callback Like in setState from useState Hook

Has anyone managed to create a sync callback for the update piece of the useState hook in react 16.8? I have been looking for one so that I can deal with synchronous actions with a 3rd party library and I can't seem to make one work for my needs.
If anyone has any references to people that have successfully completed this please add them here.
Cheers,
With hooks, you no longer need a callback from the setState function. Now, you can set state with the useState hook, and listen for its value to update with the useEffect hook. The optional second parameter of the useEffect hook takes an array of values to listen to for changes. In the example below, we are just monitoring one value for changes:
const Example = () => {
const [value, setValue] = useState(null);
useEffect(() => {
// do something when value changes
}, [value]);
return (
<button onClick={() => setValue('Clicked!')}>
Click Me!
</button>
);
};
Read more about the useEffect hook here.
You can use useEffect/useLayoutEffect to achieve this:
const SomeComponent = () => {
const [count, setCount] = React.useState(0)
React.useEffect(() => {
if (count > 1) {
document.title = 'Threshold of over 1 reached.';
} else {
document.title = 'No threshold reached.';
}
}, [count]);
return (
<div>
<p>{count}</p>
<button type="button" onClick={() => setCount(count + 1)}>
Increase
</button>
</div>
);
};
If you are looking for an out of the box solution, check out this custom hook that works like useState but accepts as second parameter a callback function:
import useStateWithCallback from 'use-state-with-callback';
const SomeOtherComponent = () => {
const [count, setCount] = useStateWithCallback(0, count => {
if (count > 1) {
document.title = 'Threshold of over 1 reached.';
} else {
document.title = 'No threshold reached.';
}
});
return (
<div>
<p>{count}</p>
<button type="button" onClick={() => setCount(count + 1)}>
Increase
</button>
</div>
);
};
It can be installed via npm install use-state-with-callback
If you want to make synchrone layout updates, use import { useStateWithCallbackInstant } from 'use-state-with-callback'; instead.

How to notify parent component of property change when using react hooks?

Lets say I have a parent component and child component. The parent component is composed of several child components. The parent component holds and manages a very complex and deep data object. Each child component provides the UI to manage various child objects and properties of the main data object. Whenever the child component changes a property value in the data object hierarchy, that change needs to bubble up to the main data object.
Here is how I might do it in a child component class by passing in a callback object...
<div>
<button onClick={e => this.setState({propA: e.target.value}, () => props.onChangePropA(this.state.propA)}>Prop A</button>
<button onClick={e => this.setState({propB: e.target.value}, () => props.onChangePropB(this.state.propB)}>Prop B</button>
</div>
Versus how I think I need to do it using hooks. The main problem I'm seeing is that there is no callback option for after the state change has completed. So I have to detect it in the useEffect and figure out which property just changed...
let prevPropA = props.propA;
let prevPropB = props.propB;
const [propA, setPropA] = useState(props.propA);
const [propB, setPropB] = useState(props.propB);
useEffect(() => {
if (prevPropA != propA) props.onChangePropA(propA);
if (prevPropB != propB) props.onChangePropB(propB);
});
<div>
<button onClick={e => {prevPropA = propA; setPropA(e.target.value)}}>Prop A</button>
<button onClick={e => {prevPropB = propB; setPropB(e.target.value)}}>Prop B</button>
</div>
I see this method getting extremely cumbersome and messy. Is there a more robust/proper way to accomplish this?
Thanks
=============================================================
Below is updated sample code based on Shubham's answer and
Ryan's feedback. Shubham answered the question as asked, but
Ryan is suggesting I give a more thorough example to ensure
I'm giving the right info for the right answer.
Here is sample code that more closely follows my real world
situation... although still a simplified example.
The parent component manages comments from users. Imagine
they can create new comments and select a date or a date-range.
They can also update existing comments. I have put the date
and date-range selector in its own component.
Therefore the parent comment manager component needs the ability
to create/load comments and pass the associated date(s) down to the
date-selector component. The user can then change the date(s)
and those values need to be propagated back up to the parent comment
manager to later be sent to the server and saved.
So you see, there is a bidirectional flow of property values (dates, etc)
that can be changed at any time from either end.
NOTE: This new example is updated using a method similar to what
Shubham suggested based on my original question.
=============================================================
const DateTimeRangeSelector = (props) =>
{
const [contextDateStart, setContextDateStart] = useState(props.contextDateStart);
const [contextDateEnd, setContextDateEnd] = useState(props.contextDateEnd);
const [contextDateOnly, setContextDateOnly] = useState(props.contextDateOnly);
const [contextDateHasRange, setContextDateHasRange] = useState(props.contextDateHasRange);
useEffect(() => { setContextDateStart(props.contextDateStart); }, [ props.contextDateStart ]);
useEffect(() => { if (contextDateStart !== undefined) props.onChangeContextDateStart(contextDateStart); }, [ contextDateStart ]);
useEffect(() => { setContextDateEnd(props.contextDateEnd); }, [ props.contextDateEnd ]);
useEffect(() => { if (contextDateEnd !== undefined) props.onChangeContextDateEnd(contextDateEnd); }, [ contextDateEnd ]);
useEffect(() => { setContextDateOnly(props.contextDateOnly); }, [ props.contextDateOnly ]);
useEffect(() => { if (contextDateOnly !== undefined) props.onChangeContextDateOnly(contextDateOnly); }, [ contextDateOnly ]);
useEffect(() => { setContextDateHasRange(props.contextDateHasRange); }, [ props.contextDateHasRange ]);
useEffect(() => { if (contextDateHasRange !== undefined) props.onChangeContextDateHasRange(contextDateHasRange); }, [ contextDateHasRange ]);
return <div>
<ToggleButtonGroup
exclusive={false}
value={(contextDateHasRange === true) ? ['range'] : []}
selected={true}
onChange={(event, value) => setContextDateHasRange(value.some(item => item === 'range'))}
>
<ToggleButton value='range' title='Specify a date range' >
<FontAwesomeIcon icon='arrows-alt-h' size='lg' />
</ToggleButton>
</ToggleButtonGroup>
{
(contextDateHasRange === true)
?
<DateTimeRangePicker
range={[contextDateStart, contextDateEnd]}
onChangeRange={val => { setContextDateStart(val[0]); setContextDateEnd(val[1]); }}
onChangeShowTime={ val => setContextDateOnly(! val) }
/>
:
<DateTimePicker
selectedDate={contextDateStart}
onChange={val => setContextDateStart(val)}
showTime={! contextDateOnly}
/>
}
</div>
}
const CommentEntry = (props) =>
{
const [activeComment, setActiveComment] = useState(null);
const createComment = () =>
{
return {uid: uuidv4(), content: '', contextDateHasRange: false, contextDateOnly: false, contextDateStart: null, contextDateEnd: null};
}
const editComment = () =>
{
return loadCommentFromSomewhere();
}
const newComment = () =>
{
setActiveComment(createComment());
}
const clearComment = () =>
{
setActiveComment(null);
}
return (
<div>
<Button onClick={() => newComment()} variant="contained">
New Comment
</Button>
<Button onClick={() => editComment()} variant="contained">
Edit Comment
</Button>
{
activeComment !== null &&
<div>
<TextField
value={(activeComment) ? activeComment.content: ''}
label="Enter comment..."
onChange={(event) => { setActiveComment({...activeComment, content: event.currentTarget.value, }) }}
/>
<DateTimeRangeSelector
onChange={(val) => setActiveComment(val)}
contextDateStart={activeComment.contextDateStart}
onChangeContextDateStart={val => activeComment.contextDateStart = val}
contextDateEnd={activeComment.contextDateEnd}
onChangeContextDateEnd={val => activeComment.contextDateEnd = val}
contextDateOnly={activeComment.contextDateOnly}
onChangeContextDateOnly={val => activeComment.contextDateOnly = val}
contextDateHasRange={activeComment.contextDateHasRange}
onChangeContextDateHasRange={val => activeComment.contextDateHasRange = val}
/>
<Button onClick={() => clearComment()} variant="contained">
Cancel
</Button>
<Button color='primary' onClick={() => httpPostJson('my-url', activeComment, () => console.log('saved'))} variant="contained" >
<SaveIcon/> Save
</Button>
</div>
}
</div>
);
}
useEffect takes a second argument which denotes when to execute the effect. You can pass in the state value to it so that it executes when state updates. Also you can have multiple useEffect hooks in your code
const [propA, setPropA] = useState(props.propA);
const [propB, setPropB] = useState(props.propB);
useEffect(() => {
props.onChangePropA(propA);
}, [propA]);
useEffect(() => {
props.onChangePropB(propB);
}, [propB]);
<div>
<button onClick={e => {setPropA(e.target.value)}}>Prop A</button>
<button onClick={e => {setPropB(e.target.value)}}>Prop B</button>
</div>

Resources