MUI makeStyles/useStyles hook with theme update not re-rendering component - reactjs

I have a React project using MUI that currently has state for a theme (either light or dark). I am using the makeStyles/useStyles custom hook pattern in my component. I pass my theme state as a prop to the useStyles call in my component, but it does not update the component when theme changes. For example:
export const useStyles = makeStyles({
layout: ({ theme }: Props) => theme === LIGHT ?
lightObject... : darkObject...,
...rest
})
const Layout = ({ theme, children }: Props) => {
const { layout } = useStyles({ theme })
return (
<Box className={layout}>
{children}
</Box>
)
}
Calling setTheme in the component below (the parent of the component above), I assumed that because the parent component of this one changed state, it should have re-rendered the child. However, this is not the case. I found a "hacky" fix as shown below using the theme as a key:
// Theme, DARK, LIGHT are Typescript and Globals defined elsewhere
const App = () => {
const [theme, setTheme] = useState<Theme>(DARK)
const toggleTheme = () => setTheme(theme === LIGHT ? DARK : LIGHT)
return (
<div key={theme}>
<Layout theme={theme}>{some child...}</Layout>
</div>
);
}
When adding the key, I force the re-render. However, this again feels hacky. Is there a better way to do this? I don't really like using the ThemeProvider simply because this is a small enough app and I like to add a lot of customization and design specific elements. I also do not want to use the styled-components API.

Related

How to prevent components from refreshing when state in context is changed (React)

Im shooting my shot at making a tiktok clone as a first project to learn React. I want to have a global isVideoMuted state. When you toggle it, it should mute or unmute all sound of all videos.
Except something is not working properly. I understand that react re-renders everything when you change one thing within the contextprovider parent component from a childcomponent. This resets the tiktok "scrollprogress" to zero, since its a simple vertical slider. Is there anyway I can prevent this from happening?
This is my VideoContextProvider:
const VideoContextProvider = ({ children }: any) => {
const [isVideoMuted, setIsVideoMuted] = useState(true);
const [videos, setVideos] = useState([] as Video[]);
return (
<VideoContext.Provider
value={{
videos,
setVideos,
isVideoMuted,
setIsVideoMuted,
}}
>
{children}
</VideoContext.Provider>
);
};
And this is the VideoCard.tsx (one single video):
const VideoCard: FC<Props> = ({ video }) => {
const router = useRouter();
const [isLiked, setIsLiked] = useState(false);
const [isVideoPlaying, setIsVideoPlaying] = useState(false);
const { isVideoMuted, setIsVideoMuted } = useContext(VideoContext);
return (
.... (all the remaining videocode is here, including the <video>)
<Box p="xs">
{isVideoMuted ? (
<VscMute
fontSize="28px"
color="white"
onClick={() => setIsVideoMuted(false)}
/>
) : (
<VscUnmute
fontSize="28px"
color="white"
onClick={() => setIsVideoMuted(true)}
/>
)}
</Box>
...
);
};
export default VideoCard;
// export const VideoCardMemoized = memo(VideoCard);
)
See this video for an example of the bug: https://streamable.com/8ljkhl
Thanks!
Edit:
What I've tried so far:
Making a memoized version of the VideoCard and using that, refreshing still occurs
Moving const { isVideoMuted, setIsVideoMuted } = useContext(VideoContext); to a seperate component (VideoSidebar). Problem still occurs, since I have to 'subscribe' to the isVideoMuted variable in the video element
It rerenders only components that are subscribed to that context.
const { isVideoMuted, setIsVideoMuted } = useContext(VideoContext);
To prevent rerendering child components of the subscribed components you can use React.memo
export default React.memo(/* your component */)

React Element not re-rendering on setState when state is passed

