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).
Related
I am trying to create my own "vanilla-React" toast notification and I did manage to make it work however I cannot wrap my head around why one of the solutions that I tried is still not working.
So here we go, onFormSubmit() I want to run the code to get the notification. I excluded a bunch of the code to enhance readability:
const [notifications, setNotifications] = useState<string[]>([]);
const onFormSubmit = (ev: FormEvent<HTMLFormElement>) => {
ev.preventDefault();
const newNotifications = notifications;
newNotifications.push("success");
console.log(newNotifications);
setNotifications(newNotifications);
};
return (
<>
{notifications.map((state, index) => {
console.log(index);
return (
<ToastNotification state={state} instance={index} key={index} />
);
})}
</>
</section>
);
Inside the Toast I have the following:
const ToastNotification = ({
state,
instance,
}:
{
state: string;
instance: number;
}) => {
const [showComponent, setShowComponent] = useState<boolean>(true);
const [notificationState, setNotificationState] = useState(
notificationStates.empty
);
console.log("here");
const doNotShow = () => {
setShowComponent(false);
};
useEffect(() => {
const keys = Object.keys(notificationStates);
const index = keys.findIndex((key) => state === key);
if (index !== -1) {
const prop = keys[index] as "danger" | "success";
setNotificationState(notificationStates[prop]);
}
console.log(state);
}, [state, instance]);
return (
<div className={`notification ${!showComponent && "display-none"}`}>
<div
className={`notification-content ${notificationState.notificationClass}`}
>
<p className="notification-content_text"> {notificationState.text} </p>
<div className="notification-content_close">
<CloseIcon color={notificationState.closeColor} onClick={doNotShow} />
</div>
</div>
</div>
);
};
Now for the specific question - I cannot understand why onFormSubmit() I just get a log with the array of strings and nothing happens - it does not even run once - the props get updated with every instance and that should trigger a render, the notifications are held into a state and even more so, should update.
What is wrong with my code?
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.
I have an array of objects called data. I loop this array and render the Counter component. Increment and decrement of the counter value are passed as props to the component.
But if I change the value in a one-component, the other two components also re-renders. Which is not needed. How do I prevent this behavior? I tried memo and useCallback but seems not implemented correctly.
Counter.js
import React, { useEffect } from "react";
const Counter = ({ value, onDecrement, onIncrement, id }) => {
useEffect(() => {
console.log("Function updated!", id);
}, [onDecrement, onIncrement]);
return (
<div>
{value}
<button onClick={() => onDecrement(id)}>-</button>
<button onClick={() => onIncrement(id)}>+</button>
</div>
);
};
export default React.memo(Counter);
Home.js
import React, { useState, useCallback } from "react";
import Counter from "../components/Counter";
export default function Home() {
const [data, setData] = useState([
{
id: 1,
value: 0,
},
{
id: 2,
value: 0,
},
{
id: 3,
value: 0,
},
]);
const onIncrement = useCallback(
(id) => {
setData((e) =>
e.map((record) => {
if (record.id === id) {
record.value += 1;
}
return record;
})
);
},
[data]
);
const onDecrement = useCallback(
(id) => {
setData((e) =>
e.map((record) => {
if (record.id === id) {
record.value -= 1;
}
return record;
})
);
},
[data]
);
return (
<div>
<h1>Home</h1>
{data.map((e) => {
return (
<Counter
value={e.value}
onDecrement={onDecrement}
onIncrement={onIncrement}
id={e.id}
/>
);
})}
</div>
);
}
I suspect useCallback & useMemo are not helpful in this case, since you're running an inline function in your render:
{data.map(e => <Counter ...>)}
This function will always returns a fresh array & the component will always be different than the previous one.
In order to fix this, I think you'd want to memoize that render function, not the Counter component.
Here's a simple memoized render function with useRef:
// inside of a React component
const cacheRef = useRef({})
const renderCounters = (data) => {
let results = []
data.forEach(e => {
const key = `${e.id}-${e.value}`
const component = cacheRef.current[key] || <Counter
value={e.value}
key={e.id}
onDecrement={onDecrement}
onIncrement={onIncrement}
id={e.id}
/>
results.push(component)
cacheRef.current[key] = component
})
return results
}
return (
<div>
<h1>Home</h1>
{renderCounters(data)}
</div>
);
In the codesandbox below, only the clicked component log its id:
https://codesandbox.io/s/vibrant-wildflower-0djo4?file=/src/App.js
Disclaimer: With this implementation, the component will only rerender if its data value changes. Other props (such as the increment/decrement callbacks) will not trigger changes. There's also no way to clear the cache.
Memoize is also trading memory for performance — sometimes it's not worth it. If there could be thousands of Counter, there're better optimiztion i.e. changing UI design, virtualizing the list, etc.
I'm sure there's a way to do this with useMemo/React.memo but I'm not familiar with it
I'm building a simple toast notification system using React Context. Here is a link to a simplified but fully working example which shows the problem https://codesandbox.io/s/currying-dust-kw00n.
My page component is wrapped in a HOC to give me the ability to add, remove and removeAll toasts programatically inside of this page. The demo has a button to add a toast notification and a button to change the activeStep (imagine this is a multi-step form). When the activeStep is changed I want all toasts to be removed.
Initially I did this using the following...
useEffect(() => {
toastManager.removeAll();
}, [activeStep]);
...this worked as I expected, but there is a react-hooks/exhaustive-deps ESLint warning because toastManager is not in the dependency array. Adding toastManager to the array resulted in the toasts being removed as soon as they were added.
I thought I could have fixed that using useCallback...
const stableToastManager = useCallback(toastManager, []);
useEffect(() => {
stableToastManager.removeAll();
}, [activeStep, stableToastManager]);
...however, not only does this not work but I would rather fix the issue at the source so I don't need to do this every time I want this kind of functionality, as it is likely to be used in many places.
This is where I am stuck. I'm unsure as to how to change my Context so that I don't need add additional logic in the components that are being wrapped by the HOC.
export const ToastProvider = ({ children }) => {
const [toasts, setToasts] = useState([]);
const add = (content, options) => {
// We use the content as the id as it prevents the same toast
// being added multiple times
const toast = { content, id: content, ...options };
setToasts([...toasts, toast]);
};
const remove = id => {
const newToasts = toasts.filter(t => t.id !== id);
setToasts(newToasts);
};
const removeAll = () => {
if (toasts.length > 0) {
setToasts([]);
}
};
return (
<ToastContext.Provider value={{ add, remove, removeAll }}>
{children}
<div
style={{
position: `fixed`,
top: `10px`,
right: `10px`,
display: `flex`,
flexDirection: `column`
}}
>
{toasts.map(({ content, id, ...rest }) => {
return (
<button onClick={() => remove(id)} {...rest}>
{content}
</button>
);
})}
</div>
</ToastContext.Provider>
);
};
export const withToastManager = Component => props => {
return (
<ToastContext.Consumer>
{context => {
return <Component toastManager={context} {...props} />;
}}
</ToastContext.Consumer>
);
};
If you want to "Fix it from the core", you need to fix ToastProvider:
const add = useCallback((content, options) => {
const toast = { content, id: content, ...options };
setToasts(pToasts => [...pToasts, toast]);
}, []);
const remove = useCallback(id => {
setToasts(p => p.filter(t => t.id !== id));
}, []);
const removeAll = useCallback(() => {
setToasts(p => (p.length > 0 ? [] : p));
}, []);
const store = useMemo(() => ({ add, remove, removeAll }), [
add,
remove,
removeAll
]);
Then, the useEffect will work as expected, as the problem was that you re-initialized the ToastProvider functionality on every render when it needs to be a singleton.
useEffect(() => {
toastManager.removeAll();
}, [activeStep, toastManager]);
Moreover, I would recommend to add a custom hook feature as the default use case, and providing wrapper only for class components.
In other words, do not use wrapper (withToastManager) on functional components, use it for classes, as it is considered an anti-pattern, you got useContext for it, so your library should expose it.
// # toastContext.js
export const useToast = () => {
const context = useContext(ToastContext);
return context;
};
// # page.js
import React, { useState, useEffect } from 'react';
import { useToast } from './toastContext';
const Page = () => {
const [activeStep, setActiveStep] = useState(1);
const { removeAll, add } = useToast();
useEffect(() => {
removeAll();
}, [activeStep, removeAll]);
return (
<div>
<h1>Page {activeStep}</h1>
<button
onClick={() => {
add(`Toast at ${Date.now()}!`);
}}
>
Add Toast
</button>
<button
onClick={() => {
setActiveStep(activeStep + 1);
}}
>
Change Step
</button>
</div>
);
};
export default Page;
First of all, my approach could just be misguided from the start.
I have a component that lists objects added by a sibling component.
I would like the list component to update when a new object is added.
As you can see, I'm calling the same function (getHostedServiceList) in both components. Obviously, this would need t be cleaned up, but I'd like to get it working first
I'm using hooks to accomplish this.
//input
const options = [
{ value: '1', label: '1' },
{ value: '2', label: '2' },
{ value: '3', label: '3' },
];
// class Remotes extends Component {
const Remotes = ({ ...props }) => {
const [service, setService] = useState();
const [url, setUrl] = useState();
const [token, setToken] = useState();
const [displayName, setDisplayName] = useState();
const [apiUrl, setApiUrl] = useState();
const [services, setServices] = useState();
let HOME = process.env.HOME || '';
if (process.platform === 'win32') {
HOME = process.env.USERPROFILE || '';
}
const getHostedServiceList = () => {
console.log('props', props);
if (!fs.existsSync(`${HOME}/providers.json`)) {
return newMessage(
`Unable to locate ${HOME}/providers.json`,
'error',
);
}
const payload = JSON.parse(
fs.readFileSync(`${HOME}/providers.json`),
);
setServices(payload);
};
const setProvider = selectedOption => {
setService(selectedOption.value);
setUrl(`http://www.${selectedOption.value}.com`);
setApiUrl(`http://www.${selectedOption.value}.com/api/v1`);
};
const { onAddRemote } = props;
return (
<div>
<div>Add a remote host:</div>
<StyledSelect
value="Select Provider"
onChange={setProvider}
options={options}
/>
{console.log('service', service)}
<TextInput
label="Url"
defaultValue={url}
onChange={e => {
setProvider(e.target.value);
}}
disabled={!service ? 'disabled' : ''}
/>
<TextInput
label="API Url"
defaultValue={apiUrl}
onChange={e => setApiUrl(e.target.value)}
disabled={!service ? 'disabled' : ''}
/>
<TextInput
label="Token"
onChange={e => setToken(e.target.value)}
disabled={!service ? 'disabled' : ''}
/>
<TextInput
label="Display Name"
onChange={e => setDisplayName(e.target.value)}
disabled={!service ? 'disabled' : ''}
/>
<Button
disabled={!service || !url || !token}
onClick={() => {
onAddRemote({ service, url, apiUrl, token, displayName });
getHostedServiceList();
}}
>
Add Remote
</Button>
</div>
);
};
//list
const HostedProviderList = ({ ...props }) => {
const [services, setServices] = useState();
let HOME = process.env.HOME || '';
if (process.platform === 'win32') {
HOME = process.env.USERPROFILE || '';
}
const getHostedServiceList = () => {
console.log('props', props);
if (!fs.existsSync(`${HOME}/providers.json`)) {
return newMessage(
`Unable to locate ${HOME}/providers.json`,
'error',
);
}
const payload = JSON.parse(
fs.readFileSync(`${HOME}/providers.json`),
);
setServices(payload);
};
useEffect(() => {
// console.log('props 1', services);
getHostedServiceList();
}, []);
return (
<Wrapper>
<Flexbox>
<Title>Provider List</Title>
</Flexbox>
<div>
{services &&
services.map((service, i) => (
<Service key={i}>
<ServiceName>{service.displayName}</ServiceName>
<ServiceProvider>{service.service}</ServiceProvider>
</Service>
))}
</div>
</Wrapper>
);
};
I would like the list component to update when a new object is added.
Yes, you could use Redux (or React's own 'context') for global state handling. However, a simpler solution to be considered might just be to send the data to the parent and pass to the list component like so:
class Parent extends Component {
state = { objectsAdded: [] }
onObjectAdded = ( obj ) => {
// deepclone may be needed
this.setState({objectsAdded: this.state.objectsAdded.concat(obj)})
}
render() {
return (
<Fragment>
<ListComponent objects={this.state.objectsAdded} />
<ObjectAdder onObjectAdded={this.onObjectAdded} />
</Fragment>
}
}
This is where something like Redux or MobX comes in handy. These tools allow you to create a "store" - the place where you load and store data used by different components throughout your app. You then connect your store to individual components which interact with the data (displaying a list, displaying a create/edit form, etc). Whenever one component modifies the data, all other components will receive the updates automatically.
One way this cross-communication is accomplished is through a pub/sub mechanism - whenever one component creates or modifies data, it publishes (or "dispatches") an event. Other components subscribe (or "listen") for these events and react (or "re-render") accordingly. I will leave the research and implementation up to the reader as it cannot be quickly summarized in a StackOverflow answer.
You might also try the new React hooks, as this allows you to easily share data between components. If you choose this option, please take special care to do it properly as it is easy to be lazy and irresponsible.
To get you started, here are some articles to read. I highly recommend reading the first one:
https://reactjs.org/docs/thinking-in-react.html
https://redux.js.org/basics/usage-with-react
https://mobx.js.org/getting-started.html