Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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>
    );
}
like image 662
Jim Ott Avatar asked Jan 29 '19 17:01

Jim Ott


1 Answers

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>
like image 103
Shubham Khatri Avatar answered Nov 08 '22 04:11

Shubham Khatri