I have some code below where I have decalared state in my parent component for size as an empty string.
What I am trying to do is create a handler function that will take the innerText of a button onClick and update the state of size with that value.
import { Child } from './Child';
import React, { useState } from 'react';
export default function Parent() {
const [state, setState] = useState({
name: 'bob',
size: '',
});
const handleSize = (e) => {
setState((state) => ({
...state,
size: e.target.innerText,
}));
};
return (
<Child
size={state.size}
onClick={handleSize}
/>
);
}
Here is the child component with the button that has innerText of 500. - I simply want to set the state of size to whatever value is inside my button element but this does not seem to work.
EDIT1: updated code to send size down to the child.
EDIT2: this current code throws a TYPE ERROR: 'Cannot read property 'innerText' of null' Is this because getting innerText of JSX elements is different than HTML elements?
EDIT3: I have answered my own question below. My handler function required adjustments.
export const Child = ({
size,
onClick,
}) => {
return (
<div>
<button onClick={onClick}>500</button>
</div>
);
};
Try:
const handleSize = (e) => {
setState((state) => ({
...state,
size: e.target.innerText,
}))
}
https://reactjs.org/docs/hooks-reference.html#functional-updates
Ok found the issue, my handler function needed some adjustments.
const handleSize = (e) => {
const { value } = e.target;
setState((state) => ({
...state,
size: value,
}));
};
Instead of using the innerText of the button I decided to use value instead.
const { value } = e.target; //assigns the target value to my size property
Below is the updated button that has a static value of "500". The handleSize function will update the size onClick using the assigned value of the button.
export const Child = ({
size,
onClick,
}) => {
return (
<div>
<button value="500" onClick={onClick}>500</button>
</div>
);
};
Related
I have an array that I am looping through to create div elements, and based on which of these divs is 'active' (derived from state) I want them to have the ref property. This ref is also the parameter for a hook I'm using that adds an onclick listener to that ref. This works ok when the first div is active, however when my state changes to make the 2nd div active, the onclick listener isn't active on the 2nd div. Any idea on how I can resolve this?
Component file:
const [active, setActive] = useState(0);
const ref = useRef<HTMLDivElement>(null);
const { element } = useClick(ref);
return (
<div style={{ position: 'relative' }}>
{cards.map((card, index) => (
<div
{...(index === active ? { ref } : { 'aria-hidden': 'true' })}
>
<Card>
<div className={styles.title}>{card}</div>
</Card>
</div>
))}
</div>
);
Custom hook:
const useClick = (
element: RefObject<HTMLElement>
): {
element: RefObject<HTMLElement>;
} => {
const onClick = (): void => {
console.log('clicked');
};
useEffect(() => {
if (!element.current) {
return undefined;
}
element.current.addEventListener('click', onClick);
return (): void => {
element.current?.removeEventListener('click', onClick);
};
}, [element]);
return { element };
};
export default useClick;
I'd expect the onclick handler to be removed from the first element when it is no longer active and for this to be moved to the 2nd div in the loop. I tried an approach with using an array of refs so that each div had a unique ref but wasn't able to achieve this when using the ref index in the parameter of my custom hook. I can see my state is changing (active) correctly but I've redacted this code as its quite long and complex with IP and doesn't seem to coincide with the problem
If all you care about is the onClick, use it directly without a ref and setting it manually in useEffect:
<div
{...(index === active
? { onClick }
: { 'aria-hidden': 'true' })
}
>
Why doesn't it work?
The ref that you're using is an object reference, which have the current property (ref = { current }). While current changes according to the div it's used on ref stays the same, and the useEffect is not triggered.
Change the dependencies to look at element.current, and also add onClick because it's a dependency of useEffect as well:
useEffect(() => {
if (!element.current) {
return undefined;
}
element.current.addEventListener('click', onClick);
return (): void => {
element.current?.removeEventListener('click', onClick);
};
}, [element.current, onClick]);
Another option is to use a function ref with useState:
const [active, setActive] = useState(0);
const [ref, setRef] = useState(null); // the state is used as ref
const { element } = useClick(ref);
<div
// use setRef as a function ref
{...(index === active ? { ref: setRef } : { 'aria-hidden': 'true' })}
>
// in your custom hook
useEffect(() => {
// in this case element doesn't have current
if (!element) return undefined;
element?.addEventListener('click', onClick);
return (): void => {
element?.removeEventListener('click', onClick);
};
}, [element]);
My project use dvajs(Based on redux and redux-saga), The code below is to send a request after clicking the button, change the status through connect, and then call the ant design component message.error an message.success(Similar to alert) to remind
import type { Dispatch } from 'umi';
import ProForm, { ProFormText } from '#ant-design/pro-form';
import { message } from 'antd';
const tip = (type: string, content: string) => {
if (type === 'error') message.error(content, 5);
else message.success(content, 5);
};
const RegisterFC: React.FC<RegisterProps> = (props) => {
const { registerResponseInfo = {}, submitting, dispatch } = props;
const { status } = registerResponseInfo;
const handleSubmit = (values: RegisterParamsType) => {
dispatch({
type: 'register/register',
payload: { ...values },
});
};
return (
<div>
<ProForm
onFinish={(values) => {
handleSubmit(values as RegisterParamsType);
return Promise.resolve();
}}
>
<ProFormText/>
...
{
status === '1' && !submitting && (
tip('error',
intl.formatMessage({
id: 'pages.register.status1.message',
defaultMessage: 'error'
})
)
)
}
<<ProForm>/>
</div>
)
}
const p = ({ register, loading }: { register: RegisterResponseInfo, loading: Loading; }) => {
console.log(loading);
return {
registerResponseInfo: register,
submitting: loading.effects['register/register'],
};
};
export default connect(p)(RegisterFC);
When I click the button, the console prompts:
Warning: Render methods should be a pure function of props and state;
triggering nested component updates from render is not allowed. If
necessary, trigger nested updates in componentDidUpdate.
Doesn't the component re-render when the state changes? Does the tip function change the state?
Solution: Call tip Outside of return
tip is just a function that you are calling. You should call it outside of the return JSX section of your code. I think it makes the most sense to call it inside of a useEffect hook with dependencies on status and submitting. The effect runs each time that status or submitting changes. If status is 1 and submitting is falsy, then we call tip.
const RegisterFC: React.FC<RegisterProps> = (props) => {
const { registerResponseInfo = {}, submitting, dispatch } = props;
const { status } = registerResponseInfo;
const handleSubmit = (values: RegisterParamsType) => {
dispatch({
type: 'register/register',
payload: { ...values },
});
};
React.useEffect(() => {
if (status === '1' && !submitting) {
tip('error',
intl.formatMessage({
id: 'pages.register.status1.message',
defaultMessage: 'error'
})
);
}
}, [status, submitting]);
return (
<div>...</div>
)
}
Explanation
Render methods should be a pure function of props and state
The render section of a component (render() in class component or return in a function component) is where you create the JSX (React HTML) markup for your component based on the current values of props and state. It should not have any side effects. It creates and returns JSX and that's it.
Calling tip is a side effect since it modifies the global antd messsage object. That means it shouldn't be in the render section of the code. Side effects are generally handled inside of useEffect hooks.
You are trying to conditionally render tip like you would conditionally render a component. The problem is that tip is not a component. A function component is a function which returns a JSX Element. tip is a void function that returns nothing, so you cannot render it.
I am trying to change the ckeckbox property, every time I write a new value to the map, in my case when I click on the checkbox, it only changes the value of the checked property, for example, the initial value is true, on all subsequent clicks it will be false, false, false ... What's wrong here?
import React,{ useState,useEffect } from "react";
import {useSelector} from 'react-redux'
const ChangeArticle = (props) => {
const prevArticle = props.changeArtcle;
const age_groups = useSelector(state => state.app.age_groups);
const [checkedAges, setCheckAges] = useState(new Map());
const handleChangeCheckBoxe = (event) => {
setCheckAges(checkedAges => checkedAges.set(event.target.value, event.target.checked));
console.log("checkedItems: ", checkedAges);
}
useEffect(() => {
if(prevArticle.ages){
prevArticle.ages.forEach((age) =>{
setCheckAges(checkedAges => checkedAges.set(age.toString(), true));
});
}
},[prevArticle]);
return (<div>
{age_groups.map(age => {
return (<div key={age.id}>
<input type="checkbox" checked={checkedAges.has(age.id.toString()) ? checkedAges.get(age.id.toString()) : false} value={age.id} onChange={handleChangeCheckBoxe}
/>
{ age.title }
</div>)
}) }
</div>);
}
export default ChangeArticle;
In the handleChangeCheckBoxe function, you are only changing the values withing the Map. React only does a shallow reference check to see if the Map had changed. Since the reference is the same, the it will not re-render the component as you would expect.
You can change the function to be similar to the following to create a new Map and assign it to state.
const handleChangeCheckBoxe = (event) => {
setCheckAges(checkedAges => new Map(checkedAges.set(event.target.value, event.target.checked)));
console.log("checkedItems: ", checkedAges);
}
You can see this working in the following code sandbox https://codesandbox.io/s/wispy-leftpad-9sji0?fontsize=14&hidenavigation=1&theme=dark
I have a weird bug that only happens some of the time - onChange fires but does not change the value. Then if I click outside of the input with the onChange function, then click back inside the input box, the onChange function starts working.
The onChange function is like so:
const handleBarAmountChange = (event) => {
let newWidthAmount = event.target.value / 10;
setNewWidth(newWidthAmount);
setNewBarAmount(event.target.value);
};
A parent div is using a ref with useRef that is passed to this function:
import { useEffect, useState } from 'react';
const useMousePosition = (barRef, barInputRef, barContainerRef) => {
const [ mouseIsDown, setMouseIsDown ] = useState(null);
useEffect(() => {
const setMouseDownEvent = (e) => {
if (e.which == 1) {
if (barContainerRef.current.contains(e.target) && !barInputRef.current.contains(e.target)) {
setMouseIsDown(e.clientX);
} else if (!barInputRef.current.contains(e.target)) {
setMouseIsDown(null);
}
}
};
window.addEventListener('mousemove', setMouseDownEvent);
return () => {
window.removeEventListener('mousemove', setMouseDownEvent);
};
}, []);
return { mouseIsDown };
};
Is the onChange conflicting somehow with the eventListener?
How do I get round this?
There were a few syntax errors and missing hook dependencies that were the cause of your bugs. However, you can simplify your code quite a bit with a few tweaks.
When using state that relies upon other state, I recommend lumping it into an object and using a callback function to synchronously update it: setState(prevState => ({ ...prevState, example: "newValue" }). This is similar to how this.setState(); works in a class based component. By using a single object and spreading out it properties ({ ...prevState }), we can then overwrite one of its properties by redefining one of them ({ ...prevState, newWidth: 0 }). This way ensures that the values are in sync with each other.
The example below follows the single object pattern mentioned above, where newWidth, newBarAmount and an isDragging are properties of a single object (state). Then, the example uses setState to update/override the values synchronously. In addition, the refs have been removed and allow the bar to be dragged past the window (if you don't want this, then you'll want to confine it within the barContainerRef as you've done previously). The example also checks for a state.isDragging boolean when the user left mouse clicks and holds on the bar. Once the left click is released, the dragging is disabled.
Here's a working example:
components/Bar/index.js
import React, { useEffect, useState, useCallback } from "react";
import PropTypes from "prop-types";
import "./Bar.css";
function Bar({ barName, barAmount, colour, maxWidth }) {
const [state, setState] = useState({
newWidth: barAmount / 2,
newBarAmount: barAmount,
isDragging: false
});
// manual input changes
const handleBarAmountChange = useCallback(
({ target: { value } }) => {
setState(prevState => ({
...prevState,
newWidth: value / 2,
newBarAmount: value
}));
},
[]
);
// mouse move
const handleMouseMove = useCallback(
({ clientX }) => {
if (state.isDragging) {
setState(prevState => ({
...prevState,
newWidth: clientX > 0 ? clientX / 2 : 0,
newBarAmount: clientX > 0 ? clientX : 0
}));
}
},
[state.isDragging]
);
// mouse left click hold
const handleMouseDown = useCallback(
() => setState(prevState => ({ ...prevState, isDragging: true })),
[]
);
// mouse left click release
const handleMouseUp = useCallback(() => {
if (state.isDragging) {
setState(prevState => ({
...prevState,
isDragging: false
}));
}
}, [state.isDragging]);
useEffect(() => {
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};
}, [handleMouseMove, handleMouseUp]);
return (
<div className="barContainer">
<div className="barName">{barName}</div>
<div
style={{ cursor: state.isDragging ? "grabbing" : "pointer" }}
onMouseDown={handleMouseDown}
className="bar"
>
<svg
width={state.newWidth > maxWidth ? maxWidth : state.newWidth}
height="40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
colour={colour}
>
<rect width={state.newWidth} height="40" fill={colour} />
</svg>
</div>
<div className="barAmountUnit">£</div>
<input
className="barAmount"
type="number"
value={state.newBarAmount}
onChange={handleBarAmountChange}
/>
</div>
);
}
// default props (will be overridden if defined)
Bar.defaultProps = {
barAmount: 300,
maxWidth: 600
};
// check that passed in props match patterns below
Bar.propTypes = {
barName: PropTypes.string,
barAmount: PropTypes.number,
colour: PropTypes.string,
maxWidth: PropTypes.number
};
export default Bar;
React uses SyntheticEvent and Event Pooling, from the doc:
Event Pooling
The SyntheticEvent is pooled. This means that the SyntheticEvent object will be reused and all properties will be nullified after the event callback has been invoked. This is for performance reasons. As such, you cannot access the event in an asynchronous way.
You could call event.persist() on the event or store the value in a new variable and use it as follows:
const handleBarAmountChange = (event) => {
// event.persist();
// Or
const { value } = event.target;
let newWidthAmount = value / 10;
setNewWidth(newWidthAmount);
setNewBarAmount(value);
};
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