Any reason why a <ContextProvider> HOC won't change it's default value? - reactjs

export const FocusContext = React.createContext({
isUsingMouse: true,
setIsUsingMouse: () => {
console.error("FocusContext.Provider value not initialized");
},
});
export const FocusContextProvider = ({ children }) => {
const [isUsingMouse, setIsUsingMouse] = React.useState(true);
return (
<FocusContext.Provider value={{ isUsingMouse, setIsUsingMouse }}>
{children}
</FocusContext.Provider>
);
};
Aside from this file and it's instantiation near the root of the app, most files just import and useContext(FocusContext)
For some reason, the value is never taking effect, and setIsUsingMouse logs the defaultValue error
Is there any reason why this implementation of context as a HOC won't work?

It turns out this method works perfectly well.
The issue was that our <ContextProvider> wasn't being used in our Storybook, rather an outer integration layer (to interface with the mother app)

Related

Using React Context in a custom hook always returns undefined

I'm trying to create a custom hook which will eventually be packaged up on NPM and used internally on projects in the company I work for. The basic idea is that we want the package to expose a provider, which when mounted will make a request to the server that returns an array of permission strings that are then provided to the children components through context. We also want a function can which can be called within the provider which will take a string argument and return a boolean based on whether or not that string is present in the permissions array provided by context.
I was following along with this article but any time I call can from inside the provider, the context always comes back as undefined. Below is an extremely simplified version without functionality that I've been playing with to try to figure out what's going on:
useCan/src/index.js:
import React, { createContext, useContext, useEffect } from 'react';
type CanProviderProps = {children: React.ReactNode}
type Permissions = string[]
// Dummy data for fake API call
const mockPermissions: string[] = ["create", "click", "delete"]
const CanContext = createContext<Permissions | undefined>(undefined)
export const CanProvider = ({children}: CanProviderProps) => {
let permissions: Permissions | undefined
useEffect(() => {
permissions = mockPermissions
// This log displays the expected values
console.log("Mounted. Permissions: ", permissions)
}, [])
return <CanContext.Provider value={permissions}>{children}</CanContext.Provider>
}
export const can = (slug: string): boolean => {
const context = useContext(CanContext)
// This log always shows context as undefined
console.log(context)
// No functionality built to this yet. Just logging to see what's going on.
return true
}
And then the simple React app where I'm testing it out:
useCan/example/src/App.tsx:
import React from 'react'
import { CanProvider, can } from 'use-can'
const App = () => {
return (
<CanProvider>
<div>
<h1>useCan Test</h1>
{/* Again, this log always shows undefined */}
{can("post")}
</div>
</CanProvider>
)
}
export default App
Where am I going wrong here? This is my first time really using React context so I'm not sure where to pinpoint where the problem is. Any help would be appreciated. Thanks.
There are two problems with your implementation:
In your CanProvider you're reassigning the value in permissions with =. This will not trigger an update in the Provider component. I suggest using useState instead of let and =.
const [permissions, setPermissions] = React.useState<Permissions | undefined>();
useEffect(() => {
setPermissions(mockPermissions)
}, []);
This will make the Provider properly update when permissions change.
You are calling a hook from a regular function (the can function calls useContext). This violates one of the main rules of Hooks. You can learn more about it here: https://reactjs.org/docs/hooks-rules.html#only-call-hooks-from-react-functions
I suggest creating a custom hook function that gives you the can function you need.
Something like this, for example
const useCan = () => {
const context = useContext(CanContext)
return () => {
console.log(context)
return true
}
}
Then you should use your brand new hook in the root level (as per the rules of hooks) of some component that's inside your provider. For example, extracting a component for the content like so:
const Content = (): React.ReactElement => {
const can = useCan();
if(can("post")) {
return <>Yes, you can</>
}
return null;
}
export default function App() {
return (
<CanProvider>
<div>
<h1>useCan Test</h1>
<Content />
</div>
</CanProvider>
)
}
You should use state to manage permissions.
Look at the example below:
export const Provider: FC = ({ children }) => {
const [permissions, setPermissions] = useState<string[]>([]);
useEffect(() => {
// You can fetch remotely
// or do your async stuff here
retrivePermissions()
.then(setPermissions)
.catch(console.error);
}, []);
return (
<CanContext.Provider value={permissions}>{children}</CanContext.Provider>
);
};
export const useCan = () => {
const permissions = useContext(CanContext);
const can = useCallback(
(slug: string) => {
return permissions.some((p) => p === slug);
},
[permissions]
);
return { can };
};
Using useState you force the component to update the values.
You may want to read more here

Use swr with Next global context: entire page gets re-rendered

