How to solve component refresh in Sitecore XM Cloud using Higher-Order Components
Introduction
When working with Sitecore XM Cloud, combined with tools like Chakra UI, developers may encounter issues related to component refreshing within Sitecore Pages. One major inconvenience is that some components do not refresh automatically when changes are made, which can be frustrating, especially when aiming for a smooth workflow that allows real-time editing.
To address this issue, we reached out to Sitecore support and engaged with the Sitecore Slack community, where Jeff L'Heureux and David Ly kindly offered a solution. In this article, we will explore the problem, describe the proposed solution, and provide a step-by-step guide to enhance the editing experience in Sitecore Pages.
Best Practices: Component Class Name Requirements
According to Sitecore best practices, every component should include a class name to support the advanced editing features of XM Cloud. Class names are passed through rendering parameters, and these must be applied to the components. Creating New Components
The following structure demonstrates how to incorporate class names in a compliant manner:
<div className="{`component" promo ${props.params.styles}`} id="{id" ? id : undefined}> <p>Component Markup Here</p>
</div>
However, in our project, we are using Chakra UI for component styling. Chakra UI offers a highly modular and CSS-in-JS-based styling approach that provides greater control over our styles, eliminating the need to manually manage CSS class names. This reduces the risk of conflicts and makes our code cleaner and more maintainable.
In our case, we do not want to override the className attribute because it would break the structure and integration provided by Chakra UI, which relies on props to dynamically apply styles, such as Box, Flex, and other Chakra components. Instead of overriding or manually managing class names, we utilize Chakra’s prop-based styling for layout and design, which helps in maintaining consistency and flexibility across the project.
Thus, while adhering to the spirit of Sitecore’s best practices, we choose not to apply the className attribute directly. Instead, we use a customized solution to ensure that the core functionalities required by Sitecore, including rendering parameters and editing capabilities, are preserved without compromising on the styling approach provided by Chakra UI
The Problem
The main issue arises when components do not refresh correctly in Sitecore Pages while using Chakra UI to manage styles. This limitation affects real-time changes while editing the page, which can be quite disruptive. This happens even in a native Sitecore implementation, where some containers and other components do not update as expected. To solve this, we needed a reliable way to force the refresh of these components.
Here is an example of the issue before applying the solution:
Objective
Our objective is to implement a solution that forces React components to update when their parameters or styles change, ensuring that the changes are reflected automatically in Sitecore Pages without requiring manual adjustments.
Solution: Use a Higher-Order Component (HOC) to Force Updates
To solve this problem, we use a Higher-Order Component (HOC), a common pattern in React that allows extending a component's functionality without directly modifying its code. In this case, we create an HOC that monitors changes in the component's styles and forces an update.
Step-by-Step to Implement the Solution
1.Create the HOC to Detect Style Changes
The first step is to create a component called paramsWatcher. This component is responsible for wrapping any component to which we want to apply the refresh logic.
// Author: David Ly
import { useSitecoreContext } from '@sitecore-jss/sitecore-jss-nextjs'; import { ComponentProps } from 'lib/component-props'; import { useRef, useState, useEffect } from 'react'; export function paramsWatcher<P extends ComponentProps>( WrappedComponent: React.ComponentType<P> ) { function WatcherComponent(props: P) { const ref = useRef<HTMLDivElement>(null); const [styles, setStyles] = useState(props.params.Styles); const context = useSitecoreContext(); const isEditing = context?.sitecoreContext?.pageEditing; useEffect(() => { if (!ref.current || !isEditing) { return; } console.log('Configurando el observador de mutaciones...'); const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { const [, ...classes] = ref.current?.classList.value.split(' ') ?? []; console.log('Cambio detectado en las clases:', classes); setStyles(classes.join(' ')); } }); }); observer.observe(ref.current, { attributes: true }); return () => { console.log('Desconectando el observador de mutaciones...'); observer.disconnect(); }; }, [isEditing, props.params]); if (!isEditing) { return <WrappedComponent {...props} />; } // Actualizar los estilos en los parámetros antes de renderizar props.params.Styles = styles; return ( <> <div ref={ref} className={'component ' + styles} style={{ display: 'none' }} /> <WrappedComponent {...props} /> </> ); } return WatcherComponent; }
import React from 'react'; import { Image as JssImage, Text as JssText } from '@sitecore-jss/sitecore-jss-nextjs'; import { Box, Text, Flex } from '@chakra-ui/react'; import { AlignmentStyle, ColorStyle, HeroModel, UpdateHeroParams, } from 'lib/classes/Components/Hero/HeroLib'; import { withPagesStyleChangeWatcher } from 'src/util/withPagesStyleChangeWatcher'; const HeroDefaultComponent = (): JSX.Element => ( <Box textAlign="center"> <Text fontSize="xl">No hay contenido disponible. Asigne una fuente de datos.</Text> </Box> ); const HeroComponent = ( model: HeroModel & { heroStyles?: { Alignment: AlignmentStyle; Color: ColorStyle } } ): JSX.Element => { const params = model.heroStyles && model.heroStyles.Alignment && model.heroStyles.Color ? model.heroStyles : UpdateHeroParams(model); if (model.fields && model.fields.Image && model.fields.Title) { return ( <Box position="relative"> <JssImage field={model.fields.Image} /> <Flex color={{ base: 'uniblue', md: params.Color.textColor }} position={{ base: 'static', md: 'absolute' }} height="100%" width="100%" top="0" flexDirection="column" justifyContent="center" > <Box ml={params.Alignment.marginLeft} mr={params.Alignment.marginRight} maxW={{ base: '100%', md: '50%' }} textAlign="center" > <Text variant="h1" mb={0}> <JssText field={model.fields.Title} /> </Text> <Text variant="subtitle"> <JssText field={model.fields.Description} /> </Text> </Box> </Flex> </Box> ); } return <HeroDefaultComponent />; }; export default paramsWatcher(HeroComponent);
3. Testing the Solution in Sitecore Pages
Once the HOC was implemented and the component wrapped, we tested the solution in Sitecore XM Cloud Pages. Below, you can see the result after applying the solution:
Conclusion
The solution presented uses a Higher-Order Component to observe changes in the component's styles and force a refresh when necessary. By combining the power of React with observing changes in the DOM using MutationObserver, we can address the issue of components that typically do not update automatically in Sitecore.
While this is an effective solution, it is important to note that further research is required for certain types of components (like containers), and it depends on how Sitecore manages refresh in Pages. React provides a powerful tool to handle these cases, but it's essential to test and adjust accordingly.
We would like to thank Jeff L'Heureux and David Ly for his support and the Sitecore Slack community for helping us find a viable solution to this problem.
References