React synchronously remove an item from the dom - javascript

I need to synchronously remove a child from the container. I created a simplified code to show my current solution (Which makes use of normal pattern using useState hook)
type ChildProps = {
index: number;
id: string;
remove: (index: number) => void;
};
function Child(props: ChildProps) {
const handleClick = () => {
console.log('FIRST');
props.remove(props.index);
console.log('THIRD');
};
return (
<button id={props.id} onClick={handleClick}>
Child {props.index}
</button>
);
}
export default function Home() {
const [childrens, setChildrens] = React.useState<{ el: React.ReactNode; index: number }[]>([]);
const removeChildren = (index: number) => {
setChildrens((old) => old.filter((e) => e.index !== index));
};
const addChildren = () => {
const index = new Date().getTime();
setChildrens((old) => [
...old,
{ index, el: <Child id={`${index}`} index={index} remove={removeChildren} /> },
]);
};
console.log('SECOND');
return (
<div>
<button onClick={addChildren}>Add</button>
<div>{childrens.map((e) => e.el)}</div>
</div>
);
}
When the remove handler is called, FIRST is printed, then the remove function is called (which dispatches the parent component's status update), then THIRD is printed, and finally SECOND will be printed when the parent component is updated which will "remove" the child.
I need the component to be removed on the next instruction after the removal method call.
In my application in the child components I make use of Tanstack Query hooks to use the data from the api.
After calling a delete endpoint I delete the component and invalidate the cache.
The problem is that the cache is invalidated before the component is unmounted and this causes the endpoint to be called to get the component data (because the Tanstack hook is still active since the component is still present in the DOM) this causes an error in the endpoint call as the entry no longer exists.
I've tried looking for ways to manipulate the react virtual DOM but haven't found anything useful.
Manipulating the DOM directly is not a good option
document.getElementById(`${index}`)?.remove()
This causes the following exception:
Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node

I found the answer into React new docs.
To fix this issue, you can force React to update (“flush”) the DOM synchronously. To do this, import flushSync from react-dom and wrap the state update into a flushSync call
Link to the doc

Related

Calling function defined within a react function component on a click event form another function component - React.js

I am constructing some node objects in a function(prepareNodes) to pass to React Flow within a functional component A (lets say), and I have defined a custom node component(CardNode) stateless, which has a button. On button click it should trigger the function(prepareNodes) defined within Component A.
function ComponentA = ({ selectedNodes }) => {
const reactFlowWrapper = useRef(null);
const [elements, setElements] = useState([]);
const [edges, setEdges] = useState([]);
const prepareNode = async (nodeid) => {
//some service calls to fetch data and constuct nodes
setElements([ ...nodes]);
setEdges([...edges]);
}
return (
<ReactFlowProvider>
<div className="reactflow-wrapper" ref={reactFlowWrapper}>
<ReactFlow
nodes={elements}
edges={edges}
//some properties
>
</ReactFlow>
</div>
</ReactFlowProvider>
)
};
export default ComponentA;
function CardNode({ data }) {
const renderSubFlowNodes = (id) => {
console.log(id);
//prepareNode(id)
}
return (
<>
<Handle type="target" position={Position.Top} />
<div className="flex node-wrapper">
<button className="btn-transparent btn-toggle-node" href="#" onClick={() => renderSubFlowNodes(data['id']) }>
<div>
<img src={Icon}/>
</div>
</button>
</div>
<Handle type="source" position={Position.Bottom}/>
</>
);
}
export default CardNode;
I looked for some references online, and most of them suggest to move this resuable function out of the component, but since this function carries a state that it directly sets to the ReactFlow using useState hook, I dont think it would be much of a help.
Other references talks about using useCallback or useRefs and forwardRef, useImperativeHandle especially for functional component, Which I did not quite understand well.
Can someone suggest me a solution or a work around for this specific use-case of mine.
You can add an onClick handler to the each node, and within the node view you call this handler on click.
In the parent Component within the onClick handler you can call prepareNode as needed.
useEffect(() => {
setElements(
elements.map(item => {
...item,
onClick: (i) => {
console.log(i);
prepareNode();
},
})
)},
[]);
The classical approach is to have a parent object that defines prepareNode (along with the state items it uses) and pass the required pieces as props into the components that use them.
That "parent object" could be a common-ancestor component, or a Context (if the chain from the parent to the children makes it cumbersome to pass the props all the way down it).

Is there any way I can update a mounted React node's props in my current setup?

So I have a mounted React application that is rendered like this (it's via another framework on top):
const componentRender = (entryComponent, initialProps, targetNode) => {
root.render(wrapper);
}
On top level, I call a 3rd party library and wrap my React application inside of it.
What's the best way to integrate React inside of a 3rd party library that its supposed to render into?
The best practice is to update props within a component, not outside of the render function. Otherwise, as you've said, it will cause a full remount.
So the solution would be to call your Ext.extend function from within the App component.
function App() {
const [ready, setReady] = useState(false);
const [props, setProps] = useState({});
// Calls once on first mount.
useEffect(() => {
Ext.extend(Ext.Container, {
listeners: {
afterrender: function () {
setProps(this.myProps);
setReady(true);
},
},
constructor: function (...args) {
Object.assign(this, args);
},
onServerSuccess: function(data) {
// here is where I receive new data
setProps(data);
}
});
}, [])
return (
ready ? <Component {...props} /> : null
);
}
ReactDOM.render(App, rootNode);
If there is more interaction between Ext and the react component, for example you need to pass this.root into a function, you can still do that with the normal DOM APIs like document.getElementById. Or you can add it like this:
// use this reference to do stuff with the react object
this.root = <Component {...props} />
return this.root;
but changing the react component object directly would be an anti-pattern. All state/props updates should happen using APIs provided by the react library.
Also if for some reason you need to render the react component directly inside the Ext.Container node, then you should use ReactDOM.createPortal instead of render.
Here's how that would look.
import { createPortal } from 'react-dom';
const Portal = memo(({ children, domNode }) => createPortal(
children,
domNode,
));
function App() {
const [ready, setReady] = useState(false);
const [props, setProps] = useState({});
// Calls once on first mount.
useEffect(() => {
Ext.extend(Ext.Container, {
listeners: {
afterrender: function () {
setProps(this.myProps);
setReady(true);
},
},
constructor: function (...args) {
Object.assign(this, args);
},
onServerSuccess: function(data) {
// here is where I receive new data
setProps(data);
}
});
}, [])
return (
ready ?
<Portal domNode={extComponentNode}>
<Component {...props} />
</Portal> : null
);
}
Where extComponentNode is the node you want it rendered in. createPortal allows the react component to share state with your react App even though it's rendered in a different place, without the issue of calling render again.
I suppose you want to re-render the component with updated states without unmounting-mouting it whenever the prop(s) passed to the component changes.
Create a useEffect inside this component to re-render the component whenever the required prop(s) changes. To do this, add the prop(s) for which you want to re-render the component in the dependency array of useEffect and pass a function that will update the states of the component.
useEffect(
<function which will update the states, causing re-rendering of the component>,
[ <all props for which you want to run the function separated by a comma> ]
)

React forwardRef is never defined, even after re-render

EDIT: This problem was caused because I was attempting to use forwardRef in conjunction with Redux connect. Take a look at this post for the solution.
I am attempting to access a DOM element in a parent component by using React.forwardRef. The problem is that ref.current never gets defined, even after the first render.
In this parent component, I want to access a DOM element from one of the child components:
const MyFunctionComponent = () => {
const bannerRef = useRef<HTMLDivElement>(null);
let bannerIndent = useRef<string>();
useEffect(() => {
// bannerRef.current is ALWAYS null. Why?
if (bannerRef.current) {
// this conditional block never runs, so bannerIndent is never defined
const bannerWidth = bannerRef.current.getBoundingClientRect().width;
bannerIndent.current = `${bannerWidth}px`;
}
}, []);
return (
<>
<Banner ref={bannerRef} />
<ComponentTwo bannerIndent={bannerIndent.current} />
</>
)
}
The component which receives the forwarded ref:
const Banner = React.forwardRef<HTMLDivElement, Props> ((props, ref) => (
<div ref={ref} />
));
The component which needs some data derived from the forwarded ref:
const ComponentTwo = (props: {bannerIndent?: string}) => (
<StyledDiv bannerIndent={props.bannerIndent} />
)
// styled.div is a styled-component
const StyledDiv = styled.div<{bannerIndent?: string}>`
... // styles, some of which depend on bannerIndent
`
Answers to existing SO questions which discuss forwardRef always being null or undefined point out that the ref will only be defined after the first render. I don't think that's the issue here, since the useEffect should run after the first render, yet bannerRef.current is still null.
The logic for setting the value of bannerIndent with a useEffect hook works correctly if used inside of the Banner component with a normal ref. However, I need to put the ref in the parent component so that bannerWidth (generated using the forwarded ref) can be passed to a sibling of the Banner component.
Help would be greatly appreciated :D

How to re-render React Component when outside function is called?

I want to be able to receive mqtt messages and display them on a web app. I'm working with AWS Amplify's PubSub and the function calls happen outside of the class component. I can't directly access any of the instance functions/properties from the function outside of it but I need some way to trigger a setState change so that the web app can be re-rendered, right?
I've tried just directly calling the function from the React class, but there's still no re-rendering happening. I've tried making an outside variable, storing the message received in it and then accessing the variable from the React class but still no trigger. Then I researched and found that I could force a re-render every couple of seconds but read that doing such a thing should be avoided. I should be using ComponentDidMount and setState functions but not really understanding how to get this to work.
Again all I'm really trying to do is get the message and update the web app to display the new message. Sounds pretty simple but I'm stuck.
import...
var itemsArr = [];
function subscribe() {
// SETUP STUFF
Amplify.configure({
...
});
Amplify.addPluggable(new AWSIoTProvider({
...
}));
// ACTUAL SUBSCRIBE FUNCTION
Amplify.PubSub.subscribe('item/list').subscribe({
next: data => {
// LOG DATA
console.log('Message received', data);
// GET NAMES OF ITEMS
var lineItems = data.value.payload.lineItems;
lineItems.forEach(item => itemsArr.push(item.name));
console.log('Items Ordered', itemsArr);
// THIS FUNCTION CALL TRIGGERS AN ERROR
// CANT ACCESS THIS INSTANCE FUNCTION
this.update(itemsArr);
},
error: error => console.error(error),
close: () => console.log('Done'),
});
}
// REACT COMPONENT
class App extends Component {
constructor(props){
super(props);
this.state = {
items:null,
};
}
// THOUGHT I COULD USE THIS TO UPDATE STATE
// TO TRIGGER A RE-RENDER
update(stuff){
this.setState({items: stuff});
}
render() {
// THINK SUBSCRIBE METHOD CALL SHOULDN'T BE HERE
// ON RE-RENDER, KEEPS SUBSCRIBING AND GETTING
// SAME MESSAGE REPEATEDLY
subscribe();
console.log('items down here', itemsArr);
return (
<div className="App">
<h1>Test</h1>
<p>Check the console..</p>
<p>{itemsArr}</p>
</div>
);
}
}
export default App;
Ideally, I'd like columns of the list of items to be displayed as messages come in but I'm currently getting an error - "TypeError: Cannot read property 'update' of undefined" because the subscribe function outside of the class doesn't have access to the update function inside the class.
Put subscribe method inside the App component so you can call it. You can call subscribe method in componentDidMount lifecycle to execute it (to get the items) after App component renders the first time. And then, update method will run this.setState() (to store your items in the state) causing App component to re-render. Because of this re-render, your this.state.items will contain something and it will be displayed in your paragraph.
class App extends Component {
constructor(props){
super(props);
this.state = {
items: [],
};
this.subscribe = this.subscribe.bind(this);
this.update = this.update.bind(this);
}
update(stuff){
this.setState({ items: stuff });
}
subscribe() {
// SETUP STUFF
Amplify.configure({
...
});
Amplify.addPluggable(new AWSIoTProvider({
...
}));
// ACTUAL SUBSCRIBE FUNCTION
Amplify.PubSub.subscribe('item/list').subscribe({
next: data => {
// LOG DATA
console.log('Message received', data);
// GET NAMES OF ITEMS
let itemsArr = [];
var lineItems = data.value.payload.lineItems;
lineItems.forEach(item => itemsArr.push(item.name));
console.log('Items Ordered' + [...itemsArr]);
this.update(itemsArr);
},
error: error => console.error(error),
close: () => console.log('Done'),
});
}
componentDidMount() {
this.subscribe();
}
render() {
console.log('items down here ' + [...this.state.items]);
return (
<div className="App">
<h1>Test</h1>
<p>Check the console..</p>
<p>{this.state.items !== undefined ? [...this.state.items] : "Still empty"}</p>
</div>
);
}
}
export default App;
By using an Object as a prop which stores references to internal methods of a child-component, it is possible to then access them from the parent.
The below (overly-simplified) example shows this.
The RandomNumber purpose is to generate a single random number. It's all it does.
Then, at its parent-level, some user action (button onClick event) is triggering the RandomNumber component to re-render, using a custom hook called useShouldRender, which generates a random number every time it's setter function is invoked, so by exposing the setter function to the "exposed" prop object, it is possible to interact with internal component operations (such as re-render)
const {useState, useMemo, useReducer} = React
// Prints a random number
const RandomNumber = ({exposed}) => {
// for re-render (https://stackoverflow.com/a/66436476/104380)
exposed.reRender = useReducer(x => x+1, 0)[1];
return Math.random(); // Over-simplification. Assume complex logic here.
}
// Parent component
const App = () => {
// create a memoed object, which will host all desired exposed methods from
// a child-component to the parent-component:
const RandomNumberMethods = useMemo(() => ({}), [])
return (
<button onClick={() => RandomNumberMethods.reRender()}>
<RandomNumber exposed={RandomNumberMethods}/>
</button>
)
}
// Render
ReactDOM.render(<App />, root)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Passing jsx content to redux state and rendering it from there

I am having trouble figuring out how to pass JSX to my redux state i.e. for modal component that is used globally and has its redux state where one parameter is content such content can be updated to include JSX code.
At the moment I am getting it to render correct content however it doesn't seem that functions are called correctly and I am also getting following error:
invariant.js:38 Uncaught Error: Objects are not valid as a React child
(found: object with keys {dispatchConfig, _targetInst,
isDefaultPrevented, isPropagationStopped, _dispatchListeners,
_dispatchInstances, nativeEvent, type, target, currentTarget, eventPhase, bubbles, cancelable, timeStamp, defaultPrevented,
isTrusted, view, detail, screenX, screenY, clientX, clientY, ctrlKey,
shiftKey, altKey, metaKey, getModifierState, button, buttons,
relatedTarget, pageX, pageY}). If you meant to render a collection of
children, use an array instead or wrap the object using
createFragment(object) from the React add-ons. Check the render method
of styled.div.
With a lot of following errors:
warning.js:36 Warning: This synthetic event is reused for performance
reasons. If you're seeing this, you're accessing the property
nativeEvent on a released/nullified synthetic event. This is set to
null. If you must keep the original synthetic event around, use
event.persist(). See https://fbme(replaced this as so doesn't allow links to fb)/react-event-pooling for more
information.
Example implementation:
Function called from a page to show modal and add contents to it
onToggleModal = () => {
this.props.modalToggle(
(<TopUp account={getSession().accounts[0] || {}} />)
);
}
Where this.props.modalToggle is a redux action like this:
export const modalToggle = (content = '') => ({
type: MODAL_TOGGLE,
payload: content
});
I then try to render such content inside my Modal container:
return (
<div>{this.props.content}</div>
)
I imported React into my reducer, in hopes to resolve jsx issues, but had no luck. It also seems like components are passed to reducer as some sort of weird objects.
Redux state can only contain plain objects and data types, so a JSX object would probably cause issues. Also it's most likely not a good idea to have a JSX object (which is basically a view) as part of your state.
Instead, you should pass around whatever data is required to render the final view. I think this would work:
onToggleModal = () => {
this.props.modalToggle(getSession().accounts[0] || {});
}
export const modalToggle = (account = {}) => ({
type: MODAL_TOGGLE,
account: account
});
return (
<div><TopUp account={account} /></div>
)
Hey it's been a while now but for the record I stumbled upon the same use case you are explaining in the reply to the accepted answer.
You can achieve this by adding a property of JsxElement | ReactElement | HTMLElement type in your modalSlice state:
export interface modalState {
active: boolean;
component?: ReactElement;
}
reducers: {
updateModalComponent: (state, value: PayloadAction<ReactElement>) => {
state.component = value.payload;
},
}
Then your modal component file could look something like this:
import { ReactElement } from 'react';
import './modal.scss';
interface Props {
active: boolean,
children: ReactElement | JsxElement | HTMLElement,
}
export const Modal = (props: Props) => {
return (
<div id="modal-container">
{
props.active &&
<div id="overlay">
<div id="modal-content-container">
{props.children}
</div>
</div>
}
</div>
)
}
Finally use it anywhere!
const element = <OtherComponent />;
dispatch(updateModalComponent(element));
Cheers!

Categories