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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With