Force re-render React component when modified element HTML attribute - javascript

I have a button component rendered like below. I can't modify the button component itself but I can modify the parent component that renders it. I want to make it so that when I modify the button element's disabled attribute elsewhere in the code (that is modify the DOM like button.disabled = true) Given that I can't pass it from parent props, the button component gets re-rendered. I tried to use useRef and useEffect hook but it didn't work. I think I used them wrongly. Is there anyway I can achieve what I want?
const elementRef = useRef()
const [disabled, setDisabled] = useState(elementRef.current?.disabled)
useEffect(() => {
const buttonElement = elementRef.current
buttonElement.addEventListener('disabled', handleDisabled)
}, [elementRef.current])
const handleDisabled = (e: any) => {
setDisabled(e.target?.disabled)
}
return( <Button ref={elementRef} disabled={props.isDisabled || disabled}></Button> )

You seemingly are working in two different "worlds": ReactJs and the DOM.
I suggest to use only React, and never modify any DOM properties directly.
You don't show how you want to change the disabled attribute/property "elsewhere in the code", but I assume you want to do something like
const setMyButtonDisabled = function(){
document.querySelector('#myButton').disabled = true;
}
React is simply not able to know about this change. You have to tell React explicitly, like:
// not recommended:
const setMyButtonDisabled = function( setButtonDisabledCallback ){
document.querySelector('#myButton').disabled = true;
setButtonDisabledCallback( true );
}
And then find a way to pass the props around, so that the desired components have it
(I can't know the relation between your code example and the "elsewhere").
But ideally you would never set the DOM buttonElements .disabled property, but set a React state buttonDisabled instead,
and then just pass that around to where ever you need it:
// recommended:
const setMyButtonDisabled = function( setButtonDisabledCallback ){
setButtonDisabledCallback( true );
}

Related

How to test the reaction to a component event in Svelte?

In Svelte, I have a parent component which listens to a component event dispatched by a child component.
I know how to use component.$on to check that the dispatched event does the right thing within the component which is dispatching, like so.
But I can't figure out how to check that the component which receives the dispatch does the right thing in response.
Here's a basic example:
Child.svelte
<script>
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
function handleSubmit(event) {
dispatch('results', 'some results')
}
</script>
<form on:submit|preventDefault={ handleSubmit }>
<button type='submit'>Submit</button>
</form>
Parent.svelte
<script>
import Child from './Child.svelte'
let showResults = false
function handleResults(event) {
showResults = true
}
</script>
<Child on:results={ handleResults } />
{ #if showResults }
<p id='results'>Some results.</p>
{ /if }
The idea is to eventually write a test using #testing-library/svelte like:
import { render } from '#testing-library/svelte'
import Parent from './Parent.svelte'
test('shows results when it receives them', () => {
const rendered = render(Parent)
// ***
// Simulate the `results` event from the child component?
// ***
// Check that the results appear.
})
If the parent were reacting to a DOM event, I would use fireEvent.
But I don't know how I would get a hold of the <Child> component in this case, and even if I could I'm guessing that Svelte is using a different mechanism for component events.
(Just to test it out, I used createEvent to fire a custom results event on one of the DOM elements rendered by <Child> but it didn't seem to do anything.)
Anyone have any ideas? Thanks!
If you're already planning on using #testing-library/svelte, I think the easiest way is not to try to manually trigger the Child component's results event, but to use Testing Library to grab the form/submit elements and trigger the submit event (using fireEvent a SubmitEvent on the <form> or their #testing-library/user-event library, or even a vanilla dispatchEvent). Svelte would then dispatch the custom results event that Parent is listening on.
Something like:
test('shows results when it receives them', async () => {
// Arrange
const rendered = render(Parent)
const submitButton = rendered.getByRole('button', {
name: /submit/i
});
const user = userEvent.setup();
// Act
await user.click(submitButton);
// Assert
const results = rendered.queryByText(/some results\./i);
expect(results).not.toBe(null);
});
Hope this is what you had in mind.
Edit:
For mocking Child.svelte, something like this in a __mocks__/Child.svelte should work:
<script>
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
function handleSubmit(event) {
dispatch("results", "some results");
}
</script>
<form on:submit|preventDefault={handleSubmit}>
<button type="submit">Test</button>
</form>
Which is the exact same implementation as the actual module (I gave the button a different label just to make it clear it's the mocked version when querying it), but the idea is that this would never need to change and is only used to dispatch a results event. Then you'd just need to tell Jest or whatever you're using that you're mocking it (jest.mock("./Child.svelte");), change the getByRole query to match the new name (or just leave the mock with the original name), then it should just work.
Whether you think that's worth it or not is up to you. I've generally had success testing the UI as a whole rather than mocking sub-components, but I guess it comes down to preference. Yes, you might have to change the test if the Child component changes, but only if you change the label of the button or change the user interaction mechanism.
You don't need to know about the details of the components, you don't even need to know that it's split into a separate Child component, all the test would care about is a general idea of the structure of the UI—that there's a button called "Submit" and that clicking on it should show an additional <p> tag.

Reactjs - How to avoid creating a new clickhandler function in each render

In my react component on a button click, i am passing a parameter to my click handler exactly like this
<a
id={`_primaryAction_${messageObject.id}`}
href="#"
class='message'
onClick={(e: MouseEvent) =>
this.handleClick(e, messageObject)
}
>
I have a usecase where my props are changing and re render is happening . so in each new render this click handler new instance will create. Is there a way to avoid this ?
Edited: removed id and passing wholeObject as it is my use case. Yes this is in loop . This a tag will create for the array of messages.
First of all, do more research to see if the re-rendering is indeed a cause for concern, as it might not be such a big deal performance-wise.
As a solution, you could create another component which you pass the object.
const ActionLink = (props) => {
const {
handleClick,
messageObject,
...otherProps
} = props;
const clickHandler = React.useCallback((e: MouseEvent) => {
handleClick(e, messageObject);
}, [handleClick, messageObject]);
return <a
{...otherProps}
onClick={ clickHandler }
/>;
}
export default ActionLink;
And in your case, you can use it like the following (instead of the a)
<ActionLink
id={`_primaryAction_${messageObject.id}`}
href="#"
class="message"
messageObject={messageObject}
handleClick={this.handleClick} >...</ActionLink>
And if required, you can further protect against re-renders by passing it through React.memo
export default React.memo(ActionLink);
Lastly as an alternative, you could do as others have suggested and provide the id to an attribute of the link, and use that inside the handleClick method to retrieve the correct message from the list
something like
<a
id={`_primaryAction_${messageObject.id}`}
href="#"
class='message'
data-message-id={messageObject.id}
onClick={this.handleClick}
>
and in your handleClick
handleClick(e){
const messageId = e.target.getAttribute('data-message-id');
// assuming your message list is named messageList
// adjust accordingly
const message = messageList.find(({ id }) => id === messageId);
// ... do what you were already doing with the message here
}
checkout useCallback
useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders
https://reactjs.org/docs/hooks-reference.html#usecallback
I think you are using a class component
since you want to pass an object which I think is coming dynamically and not some constant in component (i.e. object is part of a map) and also don’t want to create a new function on every render I would suggest set your button attribute's value as the value of your object and you can access it e.target.value and bind the method than using the inline callback
and it will not create a new function now here's the working example
I see you're using class component. In that case, just move the handler into a separate function.
class MyComponent extends React.Component {
handleClick = (e) => {
this.deleteRow(id, e)
}
render() {
return <button onClick={this.handleClick}>Delete Row</button>
}
}

Dynamically replace elements using jsx

I'm having an array data.info that is being updated over time and I'm trying to replace placeholder rendered elements with another. So by default app.js looks like this
return (
<Fragment>
{data.info.map((index) => {
return <Typography key={index} variant="h6" className={classes.title}>Demo</Typography>
})}
</Fragment>
)
Also I have a hook with async function to subscribed to data.info.length.
useEffect(
() => {
if (!initialRender.current) {
if (data.info.length!==0) {
for (let i = data.info.length-iScrollAmount+1 ; i < data.info.length+1; i++) {
firstAsync(i)
}
}
} else {
initialRender.current = false
}
},
[data.info.length]
)
async function firstAsync(id) {
let promise = new Promise(() => {
setTimeout(() => console.log(document.getElementById(id)), 500)
});
}
With document.getElementById() and id I can get to every element that was rendered and change it. And here goes the problems.
I'm using material-ui so I can't get to <Typography/> because it is transformed into <h6/>. Probably that is not a problem since I need to replace contents, so I can find parent element and remove all children. Is that way correct?
After I delete children how do I add content using jsx? What I mean is that in async function I'll get an array that I want to use in new element <NewCard/> to dynamically put into <Fragment/>. Yet I did not find any example how to do that.
It is not a good practice to change DOM Nodes directly in React, and you need to let React do the rendering for you and you just tell react what to do.
in your case you need to define a React State for your data and set your state inside your firstAsync function and then use your state to render whatever html element or React component which you want
React does not encourage the practice of manipulating the HTML DOM nodes directly.
Basically you need to see 2 things.
State which is a special variable whose value is retained on subsequent refresh. Change in reference in this variable will trigger component and its children a refresh/re-render.
Props which is passed to every Component and is read only. Changing in props causes refresh of component by default.
In your example, based on data.info you want to render Typography component.
Solution
First thing is your map function is incorrect. First parameter of map function is item of list and second is index. If you are not sure if info will always be present in data, you may want to have a null check as well.
{(data.info || []).map((info, index) => {
return <Typography key={index} variant="h6" className={classes.title}>{info.text}</Typography>
})}
You should be passing info from map to Typography component. Or use info value in content of Typography as shown above.
Update data.info and Typography will update automatically. For this, please make sure, data.info is a component state and not a plain variable. Something like
const [data, setData] = React.useState({});
And when you have value of data (assuming from API), then
setData(responseApi);

Trigger event whenever validity changes in redux form

In my React + Redux + Redux-Form app, I need to enable/disable a "Next" button depending on the validity state of the form. This button is controlled by the parent component, so I can't just have <Button disabled={!props.valid}/>. Ideally redux-form would give me an event like onValidityChanged that I could listen for, and call the setCompleted() function passed down by the parent component whenever this happens. However such an event doesn't seem to exist. I also can't use the onChange hook, because at this point it won't have access to the props passed by the parent.
e.g.
function Parent(props){
const [completed, setCompleted] = React.useState(false);
return (
<div>
<Child setCompleted=setCompleted/>
<button disabled={!completed}>Next</button>
</div>
);
}
const Child = reduxForm({form: 'form'})(props => {
const {setCompleted} = props;
// Call setCompleted once this form becomes valid
});
How can I run an event whenever the validity of the form changes, but still have access to props?
One simple way of doing so would be to read the current validity status as Redux-Form updates it in your redux state, using a selector. You can even use one provided by the redux-form package: isValid. Since you're using hooks, you can also use react-redux's useSelector hook (if you're using react-redux v7.1.0 or higher).
The result would be:
import { useSelector } from 'react-redux`;
import { reduxForm, isValid } from 'redux-form`;
const Child = reduxForm({form: 'form'})(props => {
const { setCompleted } = props;
const isFormValid = useSelector(state => isValid('form')(state));
// Call setCompleted once this form becomes valid
if (isFormValid) {
setCompleted(true);
}
});
Of course you may want to extract this selector in another file and memoize it.
You may also want to use this hook (or its mapStateToProps equivalent) directly inside the parent component to avoid using the useState hook in the first place.

How to find the DOM element of a functional children in React?

Apparently this is not possible, but since I'm not a React expert I'd like to get a confirmation.
The usual way of getting the DOM element of a child is with:
render () {
const child = React.Children.only(this.props.children);
const cloned = React.cloneElement(child, {
ref: this.someRef
});
return cloned;
}
The problem here is that according to the docs:
You may not use the ref attribute on function components because they
don’t have instances
Another option to find the DOM element could be using React.findDOMNode() but according to a comment from this issue when trying to React.findDOMNode(child):
If you want to find one of your children's DOM nodes, you have to add
a ref first (using React.cloneElement) and then use that ref.
And we're back to the previous problem.
So, is it possible to find the DOM element of a functional component?
As suggested in this question, this may be one of few acceptable cases for calling stateless component function directly:
function withRef(SFC) {
return React.forwardRef((props, ref) => SFC({ref, ...props}));
}
...
render () {
const child = React.Children.only(this.props.children);
const isFunctional = typeof child === 'function' &&
(!child.prototype || !child.prototype.isReactComponent);
if (isFunctional) {
const Child = withRef(child.type);
return <Child {...child.props} ref={this.someRef}/>;
} else {
...
}
}
This won't work if functional component's child cannot accept a ref, too.
In this form it's inefficient because a child will be re-rendered. Depending on what's the purpose, there may be better ways to achieve this. A component could be designed to receive a component to render as a prop instead of a child. Or if DOM element is needed for specific task like setting up DOM event listener, this may be done like in this case .

Categories

Resources