Create trigger onChange in React Component - reactjs

I have a component in react and I want to create an event "onChange" who can be detect by react by an other component.
const MyFirstComponent = () => {
//doSomething
//How can I create event here who can be detect by the onChange in the MySecondComponent
return (
<p>
Something
</p>
)
}
const MySecondComponent = () => {
const handleOnChange = (event) => {//I want this can be launch by MyFirstComponent
//doSomething
}
return (
<MyFirstComponent onChange={handleOnChange}>
)
}
Maybe there are better ways to get information out ?

Pass the HandleOnChange as props as you did it's a good idea . And then you can get it in your first composant like this :
const MyFirstComponent = (props) => {
return (
<p onChange={props.OnChange}></p>
)
}

Related

Ref always return null in Loadable Component

I am using this react library https://github.com/gregberge/loadable-components to load a Component with Ref to access instance values using useImperativeHandle but ref is always null.
Here is my code
import loadable from '#loadable/component';
export default function ParentComponent(props){
const currentPageRef = useRef();
const[currentPage,setCurrentPage]=useState();
const loadPage= (page= 'home') => {
const CurrentPage = loadable(() => import(`./${page}`));
return (
<CurrentPage
ref={currentPageRef}
/>
);
}
useEffect(() => {
console.log(currentPageRef); //This is always logging current(null);
let pageTitle= currentPageRef.current?.getTitle();
let pageSubTitle= currentPageRef.current?.getSubTitle();
console.log(` Page Title=${pageTitle}`); //This is always coming back as null
console.log(`Page SubTitle=${pageSubTitle}`); //This is also always coming back as null
}, [currentPage]);
return (
<div>
<button onClick={() => {
setCurrentPage(loadPage('youtube));
}}>
LoadPage
</button>
</div>
);
}
Where each of the child components contains a useImperativeHandle to expose instance functions but I can't seem to access any of the functions because currentPageRef is always null
Here is an example of one of the child pages that contains the useImperativeHandle implementation
const YouTubePage= React.forwardRef((props,ref)=>{
const [connected, setConnected] = useState(false);
const getTitle = () => {
return connected ? "Your YouTube Channels" : "YouTube";
}
const getSubTitle = () => {
return connected ? "Publishable content is pushed to only connected channels. You may connect or disconnect channel(s) as appropriate" : "Connect a YouTube account to start posting";
}
useImperativeHandle(ref, () => ({ getTitle, getSubTitle }));
return (<div></div>);
});
Any ideas as to why that might be happening?
Thank you
From your code example your aren't actually rendering the component which you set by the state setter:
export default function ParentComponent(props) {
//...
// Render the page
return (
<>
{currentPage}
<div>...</div>
</>
);
}

React parent pass children will trigger rerender without state/props change

When I have some children(HTML dom) as props to a child component and control in parent, I found that will trigger rerender!
Why React parent pass children will trigger rerender without state/props change?
How to avoid it? Check following.
const InsideChild = React.memo(({children}) => {
const countRef = useRef(0)
countRef.current += 1
return (
<div>render count: {countRef.current} {children}</div>
)
})
const OutsideParent = () => {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<div>
Test1:
<InsideChild />
</div>
<div>
Test2:
<InsideChild>
<p>children as html dom will not trigger rerender.</p>
</InsideChild>
</div>
</div>
)
}
as sample code, Test1 will not trigger rerender, Test2 will. Is that possible to avoid it?
More detail and working sample here:
https://codepen.io/sky790312/pen/QWqygxQ
The React.memo is not working when you are sending html is becuase you the children is an object in that case, the only way you can compare it is with a deep compare or something.
You can add a comparer function to your memo, and compare the inner props of the objects in the case you send an html.
In the example i just check if the props are an object and then compare the object inside, hope it helps you
const comparisonFn = (prevProps, nextProps) => {
if(typeof prevProps?.children === 'object') {
return prevProps?.children.props.children ==
nextProps?.children.props.children
} else {
return prevProps?.children === nextProps?.children;
}
}
const InsideChild = React.memo(({children}) => {
const countRef = useRef(0)
countRef.current += 1
return (
<div>render count: {countRef.current} {children}</div>
)
}, comparisonFn)

React useState no changing value in render

I have a component with customButton for PWA:
const PWA = () => {
const [supportsPWA, setSupportsPWA] = useState(false)
const [deferredPrompt, setDeferredPrompt] = useState(null)
useEffect(() => {
const handler = e => {
e.preventDefault()
setDeferredPrompt(e)
setSupportsPWA(true)
}
window.addEventListener('beforeinstallprompt', handler)
return () =>
window.removeEventListener('beforeinstallprompt', handler)
}, [])
return (
<>
{supportsPWA.toString()}
{console.log(supportsPWA)}
{supportsPWA && <div>test</div>}
</>
)
}
export default PWA
I have beforeinstallprompt which I change supportsPWA status from false to true. In render I have supportsPWA.toString() it's always false. Console log return me: false and next true, so if supportsPWA is true why React not render div test?
EDIT:
I found where is the problem. I render PWA component in another component: {showMobileMenu && (<PWA />)}. showMobileMenu I changing by button (from false to true ) and here is problem. When I remove showMobileMenu && ... and I have only <PWA /> then div test is rendering correctly. But I dont know how I can resolve it?
Well in your case instead of doing {showMobileMenu && (<PWA />)} which dismounts and mounts your component every time just show/hide content in your PWA component, if that is acceptable:
const PWA = ({ showPWAComponent }) => {
const [supportsPWA, setSupportsPWA] = useState(false)
const [deferredPrompt, setDeferredPrompt] = useState(null)
useEffect(() => {
const handler = e => {
e.preventDefault()
setDeferredPrompt(e)
setSupportsPWA(true)
}
window.addEventListener('beforeinstallprompt', handler)
return () =>
window.removeEventListener('beforeinstallprompt', handler)
}, [])
return showPWAComponent && (
<div>
{supportsPWA.toString()}
{console.log(supportsPWA)}
{supportsPWA && <div>test</div>}
</div>
)
}
export default PWA
And in the other component where you use PWA go:
{<PWA showPWAComponent={showMobileMenu} />)}
In any other case this case that you have will not work exactly because you have that removal of component there.

