React throws the following error when I am trying to render different components
Warning: React has detected a change in the order of Hooks called by GenericDialog. This will lead to bugs and errors if not fixed.
Previous render
Next render
useRef
useRef
useState
useState
useState
useState
useState
useState
useState
useState
useState
useState
useContext
useState
I do agree this would be inappropriate when I would be rendering the same component each time but with different order of hooks. What I am trying to achieve is render a different component each time so it is quite obvious the order of hooks won't be identical.
I have created this GenericDialog component which renders a multistep dialog.
import React, { useRef, useState, useEffect } from 'react';
import { DialogFooterNavigation } from './DialogFooterNavigation';
import { Dialog } from '../../../../Dialog';
import { Subheader } from '../../../../Subheader';
import { Loading } from '../../../../Loading';
export interface FooterConfiguration {
onContinue?: () => Promise<boolean | void>;
isContinueDisabled?: boolean;
continueLabel?: string;
isBackHidden?: boolean;
isCancelHidden?: boolean;
}
export interface HeaderConfiguration {
subheader?: string;
}
export interface DialogStepProps {
setHeaderConfiguration: (config: HeaderConfiguration) => void;
setFooterConfiguration: (config: FooterConfiguration) => void;
}
export type DialogStep = (props: DialogStepProps) => JSX.Element;
interface GenericDialogProps {
isShown: boolean;
hideDialog: () => void;
steps: DialogStep[];
header: string;
}
export const GenericDialog = ({
isShown,
hideDialog,
steps,
header,
}: GenericDialogProps) => {
const buttonRef = useRef(null);
const [step, setStep] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [headerConfiguration, setHeaderConfiguration] = useState<HeaderConfiguration | undefined>(
undefined,
);
const [footerConfiguration, setFooterConfiguration] = useState<FooterConfiguration | undefined>(
undefined,
);
const [loadingMessage, setLoadingMessage] = useState<string>('');
const dialogBody = steps[step]({
setHeaderConfiguration,
setFooterConfiguration,
});
const nextStep = () => {
if (step < steps.length - 1) {
setStep(step + 1);
}
};
const prevStep = () => step > 0 && setStep(step -1);
const isBackPossible = step > 0;
const onBack = () => (isBackPossible || footerConfiguration?.isBackHidden ? undefined : prevStep);
const onContinue = async () => {
setIsLoading(true);
const result = await footerConfiguration?.onContinue?.call(undefined);
setIsLoading(false);
if (result === false) {
return;
}
nextStep();
};
return (
<Dialog isShown={isShown} onHide={hideDialog}>
<div>
{header}
{headerConfiguration?.subheader && (
<Subheader>{headerConfiguration.subheader}</Subheader>
)}
</div>
{isLoading && loadingMessage ? <Loading msg={loadingMessage} /> : dialogBody}
{!isLoading && (
<DialogFooterNavigation
onBack={isBackPossible ? onBack : undefined}
onContinue={onContinue}
isContinueDisabled={footerConfiguration?.isContinueDisabled}
/>
)}
</Dialog>
);
};
const FirstStep = (props: DialogStepProps) => {
// Here I need useContext
const { id, name } = useCustomerContext();
useEffect(() => {
props.setFooterConfiguration({
isContinueDisabled: !id || !name,
})
}, [id, name]);
return (
<>
<div>ID: {id}</div>
<div>Name: {name}</div>
</>
);
};
const SecondStep = (props: DialogStepProps) => {
// Here I don't need useContext but I do need useState
const [inputValue, setInputValue] = useState({});
useEffect(() => {
props.setFooterConfiguration({
isContinueDisabled: !inputValue,
});
}, [inputValue]);
return <input value={inputValue} onChange={(event) => setInputValue(event.target.value)} />;
}
const MyDialogExample = () => {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const steps: DialogStep[] = [
FirstStep,
SecondStep,
];
return (
<>
<button onClick={() => setIsDialogOpen(true)}>Open Dialog</button>
<GenericDialog
isShown={isDialogOpen}
hideDialog={() => setIsDialogOpen(false)}
steps={steps}
header="Dialog example"
/>
</>
);
};
The problem is here:
const dialogBody = steps[step]({
setHeaderConfiguration,
setFooterConfiguration,
});
Try changing it to something like this:
const DialogBody = steps[step];
And then, in your return statement:
{isLoading && loadingMessage ? <Loading msg={loadingMessage} /> : <DialogBody setHeaderConfiguration={setHeaderConfiguration} setFooterConfiguration={setFooterConfiguration} />}
Please note that it can be done differently, like:
const DialogBody = steps[step];
const dialogBody = <DialogBody setHeaderConfiguration={setHeaderConfiguration} setFooterConfiguration={setFooterConfiguration} />;
And keeping your return statement unaltered.
Explanation
Your code isn't entirely wrong though. When working with functional components, there is a subtle difference between an actual component, a hook and a simple function that returns an instantiated component based on some logic. The problem is that you are mixing those three.
You can't manually instantiate a component by calling its corresponding function (just like you can't instantiate a class component by using the new operator). Either you use JSX (like <DialogBody />) or directly use React inner methods (Like React.createElement()). Both alternatives are different from just doing dialogBody(). For example, if you see the compiled JSX code you will note that <DialogBody /> compiles to code that uses React.createElement() and the latter returns a real React element instance containing many special properties and methods.
dialogBody() would work if its only goal was to return an instantiated element (Using one of the methods above) based on some logic. This implies not using any hook along with some other constraints.
Instead, your dialogBody 'function' contains hooks and it acts as a custom hook itself. This is why React complains about hooks execution order. You are executing hooks conditionally.
Related
I get this error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
In itself, this is nothing new to me and I know how to fix this, but I can't figure it out in this case.
import reactStringReplace from 'react-string-replace';
import { Entity, Player } from './LogItem';
export default function PrepareText({
subjects,
text,
EntityPill,
PlayerPill,
}: {
subjects: { player: Player; entity: Entity };
text: string;
EntityPill: (text: string | null) => React.ReactNode;
PlayerPill: (text: string | null) => React.ReactNode;
}) {
return (
<>
{reactStringReplace(text, /(\${\w+})/g, (match, i) => {
const key = match
.replace('${', '')
.replace('}', '') as keyof typeof subjects;
return (
<span key={i}>
{key === 'player'
? PlayerPill(subjects[key])
: EntityPill(subjects[key])}
</span>
);
})}
</>
);
}
I think the problem is with conditionally rendering the two components PlayerPill and EntityPill because these use hooks inside. But usually, it's not e problem to conditionally render components.
Is it because I call them as functions? Is there e different way to pass props to a React.ReactNode?
If there is a better option to do this I would be very excited to implement it.
Edit
As requested here is the code of the Pills:
import useSteamUser from '../../../../hooks/useSteamUser';
import { CellProps } from '../LogItem';
export default function TargetCell({ value: text, restricted }: CellProps) {
return (
<span className="text-cyan-700 bg-cyan-700/5 px-2 py-0.5 rounded-full border border-cyan-700 text-sm">
{text}
</span>
);
}
and these pills are stored in an Object to be dynamically accessed like this:
NAME_CHANGED: {
Icon: (
<GiBodySwapping className="text-3xl fill-sand-500/60 group-hover:fill-sand-500 transition-colors" />
),
text: "You've changed your name!",
EntityCell: PlayerCell,
PlayerCell: PlayerCell,
},
Call of PrepareText. My real problem is that I need to pass text to the Pills but the Pills are used in PrepareText.
<PrepareText
subjects={{
player: data.player,
entity: data.entity,
}}
text={EVENTS[event].text}
EntityPill={(text) =>
EVENTS[event].EntityCell({ value: text || '', restricted })
}
PlayerPill={(text) =>
EVENTS[event].PlayerCell({ value: text || '', restricted })
}
/>
Sorry, I'm confused: why don't you simply do this:
return (
<span key={i}>
{key === 'player'
? <PlayerPill text={subjects[key]}/>
: <EntityPill text={subjects[key]}/>
}
</span>
);
Is it because I call them as functions?
Yes. You're not using them as components, you're using them as sub-functions of PrepareText. That means they use the component context of PrepareText, not their own context. So hooks save information to the underlying PrepareText instance, not their own, and so if the number of times you call them varies from render to render, it won't work correctly.
Instead, make them actual components and pass them information as props, not arguments; or make them render components using the arguments you pass them. (See also my answer to this related question.)
Here's an example of passing component functions (CompA/CompB, passed as Sub1/Sub2) to another component (Example) that uses them conditionally based on a 50/50 coin flip:
const { useState, useEffect } = React;
const flipCoin = () => Math.random() < 0.5;
const CompA = ({text}) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCounter(c => c + 1);
}, 800);
}, []);
return <div>CompA, {text}, counter = {counter}</div>;
};
const CompB = ({text}) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCounter(c => c + 1);
}, 800);
}, []);
return <div>CompB, {text}, counter = {counter}</div>;
};
const Example = ({Sub1, Sub2}) => {
const [counter, setCounter] = useState(0);
return <div>
<input type="button" value="Re-render" onClick={() => setCounter(c => c + 1)} />
{flipCoin() && <Sub1 text="a" />}
{flipCoin() && <Sub2 text="b" />}
</div>;
};
const App = () => {
return <Example Sub1={CompA} Sub2={CompB} />;
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
The components use hooks so that you can see that it works just fine that Example doesn't always render them when it renders (because of the flipCoin calls).
I have these all context created, but now I want to make it optimized. How do I pass all the created functions using only one context provider.
I have three functions to handle the cart options
handleAddProduct
handleRemoveProduct
handleCartClearance
I've created context for each function but now I want all the functions to be wraped in a single context provider.
and also need some optimizations for my all three functions because they are taking long to perform the action.
import './App.css';
import data from './component/back/Data/data'
import Header from './component/front/Header/Header';
import { BrowserRouter as Router } from 'react-router-dom'
import Routiis from './component/front/Routiis/Routiis'
import { createContext, useState } from 'react';
const AddProduct = createContext();
const RemoveProduct = createContext();
const EmptyCart = createContext();
const App = () => {
const { productItems } = data;
const [cartItems, setCartItems] = useState([]);
// function for adding the product...............................................................................................
const handleAddProduct = (product) => {
const ProductExist = cartItems.find((item) => item.id === product.id)
if (ProductExist) {
setCartItems
(cartItems.map((item) => item.id === product.id ?
{ ...ProductExist, quantity: ProductExist.quantity + 1 } : item))
}
else {
setCartItems([...cartItems, { ...product, quantity: 1 }])
}
}
// function for removing the product..............................................................................................
const handleRemoveProduct = (product) => {
const ProductExist = cartItems.find((item) => item.id === product.id)
if (ProductExist.quantity === 1) {
setCartItems(cartItems.filter((item) => item.id !== product.id))
}
else {
setCartItems(
cartItems.map((item) => item.id === product.id ? { ...ProductExist, quantity: ProductExist.quantity - 1 } : item,)
)
}
}
// function for clearing the cart...................................................................................................
const handleCartClearance = () => {
setCartItems([]);
}
return (
<>
<div className='site-bg'>
<AddProduct.Provider value={handleAddProduct}>
<RemoveProduct.Provider value={handleRemoveProduct}>
<EmptyCart.Provider value={handleCartClearance}>
<Router>
<Header cartItems={cartItems} />
<Routiis productItems={productItems} cartItems={cartItems} />
</Router>
</EmptyCart.Provider>
</RemoveProduct.Provider>
</AddProduct.Provider>
</div>
</>
);
}
export default App;
export { AddProduct, RemoveProduct, EmptyCart }
The Provider component accepts a value prop to be passed to consuming components that are descendants of this Provider. you could provide it with an object that carries the properties of the functions. and hence you only need to consume the Product
this answers your question
<>
<div className="site-bg">
<Product.Provider
value={{
handleAddProduct: handleAddProduct,
handleRemoveProduct: handleRemoveProduct,
handleCartClearance: handleCartClearance,
}}
>
<Router>
<Header cartItems={cartItems} />
<Routiis productItems={productItems} cartItems={cartItems} />
</Router>
</Product.Provider>
</div>
</>
Take a quick look at the official React context docs, it might be helpful.
Actually it is very easy. instead of passing one parameter as value pass it as object as below
<AddProduct.Provider value={{handleAddProduct:handleAddProduct,handleRemoveProduct:handleRemoveProduct}}>
</AddProduct.Provider>
like this and in your code where you want to use these function just do like this
const { handleRemoveProduct,handleAddProduct } = useContext(AddProduct);
The useSWR hook from swr works everywhere if I explicitly enter the fetcher.
const { data } = useSWR("http://...", fetcher);
However, if I used swr global configuration as shown below, the useSWR only works in First page but not in HeaderLayout component. I did some debugging and found out that in HeaderLayout doesn't receive the value from swr global configuration (SWRConfig in _app.tsx) even though it is wrapped inside.
I followed this doc https://nextjs.org/docs/basic-features/layouts#per-page-layouts for the page layout implementation
// _app.tsx
type NextPageWithLayout = NextPage & {
getLayout?: (page: React.ReactElement) => React.ReactNode;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);
return (
<SWRConfig
value={{
fetcher: (resource, init) =>
fetch(resource, init).then((res) => res.json()),
}}
>
{getLayout(<Component {...pageProps} />)}
</SWRConfig>
);
}
// pages/first
const First = () => {
const [searchInput, setSearchInput] = useState("");
const router = useRouter();
const { data } = useSWR("http://...");
return (
<div>...Content...</div>
);
};
First.getLayout = HeaderLayout;
// layout/HeaderLayout
const HeaderLayout = (page: React.ReactElement) => {
const router = useRouter();
const { project: projectId, application: applicationId } = router.query;
const { data } = useSWR(`http://...`);
return (
<>
<Header />
{page}
</>
);
};
Helpful links:
https://nextjs.org/docs/basic-features/layouts#per-page-layouts
https://swr.vercel.app/docs/global-configuration
Next.js context provider wrapping App component with page specific layout component giving undefined data
Your First.getLayout property should be a function that accepts a page and returns that page wrapped by the HeaderLayout component.
First.getLayout = function getLayout(page) {
return (
<HeaderLayout>{page}</HeaderLayout>
)
}
The HeaderLayout is a React component, its first argument contains the props passed to it. You need to modify its signature slightly to match this.
const HeaderLayout = ({ children }) => {
const router = useRouter();
const { project: projectId, application: applicationId } = router.query;
const { data } = useSWR(`http://...`);
return (
<>
<Header />
{children}
</>
);
};
Layouts doesnt work if you declare Page as const. So instead of const First = () => {...} do function First() {...}
I don't understand why the second line, which reads data from the props, is not displayed as instantly as the first, i would like them to be displayed instantly
I update the state when a button is clicked, which calls api, data is coming in, the state is updating, but the second line requires an additional press to display
How to display both lines at once after a call? What's my mistake?
I'm using react hooks, and i know that required to use useEffect for re-render component, i know, that how do work asynchronous call,but i'm a little confused, how can i solve my problem, maybe i need to use 'useDeep effect' so that watching my object properties, or i don't understand at all how to use 'useEffect' in my situation, or even my api call incorrectly?
I have tried many different solution methods, for instance using Promise.all, waiting for a response and only then update the state
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./test";
ReactDOM.render(<App />, document.getElementById("root"));
app.js
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const useDataApi = (initialState) => {
const [state, setState] = useState(initialState);
const stateCopy = [...state];
const setDate = (number, value) => {
setState(() => {
stateCopy[number].date = value;
return stateCopy;
});
};
const setInfo = async () => {
stateCopy.map((item, index) =>
getFetch(item.steamId).then((res) => setDate(index, res.Date))
);
};
const getFetch = async (id) => {
if (id === "") return;
const requestID = await fetch(`https://api.covid19api.com/summary`);
const responseJSON = await requestID.json();
console.log(responseJSON);
const result = await responseJSON;
return result;
};
return { state, setState, setInfo };
};
const Children = ({ data }) => {
return (
<>
<ul>
{data.map((item) => (
<li key={item.id}>
{item.date ? item.date : "Not data"}
<br></br>
</li>
))}
</ul>
</>
);
};
const InfoUsers = ({ number, steamid, change }) => {
return (
<>
<input
value={steamid}
numb={number}
onChange={(e) => change(number, e.target.value)}
/>
</>
);
};
function App() {
const usersProfiles = [
{ date: "", id: 1 },
{ date: "", id: 2 }
];
const profiles = useDataApi(usersProfiles);
return (
<div>
<InfoUsers number={0} change={profiles.setID} />
<InfoUsers number={1} change={profiles.setID} />
<button onClick={() => profiles.setInfo()}>Get</button>
<Children data={profiles.state} loading={profiles} />
</div>
);
}
export default App;
To get the data, just click GET
In this example, completely removed useEffect, maybe i don’t understand how to use it correctly.
P.s: Sorry for bad english
You don't need stateCopy, as you have it in the callback of the setState:
const setInfo = async () => {
// we want to update the component only once
const results = await Promise.all(
state.map(item => getFetch(item.steamId))
);
// 's' is the current state
setState(s =>
results.map((res, index) => ({ ...s[index], date: res.Date })
);
};
There's one global hook which returns react components when used:
const { SomeComponent1, SomeComponent2 } = useHook({ prop1, prop2 })
How do I pass props to the components I am returning from the hook?
const useHook = ({ prop1, prop2 }) => {
return {
SomeComponent1, // <-- I'd like to 'bind' props to the component here
SomeComponent2,
// THE CODE BELOW IS INCORRECT
// it's a way to illustrate what I am after:
// SomeComponent1: <SomeComponent1 prop1={prop1} />
}
}
Passing the props down to destructed react components is a no-go for me:
const { Comp1, Comp2 } = useHook()
return (
<Comp1 prop={prop1} />
)
You can create the components as functional components and use useMemo inside your useHook to prevent remounting of the components on each render
const useHook = ({ prop1, prop2 }) => {
const SomeComponent1Wrap = useMemo(() => () => {
return <SomeComponent1 prop={prop1} />
}, [prop1]);
const SomeComponent2Wrap = useMemo(() => () => {
return <SomeComponent2 prop={prop2} />
}, [prop2]);
return {
SomeComponent1: SomeComponent1Wrap,
SomeComponent2: SomeComponent2Wrap
}
}
The most common and elegant solution will be conditional based rendering. You can use a ternary based operator to display your component For eg.
import React,{useState} from "react";
import {Comp1} from "./comp1";
import {Comp2} from "./comp2";
function Test(){
const [display,setDisplay] = useState(true);
render(){
<>
{display ? <Comp1 props={props} /> : <Comp2 props={props} /> }
</>
}
}