I've been struggling with the concept of overriding the behaviour of components between sibling components (components that do not have a parent-child relationship).
I have an App component that renders a header and a content component. The header contains a button that will let a user navigate back (not for real in this example). Now I want the content component to override the back button's behaviour, for example if a user is editing a form and I want to pop up a modal.
The reason I want to do this is because I want to (optionally) control navigation from within the content component itself.
I have found this (see snippet) solution, but I feel like it's not the right way to handle this. I would like to have some advice on how to handle this situation.
A few side-notes:
I'm actually building a react-native app, but in the hope of reaching more people I've simplified it down to a react example. I'm using NavigatorExperimental for navigation.
I am using redux/redux-form
Any help is appreciated.
class NavigationHeader extends React.Component {
render() {
return (
<span onClick={this.props.goBack}>Go back!</span>
);
}
}
class Content extends React.Component {
componentDidMount() {
this.props.setBackButtonBehaviour(() => console.log("BackButton overridden from Content"));
}
componentWillUnmount() {
this.props.resetBackButtonBehaviour();
}
render() {
return (<div style={{background: "red"}}>Content</div>);
}
}
class Navigator extends React.Component {
constructor(props) {
super(props);
this.state = {
overrideHeaderBackButtonBehaviour: null
}
}
setBackButtonBehaviour(func) {
this.setState({overrideHeaderBackButtonBehaviour: func});
}
resetBackButtonBehaviour() {
this.setState({overrideHeaderBackButtonBehaviour: null});
}
defaultBackButtonBehaviour() {
console.log("Default back-button behaviour");
}
render() {
return (
<div>
<NavigationHeader goBack={this.state.overrideHeaderBackButtonBehaviour || this.defaultBackButtonBehaviour} />
<Content
setBackButtonBehaviour={this.setBackButtonBehaviour.bind(this)}
resetBackButtonBehaviour={this.resetBackButtonBehaviour.bind(this)}
/>
</div>
);
}
}
ReactDOM.render(<Navigator />, document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app">
</div>
You can try something like this:
Header.js
import actions from './actions';
#connect(
// map state to props
(state, props) => ({
canNavigate: state.navigation.canNavigate,
}),
// map dispatch to props
{
goBack: actions.goBack,
showNavigationAlert: actions.showNavigationAlert,
}
)
class Header extends React.Component {
render() {
return (
{/* if 'canNavigate' flag is false and user tries to navigate, then show alert */}
<span
onClick={this.props.canNavigate ? this.props.goBack : this.props.showNavigationAlert}
>
Go back!
</span>
);
}
}
Content.js
import actions from './actions';
#connect(
// map state to props
(state, props) => ({
navigationAlert: state.navigation.navigationAlert,
}),
// map dispatch to props
{
toggleNavigation: actions.toggleNavigation,
}
)
class Content extends React.Component {
render() {
return (
<div>
{/*
Show modal if 'navigationAlert' flag is true;
From the modal, if user choose to leave page then call toggleNavigation.
You may call the corresponding navigation function after this (eg: goBack)
*/}
{this.props.navigationAlert && <Modal toggleNavigation={this.props.toggleNavigation} />}
{/* eg: disable navigation when clicked inside the div */}
<form onClick={this.props.toggleNavigation}>
Content click!
</form>
</div>
);
}
}
App.js
class App extends React.Component {
render() {
return (
<div>
<Header />
<Content />
</div>
);
}
}
navigationReducer.js
const initialState = {
canNavigate: true,
navigationAlert: false,
};
export function navigationReducer(state = initialState, action = {}) {
let newState = {};
switch (action.type) {
case TOGGLE_NAVIGATION:
return { ...state, canNavigate: !state.canNavigate };
case TOGGLE_NAVIGATION_ALERT:
return { ...state, navigationAlert: !state.navigationAlert };
default:
return state;
}
}
actions.js
export function toggleNavigation(gdsSession) {
return { type: TOGGLE_NAVIGATION };
}
export function toggleNavigationAlert(gdsSession) {
return { type: TOGGLE_NAVIGATION_ALERT };
}
// ... other actions
and render into DOM.
ReactDOM.render(<App />, document.getElementById("app"));
Related
I am new to React and am having trouble wrapping my head around props/states.
So I have component SortingVisualizer, that generates a visual representation of an unsorted array as follows:
class SortingVisualizer extends React.Component {
constructor(props){
super(props);
this.state = {
array: [],
};
}
componentDidMount(){
this.resetArray();
}
resetArray(){
const array = [];
for(let i = 0; i<100; i++){
array.push(this.getRandomInt(1,500))
}
this.setState({array});
}
//Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
render(){
const {array} = this.state;
return(
<div className="array-container">
{array.map((value, idx) => (
<div className = "array-elem" key = {idx} style = {{height: `${value}px`}}>
</div>
))}
</div>
);
}
}
export default SortingVisualizer;
Now, I have a Navbar Component with a button "Generate new Array" :
class Navbar extends React.Component {
render(){
return(
<nav className = "navbar">
<button className = "new-array-btn" onClick ={this.props.GNAclick}>Generate New Array</button>
</nav>
)
}
}
export default Navbar;
What I want to achieve is that on button click, resetArray will be called on SortingVisualizer so a new array will be generated.
Here is my App.js:
class App extends React.Component {
GNAclickHandler = () => {
console.log("clicked!");
/*
this.setState((prevState) => {
return {GNA: !prevState.GNA}
});
*/
}
render() {
return (
<>
<header>
<Navbar GNAclick = {this.GNAclickHandler}/>
</header>
<div className="App">
<SortingVisualizer />
</div>
</>
);
}
}
export default App;
I am not sure how to progress from here, any help will be appreciated.
Here is the website: https://roy-05.github.io/sort-visualizer/
I would recommend you to read this article https://reactjs.org/docs/thinking-in-react.html, so you can have a better understanding of state and props.
Regarding to your app, you just need to know which component has state and which will react to props change.
App.js
// Navbar manages the state
// and provides it to their children
// so they can rerender whenever the app state changes
class App extends React.Component {
state = {
data: Date.now()
}
createNewArray = () => {
this.setState({
data: Date.now()
})
}
render() {
return (
<React.Fragment>
<Navbar onGenerate={this.createNewArray}/>
<SortingVisualizer data={this.state.data}/>
</React.Fragment>
)
}
}
Navbar.js
// Navbar just notifies that
// and action has to be executed
// (the parent handles that action by creating a new set of data)
function Navbar(props) {
return (
<nav>
<button onClick={props.onGenerate}>Generate array</button>
</nav>
)
}
SortingVisualizer.js
// SortingVisualizer renders what App tells it to render
function SortingVisualizer(props) {
return (
<main>
{props.data}
</main>
);
}
I have a Modal component in the Main.js app and I want to trigger it from a different component (in this case Homepage, but I have one component for each page).
I don´t know how to pass a component to be rendered inside the modal.
If it helps I´m using Context API.
App.js
const App = () => {
return (
<div>this is the main app</div>
<Modal />
)
}
ReactDOM.render(<App />, document.getElementById('root'))
Modal.js
class Modal extends Component {
constructor(props) {
super(props)
this.state = {
'open': false
}
}
componentWillReceiveProps(nextProps) {
this.setState({open: nextProps.open})
}
render() {
if (!this.state.open) {
return false
}
return(
<div className="modal">
{this.props.children}
</div>
)
}
}
export default Modal
Homepage.js
class Homepage extends Component {
constructor(props) {
super(props)
this.handleOpenModal = this.handleOpenModal.bind(this)
}
handleOpenModal() {
// here I want to open the modal and pass the <ModalContent /> component
// basically call the <Modal open="true"> from the Main component
}
render() {
return(
<div className="homepage">
<button onClick={this.handleOpenModal}>open the modal</button>
</div>
)
}
}
const ModalContent = () => {
return(
<div>this is the content I want to render inside the modal</div>
)
}
thank you.
I strongly recommend using something like react-modal (npm). It allows you to keep modal content right next to the trigger. It does this by appending a "portal" high up in the DOM and handles appending the content to is.
Your example may look like the following:
import Modal from 'react-modal';
class Homepage extends Component {
constructor(props) {
super(props)
this.state = { modalOpen: false };
this.handleOpenModal = this.handleOpenModal.bind(this)
}
handleOpenModal() {
this.setState({ modalOpen: true });
}
render() {
return(
<div className="homepage">
<button onClick={this.handleOpenModal}>open the modal</button>
<Modal open={this.state.modalOpen}>
<ModalContent />
</Modal>
</div>
)
}
}
const ModalContent = () => {
return(
<div>this is the content I want to render inside the modal</div>
)
}
class Template extends React.Component {
constructor(props) {
super(props)
this.state = {
isMenuVisible: false,
isVideoModelVisible: false,
loading: 'is-loading'
}
this.handleToggleVideoModel = this.handleToggleVideoModel.bind(this)
}
handleToggleVideoModel() {
alert('test handleToggleVideoModel ');
console.log('layout');
this.setState({
isVideoModelVisible: !this.state.isVideoModelVisible
})
}
render() {
const { children } = this.props
return (
{children()}
)
}
}
Template.propTypes = {
children: React.PropTypes.func,
handleToggleVideoModel: React.PropTypes.func
}
export default Template
pages/index.js which is rendered in Above Template file in {children()}
class HomeIndex extends React.Component {
constructor(props) {
super(props);
this.handleToggleVideoModel = this.handleToggleVideoModel.bind(this)
}
render() {
return (
<div>
<Helmet>
<title>{siteTitle}</title>
<meta name="description" content={siteDescription} />
</Helmet>
<Banner onToggleVideoModel=
{this.props.handleToggleVideoModel}/>
)
}
}
HomeIndex.propTypes = {
onToggleVideoModel: React.PropTypes.func
}
export default HomeIndex
components/Banner.js
const Banner = (props) => (
<section id="banner" className="major">
<li><a href="javascript:void(0);" className="button next" onClick=
{props.onToggleVideoModel}>Watch video</a></li>
</section>
)
export default Banner
How can i don that?
handleToggleVideoModel i want to call from Banner.js which is Grand child of layout/index Parent layuts/index.js.
child pages/index.js
grandchild components/Banner.js
I am stuck in this any body has idea that how can i access handleToggleVideoModel from Banner suggest me solutions.
Pass method reference from Component 1 to component 2 like this:
<Component 2 toggle={this.handleToggleVideoModel}/>
Now again pass this reference to Component 4 like this from Component 2
<Component 4 toggle={this.props.handleToggleVideoModel} />
Now you can call this function from Component 4 like this:
this.props.handleToggleVideoModel
I created a custom Accordion component which again consist of two child components called AccordionTitle and AccordionContent:
The AccordionTitle component has a button. When clicked, the AccordionContent part toggles its style from display:none to block and back when clicked again.
AccordionTitle.js
class AccordionTitle extends Component {
constructor() {
super();
this.show = false;
}
toggle() {
this.show = !this.show;
if (this.props.onToggled) this.props.onToggled(this.show);
}
render() {
return (
<div style={this.props.style}>
<Button onClick={e => this.toggle(e)} />
{this.props.children}
</div>
);
}
}
export default AccordionTitle;
AccordionContent.js
class AccordionContent extends Component {
render() {
let style = this.props.style ? this.props.style : {};
style = JSON.parse(JSON.stringify(style));
style.display = this.props.show ? 'block' : 'none';
return (
<div style={style}>
{this.props.children}
</div>
);
}
}
export default AccordionContent;
Also, I use the following parent component:
Accordion.js
class Accordion extends Component {
render() {
return (
<div>
{this.props.children}
</div>
);
}
}
Accordion.Title = AccordionTitle;
Accordion.Content = AccordionContent;
export default Accordion;
Now, when I use the Accordion component, it's possible that I might need multiple accordions in a row which would look like this:
ProductAccordion.js
import React, { Component } from 'react';
import Accordion from '../Accordion/Accordion';
class ProductAccordion extends Component {
constructor() {
super();
this.state = {
show: false,
};
}
toggled() {
this.setState({
show: !this.state.show,
});
}
render() {
this.productsJsx = [];
const products = this.props.products;
for (let i = 0; i < products.length; i += 1) {
this.productsJsx.push(
<Accordion.Title onToggled={e => this.toggled(e, this)}>
{products[i].name}
<img src="{products[i].imgsrc}" />
</Accordion.Title>,
<Accordion.Content show={this.state.show}>
{products[i].name}<br />
{products[i].grossprice} {products[i].currency}<br />
<hr />
</Accordion.Content>,
);
}
return (
<Accordion style={styles.container}>
{this.productsJsx}
</Accordion>
);
}
}
export default ProductAccordion;
As you can see, I am grabbing the toggled Event from Accordion.Title and I bind it to the prop show of Accordion.Content via the toggled() method.
Now, this works perfectly fine as long as there is just one product, but if there are more of them, clicking on the button will toggle all AccordionContent instances.
How can I change this so that only the content-part which belongs to the title that contains the clicked button will be toggled?
I also have the feeling that the component Accordion should take care of this (rather than ProductAccordion) by allowing Accordion.Title to delegate the toggled event directly to its sibling Accordion.Content. How can I achieve this?
I would suggest storing the index of the open item in state, instead of a boolean. Then in your render, show={this.state.show} would be something like show={this.state.show === i}.
Full example:
import React, { Component } from 'react';
import Accordion from '../Accordion/Accordion';
class ProductAccordion extends Component {
constructor() {
super();
this.state = {
show: null,
};
}
toggled(event, ind) {
const index = this.state.index;
this.setState({ show:ind === index ? null : ind });
}
render() {
this.productsJsx = [];
const products = this.props.products;
for (let i = 0; i < products.length; i += 1) {
this.productsJsx.push(
<Accordion.Title onToggled={e => this.toggled(e, i)}>
{products[i].name}
<img src="{products[i].imgsrc}" />
</Accordion.Title>,
<Accordion.Content show={this.state.show === i}>
{products[i].name}<br />
{products[i].grossprice} {products[i].currency}<br />
<hr />
</Accordion.Content>,
);
}
return (
<Accordion style={styles.container}>
{this.productsJsx}
</Accordion>
);
}
}
export default ProductAccordion;
and this
class AccordionTitle extends Component {
constructor() {
super();
}
render() {
return (
<div style={this.props.style}>
<Button onClick={this.props.onToggled} />
{this.props.children}
</div>
);
}
}
export default AccordionTitle;
Since I've seen React team have discouraged developers to use inheritance, I'm trying to use composition instead.
export class Page extends React.PureComponent {
shouldComponentUpdate(nextProps, nextState) {
return !isEqual(nextProps, this.props) || !isEqual(nextState, this.state);
}
render() {
return this.props.children;
}
}
const mapStateToProps = (state, ownProps) => {
return {}
};
const mapDispatchToProps = (dispatch) => {
return {
onNextClick: () => dispatch(customAction())
}
};
export default connect(mapStateToProps, mapDispatchToProps)(Page);
export class FirstPage extends React.PureComponent {
render() {
return (
<Page>
<div>
<h1>Title</h1>
<Button onClick={this.props.onNextClick}>Next</Button>
</div>
</Page>
);
}
}
const mapStateToProps = (state, ownProps) => {
return {}
};
const mapDispatchToProps = (dispatch) => {
return {
onNextClick: () => dispatch(customAction())
}
};
export default connect(mapStateToProps, mapDispatchToProps)(FirstPage);
In this case, several components will use Page as container and will have a next button. I'd like to expose a generic handler for the next button click in the Page component to avoid repeating the dispatch(customAction()).
I would easily achieve this with inheritance but I'm stuck with the composition pattern.
Any ideas?
I suggest you to provide additional prop for Page children:
export class Page extends React.PureComponent {
shouldComponentUpdate(nextProps, nextState) {
return !isEqual(nextProps, this.props) || !isEqual(nextState, this.state);
}
render() {
const {onNextClick} = this.props;
return React.cloneElement(this.props.children, {onNextClick});
}
}
const mapDispatchToProps = (dispatch) => {
return {
onNextClick: () => dispatch(customAction())
}
};
export default connect(null, mapDispatchToProps)(Page);
export class FirstPage extends React.PureComponent {
render() {
return (
<div>
<h1>Title</h1>
<Button onClick={this.props.onNextClick}>Next</Button>
</div>
);
}
}
export class App extends React.PureComponent {
render() {
return (
<Page>
<FirstPage />
</Page>
);
}
}
You can use ref to refer to the Page element.
class Page extends React.Component {
nextHandler(){
console.log("I am in the parent.");
}
render(){
return this.props.children;
}
}
class FirstPage extends React.Component {
render(){
return (
<Page ref={ page => this.page = page }>
<div>
<h1>Title</h1>
<button onClick={ () => this.page.nextHandler() }>Next</button>
</div>
</Page>
)
}
}
ReactDOM.render(<FirstPage />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>