React memo allowing re-rendering to forwardRef - reactjs

I have a component defined as
export default function MyComp({
...someprops
}) {
const [data, setData] = useState([]);
const searchRef = useRef();
return (
<Box>
{!showEmptyState ? (
<LzSearch
onUpdate={(items) => setData(items)}
ref={searchRef}
/>
) : (
<Box />
)}
</Box>
);
}
Where as LzSearch is defined as
const LzSearch = forwardRef((props, ref) => {
const {
...rest
} = props;
const classes = useStyles();
const hashData = {};
console.log(hashData);
function updateHashData() {
// Function is called at some point after getting data from API
setHashData(...);
onUpdate(...)
}
return (
<Box>
{`Some components`}
</Box>
);
});
export default memo(LzSearch);
After calling onUpdate(), my main component is updated however it then re-render my LzSearch component and resetting the hashData. I have added memo however its doing the same thing.
How can I avoid re rendering.

This has nothing to do with the ref. LzSearch is rerendering because the onUpdate prop is changing. MyComp will need to use useCallback to keep the onUpdate function the same between renders:
export default function MyComp({
...someprops
}) {
const [data, setData] = useState([]);
const searchRef = useRef();
const onUpdate = useCallback((items) => {
setData(items);
}, [])
return (
<Box>
{!showEmptyState ? (
<LzSearch
onUpdate={onUpdate}
ref={searchRef}
/>
) : (
<Box />
)}
</Box>
);
}
re-render my LzSearch component and resetting the hashData
Be aware that memo is just meant as a performance optimization tool, not a way to fix bugs. Your component needs to work correctly if it happens to rerender for some reason. memo might stop some renders, but it can't guarantee that the component will never rerender. Examples of things that can cause a rerender even with useMemo include: changing props, changing state, changing context, strict mode's double render feature, concurrent mode aborting and then repeating a portion of the component tree.

Related

React, Too many rerenders

