I have two separate components, which don't have a simple parent-child relationship.
ComponentFolderA
|
ButtonComponent
ComponentFolderB
|
BannerComponent
I want to setState when the user clicks on the button and send that value to the BannerComponent
What's the best way to get around this?
In functional components, you can create a state in a parent component for both ButtonComponent and BannerComponent. Then do as the following example,
const Parent = () => {
const [sampleState, setSampleState] = useState(null);
return (
<>
<BannerComponent sampleState={sampleState} />
<ButtonComponent setSampleState={setSampleState} />
</>
)
}
Then you can access the setSampleState as a prop inside the ButtonComponent.
Also, check this out Best practice way to set state from one component to another in React and you can use a state manager (Context API, Redux)
Keep state in the parent and send the state setting function down to your Button.
const Parent = () => {
const [myState, setMyState] = useState(false);
return (
<>
<BannerComponent myState={myState} />
<ComponentWithButton setMyState={setMyState} />
</>
)
}
const ComponentWithButton = props => {
return (
<Button onClick={() => props.setMyState(true)} />
)
}
Related
I have a component thats opening and showing a modal that I want to reuse because almost everything I need in multiple places. Whats different is 1. data I am iterating through (property names are different) and 2. the button that triggers the modal has different styling. The problem is also that from the parent components I pass a callback, however, I also need to pass a callback to the part where I iterate/render data another callback coming from child component which is why I cannot just render the data iteration as children prop (thus always passing different data). I tried to implement a renderprop but also failed. I hope I explained not too confusing!! How do I do it?
const Parent1 = () => {
const [reportedLine, setReportedLine] = useState(null);
const [availableLines, setAvailableLines] = useState([]);
const [searchResultId, setSearchResultId] = useState('');
return (
<AvailableLinesSelector
data={availableLines}
disabled={searchResultId}
onSelect={setReportedLine}
/>
)
};
const Parent2 = () => {
const [line, setLine] = useState(null);
return (
<AvailableLinesSelector
data={otherData}
disabled={item}
onSelect={setLine}
/>
)
};
const AvailableLinesSelector = ({data, onSelect, disabled}) => {
const [isVisible, setIsVisible] = useState(false);
const [selectedLine, setSelectedLine] = useState('Pick the line');//placeholder should also be flexible
const handleCancel = () => setIsVisible(false);
const handleSelect = (input) => {
onSelect(input)
setSelectedLine(input)
setIsVisible(false);
};
return (
<View>
<Button
title={selectedLine}
//a lot of styling that will be different depending on which parent renders
disabled={disabled}
onPress={() => setIsVisible(true)}
/>
<BottomSheet isVisible={isVisible}>
<View>
{data && data.map(line => (
<AvailableLine //here the properties as name, _id etc will be different depending on which parent renders this component
key={line._id}
line={line.name}
onSelect={handleSelect}
/>
))}
</View>
<Button onPress={handleCancel}>Cancel</Button>
</BottomSheet>
</View>
)
};
You can clone the children and pass additional props like:
React.Children.map(props.children, (child) => {
if (!React.isValidElement(child)) return child;
return React.cloneElement(child, {...child.props, myCallback: callback});
});
I am using a context provider in React to share data across several components. However since a value gets changed from one of my subcomponents, it rerenders all of my other components which partly leads to performance issues. So I want to prevent my child components to rerender. I tried using React.memo() but it's still rendering whenever I set the state of the Context Provider.
const Authenticator = React.memo(() => {
const [myChat, setMyChat] = useContext(ChatContext);
console.log("rerender"); // gets called everytime on click
return (
<Button
title="click me"
onPress={() => setMyChat({ text: "hello" })}
></Button>
);
});
My Context Provider looks like this:
const ChatProvider = ({ children }) => {
const [myChat, setMyChat] = useState([]);
return (
<ChatContext.Provider value={[myChat, setMyChat]}>
{children}
</ChatContext.Provider>
);
};
My App.js looks like this:
<ChatProvider>
<Authenticator />
</ChatProvider>
React.Memo doesn't help since you are calling the useContext hook which will cause the component to re-render every time the value from the provider changes. You should consider splitting your context into two separate contexts: one for the value, one for the state updater.
const ChatProvider = ({ children }) => {
const [myChat, setMyChat] = useState([])
return (
<ChatDispatchContext.Provider value={setMyChat}>
<ChatValueContext.Provider value={myChat}>
{children}
</ChatValueContext.Provider>
</ChatDispatchContext.Provider>
)
}
Then, update your Authenticator component to the following:
const Authenticator = React.memo(() => {
const setMyChat = useContext(ChatDispatchContext)
return (
<Button
title="click me"
onPress={() => setMyChat({ text: "hello" })}
></Button>
)
})
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
I have a parent component with an if statement to show 2 different types of buttons.
What I do, on page load, I check if the API returns an array called lectures as empty or with any values:
lectures.length > 0 ? show button A : show button B
This is the component, called main.js, where the if statement is:
lectures.length > 0
? <div onClick={() => handleCollapseClick()}>
<SectionCollapse open={open} />
</div>
: <LectureAdd dataSection={dataSection} />
The component LectureAdd displays a + sign, which will open a modal to create a new Lecture's title, while, SectionCollapse will show an arrow to show/hide a list of items.
The logic is simple:
1. On page load, if the lectures.lenght > 0 is false, we show the + sign to add a new lecture
OR
2. If the lectures.lenght > 0 is true, we change and show the collpase arrow.
Now, my issue happens when I add the new lecture from the child component LectureAdd.js
import React from 'react';
import { Form, Field } from 'react-final-form';
// Constants
import { URLS } from '../../../../constants';
// Helpers & Utils
import api from '../../../../helpers/API';
// Material UI Icons
import AddBoxIcon from '#material-ui/icons/AddBox';
export default ({ s }) => {
const [open, setOpen] = React.useState(false);
const [ lucturesData, setLecturesData ] = React.useState(0);
const { t } = useTranslation();
const handleAddLecture = ({ lecture_title }) => {
const data = {
"lecture": {
"title": lecture_title
}
}
return api
.post(URLS.NEW_COURSE_LECTURE(s.id), data)
.then(data => {
if(data.status === 201) {
setLecturesData(lucturesData + 1) <=== this doesn't trigger the parent and the button remains a `+` symbol, instead of changing because now `lectures.length` is 1
}
})
.catch(response => {
console.log(response)
});
}
return (
<>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
<AddBoxIcon />
</Button>
<Form
onSubmit={event => handleAddLecture(event)}
>
{
({
handleSubmit
}) => (
<form onSubmit={handleSubmit}>
<Field
name='lecture_title'
>
{({ input, meta }) => (
<div className={meta.active ? 'active' : ''}>
<input {...input}
type='text'
className="signup-field-input"
/>
</div>
)}
</Field>
<Button
variant="contained"
color="primary"
type="submit"
>
ADD LECTURE
</Button>
</form>
)}
</Form>
</>
)
}
I've been trying to use UseEffect to trigger a re-render on the update of the variable called lucturesData, but it doesn't re-render the parent component.
Any idea?
Thanks Joe
Common problem in React. Sending data top-down is easy, we just pass props. Passing information back up from children components, not as easy. Couple of solutions.
Use a callback (Observer pattern)
Parent passes a prop to the child that is a function. Child invokes the function when something meaningful happens. Parent can then do something when the function gets called like force a re-render.
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((lecture) => {
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Call API
let lecture = callApi();
// Notify parent of event
onLectureCreated(lecture);
}, [onLectureCreated]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Similar to solution #1, except for Parent handles API call. The benefit of this, is the Child component becomes more reusable since its "dumbed down".
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((data) => {
// Call API
let lecture = callApi(data);
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Create lecture data to send to callback
let lecture = {
formData1: '',
formData2: ''
}
// Notify parent of event
onCreateLecture(lecture);
}, [onCreateLecture]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Use a central state management tool like Redux. This solution allows any component to "listen in" on changes to data, like new Lectures. I won't provide an example here because it's quite in depth.
Essentially all of these solutions involve the same solution executed slightly differently. The first, uses a smart child that notifies its parent of events once their complete. The second, uses dumb children to gather data and notify the parent to take action on said data. The third, uses a centralized state management system.
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.