I want my parent component state to be updated by its children independently, without the parent doing micromanagement and being aware of variables modified by its children. I'm abstracting these modifications with a single method
onIssueDataChanged. The modifiedstateobject will then be transferred to GraphQL, so no need for the parent to know what's in it. My problem, is that children cannot callonIssueDataChangedinuseEffect.
Here's the concrete problem:
I have a form IssueFieldsForm that handles the saving of an object called issue.
The object has a gallery of pictures included, the pictures can be added, removed or selected as the main issue picture. That logic is isolated in a separate component called GalleryComp, and the info is stored in a non-persistent issue field called attachmentsMeta.
My problem is that I want to isolate handling of the attachmentsMeta in GalleryComp and have in IssueFieldsForm a generic method I called onIssueDataChanged which takes only changed values and updates the issue variable state with it. That seems hard to do since I initialize an attachmentsMeta state in GalleryComp with the current attachments in the useEffect hook, but I cannot send that back to the issue state object in the parent.
export default function GalleryComp(props) {
const {issue, onItemsChanged} = props
const [attachmentsMeta, setAttachmentsMeta] = useState({
images: [],
attachmentsAttributes: {},
mainAttachmentIndex: 0
})
useEffect(() => {
const newAttachmentsMeta = {...attachmentsMeta}
newAttachmentsMeta.images = []
logger.debug('attachments cont before concat: ', newAttachmentsMeta.images.length)
newAttachmentsMeta.images = unionBy(issue.attachments, issue.attachmentsMeta?.images, 'url')
logger.debug('after concat: ', newAttachmentsMeta.images.length)
newAttachmentsMeta.mainAttachmentIndex = issue.mainAttachmentIndex
logger.debug('displaying attachments: ', newAttachmentsMeta)
setAttachmentsMeta(newAttachmentsMeta)
}, [issue?.attachmentsMeta])
That is a problem for me because if I don't do any change in the attachments, onIssueDataChanged will never be called to update the variable issue.attachmentsMeta with the initial attachments. Leading me to lose my original attachments on save (since they were not transferred to my single source of truth issue.attachmentsMeta).
On the other hand, if I initialize issue.attachmentsMeta in the parent component, I lose the elegance of having each component handle its own variables.
p.s: Obviously I cannot call onIssueDataChanged in the effect since it would trigger a state change and enter an infinite loop.
How would you do this?
I solved the issue by exporting an initGalleryData function from GalleryComp and using it in the form. This way, details are hidden in the child.
This way, I can reuse the Gallery Component with any object, by simply not forgetting to initiate the object and by providing an 'onDataChange' modificator
This code is used in the weally.org project, gathering people for protests online by concens
export default function GalleryComp(props) {
const {issue, onItemsChanged} = props
const attachmentsMeta = issue.attachmentsMeta
const {t} = useTranslation('complaintList')
function setMainAttachmentIndex(index) {
const attachmentsMetaCopy = {...attachmentsMeta}
attachmentsMetaCopy.mainAttachmentIndex = index
onItemsChanged({attachmentsMeta:attachmentsMetaCopy})
}
function onAttachmentRemoveAction(attachmentId, removed) {
const attachmentsModified = {...attachmentsMeta}
let attr = attachmentsModified.attachmentsAttributes[attachmentId]
if (!attr) {
attr = {deleted: removed}
attachmentsModified.attachmentsAttributes[attachmentId] = attr
} else {
attr.deleted = removed
}
onItemsChanged({attachmentsMeta: attachmentsModified})
}
if (attachmentsMeta.images?.length === 0)
return <Divider style={sx.descDivider}/>
return (
<>
<Gallery attachments={attachmentsMeta} onRemoveAction={onAttachmentRemoveAction}
selectedIndex={attachmentsMeta.mainAttachmentIndex}
onSelectionChange={setMainAttachmentIndex}/>
<Divider style={sx.descDivider}/>
</>
)
}
export function initGalleryData(issue) {
issue.attachmentsMeta = {
images: [...issue.attachments],
attachmentsAttributes: {},
mainAttachmentIndex: 0
}
}
I then just have to initialize the needed fields at component creation.
export default function IssueFieldsForm(props) {
const {t} = useTranslation('complaintList')
const {
issue,
onIssueDataChanged,
markers,
onMarkersChanged,
onMarkerSelected,
setPlaceMarkersDisplayed,
placeMarkersDisplayed
} = props
initGalleryData(issue);
const [validationState, setValidationState] = useState(new ValidationResult())
//...
return (
<Card sx={sx.issueFieldsForm} id="IssueFieldsForm">
<Grid item sx={sx.item} ref={scrollTopRef}>
<Typography variant={'h5'} sx={sx.header}>{t('edit.header')}</Typography>
</Grid>
//...
<GalleryComp issue={issue} onItemsChanged={onIssueDataChanged}/>
//...
)