React Context Provider override not working - reactjs

The Goal
Hello 👋🏻
I have a React project with NextJS where I want to chose whether using light or dark theme depending on the background / section.
My solution
I have found that the best solution to my problem is using React Context API. To change the theme I just need to override the provider with a new one (as indicated in the documentation).
The problem
I have the start of a solution but when overriding the Provider, I noticed that the change isn't effective for every components even if in the devTools the context in theses components is the correct one. 😕
I have tried switching from useContext(context) to <context.Consumer> but there was no improvement.
The code:
/context/Theme
import { createContext, JSXElementConstructor, ReactElement, ReactFragment, ReactNode, ReactPortal } from "react";
import { getTheme, Theme } from "../constants/theme";
// type of getTheme: (theme: 'light' | 'dark') => Theme
const ThemeContext = createContext<Theme>(getTheme('light'))
export default ThemeContext
interface ThemeProviderProps {
theme: 'light' | 'dark'
children: string | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | ReactFragment | ReactPortal | null | undefined
}
interface ThemeConsumerProps {
children: (value: Theme) => ReactNode
}
// The goal here is to make the theme switching more natural
export const ThemeProvider = (props: ThemeProviderProps): JSX.Element => {
return (
<ThemeContext.Provider value={getTheme(props.theme)}>
{props.children}
</ThemeContext.Provider>
)
}
// Just for convenience
export const ThemeConsumer = (props: ThemeConsumerProps): JSX.Element => {
return <ThemeContext.Consumer>{props.children}</ThemeContext.Consumer>
}
/pages/_app (Starting point of NextJS App, equivalent to /src/App
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import { ThemeProvider } from "../context/Theme";
export default function App({ Component, pageProps }: AppProps) {
return (
<ThemeProvider theme="light">
<Component {...pageProps} />
</ThemeProvider>
)
}
/common/Popup/Popup
import {ReactNode, useContext, useState} from "react";
import styles from "./Popup.module.css"
import Typography, {Typos} from "../Typography";
import Icon from "../Icon";
import {CloseIcon} from "../../icons";
import ThemeContext, {ThemeProvider, ThemeConsumer} from "../../context/Theme";
interface Props {
children: ReactNode
title: string
open: boolean
close: () => void
}
export default function (props: Props): JSX.Element {
return (
<>
// ...
<ThemeProvider theme="dark">
<div className={styles.header}>
<Typography level={Typos.TITLE_2} bold>{ props.title }</Typography> // The useContext works inside of this components
<ThemeConsumer>
{(theme) =>
<Icon icon={CloseIcon} color={theme.texts.secondary} size={1.5}
onClick={props.close} clickable />
}
</ThemeConsumer>
</div>
<div className={styles.body}>
{ props.children } // The useContext or the <context.Consumer> doesn't work for the children
</div>
</ThemeProvider>
// ...
</>
)
}

I've just misplaced my <context.Consumer> outside of the Popup component and I learned that useContext doesn't work when overriding the Provider.

Related

Passing both props.children and a component as prop in React/Nextjs

User.tsx is a kind of navigation bar that passes all children components through via {props.children}. I also want to pass a breadcrumb component through to it as well but I'm getting an error that says:
JSX element type 'props.component' does not have any construct or call
signatures
Layout.tsx:
import UserShell from "./account/User
import Breadcrumb from "./navigation/Breadcrumb";
<User component={Breadcrumb}>{props.children}</User>;
User.tsx:
type ShellProps = {
children: React.ReactNode | React.ReactNode[];
component?: React.ComponentType;
export default function UserShell(props: ShellProps) {
return (
<>
<props.component />
^^^^^^^^^^^^^^^^^^//error is here
<div>{props.children}</div>
</>
}
Any clue on how to fix this?
import React from 'react'
type ShellProps = {
children: React.ReactNode | React.ReactNode[];
component?: React.ComponentType;
}
function UserShell(props: ShellProps) {
return (
<>
{props.component ? <props.component /> : null}
<div>{props.children}</div>
</>
}
playground
you probably need to capitalize the component (Component)
try updating to this
Layout.tsx:
import UserShell from "./account/User
import Breadcrumb from "./navigation/Breadcrumb";
<User Breadcrumb={Breadcrumb}>{props.children}</User>; //use Breadcrumb or Component (capital C)
User.tsx
type ShellProps = {
children: React.ReactNode | React.ReactNode[];
Breadcrumb?: React.ElementType; //covers all elements
export default function UserShell({Breadcrumb, children}) {
return (
<>
<Breadcrumb/>
<div>{children}</div>
</>
}

Dynamic loading icon

Hi have a script that import location, which consists of multiple svg file and a index.ts file to import and export them.
import * as Icons from '../../icons'
I then have a functional component that returns the icon based on the icon name, which I am getting error.
interface props extends React.SVGProps<SVGSVGElement> {
icon: string
title?: string | undefined
}
function Icon({ icon, ...props }: props) {
const Icon = Icons[icon] <= error here
return <Icon {...props} />
}
function SidebarContent() {
return (
//some code here
<Icon className="w-5 h-5" aria-hidden="true" icon={route.icon} />
//some code here
)
}
The error I am getting is this:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'typeof import("d:/Code/JS/meal-order/frontend/src/icons/index")'.
No index signature with a parameter of type 'string' was found on type 'typeof import("d:/Code/JS/meal-order/frontend/src/icons/index")'.ts(7053)
I would like to know how in the Icon component. How can it accept icon name (Icon[icon]) and return the correct icon? or probably there is other way to do this ?
More info:
If I write the code like the following then it works without complaining. However that doesn't fit the purpose because my intention is to dynamically load the icon based on the icon name i've passed.
function Icon({ icon, ...props }: props) {
const Icon = Icons.ButtonsIcon
return <Icon {...props} />
}
Update:
This is how I fix the type error:
import react from "react";
import * as Icons from "../../icons";
type IIcon = React.FunctionComponent<React.SVGProps<SVGSVGElement> & {
title?: string | undefined;
}>
export default function Icon({ icon = 'MenuIcon', ...props }) {
const myObj: { [index: string]: IIcon } = Icons
const NewIcon = myObj[icon]
return <NewIcon {...props} />
}
Updated:
Here's a CodeSandbox of what I believe you're trying to do.
Icon.js
import react from "react";
import * as Icons from "./icons";
export default function Icon({ icon, ...props }) {
const NewIcon = Icons[icon];
return <NewIcon {...props} />;
}
Icons/index.js
import { ReactComponent as ButtonIcon } from "./button.svg";
import { ReactComponent as BellIcon } from "./bell.svg";
export { BellIcon, ButtonIcon };
App.js
import Icon from "./Icon";
export default function App() {
return (
<div className="App">
<Icon icon="BellIcon"></Icon>
<Icon icon="ButtonIcon"></Icon>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
https://codesandbox.io/embed/frosty-stitch-li3wiz?fontsize=14&hidenavigation=1&theme=dark
If you posted a codesandbox I could assist better that way, however, as long you Icons are components themselves, not just SVGs, it should work.

Custom icon wrapper component

I want to use react-feather icons but within a standard size button component I created.
To use a react-feather component I need to create it like so <Camera size={18} /> or <Icon.Camera size={18} /> however, I don't want to keep on repeating the size and color and any other prop that stays the same.
How can I make the icon type dynamic within my Button.
I know this form is invalid but I am looking for a way that is and that is the best I could write to present my thought.
import React, { FC } from "react";
import * as Icons from "react-feather";
interface IconProps {
icon?: Icons.Icon;
}
const IconComponent: FC<IconProps> = (props) => {
return (
<button>
{props.icon && <Icons[props.icon] size={18} />}
{props.children}
</button>
);
};
export default IconComponent;
The IconComponent component below should do. You can see it live in this codesandbox.
// Icon.tsx
import React, { ReactNode } from "react";
import { Icon } from "react-feather";
interface IconComponentProps {
icon?: Icon;
children: ReactNode;
}
const IconComponent = ({ icon: FeatherIcon, children }: IconComponentProps) => {
return (
<button>
{FeatherIcon && <FeatherIcon size={18} />}
{children}
</button>
);
};
export default IconComponent;
There you could use it anywhere:
// App.tsx
import IconComponent from "./Icon";
import { Camera, Airplay } from "react-feather";
import "./styles.css";
export default function App() {
return (
<div className="App">
<IconComponent icon={Camera}>
<h1>Camera</h1>
</IconComponent>
<IconComponent icon={Airplay}>
<h1>Airlpay</h1>
</IconComponent>
<IconComponent>
<h1>No Icon</h1>
</IconComponent>
</div>
);
}

Typescript error with NextJS and Context API

I have the following code in my _app.tsx file:
import React from 'react'
import type { AppProps } from 'next/app'
/* Import Styles */
import '#themes/index.scss'
/* Import Template */
import WithAuth from '#templates/with-auth'
// This default export is required in a new `pages/_app.js` file.
export const App = ({ Component, pageProps }: {Component: unknown, pageProps: unknown}): AppProps => {
return (
<WithAuth>
<Component {...pageProps} />
</WithAuth>
)
}
export default App
I got the AppProps usage from the nextjs documentation, however I have wrapped the component with a WithAuth component which is basically a template with a MaterialUI ThemeProvider and a react context provider:
return <div className={styles['grid-page']}>
<ThemeProvider theme={theme}>
<GlobalContext.Provider value={globalProps}>
<HeaderBar />
<main>
{ children }
</main>
</GlobalContext.Provider>
</ThemeProvider>
</div>
}
When I do this, I get the typescript error:
pages/_app.tsx:12:3 - error TS2322: Type 'Element' is not assignable to type 'AppPropsType<Router, {}>'.
Property 'pageProps' is missing in type 'Element' but required in type 'AppInitialProps'.
12 return (
~~~~~~~~
13 <WithAuth>
~~~~~~~~~~~~~~
...
15 </WithAuth>
~~~~~~~~~~~~~~~
16 )
~~~
node_modules/next/dist/next-server/lib/utils.d.ts:124:5
124 pageProps: any;
~~~~~~~~~
'pageProps' is declared here.
pages/_app.tsx:14:8 - error TS2604: JSX element type 'Component' does not have any construct or call signatures.
14 <Component {...pageProps} />
I'm struggling to understand the correct types to add here
AppProps should be de-structured.
import { AppProps } from 'next/app'
const MyApp = ({ Component, pageProps }: AppProps) => {
return (
<>
<Header />
<Component {...pageProps} />
</>
)
}

React - Forwarding props with forwardRef

I want to define a LinkButton component which will behaving like a link and look like a button.
My basic implementation complains that I need to use React.forwardRef, however I'm unsure how to do this properly, especially with prop forwarding.
Even when I just type the props as any I still get a complaint from the system saying the props are invalid.
I would like to know how to properly use forwardRef in this (or any, for that matter) situation.
My attempt looks like this:
import React from 'react'
import styles from './styles'
import { Button, withStyles } from '#material-ui/core'
import { Link } from 'react-router-dom'
interface IProps {
classes: any
to: string
children: string
}
const NextLink = ({ to, ...rest }: any) => <Link to={to} {...rest} />
const LinkButton = React.forwardRef((props: any, ref: any) => {
const { classes, children, ...rest } = props // <-- ISSUE BECAUSE OF rest PROP
return (
<Button
ref={ref}
role='nextButton'
className={classes.root}
classes={{ disabled: classes.disabled }}
component={NextLink}
{...rest}
>
{children}
</Button>
)
})
LinkButton.displayName = 'LinkButton'
export default withStyles(styles)(LinkButton)
------------
It would be called like this:
<LinkButton to='/' color='secondary'>
{`Go Home`}
</LinkButton>
By wrapping NextLink in forwardRef as well I was able to solve this.
const NextLink = React.forwardRef(({ to, ...rest }: any, ref: any) => (
<Link ref={ref} to={to} {...rest} />
))
NextLink.displayName = 'NextLink'

Resources