Using React's useRef hook to handle onKeyDown event - javascript

my first experience using useRef and forwardRef. I've been given some hints on a potential solution but I'm still a little way off, however the component is small and fairly simple.
What I am trying to do is use useRef to pass the result of a handleOnKeyDown function, which basically is looking for a keyCode 13 ("Enter" on keyboard) and then "click" the findGoodsBtnRef by passing it down into the CcceAttribute component via ref={findGoodsBtn}
I know what I have at current isn't right (because it doesn't work) and my editor is complaining about my use of ForwardRef on the top line, can anyone advise where I'm potentially going wrong on trying to implement this solution? TIA
const CommodityCodeLookupAttribute = forwardRef<Props, typeof CcceAttribute>(
({ attribute: productLookupAttribute, onChange, ...props }: ref) => {
const { object } = useFormObjectContext();
const ccceAttribute = object.attributeCollection.find(isCcceResultAttribute);
if (!ccceAttribute) {
return null;
}
const findsGoodsBtnRef = React.useRef();
const findGoodsBtn = findsGoodsBtnRef.current;
const handleOnKeyDown = (event) => {
if (event.keyCode === "13") {
// event.preventDefault();
console.log("Default prevented");
findsGoodsBtnRef.current.click();
// use the ref that points to the ccce attribute button -> click()
}
};
return (
<>
<StringAttributeDefault
attribute={productLookupAttribute}
onKeyDown={handleOnKeyDown}
{...otherProps}
/>
<CcceAttribute attribute={ccceAttribute} ref={findGoodsBtn} />
</>
);
};

Related

event problem using addEventListener in a React functional component

I have an input in a React component. I'm looking to add a character autocompletion feature. If the user enters " or ' a second identical character is added and the mouse cursor is put between the two new quotes created.
I had asked a question about how to make this component in React that I managed to solve myself: first SO question
To do this I used a suite of useEffects that reprocess the user's input each time a new character is entered. This causes performance problems and strange bugs because, I think, the state variables do not have time to update in my component.
So I want to change my current implementation that uses multiple React useEffect hooks to a vanilla JS implementation triggered by a single useEffect in my component.
But I have never used vanilla JS directly in a React component, is this something that can be done and is considered a good practice in React or can it bring unwanted behaviors?
To rewrite my component with vanillla JS I rewrote the following component:
(I tried to simplify my component but it is normally reproducible)
import * as React from "react";
import { useEffect, useState, useRef } from "react";
const QuoteInput: React.FC = () => {
const inputRef = useRef<HTMLInputElement | null>(null);
const [inputChoose, setInputChoose] = useState("");
const [testQuoteAddition, setTestQuoteAddition] = useState(false);
const inputHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
const enteredRequest = event.target.value;
setRequestChoose(enteredRequest);
setTestQuoteAddition(true);
};
const handleComplete = (event: KeyboardEvent) => {
if((event.key === "'" || event.key === '"') && (-1 !== ["'", "\""].indexOf(event.key))) {
let longueur = event.target?.value as HTMLInputElement; //ERROR ON PROPERTY 'value' -> Property 'value' does not exist on type 'EventTarget'.ts(2339)
let position = event.target?.selectionStart; //ERROR ON PROPERTY 'selectionStart' -> Property 'selectionStart' does not exist on type 'EventTarget'.ts(2339)
event.target?.value = event.target?.value.substr(0, position) + event.key + event.target?.value.substr(position, longueur); //ERROR: The left-hand side of an assignment expression may not be an optional property access.ts(2779)
Property 'value' does not exist on type 'EventTarget'.ts(2339)
event.target?.setSelectionRange(position, position);
}
};
useEffect(() => {
if (testQuoteAddition=== true) {
inputRef.current?.addEventListener("keydown", e => {handleComplete(e)});
// cleanup this component
return () => {
inputRef.current?.removeEventListener("keydown", e => {handleComplete(e)});
};
}
setTestQuoteAddition(false);
}, [testQuoteAddition]);
return(
<div>
<input ref={inputRef} type="text" onChange={inputHandler} value={inputChoose} className="text-center" placeholder="enter an input" />
</div>
);
}
export default QuoteInput;
As for the inputHandler which takes as parameter an event of type React.ChangeEvent<HTMLInputElement>, it works well and updates the input entered by the user.
On the other hand for the handleComplete, the listener function of my addEventListener, which takes as parameter an event of type KeyboardEvent, I have several errors indicating me that the properties of the event to which I try to reach are not defined, I made it appear in comment in the code above in the function handleComplete.
Does anyone see what I am doing wrong? I don't see if the problem comes from my integration of vanillaJS in my React component, from my event management or from my way of creating the new listener element.
Thank you if you take the time to help me
If you know the target is an input element you can use typescript casting
(e.target as HTMLInputElement).value