I'm trying to implement a simple dark/light theme toggle to my website. In my base App.tsx I've implemented the state I use:
const [colorScheme, setColorScheme] = useState("light");
I pass that "colorScheme" variable as a prop to every other element. The theme toggle is contained in a header element, so I also pass the "setColorScheme" function to header as a prop. Within Header.tsx, the code triggered when the toggle is clicked is:
setColorScheme(s => s === "dark" ? "light" : "dark");
Within every specific element, I set the color scheme like so:
<ElementName className={"element_name element_name_"+colorScheme}/>
I have all the css for styling the component contained in the class "element_name", and then all relevant color data is contained in "element_name_light" or "element_name_dark".
When the toggle in the header is clicked, a re-render is triggered for the main body of the app, and for the header. But all of the other elements do not re-render. If I navigate to another element, the re-render happens and the color scheme appears as intended.
Attached is a gif of this happening.
I'm still learning React, so I'm sure it's something obvious I'm missing. I would appreciate any tips anyone can provide! Thanks
One note: I am using react functionally, rather than implementing classes for each component.
It's impossible to tell exactly what mistake you made since you haven't shared your code. But I can tell you the root mistake is not using React's context API. This will allow you to hold the color scheme and the toggle function as a global state and import them into every component via the useContext hook.
Here's an example on stackblitz: https://stackblitz.com/edit/react-ts-lhwstv?file=color-scheme-ctx.tsx
Here's the docs: https://reactjs.org/docs/context.html
Note: I'm using typescript, if you're using plain javascript just remove the type declarations and the generic typings <Type>.
You start by creating the context and giving a default value:
type ColorScheme = 'light' | 'dark';
type Props = { colorScheme: ColorScheme; toggleColorScheme: () => void };
export const ColorSchemeCtx = createContext<Props>({
colorScheme: 'light',
toggleColorScheme: () => {},
});
I like to then create a provider component for organization.
export const ColorSchemeCtxProvider: FC<PropsWithChildren<{}>> = ({
children,
}) => {
const [colorScheme, setColorScheme] = useState<ColorScheme>('light');
function toggleColorScheme() {
setColorScheme((s) => (s === 'dark' ? 'light' : 'dark'));
}
return (
<ColorSchemeCtx.Provider value={{ colorScheme, toggleColorScheme }}>
{children}
</ColorSchemeCtx.Provider>
);
};
Then wrap all components that need the context - probably just put it at the highest level possible.
root.render(
<StrictMode>
<ColorSchemeCtxProvider>
<App />
</ColorSchemeCtxProvider>
</StrictMode>
);
Now any component can get both the color scheme and / or the toggle function with useContext
export default function App() {
const { colorScheme, toggleColorScheme } = useContext(ColorSchemeCtx);
return (
<div>
<p>The color scheme is: {colorScheme}</p>
<button onClick={toggleColorScheme}>TOGGLE</button>
<CompOne />
<CompTwo />
<CompThree />
</div>
);
}
export default function CompOne() {
const { colorScheme } = useContext(ColorSchemeCtx);
return <div className={'comp-one ' + colorScheme}></div>;
}

How to access parent props from child component - react

Let's say that I want to build a reusable react Accordion Component, which will have an AccordionSummary and an AccordionDetails child like this:`
<Accordion>
<AccordionTitle>This is an accordion title</AccordionTitle>
<AccordionSummary>This is the accordion summary, which will be shown if the user clicks the accordion title </AccordionSummary>
</Accordion>
How can I make it, so that when the AccordionTitle is clicked, the summary will be shown to the corresponding accordion. Is there a way to share data between react child and parent components for each individual accordion in this case.
You can use a context here. Sorry for promoting, but I wrote a detailed article on this topic : https://rocambille.github.io/en/2022/05/02/how-to-do-a-modal-in-react-the-html-first-approach/
This could lead to something like that for your Accordion:
const AccordionContext = createContext();
function Accordion({ children }) {
const [someState, setSomeState] = useState();
return (
<AccordionContext.Provider value={ { someState, setSomeState } }>
{children}
</AccordionContext.Provider>
);
}
function AccordionTitle({ children }) {
const { someState, setSomeState } = useContext(AccordionContext);
return (
...
);
}
function AccordionSummary({ children }) {
const { someState, setSomeState } = useContext(AccordionContext);
return (
...
);
}
But as stated in my article, you may want to consider HTML stuff like the summary/details tags ;)

What is the purpose of shouldForwardProp option in styled()?