I have the following piece of code:
function MyApp({ Component, pageProps }: AppProps) {
const { tronLinkAuth, tronLinkLoading, mutateTronLink } = useTronLink();
const { authenticatedUser, authLoading, authLoggedOut, mutateAuth } = useAuthenticatedUser();
return (
<React.StrictMode>
<CSSReset />
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
<ChakraProvider theme={theme}>
<AuthenticationContext.Provider value={{
tronLinkAuth, tronLinkLoading, mutateTronLink,
authenticatedUser, authLoading, authLoggedOut, mutateAuth
}}>
<Component {...pageProps} />
</AuthenticationContext.Provider>
</ChakraProvider>
</React.StrictMode>
)
}
Example useAuthenticatedUser:
export default function useAuthenticatedUser() {
const { data, mutate, error } = useSWR("api_user", fetcher, {
errorRetryCount: 0
});
const loading = !data && !error;
const loggedOut = error && error instanceof UnauthorizedException;
return {
authLoading: loading,
authLoggedOut: loggedOut,
authenticatedUser: data as AuthenticatedUser,
mutateAuth: mutate
};
}
The code works, but my entire webpage gets re-rendered when swr propagates its result.
For example:
const Login: NextPage = () => {
console.log('login update');
return (
<>
<Head>
<title>Register / Login</title>
</Head>
<Navbar />
<Box h='100vh'>
<Hero />
</Box>
<Box h='100vh' pt='50px'>
Test second page
</Box>
</>
)
}
export default Login;
When using useContext in the Navbar, it also re-renders the entire LoginPage, including the Hero, while this is not my purpose.
const Navbar: React.FC = () => {
const authState = useContext(AuthenticationContext);
...
I'm also confused as for why the logs appear in the server console, as this is supposed to be executed client-side.
Edit: not an issue, this is only on first render.
How to solve?
I'm interested in using swr for this use case, because it allows me to re-verify the authentication status e.g. on focus but use the cached data meanwhile.
Edit:
Confusing. The following log:
function MyApp({ Component, pageProps }: AppProps) {
console.log('app');
const { tronLinkAuth, tronLinkLoading, mutateTronLink } = useTronLink();
const { authenticatedUser, authLoading, authLoggedOut, mutateAuth } = useAuthenticatedUser();
Also gets printed out every time I switch tabs and activate the swr.
So it re-renders the entire tree? Doesn't seem desirable...
I currently went with the easier solution, i.e. use useSwr immediately on the component that uses the data.
From the docs:
Each component has a useSWR hook inside. Since they have the
same SWR key and are rendered at the almost same time, only 1 network
request will be made.
You can reuse your data hooks (like useUser in the example above)
everywhere, without worrying about performance or duplicated requests.
So it can be leveraged to re-use it wherever needed without having to worry about global state re-renders.
In case there is an alternative response how to use the global Context Provider, don't hesitate to share.

React Context and Storybook: Changing value of a React Context inside a component story

I'm wondering how I would go about changing the value of a React Context (https://reactjs.org/docs/context.html) inside a component story in Storybook.
I imagine there is a way to control this via the Story Controls, but there isn't any documentation on it.
I used this part of the Storybook documentation to provide my Stories with two seperate Contexts: https://storybook.js.org/docs/react/writing-stories/decorators#context-for-mocking
My code in .storybook/preview.js looks like this:
export const decorators = [
(Story) => (
<FilterProvider>
<SearchQueryProvider>
<Story />
</SearchQueryProvider>
</FilterProvider>
),
];
The FilterProvider and SearchQueryProvider code looks like this:
const SearchQueryProvider = ({ children }: SearchQueryProviderProps) => {
const [searchQuery, setSearchQuery] = useState<string>("");
return (
<SearchQueryContext.Provider value={[searchQuery, setSearchQuery]}>
{children}
</SearchQueryContext.Provider>
);
};
export { SearchQueryProvider, useSearchQuery };
The initial values of these states are basically empty.
To test and preview my components which consume these Contexts I would now like to change the values inside Storybook.
If anyone has an idea how I could achieve this: I'd be delighted to know and grateful for you sharing your knowledge :)
You can add initialValue as property and set the initial value of the searchQuery state and use "" as default:
const SearchQueryProvider = ({ children, initialValue = "" }: SearchQueryProviderProps) => {
const [searchQuery, setSearchQuery] = useState<string>(initialValue);
return (
<SearchQueryContext.Provider value={[searchQuery, setSearchQuery]}>
{children}
</SearchQueryContext.Provider>
);
};
export { SearchQueryProvider, useSearchQuery };
This allows you to use your Provider like this:
export const decorators = [
(Story) => (
<FilterProvider>
<SearchQueryProvider initialValue="something">
<Story />
</SearchQueryProvider>
</FilterProvider>
),
];

Setting state in React context provider with TypeScript, functional components and hooks

I'm starting with React and TypeScript at the same time and I across a problem while implementing some basic authentication in my application. I've been using Ryan Chenkie's Orbit App and his course on React security as an example to start from.
Right now I'm stuck with compiler complaining about TS2722 error (Cannot invoke object which is possibly undefined) in SignIn.tsx. My suspicion is that all I have to do is to set proper types on all the data structures and function calls, but how and where to set them, somewhat alludes me. So, here's the code:
App.tsx: Nothing fancy here, just an App wrapped in context provider.
import { AuthContext, authData } from "./AuthContext"
const defAuth:authData = {
userId: 0,
name: '',
token: '',
expiresAt: ''
}
const App = () => {
return (
<AuthContext.Provider value={{ authData:defAuth }}>
<Main />
</AuthContext.Provider>
);
}
AuthContext.tsx: When calling createContext() I tried with various default parameters, the general idea is that I could call authContext.setState() and pass the data to it. I am using Partial prefix so that I don't have to pass the setState() to the Provider element.
export interface authData {
userId: number
name: string
token: string
expiresAt: string
}
interface IAuthContext {
authData: authData,
setState: (authInfo:authData) => void
}
const AuthContext = createContext<Partial<IAuthContext>>(undefined!)
const { Provider } = AuthContext
const AuthProvider: React.FC<{}> = (props) => {
const [authState, setAuthState] = useState<authData>()
const setAuthInfo = (data:authData) => {
console.log('Called setAuthInfo')
setAuthState({
userId: data!.userId,
name: data!.name,
token: data!.token,
expiresAt: data!.expiresAt
})
}
return (
<Provider
value={{
authData: authState,
setState: (authInfo:authData) => setAuthInfo(authInfo)
}} {...props}
/>
)
}
export { AuthContext, AuthProvider }
SignIn.tsx: This is again, just a basic sign in component with a form and an onSubmit handler. This is all working as it should until I add the authContext to it. I included only relevant code.
interface loginType extends Record<string, any>{
email: string,
password: string,
remember: boolean
}
const SignIn = () => {
const authContext = useContext(AuthContext)
const { register, handleSubmit, errors } = useForm<loginType>()
const onSubmit = async (data:loginType) => {
const ret = await apiFetch.post('process_login/', formData )
console.log(ret.data)
console.log('Printing context')
authContext.setState(ret.data)
console.log(authContext)
}
/* ... ... */
}
As mentioned before, compiler complains in SignIn.tsx at authContext.setState(ret.data) telling me that it might be undefined. I tried calling createContext() with various parameters, trying to pass it some defaults which would tell the compiler where that setState() will be defined later on in the runtime. I tried calling setState in a few different ways, but nothing really worked.
This is something that clearly works in plain JSX and I'd really like to find a way to make it work in TSX.
Here's what you need to do:
First in App.tsx, you have to use AuthProvider instead of AuthContext.Provider. This way you get rid of the value property.
<AuthProvider>
<Main />
</AuthProvider>
Then, in AuthContext.tsx there's no need to use Partial prefix when creating context. So, a little tweak to the IAuthContext and then pass some default data when creating context.
interface IAuthContext {
authData: authData | undefined,
setState: (authInfo:authData) => void
}
const AuthContext = createContext<IAuthContext>( {
authData: defaultAuthData,
setState: () => {}
})
Now you can call authContext.setState(), passing data as authData type.

React Hook does not work properly on the first render in gatsby production mode

I have the following Problem:
I have a gatsby website that uses emotion for css in js. I use emotion theming to implement a dark mode. The dark mode works as expected when I run gatsby develop, but does not work if I run it with gatsby build && gatsby serve. More specifically the dark mode works only after switching to light and back again.
I have to following top level component which handles the Theme:
const Layout = ({ children }) => {
const [isDark, setIsDark] = useState(() => getInitialIsDark())
useEffect(() => {
if (typeof window !== "undefined") {
console.log("save is dark " + isDark)
window.localStorage.setItem("theming:isDark", isDark.toString())
}
}, [isDark])
return (
<ThemeProvider theme={isDark ? themeDark : themeLight}>
<ThemedLayout setIsDark={() => setIsDark(!isDark)} isDark={isDark}>{children}</ThemedLayout>
</ThemeProvider>
)
}
The getInitalIsDark function checks a localStorage value, the OS color scheme, and defaults to false. If I run the application, and activate the dark mode the localStorage value is set. If i do now reload the Application the getInitialIsDark method returns true, but the UI Renders the light Theme. Switching back and forth between light and dark works as expected, just the initial load does not work.
If I replace the getInitialIsDark with true loading the darkMode works as expected, but the lightMode is broken. The only way I got this to work is to automatically rerender after loading on time using the following code.
const Layout = ({ children }) => {
const [isDark, setIsDark] = useState(false)
const [isReady, setIsReady] = useState(false)
useEffect(() => {
if (typeof window !== "undefined" && isReady) {
console.log("save is dark " + isDark)
window.localStorage.setItem("theming:isDark", isDark.toString())
}
}, [isDark, isReady])
useEffect(() => setIsReady(true), [])
useEffect(() => {
const useDark = getInitialIsDark()
console.log("init is dark " + useDark)
setIsDark(useDark)
}, [])
return (
<ThemeProvider theme={isDark ? themeDark : themeLight}>
{isReady ? (<ThemedLayout setIsDark={() => setIsDark(!isDark)} isDark={isDark}>{children}</ThemedLayout>) : <div/>}
</ThemeProvider>
)
}
But this causes an ugly flicker on page load.
What am I doing wrong with the hook in the first approach, that the initial value is not working as I expect.
Did you try to set your initial state like this?
const [isDark, setIsDark] = useState(getInitialIsDark())
Notice that I am not wrapping getInitialIsDark() in an additional function:
useState(() => getInitialIsDark())
You will probably crash your build because localStorage is not defined at buildtime. You might need to check if that exists inside getInitialIsDark.
Hope this helps!
#PedroFilipe is correct, useState(() => getInitialIsDark()) is not the way to invoke the checking function on start-up. The expression () => getInitialIsDark() is truthy, so depending on how <ThemedLayout isDark={isDark}> uses the prop it might work by accident, but useState will not evaluate the fuction passed in (as far as I know).
When using an initial value const [myValue, setMyValue] = useState(someInitialValue) the value seen in myValue can be laggy. I'm not sure why, but it seems to be a common cause of problems with hooks.
If the component always renders multiple times (e.g something else is async) the problem does not appear because in the second render the variable will have the expected value.
To be sure you check localstorage on startup, you need an additional useEffect() which explicitly calls your function.
useEffect(() => {
setIsDark(getInitialIsDark());
}, [getInitialIsDark]); //dependency only needed to satisfy linter, essentially runs on mount.
Although most useEffect examples use an anonymous function, you might find more understandable to use named functions (following the clean-code principle of using function names for documentation)
useEffect(function checkOnMount() {
setIsDark(getInitialIsDark());
}, [getInitialIsDark]);
useEffect(function persistOnChange() {
if (typeof window !== "undefined" && isReady) {
console.log("save is dark " + isDark)
window.localStorage.setItem("theming:isDark", isDark.toString())
}
}, [isDark])
I had a similar issue where some styles weren't taking effect because they were being applied to through classes which were set on mount (like you only on production build, everything worked fine in develop).
I ended up switching the hydrate function React was using from ReactDOM.hydrate to ReactDOM.render and the issue disappeared.
// gatsby-browser.js
export const replaceHydrateFunction = () => (element, container, callback) => {
ReactDOM.render(element, container, callback);
};
This is what worked for me, try this and let me know if it works out.
First
In src/components/ i've created a component navigation.js
export default class Navigation extends Component {
static contextType = ThemeContext // eslint-disable-line
render() {
const theme = this.context
return (
<nav className={'nav scroll' : 'nav'}>
<div className="nav-container">
<button
className="dark-switcher"
onClick={theme.toggleDark}
title="Toggle Dark Mode"
>
</button>
</div>
</nav>
)
}
}
Second
Created a gatsby-browser.js
import React from 'react'
import { ThemeProvider } from './src/context/ThemeContext'
export const wrapRootElement = ({ element }) => <ThemeProvider>{element}</ThemeProvider>
Third
I've created a ThemeContext.js file in src/context/
import React, { Component } from 'react'
const defaultState = {
dark: false,
notFound: false,
toggleDark: () => {},
}
const ThemeContext = React.createContext(defaultState)
class ThemeProvider extends Component {
state = {
dark: false,
notFound: false,
}
componentDidMount() {
const lsDark = JSON.parse(localStorage.getItem('dark'))
if (lsDark) {
this.setState({ dark: lsDark })
}
}
componentDidUpdate(prevState) {
const { dark } = this.state
if (prevState.dark !== dark) {
localStorage.setItem('dark', JSON.stringify(dark))
}
}
toggleDark = () => {
this.setState(prevState => ({ dark: !prevState.dark }))
}
setNotFound = () => {
this.setState({ notFound: true })
}
setFound = () => {
this.setState({ notFound: false })
}
render() {
const { children } = this.props
const { dark, notFound } = this.state
return (
<ThemeContext.Provider
value={{
dark,
notFound,
setFound: this.setFound,
setNotFound: this.setNotFound,
toggleDark: this.toggleDark,
}}
>
{children}
</ThemeContext.Provider>
)
}
}
export default ThemeContext
export { ThemeProvider }
This should work for you here is the reference I followed from the official Gatsby site

Resources