Has anyone successfully used react-testing-library to test change events on a draftJS Editor component?

​The fireEvent.change() just doesn't work.
It says there are no setters on the element.
I tried using aria selectors instead
const DraftEditor = getByRole('textbox')
DraftEditor.value ='something';
fireEvent.change(DraftEditor);
I tried same again using query selector
const DraftEditor = container.querySelector('.public-DraftEditor-content'));
Tried keyboard events instead.
Nothing.
Has anyone managed to text a rich text input with draftjs and react testing library?
I managed to do it by getting inspiration from some issue description for draft-js and adapting that to our case at hand
import { createEvent } from "#testing-library/dom"
import { render, fireEvent } from "#testing-library/react"
function createPasteEvent(html) {
const text = html.replace('<[^>]*>', '');
return {
clipboardData: {
types: ['text/plain', 'text/html'],
getData: (type) => (type === 'text/plain' ? text : html),
},
};
}
renderedComponent = render(<App />)
const editorNode = renderedComponent.querySelector(".public-DraftEditor-content")
const eventProperties = createPasteEvent(textToPaste)
const pasteEvent = createEvent.paste(editorNode, eventProperties)
pasteEvent.clipboardData = eventProperties.clipboardData
fireEvent(editorNode, pasteEvent)
Some additional notes:
renderedComponent in my case is the parent element in which the Editor component is rendered.
apparently, 'ClipboardEvent' is not implemented in JSDOM (see list of supported events), therefore, the call to createEvent.paste creates a generic Event, and not a ClipboardEvent. As a workaround, I copy the necessary clipboardData properties again to the generated generic event so that they will be taken into account by the function editOnPaste of the Draft-js editor, which itself will be triggered because of the fired event.
I managed to get it working mocking the editor and intercepting the onChange method so you can still execute all the lib functions:
const draftjs = require('draft-js');
draftjs.Editor = jest.fn(props => {
const modifiedOnchange = e => {
const text = e.target.value;
const content = ContentState.createFromText(text);
props.onChange(EditorState.createWithContent(content));
};
return <input className="editor" onChange={e => modifiedOnchange(e)} />;
});

React useState hook event handler using initial state

