I might be misunderstanding how parent-child relations are supposed to work in React (new to it) but the following should work in my mind:
I have a parent called <Home/> and in it, I have a child called <ProjectDialog> which is a Google Material Dialog that I'm going to customize after I get this to work.
In the child I have the following code:
handleOpen = () => {
this.setState({ open: true });
};
Pretty normal stuff honestly. But I wanna be able to change the open state from the parent, which I attempt here:
let dialog = <ProjectDialog/>;
class Home extends Component {
handleCardClick = id => {
dialog.handleOpen();
};
But when I click any of the elements that are supposed to trigger this dialogue I get the error that handleOpen() is not a function.
Is there some other way I could do this? Would it make sense to store the open state in props instead and trigger it that way?
That is not the way things are supposed to work.
You have to do it declaratively, meaning that the open/close information should be kept in the parent and transmitted with props to the child.
Something like this:
class Home extends Component {
state = {
isDialogOpen: false
}
handleOpen = () => this.setState({ isDialogOpen: true })
render() {
return (
...
<ProjectDialog isOpen={ this.state.isDialogOpen } />
...
<button onClick={ this.handleDialogOpen }>
Open project dialog
</button>
...
)
}
}
Related
I know this is probably a very basic question, but it's more of a "I don't understand the docs please help me" type of question.
I'm trying to connect two components using React-Redux: the first is a sidebar, and the second is a modal that should appear when clicking on a button in the sidebar. The components are not related in any parent-child relation (except root) so I assume redux is the best option.
I've read all the redux (and react-redux) docs and I understand the core concepts of redux, but I'm having trouble understanding how to implement them in my components.
Basically I want a button in the sidebar that toggles a stored state (true/false is enough) and according to that state the modal would appears (state==true => display:block) and disappear via a button in the modal (state==false => display:none).
What I think I need is an action to toggle a state, for example:
const modalsSlice = createSlice({
name: 'modals',
initalState,
reducers: {
toggleModal(state, action){
state = !state;
}
}
});
then connecting the action in both components (I'm writing the components in classes not as functions) by using:
const toggleModal = {type: 'modals/toggleModal', payload: ''};
const mapStateToProps = state => state.showHideModal;
export default connect(mapStateToProps, toggleModal)(Component);
Now, assuming I'm correct so far, I'm not sure how to continue. I.e. how am I suppose to receive and make the change in the components themselves? Sure, I need to put a function in a button with a onClick={foo} listener but how does the foo suppose to receive and handle the state? And am I suppose to initialize the showHideModal state somewhere? In the root component? While configuring the store?
Any help would be much appreciated.
State Initialisation
You are supposed to initialise the state showHideModal in the slice itself. Moreover, it should be named as either showModal or hideModal for a better interpretation of what this state does.
const modalSlice = createSlice({
name: 'modal',
initialState: {
showModal: false,
},
reducers: {
toggleModal(state){
state.showModal = !state.showModal;
}
}
});
export const { toggleModal } = modalSlice.actions;
SideBar Component
The onClick event handler needs to be passed explicitly via mapDispatchToProps.
import { toggleModal } from './modalSlice';
class Sidebar extends Component {
handleClick = () => {
const { toggleModal } = this.props;
toggleModal();
}
render() {
return (
<div>
{/* rest of JSX */}
<button onClick={this.handleClick}>Toggle Modal</button>
{/* rest of JSX */}
</div>
);
}
}
const mapDispatchToProps = {
toggleModal,
};
export default connect({}, mapDispatchToProps)(Sidebar);
Modal
Note: You cannot access property directly from state like you did state.showHideModal;. You need to access the slice first, followed by property present in it state.modal.showHideModal;.
class Modal extends Component {
handleClick = () => {
const { toggleModal } = this.props;
toggleModal();
}
render() {
const { showModal } = this.props;
return (
<>
{showModal ? (
<div>
<button onClick={this.handleClick}>Close</button>
</div>
) : null}
</>
);
}
}
const mapDispatchToProps = {
toggleModal,
};
const mapStateToProps = state => ({
showModal: state.modal.showModal,
});
export default connect(mapStateToProps, mapDispatchToProps)(Modal);
Update
Coming, to the the reason why Redux throws following warning:
A non-serializable value was detected in an action, in the path: payload
It's because a SyntheticEvent is being passed as a payload to the action. In order to fix this, you need to move the toggleModal call from the onClick prop to a separate handler function. For you reference, check the handleClick function in Modal and SideBar.
I'm stuck with calling my clickRemoveHandler. The idea is that I have two components: first - Layout that renders header, nav and footer components, second - calculator that is my core component with data input etc... In calculator component I have buttons with managed state, so when I click anywhere on Layout component (div), i need to call Calculator function that manipulates my buttons.
The code is below:
class Layout extends Component {
.....
clickHandler = (event) => {
Calculator.clickRemoveHandler(event);
console.log('Clikced')
};
.....
}
class Calculator extends Component {
state = {
currentServiceClass: null,
hoverIndex: null,
btnClicked: false,
selectedService: null
}
currentCursorPosition = {
el: null,
index: null,
rendered: false
}
static clickRemoveHandler = (event) => {
if ((!event.target.hasAttribute("type")) && (this.state.btnClicked)) {
this.currentCursorPosition = {
el: null,
index: null,
rendered: false
};
this.setState({currentServiceClass: null, hoverIndex: null, btnClicked: false})
}
}
....
}
There are a lot of logic in these components so they are too robust to post full code.
But the problem is that there is none of Calculator reference in Layout, calculator itself is rendered with Routing from another component, so I cannot pass any data from Layout to calculator directly.
What I want is to call static clickRemoveHandler from Layout. As I guess static is an option that make function global. So it works, but I got an error TypeError: undefined is not an object (evaluating 'Calculator.state.btnClicked'). As I see it means that when the clickRemoveHandler is called it is not associated with Calculator component, or doesn't have access to its state and props.
The question is how can I make it all work together ? Pass calculator state when calling function or is there another more elegant way to do it ?
I would suggest for the case you described (different components on different levels need access to some state and manipulate it) to use React context. You can take a look also on state managers like Redux or MobX, but in this particular case it will be overhead since your application is not so "huge". Basically you need to create some separate folder (you can call it context), inside it you should create context itself, export it and wrap in it you most up level component so that all the children will be able to use it.
You can find an example here: https://codesandbox.io/s/spring-glitter-0vzul.
Here is a link to documentation: https://reactjs.org/docs/context.html
I can provide you some more details if you need
That was a challenge, but I've done it!
Layout component:
state = {
firstMount: false,
clicked: false,
clickedEvt: null
};
clickHandler = (event) => {
console.log('Clikced')
if (this.state.clickedEvt)
this.setState({clicked: false, clickedEvt: null});
else
this.setState({clicked: true, clickedEvt: event.target}, ()=>{setTimeout(() =>
this.setState({clicked: false, clickedEvt: null})
, 50)})
};
<LayoutContext.Provider value={{
clicked: this.state.clicked,
clickedEvt: this.state.clickedEvt,
handleClick: this.clickHandler
}}>
render() {
return(
<div onClick={(event) => this.clickHandler(event)} className="web">
First I call handleClick as onclick event from Layout component, then it is called again from calculator
componentDidUpdate() {
if (this.context.clicked) {
this.clickRemoveHandler(this.context.clickedEvt)
}
}
I have made a react UI widget thats let's the user select a number of different times and dates. The user's current selection is stored in the state of a top level component, DateTimePicker. I then have a widget wrapper like so:
import ...
export default {
new: (args) => {
const store = {
reactElement: <DateTimePicker
startDate={args.startDate}
endDate={args.endDate}
/>
};
return {
getState: () => {
return store.reactElement.getState(); // DOESN'T WORK
},
render: (selector) => {
ReactDOM.render(store.reactElement, document.querySelector(selector));
}
};
}
};
I want to add a validation to make sure that at least X days/times are selected, but this validation needs to be implemented outside of the widget.
For this, I'll need someway of asking the widget of it 's state. i.e. what has the user selected? Although it seems like the state of the class is not part of the public api of a react component.
How can I acess the state, or is there another way I'm missing?
The solution to doing things imperatively from the parent to the child usually involves getting a ref to the child component. Something along these lines:
export default {
new: (args) => {
let myRef = React.createRef();
const store = {
reactElement: <DateTimePicker
ref={myRef}
startDate={args.startDate}
endDate={args.endDate}
/>
};
return {
getState: () => {
return myRef.current.getState();
},
render: (selector) => {
ReactDOM.render(store.reactElement, document.querySelector(selector));
}
};
}
};
With ref={myRef} added as a prop, whenever DateTimePicker gets mounted, it will assign a reference to the mounted component to myRef.current. You can then use that reference to interact directly with the most recently mounted component.
So my application is growing, and I am starting to wonder, what is the best approach in my case, to open dialog from mapped component.
So currently, I have events array of object, which i map and create new components.
EventFeed.js
events.map(event => <EventCard key={event._id} event={event}/>)
What i want to do from each eventcard on "More info" button click call dialog and pass event id to that dialog, my current aproach looks like this:
SingleEventCard
class RecipeReviewCard extends React.Component {
constructor(props) {
super(props);
this.state = {
expanded: false,
isgoing: this.props.isgoing,
showModal1: false
};
this.handleClick = this.handleClick.bind(this);
}
handleClick(event) { // switch the value of the showModal state
this.setState({
showModal1: !this.state.showModal1
});
}
getComponent = () => {
if (this.state.showModal1) { // show the modal if state showModal is true
return <EventView eventid={this.props.event._id} />;
} else {
return null;
}
}
render() {
return {
<Button onClick={this.handleClick}>More info</Button>
{this.getComponent()}
}
}
In render i have another views which I wont write(since no reason bunch of inputs and displays), the problem is that when I open dialog it messes up my event card style. Also I need to click two times next time in order to open. Can someone can offer more generic approach, where I can render that dialog in other component or maybe something different?
I have a dropdown as is shown in the following image:
When I click the folder icon it opens and closes because showingProjectSelector property in the state that is set to false.
constructor (props) {
super(props)
const { organization, owner, ownerAvatar } = props
this.state = {
owner,
ownerAvatar,
showingProjectSelector: false
}
}
When I click the icon, it opens and closes properly.
<i
onClick={() => this.setState({ showingProjectSelector: !this.state.showingProjectSelector })}
className='fa fa-folder-open'>
</i>
But what I'm trying to do is to close the dropdown when I click outside it. How can I do this without using any library?
This is the entire component: https://jsbin.com/cunakejufa/edit?js,output
You could try leveraging onBlur:
<i onClick={...} onBlur={() => this.setState({showingProjectSelector: false})}/>
I faced same issue with you. Solved after reading this:
Detect click outside React component
Please try:
You should use a High Order Component to wrap the component that you would like to listen for clicks outside it.
This component example has only one prop: "onClickedOutside" that receives a function.
ClickedOutside.js
import React, { Component } from "react";
export default class ClickedOutside extends Component {
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
handleClickOutside = event => {
// IF exists the Ref of the wrapped component AND his dom children doesnt have the clicked component
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
// A props callback for the ClikedClickedOutside
this.props.onClickedOutside();
}
};
render() {
// In this piece of code I'm trying to get to the first not functional component
// Because it wouldn't work if use a functional component (like <Fade/> from react-reveal)
let firstNotFunctionalComponent = this.props.children;
while (typeof firstNotFunctionalComponent.type === "function") {
firstNotFunctionalComponent = firstNotFunctionalComponent.props.children;
}
// Here I'm cloning the element because I have to pass a new prop, the "reference"
const children = React.cloneElement(firstNotFunctionalComponent, {
ref: node => {
this.wrapperRef = node;
},
// Keeping all the old props with the new element
...firstNotFunctionalComponent.props
});
return <React.Fragment>{children}</React.Fragment>;
}
}
If you want to use a tiny component (466 Byte gzipped) that already exists for this functionality then you can check out this library react-outclick.
The good thing about the library is that it also lets you detect clicks outside of a component and inside of another. It also supports detecting other types of events.
Using the library you can have something like this inside your component.
import OnOutsiceClick from 'react-outclick';
class MyComp extends Component {
render() {
return (
<OnOutsiceClick
onOutsideClick={() => this.setState({showingProjectSelector: false})}>
<Dropdown />
</OnOutsiceClick>
);
}
}
Wrapper component - i.e. the one that wrapps all other components
create onClick event that runs a function handleClick.
handleClick function checks ID of the clicked event.
When ID matches it does something, otherwise it does something else.
const handleClick = (e) => {
if(e.target.id === 'selectTypeDropDown'){
setShowDropDown(true)
} else {
setShowDropDown(false);
}
}
So I have a dropdown menu that appears ONLY when you click on the dropdown menu, otherwise it hides it.