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>;
}
Related
I'm new to React and I'm stuck trying to get this onClick function to work properly.
I have a component "Row" that contains a dynamic list of divs that it gets from a function and returns them:
export function Row({parentState, setParentState}) {
let divList = getDivList(parentState, setParentState);
return (
<div>
{divList}
</div>
)
}
Say parentState could just be:
[["Name", "info"],
["Name2", "info2"]]
The function returns a list of divs, each with their own className determined based on data in the parentState. Each one needs to be able to update its own info in parentState with an onClick function, which must in turn update the className so that the appearance of the div can change. My code so far seems to update the parentState properly (React Devtools shows the changes, at least when I navigate away from the component and then navigate back, for some reason), but won't update the className until a later event. Right now it looks like this:
export function getDivList(parentState, setParentState) {
//parentState is an array of two-element arrays
const divList = parentState.map((ele, i) => {
let divClass = "class" + ele[1];
return (
<div
key={ele, i}
className={divClass}
onClick={() => {
let newParentState =
JSON.parse(JSON.stringify(parentState);
newParentState[i][1] = "newInfo";
setParentState(newParentState);}}>
{ele[0]}
</div>
)
}
return divList;
}
I have tried to use useEffect, probably wrong, but no luck. How should I do this?
Since your Row component has parentState as a prop, I assume it is a direct child of this parent component that contains parentState. You are trying to access getDivList in Row component without passing it as a prop, it won't work if you write your code this way.
You could use the children prop provided by React that allow you to write a component with an opening and closing tag: <Component>...</Component>. Everything inside will be in the children. For your code it would looks like this :
import React from 'react';
import { render } from 'react-dom';
import './style.css';
const App = () => {
const [parentState, setParentState] = React.useState([
['I am a div', 'bg-red'],
['I am another div', 'bg-red'],
]);
React.useEffect(
() => console.log('render on ParentState changes'),
[parentState]
);
const getDivList = () => {
return parentState.map((ele, i) => {
return (
<div
key={(ele, i)}
className={ele[1]}
onClick={() => {
// Copy of your state with the spread operator (...)
let newParentState = [...parentState];
// We don't know the new value here, I just invented it for the example
newParentState[i][1] = [newParentState[i][1], 'bg-blue'];
setParentState(newParentState);
}}
>
{ele[0]}
</div>
);
});
};
return <Row>{getDivList()}</Row>;
};
const Row = ({ children }) => {
return <>{children}</>;
};
render(<App />, document.getElementById('root'));
And a bit of css for the example :
.bg-red {
background-color: darkred;
color: white;
}
.bg-blue {
background-color:aliceblue;
}
Here is a repro on StackBlitz so you can play with it.
I assumed the shape of the parentState, yu will have to adapt by your needs but it should be something like that.
Now, if your data needs to be shared across multiple components, I highly recommand using a context. Here is my answer to another post where you'll find a simple example on how to implement a context Api.
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
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.
(I hope and assume that there is a simple answer to this question, but I could not find it in the docs)
I've just converted a React project to use Styled Components, and love the DRYness and reusability. But, I've not yet figured out the syntax for using styled components in existing functional components that do other work too.
Here's one example:
const StyledSearchBarPane = styled(Pane)`
grid-area: search-bar;
`;
const SearchBarPane = () => {
const {query} = useContext(panelContext);
let [newQuery, setNewQuery] = useState(query);
return (
<StyledSearchBarPane>
<Bar>
<SearchInput newQuery={newQuery} setNewQuery={setNewQuery}/>
</Bar>
</StyledSearchBarPane>
);
};
How can I avoid naming StyledSearchBarPane? It's only used once -- in SearchBarPane -- and I'd rather it were simply part of the latter's definition.
Yes you can.
Basically what you need to do is use your component as you would normally do and if you want to use a local style on the component without creating one you can use a Wrapper like you did.
But the change is, instead of exporting your component you export the wrapper. Then your component will receive as props className and you need to put it where you want to apply thoses style, in the example since it's globally you put in the root element.
If you don't put the className props it won't style anything.
const SearchBarPane = ({className}) => {
const {query} = useContext(panelContext);
let [newQuery, setNewQuery] = useState(query);
return (
<div className={className}>
<Bar>
<SearchInput newQuery={newQuery} setNewQuery={setNewQuery}/>
</Bar>
</div>
);
};
const StyledSearchBarPane = styled(SearchBarPane)`
grid-area: search-bar;
`;
export default StyledSearchBarPane;
https://styled-components.com/docs/advanced
I'd like part of the record to be included in the label for a BooleanField (and BooleanInput). I'm trying to use WithProps to accomplish this.
If I use
<BooleanField source="FileSystem" label="FileSystem" />
This seems to work just fine. If, instead I try to wrap it
const makeLabel = (props)=>{
let label = `Filesystem for ${props.record.id}`;
return {label};
}
const withLabel = withProps(makeLabel);
const BooleanFieldWithLabel = compose(withLabel)((props)=>{
console.log("props after compose",props);
return <BooleanField {...props}/>
});
And then use <BooleanFieldWithLabel source="FileSystem" /> It doesn't render any label. I've tried a few different ways and nothing seems to work even though I can see in the console.log that the correct label is in props. What am I doing wrong here?
I have the same question, I cannot display the label base on field's value on "Show" page.
From react-admin source code, it seems only I set "addLabel" prop on the direct child of "SimpleShowLayout" or "TabbedShowLayout", then I can see label on my custom field.
But it is not configurable, I want to show/hide label base on field's value. Do I need to implement my own custom "SimpleShowLayout" or "TabbedShowLayout"? Or is there any better approaches?
Update my post.
I just figure out the solution by implementing an HOC like below. I am wondering is there any better approaches to implement the same feature?
import React from "react";
import get from "lodash/get";
import { TextField, DateField, Labeled } from "react-admin";
const NullableField = WrappedComponent => props => {
const { record, source } = props;
const value = get(record, source);
return value ? (
<Labeled {...props}>
<WrappedComponent {...props} />
</Labeled>
) : null;
};
const NullableTextField = NullableField(TextField);
const NullableDateField = NullableField(DateField);
export { NullableTextField, NullableDateField };