I'm still getting my head around react hooks but struggling to see what I'm doing wrong here. I have a component for resizing panels, onmousedown of an edge I update a value on state then have an event handler for mousemove which uses this value however it doesn't seem to be updating after the value has changed.
Here is my code:
export default memo(() => {
const [activePoint, setActivePoint] = useState(null); // initial is null
const handleResize = () => {
console.log(activePoint); // is null but should be 'top|bottom|left|right'
};
const resizerMouseDown = (e, point) => {
setActivePoint(point); // setting state as 'top|bottom|left|right'
window.addEventListener('mousemove', handleResize);
window.addEventListener('mouseup', cleanup); // removed for clarity
};
return (
<div className="interfaceResizeHandler">
{resizePoints.map(point => (
<div
key={ point }
className={ `interfaceResizeHandler__resizer interfaceResizeHandler__resizer--${ point }` }
onMouseDown={ e => resizerMouseDown(e, point) }
/>
))}
</div>
);
});
The problem is with the handleResize function, this should be using the latest version of activePoint which would be a string top|left|bottom|right but instead is null.
How to Fix a Stale useState
Currently, your issue is that you're reading a value from the past. When you define handleResize it belongs to that render, therefore, when you rerender, nothing happens to the event listener so it still reads the old value from its render.
There are a several ways to solve this. First let's look at the most simple solution.
Create your function in scope
Your event listener for the mouse down event passes the point value to your resizerMouseDown function. That value is the same value that you set your activePoint to, so you can move the definition of your handleResize function into resizerMouseDown and console.log(point). Because this solution is so simple, it cannot account for situations where you need to access your state outside of resizerMouseDown in another context.
See the in-scope function solution live on CodeSandbox.
useRef to read a future value
A more versatile solution would be to create a useRef that you update whenever activePoint changes so that you can read the current value from any stale context.
const [activePoint, _setActivePoint] = React.useState(null);
// Create a ref
const activePointRef = React.useRef(activePoint);
// And create our custom function in place of the original setActivePoint
function setActivePoint(point) {
activePointRef.current = point; // Updates the ref
_setActivePoint(point);
}
function handleResize() {
// Now you'll have access to the up-to-date activePoint when you read from activePointRef.current in a stale context
console.log(activePointRef.current);
}
function resizerMouseDown(event, point) {
/* Truncated */
}
See the useRef solution live on CodeSandbox.
Addendum
It should be noted that these are not the only ways to solve this problem, but these are my preferred methods because the logic is more clear to me despite some of the solutions being longer than other solutions offered. Please use whichever solution you and your team best understand and find to best meet your specific needs; don't forget to document what your code does though.
You have access to current state from setter function, so you could make it:
const handleResize = () => {
setActivePoint(activePoint => {
console.log(activePoint);
return activePoint;
})
};
useRef for the callback
A similar approach to Andria's can be taken by using useRef to update the event listener's callback itself instead of the useState value. This allows you to use many up-to-date useState values inside one callback with only one useRef.
If you create a ref with useRef and update its value to the handleResize callback on every render, the callback stored in the ref will always have access to up-to-date useState values, and the handleResize callback will be accessible to any stale callbacks like event handlers.
function handleResize() {
console.log(activePoint);
}
// Create the ref,
const handleResizeRef = useRef(handleResize);
// and then update it on each re-render.
handleResizeRef.current = handleResize;
// After that, you can access it via handleResizeRef.current like so
window.addEventListener("mousemove", event => handleResizeRef.current());
With this in mind, we can also abstract away the creation and updating of the ref into a custom hook.
Example
See it live on CodeSandbox.
/**
* A custom hook that creates a ref for a function, and updates it on every render.
* The new value is always the same function, but the function's context changes on every render.
*/
function useRefEventListener(fn) {
const fnRef = useRef(fn);
fnRef.current = fn;
return fnRef;
}
export default memo(() => {
const [activePoint, setActivePoint] = useState(null);
// We can use the custom hook declared above
const handleResizeRef = useRefEventListener((event) => {
// The context of this function will be up-to-date on every re-render.
console.log(activePoint);
});
function resizerMouseDown(event, point) {
setActivePoint(point);
// Here we can use the handleResizeRef in our event listener.
function handleResize(event) {
handleResizeRef.current(event);
}
window.addEventListener("mousemove", handleResize);
// cleanup removed for clarity
window.addEventListener("mouseup", cleanup);
}
return (
<div className="interfaceResizeHandler">
{resizePoints.map((point) => (
<div
key={point}
className={`interfaceResizeHandler__resizer interfaceResizeHandler__resizer--${point}`}
onMouseDown={(event) => resizerMouseDown(event, point)}
/>
))}
</div>
);
});
const [activePoint, setActivePoint] = useState(null); // initial is null
const handleResize = () => {
setActivePoint(currentActivePoint => { // call set method to get the value
console.log(currentActivePoint);
return currentActivePoint; // set the same value, so nothing will change
// or a different value, depends on your use case
});
};
Just small addition to the awe ChrisBrownie55's advice.
A custom hook can be implemented to avoid duplicating this code and use this solution almost the same way as the standard useState:
// useReferredState.js
import React from "react";
export default function useReferredState(initialValue) {
const [state, setState] = React.useState(initialValue);
const reference = React.useRef(state);
const setReferredState = value => {
reference.current = value;
setState(value);
};
return [reference, setReferredState];
}
// SomeComponent.js
import React from "react";
const SomeComponent = () => {
const [someValueRef, setSomeValue] = useReferredState();
// console.log(someValueRef.current);
};
For those using typescript, you can use this function:
export const useReferredState = <T>(
initialValue: T = undefined
): [T, React.MutableRefObject<T>, React.Dispatch<T>] => {
const [state, setState] = useState<T>(initialValue);
const reference = useRef<T>(state);
const setReferredState = (value) => {
reference.current = value;
setState(value);
};
return [state, reference, setReferredState];
};
And call it like that:
const [
recordingState,
recordingStateRef,
setRecordingState,
] = useReferredState<{ test: true }>();
and when you call setRecordingState it will automatically update the ref and the state.
You can make use of the useEffect hook and initialise the event listeners every time activePoint changes. This way you can minimise the use of unnecessary refs in your code.
When you need to add event listener on component mount
Use, useEffect() hook
We need to use the useEffect to set event listener and cleanup the same.
The use effect dependency list need to have the state variables which are being used in event handler. This will make sure handler don't access any stale event.
See the following example. We have a simple count state which gets incremented when we click on given button. Keydown event listener prints the same state value. If we remove the count variable from the dependency list, our event listener will print the old value of state.
import { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const clickHandler = () => {
console.log({ count });
setCount(c => c + 1);
}
useEffect(() => {
document.addEventListener('keydown', normalFunction);
//Cleanup function of this hook
return () => {
document.removeEventListener('keydown', normalFunction);
}
}, [count])
return (
<div className="App">
Learn
<button onClick={clickHandler}>Click me</button>
<div>{count}</div>
</div>
);
}
export default App;