Using React hook form getValues() within useEffect return function, returns {}

I'm using react-hook-form library with a multi-step-form
I tried getValues() in useEffect to update a state while changing tab ( without submit ) and it returned {}
useEffect(() => {
return () => {
const values = getValues();
setCount(values.count);
};
}, []);
It worked in next js dev, but returns {} in production
codesandbox Link : https://codesandbox.io/s/quirky-colden-tc5ft?file=/src/App.js
Details:
The form requirement is to switch between tabs and change different parameters
and finally display results in a results tab. user can toggle between any tab and check back result tab anytime.
Implementation Example :
I used context provider and custom hook to wrap setting data state.
const SomeContext = createContext();
const useSome = () => {
return useContext(SomeContext);
};
const SomeProvider = ({ children }) => {
const [count, setCount] = useState(0);
const values = {
setCount,
count
};
return <SomeContext.Provider value={values}>{children}</SomeContext.Provider>;
};
Wrote form component like this ( each tab is a form ) and wrote the logic to update state upon componentWillUnmount.
as i found it working in next dev, i deployed it
const FormComponent = () => {
const { count, setCount } = useSome();
const { register, getValues } = useForm({
defaultValues: { count }
});
useEffect(() => {
return () => {
const values = getValues(); // returns {} in production
setCount(values.count);
};
}, []);
return (
<form>
<input type="number" name={count} ref={register} />
</form>
);
};
const DisplayComponent = () => {
const { count } = useSome();
return <div>{count}</div>;
};
Finally a tab switching component & tab switch logic within ( simplified below )
const App = () => {
const [edit, setEdit] = useState(true);
return (
<SomeProvider>
<div
onClick={() => {
setEdit(!edit);
}}
>
Click to {edit ? "Display" : "Edit"}
</div>
{edit ? <FormComponent /> : <DisplayComponent />}
</SomeProvider>
);
}

How to optimize React components with React.memo and useCallback when callbacks are changing state in the parent

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

Resources