I was able to put together that shouldForwardProp specifies which props should be forwarded to the wrapped element passed as an option to styled(), but I am having trouble finding a comprehensible example of its use case.
Is prop forwarding here akin to passing down props in React?
Why would one want to prevent certain props from being forwarded to the wrapped element while using styled()?
Forgive me for my ignorance or if my question lacks clarity - I am still learning MUI and attempting to wrap my head around it.
If you're using a built-in components like div or span and you want to allow the user to customize the styles via some props.
const MyComponent = styled('div')(({ bgColor }) => ({
backgroundColor: bgColor,
}));
When you're using it like this:
<MyComponent bgColor='red'>
The prop is passed to the real element in the DOM tree as attribute:
And react will complain, something like:
Warning: React does not recognize the `bgColor` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `bgcolor` instead. If you accidentally passed it from a parent component, remove it from the DOM element.
This is why shouldForwardProp exists, to prevent styling props from being passed down and create invalid attribute:
const MyComponent = styled('div', {
shouldForwardProp: (props) => props !== 'bgColor',
})(({ bgColor }) => ({
backgroundColor: bgColor,
}));
Already great answer by #NearHuscarl!
If you are using TypeScript, I use utility function for it, so I always type prop names correctly:
export const shouldForwardProp = <CustomProps extends Record<string, unknown>>(
props: Array<keyof CustomProps>,
prop: PropertyKey,
): boolean => !props.includes(prop as string);
const MyComponent = styled('div', {
shouldForwardProp: (prop) => shouldForwardProp<MyComponentProps>(['isDisabled', 'bgColor'], prop),
})<MyComponentProps>(({ theme, isDisabled, size, bgColor }) => ({
...

React does not recognize the X prop on a DOM element

I am beginner developer and I am working on react (gatsby, TS, styled components) project. I am getting this error:
"React does not recognize the isOpen prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase isopen instead. If you accidentally passed it from a parent component, remove it from the DOM element."
export const Navigation = () => {
const [isNavigationOpen, setIsNavigationOpen] = useState(false);
const { isTablet } = useQuery();
const showNavbar = () => {
setIsNavigationOpen((previousState) => !previousState);
};
const renderElement = isTablet ? (
<>
<SvgStyled
src='bars_icon'
isOpen={isNavigationOpen}
onClick={showNavbar}
/>
<MobileNavigation isOpen={isNavigationOpen}>
{NAVIGATION_DATA.map(({ id, url, text }) => (
<LinkMobile key={id} to={url}>
<ExtraSmallParagraph>{text}</ExtraSmallParagraph>
</LinkMobile>
))}
</MobileNavigation>
</>
) : (
<FlexWrapper>
{NAVIGATION_DATA.map(({ id, url, text }) => (
<LinkDekstop key={id} to={url}>
<ExtraSmallParagraph>{text}</ExtraSmallParagraph>
</LinkDekstop>
))}
</FlexWrapper>
);
return renderElement;
};
I am sure that I am missing some fundamental react stuff or something. Maybe someone could help me and explain the reason of this error.
When this happens it is because all props passed to the styled component are then also passed down to the DOM element that you are styling.
You've likely a component that looks like the following:
const SvgStyled = styled(SVG)<{ isOpen: boolean }>`
// your CSS and logic referencing the `isOpen` prop
`;
To resolve this issue you refactor the styled component definition and explicitly pass only the props you want to the element being styled. Use an anonymous function component and destructure the prop you don't want to pass on to the DOM element, and spread the rest of the props. This ensures the className prop that styled-components is creating a CSS class for is passed through.
Example:
interface SvgStyledProps {
className?: string,
isOpen: boolean,
}
const SvgStyled = styled(({ isOpen, ...props}) => (
<Svg {...props} />
))<SvgStyledProps>`
// your CSS and logic referencing the `isOpen` prop
`;
For any other Typescript specifics/caveats with styled-components see docs.
As of styled components v5.1, you can alternatively prevent undesired props from being passed down to your React node by prefixing it with a dollar sign ($) and designating it as a transient prop:
const SvgStyled = styled(SVG)<{ $isOpen: boolean }>`
// your CSS and logic referencing the `$isOpen` prop
`;
// SVG does NOT receive props.$isOpen
docs

Resources