How to test react-router-dom?

Problem
I have read https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/guides/testing.md
I want to test react-router-dom, I don't care about how it work, I just need to make sure the library is working into my project boilerplate.
Reproduction
I am testing this component
<Link to="/toto">
toto
</Link>
This is the test
it('it expands when the button is clicked', () => {
const renderedComponent = mount(<Wrapper>
<MemoryRouter initialEntries={['/']}>
<Demo />
</MemoryRouter>
</Wrapper>);
renderedComponent.find('a').simulate('click');
expect(location.pathname).toBe('toto');
});
Expected
to be true
Result
blank
Question
How can I test react-router-dom?
If you look at the code for Link, you see this code:
handleClick = event => {
if (this.props.onClick) this.props.onClick(event);
if (
!event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
!this.props.target && // let browser handle "target=_blank" etc.
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
event.preventDefault();
const { history } = this.context.router;
const { replace, to } = this.props;
if (replace) {
history.replace(to);
} else {
history.push(to);
}
}
};
So, presumably you find Link instead of a and override this method to return a value to your own callback you can validate the path set on the <Link>, This doesn't directly test react-router but it will validate that the paths you have set in your link are correct which is what your tests seem to be validating.
So something like (untested code):
const link = renderedComponent.find(Link)
let result = null
link.handleClick = event => {
const { replace, to } = link.props;
if (replace) {
result = null //we are expecting a push
} else {
result = to
}
}
};
link.simulate('click')
expect(result).toEqual('/toto') // '/toto' or 'toto'?
UPDATE
I've realised that the above doesn't work with a shallow render, however, if you just want to check if the to property is correct, you can probably just do it with expect(link.props.to).toEqual('/toto').

How to detect overflow of React component without ReactDOM?

