I have been working on a React web application with a dynamic theme using the emotion-theming library. So a user can switch between environments and each environment has its own theme.
I have created my own CustomThemeProvider which I use to dynamicly change the theme. Below is the code.
export interface CustomThemeContextValue {
customTheme?: Theme;
setCustomTheme: (theme: Theme) => void;
};
const CustomThemeContext = React.createContext<CustomThemeContextValue>({
customTheme: undefined,
setCustomTheme: (theme) => { }
});
interface CustomThemeProviderProps {
}
export const CustomThemeProvider: FC<CustomThemeProviderProps> = (props) => {
const [customTheme, setCustomTheme] = useState<Theme>(theme);
const context: CustomThemeContextValue = React.useMemo(() => ({
customTheme,
setCustomTheme
}), [customTheme, setCustomTheme]);
return (
<CustomThemeContext.Provider value={context}>
<ThemeProvider theme={customTheme} {...props} />
</CustomThemeContext.Provider>
);
};
export const useCustomTheme = () => {
const context = React.useContext(CustomThemeContext);
if (!context) {
throw new Error('useCustomTheme must be used within a CustomThemeProvider');
}
return context;
};
The provider is implemented in the root like so
const Root = () => {
return (
<StrictMode>
<CustomThemeProvider>
<Normalize />
<Global styles={globalStyle} />
<App />
</CustomThemeProvider>
</StrictMode>
);
};
So this code is working, I can get the theme within a function component using the emotion useTheme hook like below:
const theme: Theme = useTheme();
But the question is how to get the theme out of the emotion ThemeProvider and use it in certain situations. Is it possibe to use it in a context like
export const style: Interpolation = {
cssProp: value
};
Or is it usable in a context like below where styled.button is from emotion/styled.
const Button: FC<HTMLProps<HTMLButtonElement> & ButtonProps> = styled.button([]);
and is it usable in the emotion/core method css() like below
const style = css({
cssProp: value
});
I find it very hard to find answers to these questions using google so I hope somebody here can help me out.
So after a while I have finally found an answer to my own question and I like to share it with everyone as it is very hard to find. so here it is.
Instead of Interpolation you can use InterpolationWithTheme like below:
import { InterpolationWithTheme } from '#emotion/core';
export const style: InterpolationWithTheme<Theme> = (theme) => ({
cssProp: theme.value
});
This way you can get the theme out of the ThemeProvider.
With the styled componenents you can implement it like below:
const Button: FC<HTMLProps<HTMLButtonElement> & ButtonProps>
= styled.button(({ theme }: any) => ([
{
cssProp: theme.value
}
]);
And finally when you want to use the css() with the themeProvider you have to replace it with the InterpolationWithTheme to make it work just like in the first example of this answer.
These answers have been found by a combination of looking in the emotionjs docs and inspecting the emotionjs types/interfaces.
Related
I wanted to know how I can load in external javascript into a specific story in storybook. The only documentation I can find right now is how to do it globally https://storybook.js.org/docs/react/configure/story-rendering. Doing this works, i would just like to save on performance since only one of my stories uses an external js script.
No, there isn't a standard way to do this in storybook currently (version 6.5).
However you can achieve it with a decorator.
Depending on your needs it could look something like this (this is for a React story):
import { Story, Meta } from '#storybook/react';
import { useEffect } from '#storybook/addons';
export default {
title: 'My Story',
component: MyStory,
decorators: [
(Story) => {
useEffect(() => {
const script = document.createElement('script');
script.src = '/my-script';
document.body.appendChild(script);
}, []);
return <Story />;
},
],
};
There are some caveats here though:
The scripts will remain loaded as you navigate to other stories.
The story will render before the script has loaded.
To handle these caveats you can:
Add a cleanup handler to useEffect.
Don't render your <Story/> until the script has loaded.
For example:
import { Story, Meta } from '#storybook/react';
import { useEffect, useState } from '#storybook/addons';
export default {
title: 'My Story',
component: MyStory,
decorators: [
(Story) => {
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const script = document.createElement('script');
script.onload = () => {
setIsLoaded(true);
};
script.src = '/my-script';
document.body.appendChild(script);
return () => {
// clean up effects of script here
};
}, []);
return isLoaded ? <Story /> : <div>Loading...</div>;
},
],
};
If you have multiple scripts you'll have to wrap all the onload events into a Promise.all.
This could be wrapped up in an addon similar to storybook-addon-run-script.
I want to load a component dynamically based on the route. I'm trying to make a single page which can load any individual component for testing purposes.
However whenever I try to do import(path) it shows the loader but never actually loads. If I hard code the exact same string that path contains then it works fine. What gives? How can I get nextjs to actually dynamically import the dynamic import?
// pages/test/[...component].js
const Test = () => {
const router = useRouter();
const { component } = router.query;
const path = `../../components/${component.join('/')}`;
console.log(path === '../../components/common/CircularLoader'); // prints true
// This fails to load, despite path being identical to hard coded import
const DynamicComponent = dynamic(() => import(path), {
ssr: false,
loading: () => <p>Loading...</p>,
});
// This seems to work
const DynamicExample = dynamic(() => import('../../components/Example'), {
ssr: false,
loading: () => <p>Loading...</p>,
});
return (
<Fragment>
<h1>Testing {path}</h1>
<div id="dynamic-component">
<DynamicComponent /> <!-- this always shows "Loading..." -->
<DynamicExample /> <!-- this loads fine. -->
</div>
</Fragment>
);
};
export default Test;
I put dynamic outside of the component, and it work fine.
const getDynamicComponent = (c) => dynamic(() => import(`../components/${c}`), {
ssr: false,
loading: () => <p>Loading...</p>,
});
const Test = () => {
const router = useRouter();
const { component } = router.query;
const DynamicComponent = getDynamicComponent(component);
return <DynamicComponent />
}
I had the same issue like the thread opener.
The Documentation describe, that it's not possible to use template strings in the import() inside dynamic:
In my case it was also impossible to add an general variable with the path there...
Solution
I've found an easy trick to solve this issue:
// getComponentPath is a method which resolve the path of the given Module-Name
const newPath = `./${getComponentPath(subComponent)}`;
const SubComponent = dynamic(() => import(''+newPath));
All the MAGIC seems to be the concatenation of an empty String with my generated Variable newPath: ''+newPath
Another Solution:
Another Solution (posted by bjn from the nextjs-Discord-Channel):
const dynamicComponents = {
About: dynamic(() => import("./path/to/about")),
Other: dynamic(() => import("./path/to/other")),
...
};
// ... in your page or whatever
const Component = dynamicComponents[subComponent];
return <Component />
This example might be useful, if you know all dynamically injectable Components.
So you can list them all and use it later on in your code only if needed)
The below code worked for me with dynamic inside the component function.
import dynamic from "next/dynamic";
export default function componentFinder(componentName, componentPath) {
const path = componentPath; // example : "news/lists"
const DynamicComponent = dynamic(() => import(`../components/${path}`),
{
ssr: false,
loading: () => <p>Loading Content...</p>,
});
return <DynamicComponent />;
}
It happens because router.query is not ready and router.query.component is undefined at the very first render of dynamic page.
This would print false at first render and true at the following one.
console.log(path === '../../components/common/CircularLoader');
You can wrap it with useEffect to make sure query is loaded.
const router = useRouter();
useEffect(() => {
if (router.asPath !== router.route) {
// router.query.component is defined
}
}, [router])
SO: useRouter receive undefined on query in first render
Github Issue: Add a ready: boolean to Router returned by useRouter
As it was said here before the dynamic imports need to be specifically written without template strings. So, if you know all the components you need beforehand you can dynamically import them all and use conditionals to render only those you want.
import React from 'react';
import dynamic from 'next/dynamic';
const Component1 = dynamic(() => import('./Component1').then((result) => result.default));
const Component2 = dynamic(() => import('./Component2').then((result) => result.default));
interface Props {
slug: string;
[prop: string]: unknown;
}
export default function DynamicComponent({ slug, ...rest }: Props) {
switch (slug) {
case 'component-1':
return <Component1 {...rest} />;
case 'component-2':
return <Component2 {...rest} />;
default:
return null;
}
}
I'd like to implement switching between dark/light theme dynamically with Ant design v4.
It's possible to customize the theme with other CSS/LESS imports as it's written here:
https://ant.design/docs/react/customize-theme#Use-dark-theme
But I'm not sure how to switch between those themes dynamically from the code. I have a variable in my React app (darkMode) which indicates if the dark theme is currently used. I have to provide correct CSS files when this variable is changed. But I can't import CSS dynamically only when some condition is fulfilled, because it's not way how the imports work.
I tried to do something messy with require like in the following code, but it's a very very bad approach and it's still not working properly (because CSS is injected but probably not withdrawn.
):
const Layout = () => {
...
useEffect(() => {
if (darkMode === true) {
require("./App.dark.css")
} else {
require("./App.css")
}
}, [darkMode])
return (
<Home />
)
}
It should be possible to switch themes somehow because it's already implemented in Ant design docs (https://ant.design/components/button/):
Do you have any idea how to do it?
Thanks!
This is what I am using for now -
PS -
I don't know if this will yield optimal bundle size.
changing theme results in a page reload.
make a folder called "themes" - it would have 6 files -> dark-theme.css, dark-theme.jsx, light-theme.css, light-theme.jsx, use-theme.js, theme-provider.jsx. Each of them is described below.
dark-theme.css
import "~antd/dist/antd.dark.css";
dark-theme.jsx
import "./dark-theme.css";
const DarkTheme = () => <></>;
export default DarkTheme;
light-theme.css
#import "~antd/dist/antd.css";
light-theme.jsx
import "./light-theme.css";
const LightTheme = () => <></>;
export default LightTheme;
use-theme.js A custom hook that different components can use -
import { useEffect, useState } from "react";
const DARK_MODE = "dark-mode";
const getDarkMode = () => JSON.parse(localStorage.getItem(DARK_MODE)) || false;
export const useTheme = () => {
const [darkMode, setDarkMode] = useState(getDarkMode);
useEffect(() => {
const initialValue = getDarkMode();
if (initialValue !== darkMode) {
localStorage.setItem(DARK_MODE, darkMode);
window.location.reload();
}
}, [darkMode]);
return [darkMode, setDarkMode];
};
theme-provider.jsx
import { lazy, Suspense } from "react";
import { useTheme } from "./use-theme";
const DarkTheme = lazy(() => import("./dark-theme"));
const LightTheme = lazy(() => import("./light-theme"));
export const ThemeProvider = ({ children }) => {
const [darkMode] = useTheme();
return (
<>
<Suspense fallback={<span />}>
{darkMode ? <DarkTheme /> : <LightTheme />}
</Suspense>
{children}
</>
);
};
change index.js to -
ReactDOM.render(
<React.StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</React.StrictMode>,
document.getElementById("root")
);
now, in my navbar suppose I have a switch to toggle the theme. This is what it would look like -
const [darkMode, setDarkMode] = useTheme();
<Switch checked={darkMode} onChange={setDarkMode} />
you must create 2 components
the first one :
import './App.dark.css'
const DarkApp =() =>{
//the app container
}
and the second :
import './App.light.css'
const LightApp =() =>{
//the app container
}
and create HOC to handle darkMode like this :
const AppLayout = () =>{
const [isDark , setIsDark] = useState(false);
return (
<>
{
isDark ?
<DarkApp /> :
<LightApp />
}
</>
)
}
ANTD internally uses CSS variables for the "variable.less" file. We can override these variables to make the default variables have dynamic values at runtime.
This is one way it can be achieved:
app-colors.less file:
:root{
--clr-one: #fff;
--clr-two: #000;
--clr-three: #eee;
}
// Dark theme colors
[data-thm="dark"]{
--clr-one: #f0f0f0;
--clr-two: #ff0000;
--clr-three: #fff;
}
We can now override the default ANTD variables in antd-overrides.less file:
#import <app-colors.less file-path>;
#primary-color: var(--clr-one);
#processing-color: var(--clr-two);
#body-background: var(--clr-three);
We usually import the antd.less file to get the antd styles, We would change that to "antd.variable.less"(to use css variables).
index.less file:
#import "~antd/dist/antd.variable.less";
#import <antd-overrides.less file-path>;
Now we need to toggle the "data-thm" attribute on a parent container(body tag recommended) to change the set of CSS variables that get used.
const onThemeToggle = (themeType) => {
const existingBodyAttribute = document.body.getAttribute("data-thm");
if (themeType === "dark" && existingBodyAttribute !== "dark") {
document.body.setAttribute("data-thm", "dark");
} else if (themeType === "light" && existingBodyAttribute) {
document.body.removeAttribute("data-thm");
}
};
The above piece of code can be called on a Theme toggle button or during component mount.
In Ant's example one suggestion is to import your "dark mode" CSS or LESS file into your main style sheet.
// inside App.css
#import '~antd/dist/antd.dark.css';
Instead of trying to toggle stylesheets, the "dark" styles are combined with base styles in one stylesheet. There are different ways to accomplish this, but the common pattern will be:
have a dark-mode selector of some sort in your CSS
put that selector in your HTML
have a way to toggle it on or off.
Here is a working example:
https://codesandbox.io/s/compassionate-elbakyan-f7tun?file=/src/App.js
In this example, toggling the state of darkMode will add or remove a dark-mode className to the top level container.
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [darkMode, setDarkMode] = useState(false);
return (
<div className={`App ${darkMode && "dark-mode"}`}>
<label>
<input
type="checkbox"
checked={darkMode}
onChange={() => setDarkMode((darkMode) => !darkMode)}
/>
Dark Mode?
</label>
<h1>Hello CodeSandbox</h1>
</div>
);
}
If darkMode is true, and the dark-mode className is present, those styles will be used:
h1 {
padding: 0.5rem;
border: 3px dotted red;
}
.dark-mode {
background: black;
color: white;
}
.dark-mode h1 {
border-color: aqua;
}
Ant Design newly start to support dynamic theme support. But its on experimental usage. You can find details on this link.
Conditional require won't block using previously required module. So, whenever your condition matches the require will available in your app. So, your both required module will be used. Instead of requiring them, insert stylesheet and remove to toggle between them:
const head = document.head
const dark = document.createElement('link')
const light = document.createElement('link')
dark.rel = 'stylesheet'
light.rel = 'stylesheet'
dark.href = 'antd.dark.css'
light.href = 'antd.light.css'
useEffect(() => {
const timer = setTimeout(() => {
if (darkMode) {
if (head.contains(light)) {
head.removeChild(light)
}
head.appendChild(dark)
} else {
if (head.contains(dark)) {
head.removeChild(dark)
}
head.appendChild(light)
}
}, 500)
return () => clearTimeout(timer)
}, [darkMode])
This package will help you to export and use theme vars without losing performance
Using less compiler in runtime:
https://medium.com/#mzohaib.qc/ant-design-dynamic-runtime-theme-1f9a1a030ba0
Import less code into wrapper
https://github.com/less/less.js/issues/3232
.any-scope {
#import url('~antd/dist/antd.dark.less');
}
I am creating my first ever self made web development project and have run into an infinite loop. The full project can be found on https://github.com/Olks95/my-dnd/tree/spellbook. So the question is: What causes the loop and how do I fix it?
(The loop happens somewhere in the 2nd item of the 'Playground' component when the ContentSelector - Spellbook is called. The custom hook useHandbook is called in Spellbook and continously calls the API, should obviously only happen once... refresh or click return to stop spamming )
From what I can tell the issue is not in the custom hook itself, as I have made several attempts to rewrite it and an empty dependency array is added to the end of the useEffect(). I will try to explain with example code here.
import { Component1, Component2, Component3 } from './ContentSelector.js';
const components = {
option1: Component1,
option2: Component2
option3: Component3
}
const Playground = (props) => {
const LeftItem = components['option1']
const MiddleItem = components['option2']
const RightItem = components['option3']
...
}
I wanted to be able to choose what content to put in each element and ended up making a ContentSelector component that has all the content components in one file, and individually imported/exported. This seems like a strange way to do it, but it was the only way I found to make it work. (Maybe the cause of the loop?) Since this is still fairly early on in the development the selection is hard coded. The item variables starts with a capital letter so I can later call them as components to render like so:
<LeftItem ...some properties... />
Playground then returns the following to be rendered:
return(
<React.Fragment>
<div className="container">
<div className="flex-item">
/* Working select-option to pass correct props to Component1 */
<div className="content">
<LeftItem ...some properties... />
</div>
</div
<div className="flex-item">
/* Currently the same selector that changes the content of the LeftItem */
<div className="content">
<MiddleItem ...some properties... />
</div>
</div>
/*RightItem follows the same formula but currently only renders "coming soon..." */
</div>
</React.Fragment>
)
The Content selector then has the three components where:
Component1: calls a custom hook that only runs once. The information is then sent to another component to render. All working fine.
Component2: calls a custom hook infinite times, but is expected to work the same way component 1 does...
Component3: Renders coming soon...
See Component1 and 2 below:
export const Component1 = (props) => {
const [ isLoading, fetchedData ] = useDicecloud(props.selectedChar);
let loadedCharacter = null;
if(fetchedData) {
loadedCharacter = {
name: fetchedData[0].Name,
alignment: fetchedData[0].Alignment,
/* a few more assignments */
};
}
let content = <p>Loading characters...</p>;
if(!isLoading && fetchedData && fetchedData.length > 0) {
content = (
<React.Fragment>
<Character
name={loadedCharacter.name}
alignment={loadedCharacter.alignment}
/* a few more props */ />
</React.Fragment>
)
}
return content;
}
export const Component2 = (props) => {
const [ fetchedData, error, isLoading ] = useHandbook('https://cors-anywhere.herokuapp.com/http://dnd5eapi.co/api/spells/?name=Aid')
let content = <p>Loading spells...</p>;
if(!isLoading && fetchedData) {
/* useHandbook used to return text to user in a string in some situations */
if(typeof fetchedData === 'string') {
content = (
<React.Fragment>
<p> {fetchedData} </p>
</React.Fragment>
)
} else {
content = (
<React.Fragment>
<Spellbook
/* the component that will in the future render the data from the API called in useHandbook */
/>
</React.Fragment>
)
}
}
return content;
}
I have been working on this issue for a few days and it is getting more confusing as I go along. I expected the mistake to be in useHandbook, but after many remakes it does not seem to be. The current useHandbook is very simple as shown below.
export const useHandbook = (url) => {
const [ isLoading, setIsLoading ] = useState(false);
const [ error, setError ] = useState(null);
const [ data, setData ] = useState(null);
const fetchData = async () => {
setIsLoading(true);
try {
const res = await fetch(url, {
method: "GET",
mode: 'cors'
});
const json = await res.json();
setData(json);
setIsLoading(false);
} catch(error) {
setError(error);
}
};
useEffect(() => {
fetchData();
}, []); //From what the documentation says, this [] should stop it from running more than once.
return [ data, error, isLoading ];
};
EDIT: I ran the chrome developer tools with the react extension and saw something that might be useful:
Image showing Component2 (Spellbook) run inside itself infinite times
You can modify your custom hook useHandbook's useEffect hook to have URL as dependency, since useEffect is similar to componentWillMount, componentDidUpdate and componentWillUnmount, in your case it is componentDidUpdate multiple times. So what you can do is.
useEffect(() => {
fetchData();
}, [url]);
Since there is no need to fetch data agin unless URL is changed
I found the mistake. Component2's real name was Spellbook which I had also called the rendering component that I had not yet made. It turns out I was calling the component from within itself.
Easy to see in the image at the edit part of the question.
I'm looking at a React-Redux app and try to understand how everything is working.
Inside one of the components, I saw these lines of code:
import { bindActionCreators } from "redux";
...
function mapDispatchToProps(dispatch) {
return bindActionCreators({ fetchPhotos }, dispatch);
}
export default connect(
null,
mapDispatchToProps
)(SearchBar);
If I change the above code to the following, everything still works, without any errors:
function mapStateToProps(photos) {
return { photos };
}
export default connect(
mapStateToProps,
{ fetchPhotos }
)(SearchBar);
To me, it seems that my way of using connect is easier to understand and it also doesn't need to import an extra library.
Is there any reasons, to import bindActionCreators and use mapDispatchToProps?
I'm a Redux maintainer.
Yes, the second example you showed uses the "object shorthand" form of mapDispatch.
We recommend always using the “object shorthand” form of mapDispatch, unless you have a specific reason to customize the dispatching behavior.
I personally avoid using bindActionCreators explicitly. I prefer to directly dispatch the functions with mapDispatchToProps which internally uses bindActionCreators.
const mapStateToProps = state => ({
photos: state.photos.photos
});
const mapDispatchToProps = dispatch => ({
fetchPhotos: () => dispatch(fetchPhotos())
// ...Other actions from other files
});
export default connect(mapStateToProps, mapDispatchToProps)(SearchBar);
There are two cases in which you'll use bindActionCreators explicitly, both are not best practices:
If you have a child component to SearchBar that does not connect to redux, but you want to pass down action dispatches as props to it, you can use bindActionCreators.
Best practice would be doing same with example I. You can just pass this.props.fetchPhotos to childcomponent directly without using bindActionCreators.
class SearchBar extends React.Component {
render() {
return (
<React.Fragment>
<ChildComponentOfSearchBar fetchPhotos={this.props.fetchPhotos} />
</React.Fragment>
)
}
}
const mapStateToProps = state => ({
photos: state.photos.photos
});
const mapDispatchToProps = () => bindActionCreators({ fetchPhotos }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(SearchBar);
There is another unlikely scenario where you can use bindActionCreators, defining actionCreator inside the component. This isn't maintainable & is not a good solution since action types are hard coded and not reusable.
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.fetchPhotosAction = bindActionCreators({ fetchPhotos: this.searchFunction }, dispatch);
}
searchFunction = (text) => {
return {
type: ‘SEARCH_ACTION’,
text
}
}
render() {
return (
<React.Fragment>
// Importing selectively
<ChildComponentOfSearchBar fetchPhotos={this.fetchPhotosAction} />
</React.Fragment>
)
}
}
const mapStateToProps = state => ({
photos: state.photos.photos
});
export default connect(mapStateToProps, null)(SearchBar)