I have two components, such that they are:
const ChildOn = (/*...*/) => {
//...
}
const Parent = () => {
const [is42, setIs42] = useState(true)
return (is42 ? <ChildOff ... > : <ChildOn ... />)
}
The definition of ChildOff is no important.
I want to define them as either of the following, yet I can't decide which:
Declares functions used in children, based on a variable/function in the parent, inside each child.
type ChildOnProp = { setIs42: Dispatch<SetStateAction<boolean>> };
const ChildOn = ({ setIs42 }: ChildOnProp) => {
const f1 = () => { setIs42(true); };
return <Text onPress={f1} />;
};
const Parent = () => {
return is42
? <ChildOff setIs42={setIs42} />
: <ChildOn setIs42={setIs42} />;
};
Defines functions used by children, inside the parent.
type ChildOnProps = { func: () => void }
const ChildOn = ({ func }: ChildOnProps) => {
return <Text onPress={func} />
}
const Parent = () => {
const [is42, setIs42] = useState(true)
const f1 = useCallback(() => { setIs42(true) })
const f2 = useCallback(() => { setIs42(false) })
return (is42 ? <ChildOff func={f2} /> : <ChildOn func={f1} />)
}
While (1) is much prettier to me, (2) seems a lot more efficient. Yet I don't know if I'm apt to judge that, since I've read many articles on React contradicting each other on when it's best to use useCallback.
The React community warns against useless memoizing as it could add complexity without any of the benefits in some situations.
In this case, I think it's worth memoizing the callbacks since it will reduce the number of unnecessary renders of the child components, which otherwise could have negative impact, or even introduce issues down over time.
To do this, there's a common pattern of creating a custom hook, often called useToggle, which memoize common setters.
import {useState, useMemo} from 'react';
export function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
// Defined once, so guaranteed stability
const setters = useMemo(() => ({
toggle: () => setValue(v => !v),
setFalse: () => setValue(false),
setTrue: () => setValue(true),
setValue,
}), [setValue]);
// Defined each time the value changes, so less than every render.
return useMemo(() => ({
...setters,
value
}), [value, setters]);
}
This can be used in the parent as such:
const Parent = () => {
const { value: is42, setTrue, setFalse } = useToggle(true);
return (is42 ? <ChildOff func={setFalse}> : <ChildOn func={setTrue} />);
}
TypeScript will infer all the types automatically, so it works out of the box.
If there's multiple state values that need a toggle callback, we can easily identify each one by not destructuring.
const firstState = useToggle(true);
const secondState = useToggle(true);
//...
return (
<>
<Foo onClick={firstState.toggle} />
<Bar onClick={secondState.toggle} />
</>
);
As a reference, here's a simpler implementation from Shopify.
As for memoizing too much or too little, Meta (Facebook) is apparently experimenting with memoizing everything by default at transpile time, so that devs wouldn't have to think about it.
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 rewritten a Child class component in React to a functional component. Here is the simplified code example.
For sure, as so often, this is a simplified code and more things are done with the value in the parent component. That's why we have and need it there.
const Parent = (props) => {
const [value, setValue] = useState(null);
const handleChange = (newValue) => {
// do something with newValue and probably change it
// store the result in `newChangedValue`
setValue(newChangedValue);
}
return (
<Child value={value} onChange={handleChange}/>
);
}
const Child = (props) => {
const {value} = props;
// This solution does not work for me,
// because it's always triggered, when
// `value` changes. I only want to trigger
// `logValueFromProp` after clicking the
// Button.
useEffect(() => {
logValueFromProp();
}, [value]);
const handleClick = () => {
// some calculations to get `newValue`
// are happening here
props.onChange(newValue);
logValueFromProp();
}
const logValueFromProp = () {
console.log(prop.value);
}
return (
<Button onClick={handleClick} />
);
}
What I want to do is to log a properties value, but only if it got changed by clicking the button. So just using a useEffect does not work for me.
Before changing the child component to a functional component, the property had its new value before I was calling logValueFromProp(). Afterwards it doesn't. I guess that's cause of some timing, and I was just lucky that the property was updated before the function was called.
So the question is: How would you solve this situation? One solution I thought of was a state in the child component which I set when the button is clicked and in the useEffect I only call the function when the state is set and then reset the state. But that doesn't feel like the optimal solution to me...
Three possible solutions for you
Pass logValueFromProp the value directly — but in a comment you've explained that the value might be modified slightly by the parent component before being set on the child, which would make this not applicable.
Use a flag in a ref. But if the parent doesn't always change the prop, that would be unreliable.
Have the parent accept a callback in its handleChange.
#1
If possible, I'd pass the value directly to logValueFromProp when you want to log it. That's the simple, direct solution:
const Child = (props) => {
const {value} = props;
const handleClick = () => {
props.onChange(newValue);
logValueFromProp(newValue);
};
const logValueFromProp = (newValue = prop.value) {
console.log(newValue);
};
return (
<Button onClick={handleClick} />
);
};
But in a comment you've said the new value may not be exactly the same as what you called props.onChange with.
#2
You could use a ref to remember whether you want to log it when the component function is next called (which will presumably be after it changes):
const Child = (props) => {
const {value} = props;
const logValueRef = useRef(false);
if (logValueRef.current) {
logValueFromProp();
logValueRef.current = false;
}
const handleClick = () => {
props.onChange(newValue);
logValueRef.current = true;
};
const logValueFromProp = () {
console.log(prop.value);
};
return (
<Button onClick={handleClick} />
);
};
Using a ref instead of a state member means that when you clear the flag, it doesn't cause a re-render. (Your component function is only called after handleClick because the parent changes the value prop.)
Beware that if the parent component doesn't change the value when you call prop.onChange, the ref flag will remain set and then your component will mistakenly log the next changed value even if it isn't from the button. For that reason, it might make sense to try to move the logging to the parent, which knows how it responds to onChange.
#3
Given the issues with both of the above, the most robust solution would seem to be to modify Parent's handleChange so that it calls a callback with the possibly-modified value:
const Parent = (props) => {
const [value, setValue] = useState(null);
const handleChange = (newValue, callback) => {
// ^^^^^^^^^^−−−−−−−−−−−−−−−−− ***
// do something with newValue and probably change it
// store the result in `newChangedValue`
setValue(newChangedValue);
if (callback) { // ***
callback(newChangedValue); // ***
} // ***
};
return (
<Child value={value} onChange={handleChange}/>
);
};
const Child = (props) => {
const {value} = props;
const handleClick = () => {
props.onChange(newValue, logValueFromProp);
// ^^^^^^^^^^^^^^^^^^−−−−−−−−−−−−−− ***
}
const logValueFromProp = () {
console.log(prop.value);
};
return (
<Button onClick={handleClick} />
);
};
This answer is based upon the answer of T.J. Crowder (#2).
You can create a custom hook that accepts a callback and dependencies. And returns a function that will trigger a re-render (by using useState instead of useContext) calling the callback in the process.
I've enhanced his answer by allowing you to pass a dependency array which will be used to determine if the callback is called. If the dependency array is omitted, the callback is always called. When passed, the callback is only called if there was a change in the dependency array.
I went for the name useTrigger in the example below, but depending on preference you might like another name better. For example useChange.
const { useState, useCallback } = React;
const useTrigger = (function () {
function zip(a1, a2) {
return a1.map((_, i) => [a1[i], a2[i]]);
}
// compares 2 arrays assuming the length is the same
function equals(a1, a2) {
return zip(a1, a2).every(([e1, e2]) => Object.is(e1, e2));
}
return function (callback, deps) {
const [trigger, setTrigger] = useState(null);
if (trigger) {
if (!deps || !equals(deps, trigger.deps)) {
callback(...trigger.args);
}
setTrigger(null);
}
return useCallback((...args) => {
setTrigger({ args, deps });
}, deps);
}
})();
function Parent() {
const [value, setValue] = useState(null);
function handleChange(newValue) {
// Sometimes the value is changed, triggering `logValueFromProp()`.
// Sometimes it isn't.
if (Math.random() < 0.66) newValue++;
setValue(newValue);
}
return <Child value={value} onChange={handleChange} />;
}
function Child({ value, onChange }) {
const logValueFromProp = useTrigger(() => {
console.log(value);
}, [value]);
function handleClick() {
onChange(value || 0);
logValueFromProp();
}
return (
<button onClick={handleClick}>
Click Me!
</button>
);
}
ReactDOM.render(<Parent />, document.querySelector("#demo"));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="demo"></div>
I am setupping a simple dashboard to challeging my self with ReactJS, but I have some issues preventing useless re-rendering.
I have a root component called App where I fetch some data.
const App = () => {
const [data, setData] = useState(null);
const [list1, setList1] = useState(null);
const [list2, setList2] = useState(null);
const [list3, setList3] = useState(null);
const [list4, setList4] = useState(null);
useEffect(() => {
const fetchData = fetchDataInSomeWay();
const fetchedData = getData(fetchData);
const list1Data = getList1(fetchData);
setList1(list1Data);
setData(fetchedData);
});
...
{ data !== null
&& (
<Parent
data={data}
list1={list1}
list2={list2}
list3={list3}
list4={list4}
/>
};
Then I setup a Parent component where I created some Select component and other elements which depend on the values selected by select.
I have a Select element for each list state created with useState();
const Google = ({
data,
list1,
list2,
list3,
list4,
}) => {
const [typeValue, setTypeValue] = useState('someValue');
const [list1Value, setList1Value] = useState(list1[0]);
const [list2Value, setList2Value] = useState(list2[0]);
const [list3Value, setList3Value] = useState(list3[0]);
const [list4Value, setList4Value] = useState(list4[0]);
const onChangeSelectTypeValue = (value) => {
setTypeValue(value);
};
...
const selectTypeValueElement = (
<SelectElement
select={selectType}
value={[typeValue]}
onChangeValue={onChangeSelectTypeValue}
values={list1Value}
/>
);
...
<div className="interactionHeaderChart">
{ selectTypeValueElement }
...
</div>
};
Then I have a Select element where I do not store a state, but where option selected is passed to Parent compoment.
const SelectElement = ({
select, value, values, onChangeValue,
}) => {
...
<Select
...
value={value[0]}
onChange={onChangeValue}
>
...
};
Now when I select some option from one Select, state of Parent change and all Childs re-render, all Selects components and also other components which depend on the values selected by select.
Can I prevent all Select components from re-rendering? Can I avoid to re-render all other components which does not depend on the values of option selected?
The fact that the state has changed from the onChange function and not from useEffect() is confusing me and I can not understand how to solve it.
Thanks.
You should look into shouldComponentUpdate:
https://reactjs.org/docs/react-component.html#shouldcomponentupdate
Usually, in order to use this with your SelectElement component you will first have to convert it into a Class. You can then add the shouldComponentUpdate function to it and check the previous and next props are the same or not. If they are the same, don't update.
However, if your props are not complex objects, you can actually just recreate your SelectElement as a PureComponent. This will automatically check the props and will not re-render if they're the same.
e.g.
class SelectElement extends React.PureComponet {...
you can use memo to avoid re rendering.
Way 1:
const NestedComponent = () => {
return (
<div>
ContainerComponent
</div>
);
};
export default React.memo(NestedComponent);
Way 2:
function ParentComponent(a, b) {
const childComponent = React.useMemo(() => <ChildComponent posts={a} />, [a]);
return (
<>
{childComponent}
</>
)
}
This question already has answers here:
Make React useEffect hook not run on initial render
(16 answers)
Closed last month.
I'm trying to use the useEffect hook inside a controlled form component to inform the parent component whenever the form content is changed by user and return the DTO of the form content. Here is my current attempt
const useFormInput = initialValue => {
const [value, setValue] = useState(initialValue)
const onChange = ({target}) => {
console.log("onChange")
setValue(target.value)
}
return { value, setValue, binding: { value, onChange }}
}
useFormInput.propTypes = {
initialValue: PropTypes.any
}
const DummyForm = ({dummy, onChange}) => {
const {value: foo, binding: fooBinding} = useFormInput(dummy.value)
const {value: bar, binding: barBinding} = useFormInput(dummy.value)
// This should run only after the initial render when user edits inputs
useEffect(() => {
console.log("onChange callback")
onChange({foo, bar})
}, [foo, bar])
return (
<div>
<input type="text" {...fooBinding} />
<div>{foo}</div>
<input type="text" {...barBinding} />
<div>{bar}</div>
</div>
)
}
function App() {
return (
<div className="App">
<header className="App-header">
<DummyForm dummy={{value: "Initial"}} onChange={(dummy) => console.log(dummy)} />
</header>
</div>
);
}
However, now the effect is ran on the first render, when the initial values are set during mount. How do I avoid that?
Here are the current logs of loading the page and subsequently editing both fields. I also wonder why I get that warning of missing dependency.
onChange callback
App.js:136 {foo: "Initial", bar: "Initial"}
backend.js:1 ./src/App.js
Line 118: React Hook useEffect has a missing dependency: 'onChange'. Either include it or remove the dependency array. If 'onChange' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps
r # backend.js:1
printWarnings # webpackHotDevClient.js:120
handleWarnings # webpackHotDevClient.js:125
push../node_modules/react-dev-utils/webpackHotDevClient.js.connection.onmessage # webpackHotDevClient.js:190
push../node_modules/sockjs-client/lib/event/eventtarget.js.EventTarget.dispatchEvent # eventtarget.js:56
(anonymous) # main.js:282
push../node_modules/sockjs-client/lib/main.js.SockJS._transportMessage # main.js:280
push../node_modules/sockjs-client/lib/event/emitter.js.EventEmitter.emit # emitter.js:53
WebSocketTransport.ws.onmessage # websocket.js:36
App.js:99 onChange
App.js:116 onChange callback
App.js:136 {foo: "Initial1", bar: "Initial"}
App.js:99 onChange
App.js:116 onChange callback
App.js:136 {foo: "Initial1", bar: "Initial2"}
You can see this answer for an approach of how to ignore the initial render. This approach uses useRef to keep track of the first render.
const firstUpdate = useRef(true);
useLayoutEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
} else {
// do things after first render
}
});
As for the warning you were getting:
React Hook useEffect has a missing dependency: 'onChange'
The trailing array in a hook invocation (useEffect(() => {}, [foo]) list the dependencies of the hook. This means if you are using a variable within the scope of the hook that can change based on changes to the component (say a property of the component) it needs to be listed there.
If you are looking for something like componentDidUpdate() without going through componentDidMount(), you can write a hook like:
export const useComponentDidMount = () => {
const ref = useRef();
useEffect(() => {
ref.current = true;
}, []);
return ref.current;
};
In your component you can use it like:
const isComponentMounted = useComponentDidMount();
useEffect(() => {
if(isComponentMounted) {
// Do something
}
}, [someValue])
In your case it will be:
const DummyForm = ({dummy, onChange}) => {
const isComponentMounted = useComponentDidMount();
const {value: foo, binding: fooBinding} = useFormInput(dummy.value)
const {value: bar, binding: barBinding} = useFormInput(dummy.value)
// This should run only after the initial render when user edits inputs
useEffect(() => {
if(isComponentMounted) {
console.log("onChange callback")
onChange({foo, bar})
}
}, [foo, bar])
return (
// code
)
}
Let me know if it helps.
I create a simple hook for this
https://stackblitz.com/edit/react-skip-first-render?file=index.js
It is based on paruchuri-p
const useSkipFirstRender = (fn, args) => {
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) {
console.log('running')
return fn();
}
}, args)
useEffect(() => {
isMounted.current = true
}, [])
}
The first effect is the main one as if you were using it in your component. It will run, discover that isMounted isn't true and will just skip doing anything.
Then after the bottom useEffect is run, it will change the isMounted to true - thus when the component is forced into a re-render. It will allow the first useEffect to render normally.
It just makes a nice self-encapsulated re-usable hook. Obviously you can change the name, it's up to you.
You can use custom hook to run use effect after mount.
const useEffectAfterMount = (cb, dependencies) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Here is the typescript version:
const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Example:
useEffectAfterMount(() => {
console.log("onChange callback")
onChange({foo, bar})
}, [count])
I don't understand why you need a useEffect here in the first place. Your form inputs should almost certainly be controlled input components where the current value of the form is provided as a prop and the form simply provides an onChange handler. The current values of the form should be stored in <App>, otherwise how ever will you get access to the value of the form from somewhere else in your application?
const DummyForm = ({valueOne, updateOne, valueTwo, updateTwo}) => {
return (
<div>
<input type="text" value={valueOne} onChange={updateOne} />
<div>{valueOne}</div>
<input type="text" value={valueTwo} onChange={updateTwo} />
<div>{valueTwo}</div>
</div>
)
}
function App() {
const [inputOne, setInputOne] = useState("");
const [inputTwo, setInputTwo] = useState("");
return (
<div className="App">
<header className="App-header">
<DummyForm
valueOne={inputOne}
updateOne={(e) => {
setInputOne(e.target.value);
}}
valueTwo={inputTwo}
updateTwo={(e) => {
setInputTwo(e.target.value);
}}
/>
</header>
</div>
);
}
Much cleaner, simpler, flexible, utilizes standard React patterns, and no useEffect required.
Let's say I have:
const AddItemButton = React.memo(({ onClick }) => {
// Goal is to make sure this gets printed only once
console.error('Button Rendered!');
return <button onClick={onClick}>Add Item</button>;
});
const App = () => {
const [items, setItems] = useState([]);
const addItem = () => {
setItems(items.concat(Math.random()));
}
return (
<div>
<AddItemButton onClick={addItem} />
<ul>
{items.map(item => <li key={item}>{item}</li>)}
</ul>
</div>
);
};
Any time I add an item, the <AddItemButton /> gets re-rendered because addItem is a new instance. I tried memoizing addItem:
const addItemMemoized = React.memo(() => addItem, [setItems])
But this is reusing the setItems from the first render, while
const addItemMemoized = React.memo(() => addItem, [items])
Doesn't memoize since items reference changes.
I can'd do
const addItem = () => {
items.push(Math.random());
setItems(items);
}
Since that doesn't change the reference of items and nothing gets updated.
One weird way to do it is:
const [, frobState] = useState();
const addItemMemoized = useMemo(() => () => {
items.push(Math.random());
frobState(Symbol())
}, [items]);
But I'm wondering if there's a better way that doesn't require extra state references.
The current preferred route is useCallback, which is the same as your useMemo solution, but with additional possible optimizations in the future. Pass an empty array [] to make sure the function will always have the same reference for the lifetime of the component.
Here, you also want to use the functional state update form, to make sure the item is always being added based on the current state.
const addItem = useCallback(() => {
setItems(items => [...items, Math.random()]);
}, []);