Basically i want to be able to detect if a react component has children which are overflowing. Just as in this question. I have found that the same thing is possible using ReactDOM, however i cannot/should not use ReactDOM. I don't see anything on the suggested alternative,ref, that is equivalent.
So what i need to know is if it is possible to detect overflow within a react component under these conditions. And to the same point, is it possible to detect width at all?
In addition to #jered's excellent answer, i'd like to mention the qualifier that a ref will only return an element that directly has access to the various properties of regular DOM elements if the ref is placed directly on a DOM element. That is to say, it does not behave in this way with Components.
So if you are like me and have the following:
var MyComponent = React.createClass({
render: function(){
return <SomeComponent id="my-component" ref={(el) => {this.element = el}}/>
}
})
and when you attempt to access DOM properties of this.element (probably in componentDidMount or componentDidUpdate) and you are not seeing said properties, the following may be an alternative that works for you
var MyComponent = React.createClass({
render: function(){
return <div ref={(el) => {this.element = el}}>
<SomeComponent id="my-component"/>
</div>
}
})
Now you can do something like the following:
componentDidUpdate() {
const element = this.element;
// Things involving accessing DOM properties on element
// In the case of what this question actually asks:
const hasOverflowingChildren = element.offsetHeight < element.scrollHeight ||
element.offsetWidth < element.scrollWidth;
},
The implementation of the solution proposed by #Jemar Jones:
export default class OverflowText extends Component {
constructor(props) {
super(props);
this.state = {
overflowActive: false
};
}
isEllipsisActive(e) {
return e.offsetHeight < e.scrollHeight || e.offsetWidth < e.scrollWidth;
}
componentDidMount() {
this.setState({ overflowActive: this.isEllipsisActive(this.span) });
}
render() {
return (
<div
style={{
width: "145px",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden"
}}
ref={ref => (this.span = ref)}
>
<div>{"Triggered: " + this.state.overflowActive}</div>
<span>This is a long text that activates ellipsis</span>
</div>
);
}
}
Yep, you can use ref.
Read more about how ref works in the official documentation: https://facebook.github.io/react/docs/refs-and-the-dom.html
Basically, ref is just a callback that is run when a component renders for the first time, immediately before componentDidMount is called. The parameter in the callback is the DOM element that is calling the ref function. So if you have something like this:
var MyComponent = React.createClass({
render: function(){
return <div id="my-component" ref={(el) => {this.domElement = el}}>Hello World</div>
}
})
When MyComponent mounts it will call the ref function that sets this.domElement to the DOM element #my-component.
With that, it's fairly easy to use something like getBoundingClientRect() to measure your DOM elements after they render and determine if the children overflow the parent:
https://jsbin.com/lexonoyamu/edit?js,console,output
Keep in mind there is no way to measure the size/overflow of DOM elements before they render because by definition they don't exist yet. You can't measure the width/height of something until you render it to the screen.
I needed to achieve this in React TypeScript, as such here is the updated solution in TypeScript using React Hooks. This solution will return true if there are at least 4 lines of text.
We declare the necessary state variables:
const [overflowActive, setOverflowActive] = useState<boolean>(false);
const [showMore, setShowMore] = useState<boolean>(false);
We declare the necessary ref using useRef:
const overflowingText = useRef<HTMLSpanElement | null>(null);
We create a function that checks for overflow:
const checkOverflow = (textContainer: HTMLSpanElement | null): boolean => {
if (textContainer)
return (
textContainer.offsetHeight < textContainer.scrollHeight || textContainer.offsetWidth < textContainer.scrollWidth
);
return false;
};
Lets build a useEffect that will be called when overflowActive changes and will check our current ref object to determine whether the object is overflowing:
useEffect(() => {
if (checkOverflow(overflowingText.current)) {
setOverflowActive(true);
return;
}
setOverflowActive(false);
}, [overflowActive]);
In our component's return statement, we need to bind the ref to an appropriate element. I am using Material UI coupled with styled-components so the element in this example will be StyledTypography:
<StyledTypography ref={overflowingText}>{message}</StyledTypography>
Styling the component in styled-components:
const StyledTypography = styled(Typography)({
display: '-webkit-box',
'-webkit-line-clamp': '4',
'-webkit-box-orient': 'vertical',
overflow: 'hidden',
textOverflow: 'ellipsis',
});
The same could be achieved using React hooks:
The first thing you need would be a state which holds boolean values for text open and overflow active:
const [textOpen, setTextOpen] = useState(false);
const [overflowActive, setOverflowActive] = useState(false);
Next, you need a ref on the element you want to check for overflowing:
const textRef = useRef();
<p ref={textRef}>
Some huuuuge text
</p>
The next thing is a function that checks if the element is overflowing:
function isOverflowActive(event) {
return event.offsetHeight < event.scrollHeight || event.offsetWidth < event.scrollWidth;
}
Then you need a useEffect hook that checks if the overflow exists with the above function:
useEffect(() => {
if (isOverflowActive(textRef.current)) {
setOverflowActive(true);
return;
}
setOverflowActive(false);
}, [isOverflowActive]);
And now with those two states and a function that checks the existence of an overflowing element, you can conditionally render some element (eg. Show more button):
{!textOpen && !overflowActive ? null : (
<button>{textOpen ? 'Show less' : 'Show more'}</button>
)}
To anyone who wonder how it can be done with hooks and useRef:
// This is custom effect that calls onResize when page load and on window resize
const useResizeEffect = (onResize, deps = []) => {
useEffect(() => {
onResize();
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps, onResize]);
};
const App = () => {
const [isScrollable, setIsScrollable] = useState(false);
const [container, setContainer] = useState(null);
// this has to be done by ref so when window event resize listener will trigger - we will get the current element
const containerRef = useRef(container);
containerRef.current = container;
const setScrollableOnResize = useCallback(() => {
if (!containerRef.current) return;
const { clientWidth, scrollWidth } = containerRef.current;
setIsScrollable(scrollWidth > clientWidth);
}, [containerRef]);
useResizeEffect(setScrollableOnResize, [containerRef]);
return (
<div
className={"container" + (isScrollable ? " scrollable" : "")}
ref={(element) => {
if (!element) return;
setContainer(element);
const { clientWidth, scrollWidth } = element;
setIsScrollable(scrollWidth > clientWidth);
}}
>
<div className="content">
<div>some conetnt</div>
</div>
</div>
);
};

Categories