I have created a custom react hook that allows users to toggle the color theme of my application (i.e. 'light' or 'dark'), and the state change is not causing the components that call the hook to re-render.
The hook is called useColorTheme and it's defined as follows:
import { useEffect } from "react";
import useLocalStorage from "./useLocalStorage";
export default function useColorTheme() {
const [colorTheme, setColorTheme] = useLocalStorage("color-theme", "light");
useEffect(() => {
const className = "dark";
const bodyClass = window.document.body.classList;
colorTheme === "dark"
? bodyClass.add(className)
: bodyClass.remove(className);
}, [colorTheme]);
return [colorTheme, setColorTheme];
}
As you can see, this hook calls another hook called useLocalStorage, which allows the state to persist on refresh
I got this hook from usehooks.com, and it's defined as:
import { useState } from "react";
const PREFIX = "my-app-";
export default function useLocalStorage(key: string, initialValue: string) {
const prefixedKey = PREFIX + key;
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === "undefined") {
return initialValue;
}
try {
const item = window.localStorage.getItem(prefixedKey);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
function setValue(value: unknown) {
try {
/**
* Don't fully understand function typing here
*/
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== "undefined") {
window.localStorage.setItem(prefixedKey, JSON.stringify(valueToStore));
}
} catch (error) {
console.log(error);
}
}
return [storedValue, setValue];
}
The colorTheme value is successfully being stored in localStorage, and the hook works on the initial application load, but I am having issues in components that call the useColorTheme hook, like in this DrawerContainer example:
export default function DrawerContainer() {
// calling the hook
const [theme, setTheme] = useColorTheme();
// component does not re-render when ThemeToggle component toggles theme state
return (
<div className="flex items-center justify-between gap-2">
<FiMenu size={30} className="lg:hidden mr-4 cursor-pointer" />
<Image
className="ml-10 mr-10 sm:mr-2 sm:ml-0 cursor-pointer"
src={
theme === "light"
? "/images/fiverr-logo.png"
: "/images/fiverr-logo-white.png"
}
alt="logo"
width={100}
height={100}
/>
{!user && <AuthButtons showUnderSmall />}
<ThemeToggle />
</div>
);
}
When the value of colorTheme is toggled in some other component in my application (such as my ThemeToggle component), the changed state is not being picked up in my DrawerContainer component, which prevents logic that reads from this state from happening.
I've verified that the state is indeed changing in my browser Dev Tools, so why is my DrawerContainer component not re-rendering?
Thank you very much in advance. Any insight is greatly appreciated.
Wen't thru your code and see the issue.
Your custom hook is independent,that means in every component it has its own state.
So for example you have two components A,B and both using hook.
when you changing theme using component A,new state is being enclosed inside of component A and not being passed down to B or other components that using hook as well.
To solve your issue you have to use Context API and use single state which will be passed down to other components using context.
Check out this https://reactjs.org/docs/context.html
This could be implemented like this(pseudo code):
1.Creating context
const themeContext = React.createContext();
2.Implementing hook which will be reused in components to catch context state.
function useTheme() {
return useContext(themeContext);
}
3.Implementing provider component which should be used at Root level so that can be utilized in all children components.
const {Provider} = themeContext;
function ThemeProvider(props) {
const [theme,setTheme] = useState(() => //grabbing initial theme);
const context = useMemo(() => ({theme, setTheme}), [theme])
return <Provider value={context}>{props.children}</Provider>
}
4.Wrapping components which will utilize your context.
function App() {
return (
<ThemeProvider>
<MyComponent />
</ThemeProvider>
)
}
Hope it helps!
Related
I'm beginner with React testing, learning by coding, here i have a component 'cam.tsx'
i want to test it, when i want to test Add function it goes straight like this, but when i want to test Update function it still shows Add function in my test, how to test both of them ?
Add and Update functions are forms where user can fill.
describe("Testing component ", () => {
const Camera = (): RenderResult =>
render(
<Provider store={store}>
<Cam
}}
/>{" "}
</Provider>
);
test("Cam", () => {
Camera();
const name = screen.queryByTestId(/^AddName/i);
});
});
cam.tsx:
const ADD = "ADD";
let [state, setState] = useState<State>({mode: ADD });
if (props.mode) {
state.mode = props.mode;
}
const option = state.mode;
return (
<React.Fragment>
<div data-testid="header">
{option == ADD ? Add() : <></>}
{option == UPDATE ? Update() : <></>}
</div>
</React.Fragment>
Basically cam.tsx is a component which has two forms one for updating camera and another for adding new camera.When user clicks add/update icon then cam component gets 'mode' via props ' state.mode = props.mode '
English is not my mother language, so could be mistakes
Here is how to test a component that conditionally renders components from state and can be updated via props.
import {render, screen} from '#testing-library/react';
import {Cam} from './Cam';
test('renders add by default', () => {
render(<Cam/>);
expect(screen.getByTestId('addForm'))
.toBeInTheDocument();
expect(screen.queryByTestId('updateForm'))
.not.toBeInTheDocument();
});
test('renders edit by passing props', () => {
const {rerender} = render(<Cam mode={undefined}/>);
rerender(<Cam mode={'UPDATE'} />)
expect(screen.getByTestId('updateForm'))
.toBeInTheDocument();
expect(screen.queryByTestId('addForm'))
.not.toBeInTheDocument();
});
However, it is known in the React community that updating state via props is usually an anti-pattern. This is because you now have two sources of truth for state and can be easy to have these two states conflicting. You should instead just use props to manage rendering.
If state comes from a parent component, use props.
export function Cam(props) {
const option = props.mode;
return (
<div data-testid="header">
{option === ADD ? Add() : <></>}
{option === UPDATE ? Update() : <></>}
</div>
);
}
If you really want to keep state in the child component even if props are passed in, you should update props in an useEffect hook. Additionally, you should use the setState function rather than setting state manually state.mode = props.mode
Use the useEffect hook to update state via props.
...
const [state, setState] = useState({mode: ADD});
useEffect(() => {
if (props.mode) {
setState({mode: props.mode});
}
}, [props.mode]) <-- checks this value to prevent infinite loop.
const option = state.mode;
return (
...
React memo isn't capturing the props neither the prevProps nor the nextProps and the component render well. The react docs say
If your function component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost.
my problem is to stop twice rendering using react memo, but memo seems to be not working and the component renders twice with the same props.
The component renders when the Create New Event is clicked on /events
here is the live sandbox.
Child Component located at /components/Event/CreateEvent/CreateEvent.js
the parent component is located at /Pages/Event/Event.js line number 999' from where the child component is being triggered
Here is the Code:
import React from "react";
import AuthContext from "../../context/global-context";
import CreateEvent from "../../components/Event/CreateEvent/CreateEvent";
function Events({ location }) {
// Sate Managing
const [allEvents, setAllEvents] = React.useState([]);
const [creating, setCreating] = React.useState(false);
// Context As State
const { token, email } = React.useContext(AuthContext);
// Creating Event Showing
const modelBoxHandler = () => {
// works on when the ViewEvent is open
if (eventSelected) {
setEventSelected(null);
return;
}
setCreating(!creating);
};
return (
<div className="events">
{/* New Event Creating */}
{creating && (
<CreateEvent onHidder={modelBoxHandler} allEvents={allEvents} />
)}
{console.log("Event Rendered.js =>")}
</div>
);
}
export default React.memo(Events, () => true);
Child Component where the Rect memo doesn't have props:
import React from "react";
import AuthContext from "../../../context/global-context";
function CreateEvent({ onHidder, allEvents }) {
// Context
const { token } = React.useContext(AuthContext);
console.log("CreatedEvent.js REnder");
return (
... Some code here
);
}
export default React.memo(CreateEvent, (prevProps, nextProps) => {
console.log("Hello", prevProps, nextProps);
});
Thanks in advance for your valuable answer and times!
The problem is that on basis of creating variable you are actually remounting and not rendering the CreateEvent component. What it means is that if creating variable changes, the component is unmounted and re-mounted when creating is true, so its not a re-render
Also you must note that modelBoxHandler function reference also changes on each re-render so even if your CreateEvent component is in rendered state and the parent re-rendered due to some reason , the CreateEvent component too will re-render
There are 2 changes that you need to make to make it work better
Define modelBoxHandler with a useCallback hook
perform conditional rendering in createEvent based on creating prop
// Creating Event Showing
const modelBoxHandler = useCallback(() => {
// works on when the ViewEvent is open
if (eventSelected) {
setEventSelected(null);
return;
}
setCreating(prevCreating => !prevCreating);
}, [eventSelected]);
...
return (
<div className="events">
{/* New Event Creating */}
<CreateEvent creating={creating} onHidder={modelBoxHandler} allEvents={allEvents} />
{console.log("Event Rendered.js =>")}
</div>
);
and in createEvent
function CreateEvent({ onHidder, allEvents, creating }) {
// Context
const { token } = React.useContext(AuthContext);
console.log("CreatedEvent.js REnder");
if(!creating) {
return null;
}
return (
... Some code here
);
}
export default React.memo(CreateEvent);
In your example, you don't have an additional render for React.memo to work.
According to your render logic, there aren't any nextProps, you unmount the component with conditional rendering (creating).
// You toggle with `creating` value, there is only single render each time
creating && <CreateEvent onHidder={modelBoxHandler} allEvents={allEvents}/>
// Works, because there will be multiple renders (nextProps)
true && <CreateEvent onHidder={modelBoxHandler} allEvents={allEvents} />
In this case, you might not need React.memo.
This is my first React Context implementation. I am using Gatsby and in my layout.js I added Context (with objects and handler function) that successfully gets passed to Consumer:
import AppContext from "./AppContext"
const Layout = ({ children }) => {
let doAuthenticate = () => {
authState = {
isAuth: authState.isAuth === true ? false : true,
}
}
let authState = {
isAuth: false,
doAuthenticate: doAuthenticate
}
return (
<>
<AppContext.Provider value={authState}>
<main>{children}</main>
</AppContext.Provider>
</>
)
I successfully execute function in Consumer:
<AppContext.Consumer>
{ value =>
<button onClick={value.doAuthenticate}Sign in</button>
}
</AppContext.Consumer>
I also see the value in doAuthenticate successfully gets updated.
However, another Consumer that is listening to Provider does not update the value. Why?
When you use Gatsby, each instance of the Page will we wrapped with the Layout component and hence you will see that instead of creating one Context that is shared between pages, you end up creating multiple contexts.
Now multiple contexts cannot communicate with each other
The solution here is to make use of wrapRootElement api in gatsby-ssr.js and gatsby-browser.js to wrap all your pages with a single layout component
import React from "react";
import Layout from "path/to/Layout";
const wrapRootElement = ({ element }) => {
return (
<Layout>
{element}
</Layout>
)
}
export { wrapRootElement };
I'm trying to learn to create hooks so I can re-use data that I have to change in different components.
I'm using Material UI's Tabs and need to use useTab, a custom hook to change the tab id.
import React, { useContext } from 'react';
import { ProductsContext } from './ProductsContext';
import AppBar from '#material-ui/core/AppBar';
import Tabs from '#material-ui/core/Tabs';
import Tab from '#material-ui/core/Tab';
import { useTab } from '../../hooks/tab';
const ProductsNav = () => {
const {products, categories, loading} = useContext(ProductsContext);
const [tabValue] = useTab(0);
const handleTabChange = (e, newTabValue) => {
useTab(newTabValue);
}
return (
<div className="products">
<AppBar position="static">
<Tabs value={tabValue} onChange={ handleTabChange }>
{
Array.from(categories).map(category => (
!category.unlisted && (<Tab label={category.title} key={category.id}/>)
))
}
</Tabs>
</AppBar>
</div>
);
};
export default ProductsNav;
I know it does this with child functions in the docs, but I'm trying to not just copy and paste and do it in my own way.
Here is my custom useTab hook:
import {useState, useEffect} from 'react';
export const useTab = (selectedTab) => {
const [tabValue, setTabValue] = useState(0);
useEffect(() => {
setTabValue(selectedTab);
}, []);
return [tabValue];
}
I'm of course getting an error I can't use a hook inside of a function, but I'm confused how else to do this.
How can I change tabValue from useTabs?
The error is probably here:
const handleTabChange = (e, newTabValue) => {
useTab(newTabValue);
}
You're violating one of the primary Rules of Hooks:
Don’t call Hooks inside loops, conditions, or nested functions.
Instead, always use Hooks at the top level of your React function.
The reason for this rule is a bit complex but it basically boils down to the idea that hooks should only be called at the top level of a React functional component because they must be guaranteed to run every time the component function is run.
Hence why you're getting an error "I can't use a hook inside of a function"...
At any rate, it is unclear why you are using a custom hook with a useEffect() here. That seems completely unnecessary - a regular useEffect() hook inside of your nav component should more than suffice:
const ProductsNav = () => {
const {products, categories, loading} = useContext(ProductsContext);
const [tabValue, setTabValue] = useState(0);
const handleTabChange = (e, newTabValue) => {
setTabValue(newTabValue);
}
return (
<div className="products">
<AppBar position="static">
<Tabs value={tabValue} onChange={ handleTabChange }>
{
Array.from(categories).map(category => (
!category.unlisted && (<Tab label={category.title} key={category.id}/>)
))
}
</Tabs>
</AppBar>
</div>
);
};
I'm trying to port from class component to react hooks with Context API, and I can't figure out what is the specific reason of getting the error.
First, my Codes:
// contexts/sample.jsx
import React, { createContext, useState, useContext } from 'react'
const SampleCtx = createContext()
const SampleProvider = (props) => {
const [ value, setValue ] = useState('Default Value')
const sampleContext = { value, setValue }
return (
<SampleCtx.Provider value={sampleContext}>
{props.children}
</SampleCtx.Provider>
)
}
const useSample = (WrappedComponent) => {
const sampleCtx = useContext(SampleCtx)
return (
<SampleProvider>
<WrappedComponent
value={sampleCtx.value}
setValue={sampleCtx.setValue} />
</SampleProvider>
)
}
export {
useSample
}
// Sends.jsx
import React, { Component, useState, useEffect } from 'react'
import { useSample } from '../contexts/sample.jsx'
const Sends = (props) => {
const [input, setInput ] = useState('')
const handleChange = (e) => {
setInput(e.target.value)
}
const handleSubmit = (e) => {
e.preventDefault()
props.setValue(input)
}
useEffect(() => {
setInput(props.value)
}, props.value)
return (
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleChange} />
<button type="submit">Submit</button>
</form>
)
}
Error I got:
Invariant Violation: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: 1. You might have mismatching versions of React and the renderer (such as React DOM) 2. You might be breaking the Rules of Hooks 3. You might have more than one copy of React in the same app See https://reactjs.org/warnings/invalid-hook-call-warning.html for tips about how to debug and fix this problem.
Explanation for my code:
I used Context API to manage the states, and previously I used class components to make the views. I hope the structure is straightforward that it doesn't need any more details.
I thought it should work as well, the <Sends /> component gets passed into useSample HoC function, and it gets wrapped with <SampleProvider> component of sample.jsx, so that <Sends /> can use the props provided by the SampleCtx context. But the result is failure.
Is it not valid to use the HoC pattern with React hooks? Or is it invalid to hand the mutation function(i.e. setValue made by useState()) to other components through props? Or, is it not valid to put 2 or more function components using hooks in a single file? Please correct me what is the specific reason.
So HOCs and Context are different React concepts. Thus, let's break this into two.
Provider
Main responsibility of the provider is to provide the context values. The context values are consumed via useContext()
const SampleCtx = createContext({});
export const SampleProvider = props => {
const [value, setValue] = useState("Default Value");
const sampleContext = { value, setValue };
useEffect(() => console.log("Context Value: ", value)); // only log when value changes
return (
<SampleCtx.Provider value={sampleContext}>
{props.children}
</SampleCtx.Provider>
);
};
HOC
The consumer. Uses useContext() hook and adds additional props. Returns a new component.
const withSample = WrappedComponent => props => { // curry
const sampleCtx = useContext(SampleCtx);
return (
<WrappedComponent
{...props}
value={sampleCtx.value}
setValue={sampleCtx.setValue}
/>
);
};
Then using the HOC:
export default withSample(Send)
Composing the provider and the consumers (HOC), we have:
import { SampleProvider } from "./provider";
import SampleHOCWithHooks from "./send";
import "./styles.css";
function App() {
return (
<div className="App">
<SampleProvider>
<SampleHOCWithHooks />
</SampleProvider>
</div>
);
}
See Code Sandbox for full code.
Higher order Components are functions that takes a Component and returns another Component, and the returning Components can be class component, a Functional Component with hooks or it can have no statefull logic.
In your example you're returning jsx from useSample.
const useSample = (WrappedComponent) => {
const sampleCtx = useContext(SampleCtx)
return ( // <-- here
<SampleProvider>
<WrappedComponent
value={sampleCtx.value}
setValue={sampleCtx.setValue} />
</SampleProvider>
)
}
if you want to make a HOC what you can do is something like this
const withSample = (WrappedComponent) => {
return props => {
const sampleCtx = useContext(SampleCtx)
<WrappedComponent
value={sampleCtx.value}
setValue={sampleCtx.setValue} {...props} />
}
}