Given the following code:
export default function CrearContacto({setOpening}) {
const [open, setOpen] = useState(false);
setOpen(setOpening);
useEffect(() => {
console.log(setOpening);
setOpen(setOpening);
}, [setOpening]);
const handleClose = () =>{setOpening=false};
return (
<div>
<Dialog open={open} onClose={handleClose}>
//code continues
I'm getting a Too many re-renders error, however I'm unable so see where is the loop here. {setOpening} is being used in a button in another component, when clicked, true is sent. setOpen sets new open state as true, so Dialog can pop-up. At the same time useEffect is watching setOpening for changes (probably this is also wrong, I'm trying to make this work without success). If closed, handleClose is triggered and now setOpening equals false, so useEffect detects the change and executes setOpen to false. I don't see the loop, but also the code isn't working as intended.
Edit - Added parent relative code
function Agenda() {
const MiContexto = useOutletContext();
return (<CrearContacto setOpening={MiContexto.modalState}/>
//code continues
MainLayout class:
import Navbar from "./Navbar"
import Topbar from "./Topbar"
import React, {useReducer} from "react";
import { Outlet, useOutletContext } from "react-router-dom";
export const MainContexto = React.createContext({modalState:false});
const initialState=false;
const estadosPosiblesModal = (state, action) =>{
switch(action){
case false:
return false;
case true:
return true;
default:
return state;
}
}
const MainLayout = ()=> {
const [EstadoModal, dispatch] = useReducer(estadosPosiblesModal,initialState);
return (
<MainContexto.Provider value={{modalState:EstadoModal,modalDispatch:dispatch}}>
<div className="mainlayout">
<Navbar/>
<main className="mainlayout_topPlusContent">
<Topbar className="TopBar" prueba={EstadoModal}/>
<Outlet context={{modalState:EstadoModal,modalDispatch:dispatch}}/>
</main>
</div>
</MainContexto.Provider>
);
};
export default MainLayout;
I had to use simultaneously a custom context and the Router outlet context as my context wasn't going through the outlet.
Topbar contains:
const micontext = useContext(MainContexto);
...
<Button variant="contained" onClick={()=>micontext.modalDispatch(true)}>
Your problem is you call setOpen(setOpening) on the component level CrearContacto which is causing the infinite re-renderings on that component. You can imagine this cycle: state update (setOpen(setOpening)) > re-rendering > call state update again > ...
And another problem is you cannot set setOpening=false directly, it needs to be set with a state setter from the parent component.
export default function CrearContacto({setOpening}) {
const [open, setOpen] = useState(setOpening);
//setOpen(setOpening); //remove this part
useEffect(() => {
setOpen(setOpening)
},[setOpening])
const handleClose = () =>{setOpen(false)}; //set `open` state internally instead
return (
<div>
<Dialog open={open} onClose={handleClose}>
</div>)
}
If you want to update the parent component's state, you should pass the state setter and state value to your child component which is CrearContacto too
I'd assume that you have this state on the parent component
const [opening, setOpening] = useState(false);
Note that setOpening is following the naming convention of the state setter, so I'd prefer to do it this way instead
You can try to modify your component like below
export default function CrearContacto({setOpening, opening}) {
const [open, setOpen] = useState(opening);
//setOpen(setOpening); //remove this part
useEffect(() => {
setOpen(opening)
},[opening])
const handleClose = () =>{setOpening(false)};
return (
<div>
<Dialog open={open} onClose={handleClose}>
</div>)
}
If you only use open state for Dialog, you can remove that state declaration too
export default function CrearContacto({setOpening, opening}) {
const handleClose = () =>{setOpening(false)};
return (
<div>
<Dialog open={opening} onClose={handleClose}>
</div>)
}
To expand my comment:
From inside (or a child) component, you can change the open value by using the state, so call the setOpen function.
From a parent component you can change the value of open by changing the prop setOpening (rename this prop as it sounds like it's a function), so you would have:
export default function CrearContacto({setOpening}) {
const [open, setOpen] = useState(setOpening);
useEffect(() => {
console.log(setOpening);
setOpen(setOpening);
}, [setOpening]);
const handleClose = () =>{setOpen(false)};
return (
<div>
<Dialog open={open} onClose={handleClose}>

REACT: How to set the state in the child and access it in the parent, receiving undefined

I am building this project to try and improve my understanding of react :), so I am a n00b and therefore still learning the ropes of extracting components, states, props etc =)
I have a child Component DescriptionDiv, its parent component is PlusContent and finally the parent component is PlusContentHolder. The user types some input into the DescriptionDiv which then, using a props/callback passes the user input to the PlusContent.
My question/problem is: after setting useState() in the PlusContent component, I am after a button click in the PlusContentHolder component, returned with an undefined in the console.log.
How come I cannot read the useState() in the next parent component, the PlusContentHolder?
I know that useState() is async so you cannot straight up call the value of the state in the PlusContent component, but shouldn't the state value be available in the PlusContentHolder component?
below is my code for the DescriptionDiv
import './DescriptionDiv.css';
const DescriptionDiv = props => {
const onDescriptionChangeHandler = (event) => {
props.descriptionPointer(event.target.value);
}
return (
<div className='description'>
<label>
<p>Description:</p>
<input onChange={onDescriptionChangeHandler} type='text'></input>
</label>
</div>);
}
export default DescriptionDiv;
Next the code for the PlusContent comp
import React, { useState } from "react";
import DescriptionDiv from "./div/DescriptionDiv";
import ImgDiv from "./div/ImgDiv";
import "./PlusContent.css";
import OrientationDiv from "./div/OrientationDiv";
const PlusContent = (props) => {
const [classes, setClasses] = useState("half");
const [content, setContent] = useState();
const [plusContent, setPlusContent] = useState({
orientation: "left",
img: "",
description: "",
});
const onOrientationChangeHandler = (orientationContent) => {
if (orientationContent == "left") {
setClasses("half left");
}
if (orientationContent == "right") {
setClasses("half right");
}
if (orientationContent == "center") {
setClasses("half center");
}
props.orientationInfo(orientationContent);
};
const onDescriptionContentHandler = (descriptionContent) => {
props.descriptionInfo(setPlusContent(descriptionContent));
console.log(descriptionContent)
};
const onImageChangeHandler = (imageContent) => {
props.imageInfo(imageContent);
setContent(
<>
<OrientationDiv
orientationPointer={onOrientationChangeHandler}
orientationName={props.orientationName}
/> {/*
<AltDiv altPointer={onAltDivContentHandler} />
<TitleDiv titlePointer={onTitleDivContentHandler} /> */}
<DescriptionDiv descriptionPointer={onDescriptionContentHandler} />
</>
);
};
return (
<div className={classes}>
<ImgDiv imageChangeExecutor={onImageChangeHandler} />
{content}
</div>
);
};
export default PlusContent;
and lastly the PlusContentHolder
import PlusContent from "../PlusContent";
import React, { useState } from "react";
const PlusContentHolder = (props) => {
const onClickHandler = (t) => {
t.preventDefault();
descriptionInfoHandler();
};
const descriptionInfoHandler = (x) => {
console.log(x) // this console.log(x) returns and undefined
};
return (
<div>
{props.contentAmountPointer.map((content) => (
<PlusContent
orientationInfo={orientationInfoHandler}
imageInfo={imageInfoHandler}
descriptionInfo={descriptionInfoHandler}
key={content}
orientationName={content}
/>
))}
<button onClick={onClickHandler}>Generate Plus Content</button>
</div>
);
};
export default PlusContentHolder;
The reason why the descriptionInfoHandler() function call prints undefined in its console.log() statement when you click the button, is because you never provide an argument to it when you call it from the onClickHandler function.
I think that it will print the description when you type it, however. And I believe the problem is that you need to save the state in the PlusContentHolder module as well.
I would probably add a const [content, setContent] = useState() in the PlusContentHolder component, and make sure to call setContent(x) in the descriptionInfoHandler function in PlusContentHolder.
Otherwise, the state will not be present in the PlusContentHolder component when you click the button.
You need to only maintain a single state in the PlusContentHolder for orientation.
Here's a sample implementation of your use case
import React, { useState } from 'react';
const PlusContentHolder = () => {
const [orientatation, setOrientation] = useState('');
const orientationInfoHandler = (x) => {
setOrientation(x);
};
const generateOrientation = () => {
console.log('orientatation', orientatation);
};
return (
<>
<PlusContent orientationInfo={orientationInfoHandler} />
<button onClick={generateOrientation}>generate</button>
</>
);
};
const PlusContent = ({ orientationInfo }) => {
const onDescriptionContentHandler = (value) => {
// your custom implementation here,
orientationInfo(value);
};
return <DescriptionDiv descriptionPointer={onDescriptionContentHandler} />;
};
const DescriptionDiv = ({ descriptionPointer }) => {
const handleChange = (e) => {
descriptionPointer(e.target.value);
};
return <input type="text" onChange={handleChange} />;
};
I would suggest to maintain the orientation in redux so that its easier to update from the application.
SetState functions do not return anything. In the code below, you're passing undefined to props.descriptionInfo
const onDescriptionContentHandler = (descriptionContent) => {
props.descriptionInfo(setPlusContent(descriptionContent));
};
This shows a misunderstanding of the use of state. Make sure you're reading about "lifting state" in the docs.
You're also declaring needless functions, e.g. onDescriptionContentHandler in your PlusContent. The PlusContent component could just pass the descriptionInfoHandler from PlusContentHolder prop directly down to DescriptionDiv, since onDescriptionContentHandler doesn't do anything except invoke descriptionInfoHandler.
You may want to consider restructuring your app so plusContent state is maintained in PlusContentHolder, and pass that state down as props. That state would get updated when DescriptionDiv invokes descriptionInfoHandler. It'd subsequently pass the updated state down as props to PlusContent.
See my suggested flowchart.

What's the difference between useObserver and observer in this application?

I have a react functional component that accesses the MobX store with useContext. I have found two ways to observe an array that is an observable from the store. First, the useObserver hook and wrapping the component with observer.
I thought that these are the same but that the useObserver only observes specific properties (such as the array that is passed) but I am experiencing a problem when the array reaches size 2 and then the component does not re-render. That's the case when using useObserver. When wrapping with observer, this is fixed.
Can anyone explain why this is happening and what's the difference?
const ApplesContainer = observer(() => {
const stores = useStores();
const applesArray = stores.fruits.apples;
return (
{applesArray.map(apple => (
<Apple key={apple.id} apple={apple} />
))}
);
});
// OR with useObserver()
function useGlobalState() {
const stores = useStores();
return useObserver(() => ({
applesArray: stores.fruits.apples
}));
}
const ApplesContainer = observer(() => {
const { applesArray } = useGlobalState();
return (
{applesArray.map(apple => (
<Apple key={apple.id} apple={apple} />
))}
);
});
useObserver must return JSX with an observable value.
This hook takes care of tracking changes and re-rendering them.
If no observable value exists in JSX, then it won't be re-rendered.
e.g.:
const SomeContainer =() => {
const { someStores } = useStores();
return useObserver(()=>(
{someStore.data.map(val => (
<Apple key={val.id} val={val} />
))}
));
};

Set State cause use memo rerender

Here is the code , no idea why Mem re-render after set state, as it is a memoized component, or if i wanna remember the component with set state, i should use useRef? that stupid??
const Demo = () => {
console.log("render")
const data = LoadSomeData();
return (<>{data.id}</>)
}
const Mycomp = ({...props}) => {
const [showSearch, setShowSearch] = useState(false);
const Mem = useMemo(() => <Demo />, [props.iwandToReloadData]);
return (
<>
{ showSearch ?
<button onClick={()=>setShowSearch(false)}>Back</button>
:
<>
{Mem}
<button onClick={()=>setShowSearch(true)}>Search</button>
</>
}
</>
)
}
export default Mycomp;
Refer to the comment from Tony Nguyen, it is because i use conditional render for the memorised component, thus it will trigger unmount of the memorised component. Therefore nothing can be memorised.
the solution is using css to hide it instead of not render it for my case

State changes from parent to children not reflected to TextField in React Hook

I pass a component (C) as props to a Child component (B) inside a Parent component (A). State of A is also passed to C and mapped to C's state. But when I update A's state and B's state accordingly, state of C does not update.
My code looks like this: (import statements are omitted)
const Parent = (props) => {
.............(other state)
const [info, setInfo] = React.useState(props.info);
const handleDataChanged = (d) => { setInfo(d); }
return (
<div>
........(other stuffs)
<MyModal
..........(other props)
body={ <MyComp data={ info } updateData={ handleDataChanged } /> }
/>
</div>
);
}
const MyModal = (props) => {
..........(other state)
const [content, setContent] = React.useState(props.body);
React.useEffect(() => { setContent(props.body); }, [props]);
return (
<Modal ...>
<div>{ content }</div>
</Modal>
);
}
const MyComp = (props) => {
const [data, setData] = React.useState(props.data);
React.useEffect(() => { setData(props.data); }, [props]);
return (
data && <TextField value={ data.name }
onChange={ e => {
let d = data;
d.name = e.target.value;
props.updateData(d); }} />
);
}
When I type something in the TextField, I see Parent's info changed. The useEffect of MyModal is not fired. And data in MyComp is not updated.
Update: After more checking the above code and the solution below, the problem is still, but I see that data in MyComp does get changes from Parent, but the TextField does not reflect it.
Someone please show me how can I update data from MyComp and reflect it to Parent. Many thanks!
Practically, it looks like you are trying to recreate the children api https://reactjs.org/docs/react-api.html#reactchildren.
Much easier if you use props.children to compose your components instead of passing props up and down.
const MyModal = (props) => {
...(other state)
return (
<Modal>
<div>{ props.children }</div>
</Modal>
);
}
Then you can handle functionality directly in the parent without having to map props to state (which is strongly discouraged)...
const Parent = (props) => {
...(other state)
const [info, setInfo] = React.useState(props.info);
const handleDataChanged = d => setInfo(d);
return (
<div>
...(other stuffs)
<MyModal {...props}>
<MyComp data={ info } updateData={ handleDataChanged } />
</MyModal>
</div>
);
}
The upside of this approach is that there is much less overhead. rather than passing State A to C and mapping to C's state, you can just do everything from State A (the parent component). No mapping needed, you have one source of truth for state and its easier to think about and build on.
Alternatively, if you want to stick to your current approach then just remove React.useEffect(() => { setContent(props.body); }, [props]); in MyModal and map props directly like so
<Modal>
<div>{ props.body }</div>
</Modal>
The real problem with my code is that: React Hook does not have an idea whether a specific property or element in a state object has changed or not. It only knows if the whole object has been changed.
For example: if you have an array of 3 elements or a Json object in your state. If one element in the array changes, or one property in the Json object changes, React Hook will identiy them unchanged.
Therefore to actually broadcast the change, you must deep clone your object to a copy, then set that copy back to your state. To do this, I use lodash to make a deep clone.
Ref: https://dev.to/karthick3018/common-mistake-done-while-using-react-hooks-1foj
So the code should be:
In MyComp:
onChange={e => { let d = _.cloneDeep(data); d.name = e.target.value; props.handleChange(d) }}
In Parent:
const handleChange = (data) => {
let d = _.cloneDeep(data);
setInfo(d);
}
Then pass the handleChange as delegate to MyComp as normal.

Resources