React Hook "useState" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function
I understand why I am getting that message, but at the same time I think what I want should be achievable. Here's the code:
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const children = useState([]);
return (
<div>
{children.map((child, i) => (<Child child={child} />))}
<button
onClick={() => { children[1]([...children, useState(0)]); }}
>
Add
</button>
</div>
);
}
function Child(props) {
const [state, setState] = props.child;
return (
<div>
<input
type="range"
min="1"
max="255"
value={state}
onChange={(e) => setState(e.target.value)}
></input>
</div>
);
}
I want each <Child> to be in complete control of its state without having to declare a function of type updateChild(i : Index, data) then finding the right children in the list, etc. As this doesn't scale well with deep nested view hierarchies.
Is there a way to both:
Keep the state in the parent (single source of truth)
Allow children to mutate their own state
In a nutshell, I want to achieve this but with more than one child component.
To illustrate the problem with your current approach and why React won't let you do that, here's an ersatz implementation of useState that has approximately the same behaviour as the real version (I've left triggering a re-render as an exercise to the user and it doesn't support functional updates, but the important thing here was showing the underlying state of the Parent component).
// Approximation of useState
let parentStateIndex;
const parentState = [];
const useState = (defaultValue) => {
const i = parentStateIndex;
if (parentState.length === i) {
parentState.push(defaultValue);
}
parentStateIndex += 1;
return [parentState[i], (value) => parentState[i] = value];
}
const Parent = () => {
parentStateIndex = 0; // hack required to reset on each "render"
const [state, setState] = useState([]);
return (
<>
{state.map((child) => (
<Child child={child} />
)}
<button
onClick={() => setState([...state, useState(0)])}
>
Add
</button>
</>
);
};
const Child = ({ child }) => {
const [state, setState] = child;
return (
<input
type="range"
min="0"
max="255"
value={state}
onChange={({ target }) => setState([target.value, setState])}
/>
);
};
(Note a bit of knowledge about the parent state has already crept into the child here - if it only setState(target.value) then the [value, setter] pair would be replaced by just value and other things would start exploding. But I think this way around gives a better illustration of what happens further down.)
The first time the parent is rendered, the new array passed to useState is added to the state:
parentState = [
[
[],
(value) => parentState[0] = value
],
];
All good so far. Now imagine the Add button is clicked. useState is called again, and the state is updated to add a second item and add that new item to the first item:
parentState = [
[
[
[0, (value) => parentState[1] = value]
],
(value) => parentState[0] = value,
],
[
0,
(value) => parentState[1] = value,
],
];
This also seems to have worked, but now what happens when the child value updates to e.g. 1?
parentState = [
[
[
[0, /* this gets called: */ (value) => parentState[1] = value],
],
(value) => parentState[0] = value,
],
[
/* but this gets changed: */ 1,
(value) => parentState[1] = value,
],
];
The second part of the state is updated, which changes its type completely, but the first one still holds the old value.
When the parent re-renders, useState is is only called once, so the second item in the parent state is irrelevant; the child gets the old value parentState[0][0], which is still [0, () => ...].
When the Add button gets clicked again, because that's only the second time useState gets called on this render, now we get the new value that was intended for the first child, as the second child:
parentState = [
[
[
[0, (value) => parentState[1] = value],
[1, (value) => parentState[1] = value],
],
(value) => parentState[0] = value,
],
[
1,
(value) => parentState[1] = value,
],
];
And, as you can see, changes to either the first or second child both target the same value; they would not actually appear anywhere until the Add button was clicked again and they'd suddenly be the value of the third child.
For more on how hooks work and why the call order is so important, see e.g. "Why Do React Hooks Rely on Call Order?" by Dan Abramov.
So what would work? For the child to be able to correctly update its parent's state, it needs at least the setter and its own index:
const Parent = () => {
const [state, setState] = useState([]);
return (
<>
{state.map((child, index) => (
<Child child={child} index={index} setState={setState} />
)}
<button
onClick={() => setState([...state, 0])}
>
Add
</button>
</>
);
};
const Child = ({ child, index, setState }) => {
return (
<input
type="range"
min="0"
max="255"
value={child}
onChange={({ target }) => setState((oldState) => {
return oldState.map((oldValue, i) => i === index
? target.value
: oldValue);
})}
/>
);
};
But that means the parent no longer controls its own state, and the child has to know all about its parent's state - those two components are very closely coupled, so the boundary between them is clearly in the wrong place (and maybe shouldn't exist at all).
So what's the correct solution? It's the thing you didn't want to do, having the child "phone home" with an updated value and letting the parent update its state accordingly. This keeps the child decoupled from the details of the parent and its implementation correspondingly simple:
const Parent = () => {
const [state, setState] = useState([]);
const onChange = (newValue, index) => setState((oldState) => {
return oldState.map((oldValue, i) => i === index
? newValue
: oldValue);
};
return (
<>
{state.map((child, index) => (
<Child child={child} onChange={(value) => onChange(value, index)} />
)}
<button
onClick={() => setState([...state, 0])}
>
Add
</button>
</>
);
};
const Child = ({ child, onChange }) => {
return (
<input
type="range"
min="0"
max="255"
value={child}
onChange={({ target: { value } }) => onChange(value)}
/>
);
};
Related
The problem is that my child component is not being re-rendered after I update name state using chrome.storage.sync.get.
So even though name="John" radio button is not checked.
storage.ts:
export function getName(): Promise<nameInterface> {
return new Promise((resolve) => {
chrome.storage.sync.get("name", (response: nameInterface) => {
resolve(response)
})
})
}
popup.tsx:
const [name, setName] = useState<string>()
useEffect(() => {
getName().then((storedName) => {
setName(storedName)
})
}, [])
return (
<div>
<RadioButton
name={name}
></RadioButton>
)
child.tsx:
const RadioButton: React.FC<AppProps> = ({ name}) => {
const [buttonChecked, setButtonChecked] = useState<string>(name)
return (
<input type="radio" checked={buttonChecked==="John"} />
)
}
It's re-rendering.
But what happens when this component re-renders? Nothing much. It already captured state from props the first time it rendered, and internally it only ever uses that state, and nothing ever changes that state. Since the state never changed, nothing in the UI changed.
If you want the component to use the prop instead of maintain internal state, get rid of the internal state:
const RadioButton: React.FC<AppProps> = ({name}) => {
return (
<input type="radio" checked={name==="John"} />
)
}
I'm trying to get value from onChange using setState but for some reason when I write text on input I get an error like Axis.map is not a function
Also,I'd like to delete Axisdata singly from the last one using splice or pop but whenever I click the delete button the Axis data disappeared except the first one.
Set Elements
const SetElements = ({
...
}) => {
const [Axis, setAxis] = useState([]);
const AxisHandler = e => {
setAxis([
...Axis,
{
label: "",
data: "",
backgroundColor: "",
},
]);
};
const deleteAxis = () => {
setAxis(Axis.splice(-1, 1));
};
return (
<>
<button onClick={AxisHandler}>add Line</button>
{Axis.length !== 1 && (
<button onClick={deleteAxis}>delete Line</button>
)}
{Axis.map((element, index) => (
<>
<AppendingAxis
Axis={Axis}
setAxis={setAxis}
element={element}
index={index}
/>
</>
))}
</>
)
AppendingAxis
const AppendingAxis = ({
index,
setAxis,
Axis,
}) => {
console.log(Axis);
return (
<AxisSetting>
<h4>{index + 2}Y Axis setting</h4>
<span>
<input
placeholder={index + 2 + "setting"}
type="textarea"
onChange={e => setAxis((Axis[index].label = e.target.value))}
/>
</span>
The issue is state mutation in the AppendingAxis component.
onChange={e => setAxis((Axis[index].label = e.target.value))}
You should shallow copy state, and nested state, then update specific properties.
onChange={e => setAxis(Axis => Axis.map((el, i) => i === index
? {
...el,
label: e.target.value
}
: el,
)}
I'm not a fan of passing the state updater function on to children as this make it the child component's responsibility to maintain your state invariant. I suggest moving this logic into the parent component so it can maintain control over how state is updated.
SetElements parent
const changeHandler = index => e => {
const { value } = e.target;
setAxis(Axis => Axis.map((el, i) => i === index
? {
...el,
label: value
}
: el,
);
};
...
<AppendingAxis
Axis={Axis}
onChange={changeHandler(index)}
/>
AppendingAxis child
const AppendingAxis = ({ Axis, onChange }) => {
console.log(Axis);
return (
<AxisSetting>
<h4>{index + 2}Y Axis setting</h4>
<span>
<input
placeholder={index + 2 + "setting"}
type="textarea"
onChange={onChange}
/>
</span>
And for completeness' sake, your delete handler looks to also have a mutation issue.
const deleteAxis = () => {
setAxis(Axis.splice(-1, 1));
};
.splice mutates the array in-place and returns an array containing the deleted elements. This is quite the opposite of what you want I think. Generally you can use .filter or .slice to generate new arrays and not mutate the existing one.
const deleteAxis = () => {
setAxis(Axis => Axis.slice(0, -1)); // start at 0, end at second to last
};
This is happening because of this line:
onChange={e => setAxis((Axis[index].label = e.target.value))}
Create a function:
const handleAxisChange = (e, index) => {
Axis[index].label = e.target.value;
setAxis(new Array(...Axis));
}
And then change set the onChange like this:
onChange={e => handleAxisChange(e, index)}
Your problem is because of you don't mutate state correctly. You should make a shallow copy of the state. You can change AppendingAxis to this code:
const AppendingAxis = ({
index,
setAxis,
Axis,
}) => {
console.log(Axis);
const onChange = (e,index)=>{
let copy = [...Axis];
copy[index].label = e.target.value;
setAxis(copy);
}
return (
<AxisSetting>
<h4>{index + 2}Y Axis setting</h4>
<span>
<input
placeholder={index + 2 + "setting"}
type="textarea"
onChange={e => onChange(e,index))}
/>
</span>
Suppose I have the following code snippet (Please consider it as a pseudo code)
Parent.js
const [state,action]=useState(0);
return <View><Child1/><Button onPress={()=>action(1)}/></View>
Child1.js
const [state]=useState(Math.random());
return <Text>{state}</Text>
So my question is when I click the button in the parent will the Chil1 state change or not.
On my local machine it seems it changes.
The benefit of useState is that once a component is mounted, the state value does not change across re-renders until the update state function is called, passing a new state value.
Therefore, even though your parent component Button press state change triggers a rerender of the child, since the child component is only being rerendered and not unmounted/remounted, the initial state of Math.random() would remain the same.
See useState in React: A complete guide
I don't know what exact scenario is, but if you just set default state, the state will be memorized like Scenario 1
Scenario 1
In this way, the state of Child will not be changed even if Parent re-render
const Child = () => {
const [state] = useState(Math.random());
return <div>{state}</div>
}
const Parent = () => {
const [, action] = useState(true);
return (
<>
<button onClick={() => action(false)}>Not Change</button>
<Child />
</>
);
}
Scenario 2
Unless you remove it and then re-render Parent even if memorize all Child, that is
const Child = () => {
const [state] = useState(Math.random());
return <div>{state}</div>
}
const Parent = () => {
const [state, action] = useState(true);
useEffect(() => {
if (!state) action(true)
}, [state])
return (
<>
<button onClick={() => action(false)}>Change</button>
{state && <Child />}
</>
);
}
Scenario 3
By the may, if you don't use default state, in this way, it will be changed every rendering like that
const Child = () => {
return <div>{Math.random()}</div>
}
const Parent = () => {
const [, action] = useState(true);
return (
<>
<button onClick={() => action(prev => !prev)}>Change</button>
<Child />
</>
);
}
Scenario 4
If we don't want Child to re-render, we can try memo to memorize it
const Child = memo(() => {
return <div>{Math.random()}</div>
})
Scenario 5
However, when Child has props, perhaps we should invole useCallback or useMemo to make sure the values or memory addresses of props are "fixed" like constant, so that Child won't re-render
(We don't have to use useCallback or useMemo all the time, it doesn't much matter when there is no performance problem)
const Child = memo((props) => {
return <div {...props}>{Math.random()}</div>
})
const Parent = () => {
const [, action] = useState(true);
const style = useMemo(() => ({}), [])
const onOK = useCallback(() => alert(1), [])
return (
<>
<button onClick={() => action(prev => !prev)}>Change</button>
<Child className="test" style={style} onClick={onOK} />
</>
);
}
I have the following problem:
There is child component that accepts a ref passed down from the parent component:
const ChildComp = React.forwardRef((props, ref) => <div ref={ref} />)
The parent component creates an array of refs and assigns a ref in the array to one of the children:
const ParentComp = () => {
const items = [1,2,3]
const refs = items.map(() => React.createRef())
React.useEffect(() => {
console.log(refs);
}, [refs])
return <div>{items.map((item, index) => <ChildComp ref={refs[index]>)}</div>
}
However, when I output the state of the refs array in useEffect, it outputs it only once when the parent mounts and at that point the ref.current values are still null, because children are not mounted yet.
I would like to be able to have an array of refs where each ref belongs to a child as illustrated above.
Here is a working Codesandbox: https://codesandbox.io/s/currying-leaf-tpibl?file=/src/App.tsx
I modified your example a bit:
use functional components only, as string refs (from createRef) should not be used with them
use callback refs instead of creating them, which will react to changes of the node
The important part is
refProp={(node) => (itemsRef.current[index] = node)},
which will fill the itemsRef array based on the callback ref through the ChildComp.
ChildComp
const ChildComp = ({
number,
refProp
}: {
number: number;
refProp: (node: HTMLDivElement) => void;
}) => {
// simulate internal child state for demonstration
const [childState, setChildState] = useState(0);
useEffect(() => setChildState(number * 2), [number]);
return (
<div ref={refProp}>
<p>
State from child no. {number}: {childState}
</p>
</div>
);
};
ParentComp
const ParentComp = () => {
const [items] = useState([1, 2, 3]);
// you can access the elements with itemsRef.current[n]
const itemsRef = useRef<Array<HTMLDivElement | null>>([]);
useEffect(() => {
// create initial refs array, will be filled through callback ref
itemsRef.current = itemsRef.current.slice(0, items.length);
}, [items]);
useEffect(() => {
// this logs the three <div>s of the childs
console.log(itemsRef);
}, [itemsRef]);
return (
<>
<p>Hi from parent!</p>
{items.map((_, index) => (
<ChildComp
key={index}
number={index + 1}
// will be called by React when they're changing
refProp={(node) => (itemsRef.current[index] = node)}
/>
))}
</>
);
};
Maybe each ref in the refs array is changing but the array itself didn't change. this is why useEffect didn't run again.
You can try:
React.useEffect(() => {
console.log(refs);
}, [refs[0].current])
I've come accross a performance optimization issue that I feel could be fixed somehow but I'm not sure how.
Suppose I have a collection of objects that I want to be editable. The parent component contains all objects and renders a list with an editor component that shows the value and also allows to modify the objects.
A simplified example would be this :
import React, { useState } from 'react'
const Input = props => {
const { value, onChange } = props
handleChange = e => {
onChange && onChange(e.target.value)
}
return (
<input value={value} onChange={handleChange} />
)
}
const ObjectEditor = props => {
const { object, onChange } = props
return (
<li>
<Input value={object.name} onChange={onChange('name')} />
</li>
)
}
const Objects = props => {
const { initialObjects } = props
const [objects, setObjects] = useState(initialObjects)
const handleObjectChange = id => key => value => {
const newObjects = objects.map(obj => {
if (obj.id === id) {
return {
...obj,
[key]: value
}
}
return obj
})
setObjects(newObjects)
}
return (
<ul>
{
objects.map(obj => (
<ObjectEditor key={obj.id} object={obj} onChange={handleObjectChange(obj.id)} />
))
}
</ul>
)
}
export default Objects
So I could use React.memo so that when I edit the name of one object the others don't rerender. However, because of the onChange handler being recreated everytime in the parent component of ObjectEditor, all objects always render anyways.
I can't solve it by using useCallback on my handler since I would have to pass it my objects as a dependency, which is itself recreated everytime an object's name changes.
It seems to me like it is not necessary for all the objects that haven't changed to rerender anyway because the handler changed. And there should be a way to improve this.
Any ideas ?
I've seen in the React Sortly repo that they use debounce in combination with each object editor changing it's own state.
This allows only the edited component to change and rerender while someone is typing and updates the parent only once if no other change event comes up in a given delay.
handleChangeName = (e) => {
this.setState({ name: e.target.value }, () => this.change());
}
change = debounce(() => {
const { index, onChange } = this.props;
const { name } = this.state;
onChange(index, { name });
}, 300);
This is the best solution I can see right now but since they use the setState callback function I haven't been able to figure out a way to make this work with hooks.
You have to use the functional form of setState:
setState((prevState) => {
// ACCESS prevState
return someNewState;
});
You'll be able to access the current state value (prevState) while updating it.
Then way you can use the useCallback hook without the need of adding your state object to the dependency array. The setState function doesn't need to be in the dependency array, because it won't change accross renders.
Thus, you'll be able to use React.memo on the children, and only the ones that receive different props (shallow compare) will re-render.
EXAMPLE IN SNIPPET BELOW
const InputField = React.memo((props) => {
console.log('Rendering InputField '+ props.index + '...');
return(
<div>
<input
type='text'
value={props.value}
onChange={()=>
props.handleChange(event.target.value,props.index)
}
/>
</div>
);
});
function App() {
console.log('Rendering App...');
const [inputValues,setInputValues] = React.useState(
['0','1','2']
);
const handleChange = React.useCallback((newValue,index)=>{
setInputValues((prevState)=>{
const aux = Array.from(prevState);
aux[index] = newValue;
return aux;
});
},[]);
const inputItems = inputValues.map((item,index) =>
<InputField
value={item}
index={index}
handleChange={handleChange}
/>
);
return(
<div>
{inputItems}
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
Okay, so it seems that debounce works if it's wrapped in useCallback
Not sure why it doesn't seem to be necessary to pass newObject as a dependency in the updateParent function though.
So to make this work I had to make the following changes :
First, useCallback in the parent and change it to take the whole object instead of being responsible for updating the keys.
Then update the ObjectEditor to have its own state and handle the change to the keys.
And wrap the onChange handler that will update the parent in the debounce
import React, { useState, useEffect } from 'react'
import debounce from 'lodash.debounce'
const Input = props => {
const { value, onChange } = props
handleChange = e => {
onChange && onChange(e.target.value)
}
return (
<input value={value} onChange={handleChange} />
)
}
const ObjectEditor = React.memo(props => {
const { initialObject, onChange } = props
const [object, setObject] = useState(initialObject)
const updateParent = useCallback(debounce((newObject) => {
onChange(newObject)
}, 500), [onChange])
// synchronize the object if it's changed in the parent
useEffect(() => {
setObject(initialObject)
}, [initialObject])
const handleChange = key => value => {
const newObject = {
...object,
[key]: value
}
setObject(newObject)
updateParent(newObject)
}
return (
<li>
<Input value={object.name} onChange={handleChange('name')} />
</li>
)
})
const Objects = props => {
const { initialObjects } = props
const [objects, setObjects] = useState(initialObjects)
const handleObjectChange = useCallback(newObj => {
const newObjects = objects.map(obj => {
if (newObj.id === id) {
return newObj
}
return obj
})
setObjects(newObjects)
}, [objects])
return (
<ul>
{
objects.map(obj => (
<ObjectEditor key={obj.id} initialObject={obj} onChange={handleObjectChange} />
))
}
</ul>
)
}
export default Objects